Explorar el Código

custom schedules + fluxos de status + regra bloqueio 2 por semana

Gustavo Zanatta hace 1 semana
padre
commit
e644f85b6b

+ 46 - 1
src/api/customSchedule.js

@@ -1,4 +1,4 @@
-import api from './index'
+import api from "src/api";
 
 export async function getCustomSchedules() {
   const response = await api.get('/custom-schedule')
@@ -24,3 +24,48 @@ export async function deleteCustomSchedule(id) {
   const response = await api.delete(`/custom-schedule/${id}`)
   return response.data
 }
+
+export const getAvailableOpportunities = async (providerId) => {
+  const response = await api.get(`/custom-schedule/available?provider_id=${providerId}`)
+  return response.data.payload
+}//
+
+export const getProviderProposals = async (providerId) => {
+  const response = await api.get(`/custom-schedule/provider-proposals?provider_id=${providerId}`)
+  return response.data.payload
+}//
+
+export const getOpportunityProposals = async (scheduleId) => {
+  const response = await api.get(`/custom-schedule/${scheduleId}/proposals`)
+  return response.data.payload
+}
+
+export const proposeOpportunity = async (scheduleId, providerId) => {
+  const response = await api.post(`/custom-schedule/${scheduleId}/propose`, { provider_id: providerId })
+  return response.data
+}
+
+export const acceptProposal = async (proposalId) => {
+  const response = await api.post(`/custom-schedule/${proposalId}/accept`)
+  return response.data
+}
+
+export const refuseProposal = async (proposalId) => {
+  const response = await api.post(`/custom-schedule/${proposalId}/refuse`)
+  return response.data
+}
+
+export const getProvidersProposalsAndOpportunities = async (providerId) => {
+  const response = await api.get(`/custom-schedule-proposals-provider/${providerId}`);
+  return response.data.payload;
+}
+
+export const verifyScheduleCode = async (scheduleId, code) => {
+  const response = await api.post(`/custom-schedule-verify-code/${scheduleId}`,{ code: code });
+  return response.data
+}
+
+export const refuseOpportunity = async (scheduleId, providerId) => {
+  const response = await api.post(`/custom-schedule/${scheduleId}/refuse-opportunity`, { provider_id: providerId })
+  return response.data
+}

+ 1 - 1
src/api/providerBlockedDay.js

@@ -1,4 +1,4 @@
-import api from './index'
+import api from "src/api";
 
 export const getProviderBlockedDays = async (providerId) => {
     const response = await api.get(`/provider/blocked-days/${providerId}`)

+ 1 - 1
src/api/providerPaymentMethod.js

@@ -1,4 +1,4 @@
-import api from "./index";
+import api from "src/api";
 
 export const getProviderPaymentMethods = async (providerId) => {
   const { data } = await api.get(`/provider/payment-methods/${providerId}`);

+ 5 - 0
src/api/schedule.js

@@ -10,6 +10,11 @@ export const getSchedulesGroupedByClient = async () => {
   return data.payload
 }
 
+export const getSchedulesGroupedByClientCustom = async () => {
+  const { data } = await api.get('/schedules/grouped-by-client/custom')
+  return data.payload
+}
+
 export const getScheduleById = async (id) => {
   const { data } = await api.get(`/schedule/${id}`)
   return data.payload

+ 1 - 1
src/components/defaults/DefaultDialogHeader.vue

@@ -1,5 +1,5 @@
 <template>
-  <q-bar class="q-py-md" v-bind="$attrs" style="height: 50px">
+  <q-bar class="q-py-md bg-primary text-white" v-bind="$attrs" style="height: 50px">
     <q-icon v-if="icon" :name="icon" />
     <div>{{ title() }}</div>
 

+ 13 - 3
src/css/app.scss

@@ -93,9 +93,10 @@ input[type="number"]::-webkit-outer-spin-button {
 
 .gradient-diarista {
   background: linear-gradient(
-    90deg,
-    #6a11cb 0%,
-    #2575fc 100%
+    -90deg,
+    #ec48d1 5%,
+    #6b11cb 65%,
+    #2574fc 100%
   );
 
   -webkit-background-clip: text;
@@ -103,4 +104,13 @@ input[type="number"]::-webkit-outer-spin-button {
 
   background-clip: text;
   color: transparent;
+}
+
+.gradient-diarista-bg {
+  background: linear-gradient(
+    -90deg,
+    #ec48d1 1%,
+    #6b11cbcb 65%,
+    #2574fcbd 100%
+  );
 }

+ 23 - 2
src/i18n/locales/en.json

@@ -3,6 +3,7 @@
     "actions": {
       "save": "Save",
       "cancel": "Cancel",
+      "close": "Close",
       "edit": "Edit",
       "add": "Add",
       "update": "Update",
@@ -452,6 +453,7 @@
     "provider": "Provider",
     "address": "Address",
     "date": "Date",
+    "time": "Time",
     "period": "Period",
     "period_type": "Period Type",
     "schedule_type": "Schedule Type",
@@ -495,7 +497,11 @@
       "cancelled": "Cancelled",
       "started": "Started",
       "finished": "Finished"
-    }
+    },
+    "fill_code": "Fill Code",
+    "enter_code": "Enter the code",
+    "code_verified_success": "Code verified successfully",
+    "code_verified_failed": "Invalid code, please try again"
   },
   "opportunities": {
     "singular": "Opportunity",
@@ -503,6 +509,20 @@
     "title": "Opportunities",
     "description": "Manage custom scheduling opportunities",
     "empty_state": "No opportunities found",
+    "available": "Available Opportunities",
+    "my_proposals": "My Sent Proposals",
+    "no_proposals": "You haven't sent any proposals yet",
+    "view_details": "Opportunity Details",
+    "accept_opportunity": "Accept Opportunity",
+    "proposal_sent": "Proposal Sent",
+    "proposal_refused": "Proposal Refused",
+    "proposal_accepted": "Proposal Accepted",
+    "waiting_client": "Waiting for Client",
+    "proposals_received": "Proposals Received",
+    "no_proposals_received": "No proposals received yet",
+    "accept_provider": "Accept Provider",
+    "refuse_provider": "Refuse Provider",
+    "provider_info": "Provider Information",
     "client": "Client",
     "date": "Date",
     "period": "Period",
@@ -525,7 +545,8 @@
     "select_time": "Select time",
     "types": {
       "residencial": "Residential",
-      "comercial": "Commercial"
+      "comercial": "Commercial",
+      "commercial": "Commercial"
     }
   },
   "orders": {

+ 23 - 2
src/i18n/locales/es.json

@@ -3,6 +3,7 @@
     "actions": {
       "save": "Guardar",
       "cancel": "Cancelar",
+      "close": "Cerrar",
       "edit": "Editar",
       "add": "Añadir",
       "update": "Actualizar",
@@ -452,6 +453,7 @@
     "provider": "Proveedor",
     "address": "Dirección",
     "date": "Fecha",
+    "time": "Horario",
     "period": "Período",
     "period_type": "Tipo de Período",
     "schedule_type": "Tipo de Agenda",
@@ -495,7 +497,11 @@
       "cancelled": "Cancelado",
       "started": "Iniciado",
       "finished": "Finalizado"
-    }
+    },
+    "fill_code": "Rellenar Código",
+    "enter_code": "Ingrese el código",
+    "code_verified_success": "Código verificado con éxito",
+    "code_verified_failed": "Código inválido, por favor intente nuevamente"
   },
   "opportunities": {
     "singular": "Oportunidad",
@@ -503,6 +509,20 @@
     "title": "Oportunidades",
     "description": "Gestionar oportunidades de agenda personalizada",
     "empty_state": "No se encontraron oportunidades",
+    "available": "Oportunidades Disponibles",
+    "my_proposals": "Mis Propuestas Enviadas",
+    "no_proposals": "Aún no has enviado ninguna propuesta",
+    "view_details": "Detalles de la Oportunidad",
+    "accept_opportunity": "Aceptar Oportunidad",
+    "proposal_sent": "Propuesta Enviada",
+    "proposal_refused": "Propuesta Rechazada",
+    "proposal_accepted": "Propuesta Aceptada",
+    "waiting_client": "Esperando Cliente",
+    "proposals_received": "Propuestas Recibidas",
+    "no_proposals_received": "No se han recibido propuestas aún",
+    "accept_provider": "Aceptar Proveedor",
+    "refuse_provider": "Rechazar Proveedor",
+    "provider_info": "Información del Proveedor",
     "client": "Cliente",
     "date": "Fecha",
     "period": "Período",
@@ -525,7 +545,8 @@
     "select_time": "Seleccione el horario",
     "types": {
       "residencial": "Residencial",
-      "comercial": "Comercial"
+      "comercial": "Comercial",
+      "commercial": "Comercial"
     }
   },
   "orders": {

+ 23 - 2
src/i18n/locales/pt.json

@@ -3,6 +3,7 @@
     "actions": {
       "save": "Salvar",
       "cancel": "Cancelar",
+      "close": "Fechar",
       "edit": "Editar",
       "add": "Adicionar",
       "update": "Atualizar",
@@ -452,6 +453,7 @@
     "provider": "Prestador",
     "address": "Endereço",
     "date": "Data",
+    "time": "Horário",
     "period": "Período",
     "period_type": "Tipo de Período",
     "schedule_type": "Tipo de Agendamento",
@@ -495,7 +497,11 @@
       "cancelled": "Cancelado",
       "started": "Iniciado",
       "finished": "Finalizado"
-    }
+    },
+    "fill_code": "Preencher Código",
+    "enter_code": "Digite o código",
+    "code_verified_success": "Código verificado com sucesso",
+    "code_verified_failed": "Código inválido, tente novamente"
   },
   "opportunities": {
     "singular": "Oportunidade",
@@ -503,6 +509,20 @@
     "title": "Oportunidades",
     "description": "Gerencie as oportunidades de agendamento personalizado",
     "empty_state": "Nenhuma oportunidade encontrada",
+    "available": "Oportunidades Disponíveis",
+    "my_proposals": "Minhas Propostas Enviadas",
+    "no_proposals": "Você ainda não enviou nenhuma proposta",
+    "view_details": "Detalhes da Oportunidade",
+    "accept_opportunity": "Aceitar Oportunidade",
+    "proposal_sent": "Proposta Enviada",
+    "proposal_refused": "Proposta Recusada",
+    "proposal_accepted": "Proposta Aceita",
+    "waiting_client": "Aguardando Cliente",
+    "proposals_received": "Propostas Recebidas",
+    "no_proposals_received": "Nenhuma proposta recebida ainda",
+    "accept_provider": "Aceitar Prestador",
+    "refuse_provider": "Recusar Prestador",
+    "provider_info": "Informações do Prestador",
     "client": "Cliente",
     "date": "Data",
     "period": "Período",
@@ -525,7 +545,8 @@
     "select_time": "Selecione o horário",
     "types": {
       "residencial": "Residencial",
-      "comercial": "Comercial"
+      "comercial": "Comercial",
+      "commercial": "Comercial"
     }
   },
   "orders": {

+ 10 - 5
src/pages/city/components/AddEditCityDialog.vue

@@ -47,13 +47,18 @@
             @update:model-value="serverErrors.status = null"
           />
         </q-card-section>
-        <q-card-actions align="center">
-          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-          <q-space />
+        <q-card-actions align="right" class="q-px-md q-pb-md">
           <q-btn
+            :label="$t('common.actions.close')"
+            flat
             color="primary"
-            label="OK"
-            :type="'submit'"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="secondary"
+            :label="$t('common.actions.save')"
+            unelevated
+            type="submit"
             :loading="loading"
             :disable="!hasUpdatedFields"
           />

+ 9 - 4
src/pages/country/components/AddEditCountryDialog.vue

@@ -33,13 +33,18 @@
             @update:model-value="serverErrors.status = null"
           />
         </q-card-section>
-        <q-card-actions align="center">
-          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-          <q-space />
+        <q-card-actions align="right" class="q-px-md q-pb-md">
           <q-btn
+            :label="$t('common.actions.close')"
+            flat
             color="primary"
-            label="OK"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="secondary"
+            :label="$t('common.actions.save')"
             :type="'submit'"
+            unelevated
             :loading="loading"
             :disable="!hasUpdatedFields"
           />

+ 565 - 188
src/pages/dashboard/DashboardPage.vue

@@ -3,8 +3,9 @@
     <DefaultHeaderPage />
 
     <div v-if="!isLoading" class="q-pa-md">
+      <!-- Tabs Principais: Agendamentos / Oportunidades -->
       <q-tabs
-        v-model="viewMode"
+        v-model="scheduleType"
         dense
         class="text-grey"
         active-color="primary"
@@ -12,189 +13,443 @@
         align="justify"
         narrow-indicator
       >
-        <q-tab name="client" :label="$t('schedules.view_as_client')" icon="person" />
-        <q-tab name="provider" :label="$t('schedules.view_as_provider')" icon="work" />
+        <q-tab name="default" :label="$t('ui.navigation.schedules')" icon="event" />
+        <q-tab name="custom" :label="$t('ui.navigation.opportunities')" icon="mdi-bullseye-arrow" />
       </q-tabs>
 
       <q-separator />
 
-      <q-tab-panels v-model="viewMode" animated>
-        <q-tab-panel name="client">
-          <div class="row q-col-gutter-md">
-            <div class="col-12">
-              <q-select
-                v-model="statusFilter"
-                :options="statusFilterOptions"
-                :label="$t('schedules.filter_by_status')"
-                outlined
-                dense
-                emit-value
-                map-options
-                clearable
-                class="q-mb-md"
-                style="max-width: 300px"
-              />
-            </div>
-
-            <div class="col-12">
-              <q-expansion-item
-                v-for="clientGroup in filteredGroupedSchedules"
-                :key="clientGroup.client_id"
-                :label="clientGroup.client_name"
-                icon="person"
-                header-class="bg-primary text-white"
-                expand-icon-class="text-white"
-                class="q-mb-md shadow-2 rounded-borders"
-                default-opened
-              >
-                <q-card>
-                  <q-card-section>
-                    <q-list bordered separator>
-                      <q-item
-                        v-for="schedule in clientGroup.schedules"
-                        :key="schedule.id"
-                        clickable
-                        @click="openScheduleDialog(schedule)"
-                      >
-                        <div class="q-my-auto q-pr-md" style="width: 30px">
-                          {{ schedule.id }}
-                        </div>
-                        <q-item-section avatar>
-                          <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
-                            {{ $t(`schedules.statuses.${schedule.status}`) }}
-                          </q-badge>
-                        </q-item-section>
-                        
-                        <q-item-section>
-                          <q-item-label>
-                            <q-icon name="event" size="xs" class="q-mr-xs" color="primary"/>
-                            <span class="gradient-diarista">
-                              {{ schedule.date }} {{ schedule.start_time?.substring(0, 5) }}
-                            </span>
-                          </q-item-label>
-                          <q-item-label caption>
-                            <q-icon name="person" size="xs" class="q-mr-xs" color="primary"/>
-                            <span class="gradient-diarista">
-                              {{ schedule.provider_name }}
-                            </span>
-                          </q-item-label>
-                        </q-item-section>
-                        
-                        <q-item-section side>
-                          <q-item-label>
-                            {{ schedule.period_type }} {{ $t('schedules.hours') }}
-                          </q-item-label>
-                          <q-item-label caption class="text-positive text-weight-bold">
-                            {{ formatCurrency(schedule.total_amount) }}
-                          </q-item-label>
-                        </q-item-section>
-
-                        <q-item-section side>
-                          <q-icon name="chevron_right" />
-                        </q-item-section>
-                      </q-item>
-                    </q-list>
-                  </q-card-section>
-                </q-card>
-              </q-expansion-item>
-
-              <div v-if="filteredGroupedSchedules.length === 0" class="text-center q-pa-xl">
-                <q-icon name="event_busy" size="64px" color="grey-5" />
-                <div class="text-h6 text-grey-7 q-mt-md">
-                  {{ $t('schedules.empty_state') }}
+      <q-tab-panels v-model="scheduleType" animated>
+        <!-- AGENDAMENTOS (Default) -->
+        <q-tab-panel name="default">
+          <q-tabs
+            v-model="viewMode"
+            dense
+            class="text-grey"
+            active-color="secondary"
+            indicator-color="secondary"
+            align="left"
+          >
+            <q-tab name="client" :label="$t('schedules.view_as_client')" icon="person" />
+            <q-tab name="provider" :label="$t('schedules.view_as_provider')" icon="work" />
+          </q-tabs>
+
+          <q-separator />
+
+          <q-tab-panels v-model="viewMode" animated>
+            <!-- Visão Cliente -->
+            <q-tab-panel name="client">
+              <div class="row q-col-gutter-md">
+                <div class="col-12">
+                  <q-select
+                    v-model="statusFilter"
+                    :options="statusFilterOptions"
+                    :label="$t('schedules.filter_by_status')"
+                    outlined
+                    dense
+                    emit-value
+                    map-options
+                    clearable
+                    class="q-mb-md"
+                    style="max-width: 300px"
+                  />
+                </div>
+
+                <div class="col-12">
+                  <q-expansion-item
+                    v-for="clientGroup in filteredGroupedDefaultSchedules"
+                    :key="clientGroup.client_id"
+                    :label="clientGroup.client_name"
+                    icon="person"
+                    header-class="text-white gradient-diarista-bg"
+                    expand-icon-class="text-white"
+                    class="q-mb-md shadow-2"
+                    default-opened
+                  >
+                    <q-card>
+                      <q-card-section>
+                        <q-list bordered separator>
+                          <q-item
+                            v-for="schedule in clientGroup.schedules"
+                            :key="schedule.id"
+                            clickable
+                            @click="openScheduleDialog(schedule)"
+                          >
+                            <div class="q-my-auto q-pr-md" style="width: 30px">
+                              {{ schedule.id }}
+                            </div>
+                            <q-item-section avatar>
+                              <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
+                                {{ $t(`schedules.statuses.${schedule.status}`) }}
+                              </q-badge>
+                            </q-item-section>
+                            
+                            <q-item-section>
+                              <q-item-label>
+                                <q-icon name="event" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.date }} {{ schedule.start_time?.substring(0, 5) }}
+                                </span>
+                              </q-item-label>
+                              <q-item-label caption>
+                                <q-icon name="person" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.provider_name }}
+                                </span>
+                              </q-item-label>
+                            </q-item-section>
+                            
+                            <q-item-section side>
+                              <q-item-label>
+                                {{ schedule.period_type }} {{ $t('schedules.hours') }}
+                              </q-item-label>
+                              <q-item-label caption class="text-positive text-weight-bold">
+                                {{ formatCurrency(schedule.total_amount) }}
+                              </q-item-label>
+                            </q-item-section>
+
+                            <q-item-section side>
+                              <q-icon name="chevron_right" />
+                            </q-item-section>
+                          </q-item>
+                        </q-list>
+                      </q-card-section>
+                    </q-card>
+                  </q-expansion-item>
+
+                  <div v-if="filteredGroupedDefaultSchedules.length === 0" class="text-center q-pa-xl">
+                    <q-icon name="event_busy" size="64px" color="grey-5" />
+                    <div class="text-h6 text-grey-7 q-mt-md">
+                      {{ $t('schedules.empty_state') }}
+                    </div>
+                  </div>
                 </div>
               </div>
-            </div>
-          </div>
+            </q-tab-panel>
+
+            <!-- Visão Prestador -->
+            <q-tab-panel name="provider">
+              <div class="row q-col-gutter-md">
+                <div class="col-12">
+                  <q-select
+                    v-model="statusFilter"
+                    :options="statusFilterOptions"
+                    :label="$t('schedules.filter_by_status')"
+                    outlined
+                    dense
+                    emit-value
+                    map-options
+                    clearable
+                    class="q-mb-md"
+                    style="max-width: 300px"
+                  />
+                </div>
+
+                <div class="col-12">
+                  <q-expansion-item
+                    v-for="clientGroup in filteredGroupedDefaultSchedules"
+                    :key="clientGroup.client_id"
+                    :label="clientGroup.client_name"
+                    icon="person"
+                    header-class="text-white gradient-diarista-bg"
+                    expand-icon-class="text-white"
+                    class="q-mb-md shadow-2 rounded-borders"
+                    default-opened
+                  >
+                    <q-card>
+                      <q-card-section>
+                        <q-list bordered separator>
+                          <q-item
+                            v-for="schedule in clientGroup.schedules"
+                            :key="schedule.id"
+                            clickable
+                            @click="openScheduleDialog(schedule)"
+                          >
+                            <div class="q-my-auto q-pr-md" style="width: 30px">
+                              {{ schedule.id }}
+                            </div>
+                            <q-item-section avatar>
+                              <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
+                                {{ $t(`schedules.statuses.${schedule.status}`) }}
+                              </q-badge>
+                            </q-item-section>
+                            
+                            <q-item-section>
+                              <q-item-label>
+                                <q-icon name="event" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.date }} {{ schedule.start_time?.substring(0, 5) }}
+                                </span>
+                              </q-item-label>
+                              <q-item-label caption>
+                                <q-icon name="person" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.provider_name }}
+                                </span>
+                              </q-item-label>
+                            </q-item-section>
+                            
+                            <q-item-section side>
+                              <q-item-label>
+                                {{ schedule.period_type }} {{ $t('schedules.hours') }}
+                              </q-item-label>
+                              <q-item-label caption class="text-positive text-weight-bold">
+                                {{ formatCurrency(schedule.total_amount) }}
+                              </q-item-label>
+                            </q-item-section>
+
+                            <q-item-section side>
+                              <q-icon name="chevron_right" />
+                            </q-item-section>
+                          </q-item>
+                        </q-list>
+                      </q-card-section>
+                    </q-card>
+                  </q-expansion-item>
+
+                  <div v-if="filteredGroupedDefaultSchedules.length === 0" class="text-center q-pa-xl">
+                    <q-icon name="event_busy" size="64px" color="grey-5" />
+                    <div class="text-h6 text-grey-7 q-mt-md">
+                      {{ $t('schedules.empty_state') }}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </q-tab-panel>
+          </q-tab-panels>
         </q-tab-panel>
 
-        <q-tab-panel name="provider">
-          <div class="row q-col-gutter-md">
-            <div class="col-12">
-              <q-select
-                v-model="statusFilter"
-                :options="statusFilterOptions"
-                :label="$t('schedules.filter_by_status')"
-                outlined
-                dense
-                emit-value
-                map-options
-                clearable
-                class="q-mb-md"
-                style="max-width: 300px"
-              />
-            </div>
-
-            <div class="col-12">
-              <q-expansion-item
-                v-for="clientGroup in filteredGroupedSchedules"
-                :key="clientGroup.client_id"
-                :label="clientGroup.client_name"
-                icon="person"
-                header-class="bg-primary text-white"
-                expand-icon-class="text-white"
-                class="q-mb-md shadow-2 rounded-borders"
-                default-opened
-              >
-                <q-card>
-                  <q-card-section>
-                    <q-list bordered separator>
-                      <q-item
-                        v-for="schedule in clientGroup.schedules"
-                        :key="schedule.id"
-                        clickable
-                        @click="openScheduleDialog(schedule)"
-                      >
-                        <div class="q-my-auto q-pr-md" style="width: 30px">
-                          {{ schedule.id }}
-                        </div>
-                        <q-item-section avatar>
-                          <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
-                            {{ $t(`schedules.statuses.${schedule.status}`) }} 
-                          </q-badge>
-                        </q-item-section>
-                        
-                        <q-item-section>
-                          <q-item-label>
-                            <q-icon name="event" size="xs" class="q-mr-xs" color="primary"/>
-                            <span class="gradient-diarista">
-                              {{ schedule.date }} {{ schedule.start_time?.substring(0, 5) }}
-                            </span>
-                          </q-item-label>
-                          <q-item-label caption>
-                            <q-icon name="person" size="xs" class="q-mr-xs" color="primary"/>
-                            <span class="gradient-diarista">
-                              {{ schedule.provider_name }}
-                            </span>
-                          </q-item-label>
-                        </q-item-section>
-                        
-                        <q-item-section side>
-                          <q-item-label>
-                            {{ schedule.period_type }} {{ $t('schedules.hours') }}
-                          </q-item-label>
-                          <q-item-label caption class="text-positive text-weight-bold">
-                            {{ formatCurrency(schedule.total_amount) }}
-                          </q-item-label>
-                        </q-item-section>
-
-                        <q-item-section side>
-                          <q-icon name="chevron_right" />
-                        </q-item-section>
-                      </q-item>
-                    </q-list>
-                  </q-card-section>
-                </q-card>
-              </q-expansion-item>
-
-              <div v-if="filteredGroupedSchedules.length === 0" class="text-center q-pa-xl">
-                <q-icon name="event_busy" size="64px" color="grey-5" />
-                <div class="text-h6 text-grey-7 q-mt-md">
-                  {{ $t('schedules.empty_state') }}
+        <q-tab-panel name="custom">
+          <q-tabs
+            v-model="viewMode"
+            dense
+            class="text-grey"
+            active-color="secondary"
+            indicator-color="secondary"
+            align="left"
+          >
+            <q-tab name="client" :label="$t('schedules.view_as_client')" icon="person" />
+            <q-tab name="provider" :label="$t('schedules.view_as_provider')" icon="work" />
+          </q-tabs>
+
+          <q-separator />
+
+          <q-tab-panels v-model="viewMode" animated>
+            <!-- Visão Cliente -->
+            <q-tab-panel name="client">
+              <div class="row q-col-gutter-md">
+                <div class="col-12">
+                  <q-select
+                    v-model="statusFilter"
+                    :options="statusFilterOptions"
+                    :label="$t('schedules.filter_by_status')"
+                    outlined
+                    dense
+                    emit-value
+                    map-options
+                    clearable
+                    class="q-mb-md"
+                    style="max-width: 300px"
+                  />
+                </div>
+
+                <div class="col-12">
+                  <q-expansion-item
+                    v-for="clientGroup in filteredGroupedCustomSchedules"
+                    :key="clientGroup.client_id"
+                    :label="clientGroup.client_name"
+                    icon="person"
+                    header-class="text-white gradient-diarista-bg"
+                    expand-icon-class="text-white"
+                    class="q-mb-md shadow-2 rounded-borders"
+                    default-opened
+                  >
+                    <q-card>
+                      <q-card-section>
+                        <q-list bordered separator>
+                          <q-item
+                            v-for="schedule in clientGroup.schedules"
+                            :key="schedule.id"
+                            clickable
+                            @click="openCustomScheduleDialog(schedule)"
+                          >
+                            <div class="q-my-auto q-pr-md" style="width: 30px">
+                              {{ schedule.id }}
+                            </div>
+                            <q-item-section avatar>
+                              <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
+                                {{ $t(`schedules.statuses.${schedule.status}`) }}
+                              </q-badge>
+                            </q-item-section>
+                            
+                            <q-item-section>
+                              <q-item-label>
+                                <q-icon name="event" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.date }} {{ schedule.start_time?.substring(0, 5) }}
+                                </span>
+                              </q-item-label>
+                              <q-item-label caption>
+                                <q-icon name="person" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.provider_name || 'N/A' }}
+                                </span>
+                              </q-item-label>
+                            </q-item-section>
+                            
+                            <q-item-section side>
+                              <q-item-label>
+                                {{ schedule.period_type }} {{ $t('schedules.hours') }}
+                              </q-item-label>
+                              <q-item-label caption class="text-positive text-weight-bold">
+                                {{ formatCurrency(schedule.total_amount) }}
+                              </q-item-label>
+                            </q-item-section>
+
+                            <q-item-section side>
+                              <q-icon name="chevron_right" />
+                            </q-item-section>
+                          </q-item>
+                        </q-list>
+                      </q-card-section>
+                    </q-card>
+                  </q-expansion-item>
+
+                  <div v-if="filteredGroupedCustomSchedules.length === 0" class="text-center q-pa-xl">
+                    <q-icon name="event_busy" size="64px" color="grey-5" />
+                    <div class="text-h6 text-grey-7 q-mt-md">
+                      {{ $t('opportunities.empty_state') }}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </q-tab-panel>
+
+            <!-- Visão Prestador -->
+            <q-tab-panel name="provider">
+              <div class="row q-col-gutter-md">
+                <div class="col-12 row q-gutter-md">
+                  <q-select
+                    v-model="statusFilter"
+                    :options="statusFilterOptions"
+                    :label="$t('schedules.filter_by_status')"
+                    outlined
+                    dense
+                    emit-value
+                    map-options
+                    clearable
+                    style="max-width: 300px"
+                    class="col-3"
+                  />
+                  <div class="col-3">
+                    <ProviderSelect
+                      v-model="selectedProvider"
+                      :label="$t('common.terms.user')"
+                      dense
+                    />
+                  </div>
+                </div>
+
+                <div class="col-12">
+                  <q-expansion-item
+                    v-for="clientGroup in filteredGroupedCustomSchedules"
+                    :key="clientGroup.client_id"
+                    :label="clientGroup.client_name"
+                    icon="person"
+                    header-class="text-white gradient-diarista-bg"
+                    expand-icon-class="text-white"
+                    class="q-mb-md shadow-2 rounded-borders"
+                    default-opened
+                  >
+                    <q-card>
+                      <q-card-section>
+                        <q-list bordered separator>
+                          <q-item
+                            v-for="schedule in clientGroup.schedules"
+                            :key="schedule.id"
+                            clickable
+                            @click="openCustomScheduleDialog(schedule)"
+                          >
+                            <div class="q-my-auto q-pr-md" style="width: 30px">
+                              {{ schedule.id }}
+                            </div>
+                            <q-item-section avatar>
+                              <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
+                                {{ $t(`schedules.statuses.${schedule.status}`) }}
+                              </q-badge>
+                            </q-item-section>
+                            
+                            <q-item-section>
+                              <q-item-label>
+                                <q-icon name="event" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.date }} {{ schedule.start_time?.substring(0, 5) }}
+                                </span>
+                              </q-item-label>
+                              <q-item-label caption>
+                                <q-icon name="person" size="xs" class="q-mr-xs" color="info"/>
+                                <span class="gradient-diarista">
+                                  {{ schedule.provider_name || 'N/A' }}
+                                </span>
+                              </q-item-label>
+                            </q-item-section>
+                            
+                            <q-item-section side>
+                              <q-item-label>
+                                {{ schedule.period_type }} {{ $t('schedules.hours') }}
+                              </q-item-label>
+                              <q-item-label caption class="text-positive text-weight-bold">
+                                {{ formatCurrency(schedule.total_amount) }}
+                              </q-item-label>
+                            </q-item-section>
+
+                            <q-item-section side>
+                              <q-icon name="chevron_right" />
+                            </q-item-section>
+                          </q-item>
+                        </q-list>
+                      </q-card-section>
+                    </q-card>
+                  </q-expansion-item>
+                  <div v-if="selectedProvider && providerProposals.length > 0" class="q-mt-xl">
+                    <div class="text-h6 q-mb-md">{{ $t('opportunities.my_proposals') }}</div>
+                    <DefaultTable
+                      :rows="providerProposals"
+                      :columns="proposalColumns"
+                      row-key="id"
+                      flat
+                      bordered
+                      :rows-per-page-options="[10, 20, 50]"
+                      class="sticky-header-table"
+                      no-api-call
+                      :api-call="() => {}"
+                      :open-item="true"
+                      :show-columns-select="false"
+                      :show-search-field="false"
+                      :add-item="false"
+                      @on-row-click="onOpenProposal"
+                    >
+                      <template #body-cell-status="props">
+                        <q-td :props="props">
+                          <q-badge
+                            :color="props.row.deleted_at ? 'negative' : props.row.schedule?.provider_id ? 'positive' : 'warning'"
+                            :label="props.value"
+                          />
+                        </q-td>
+                      </template>
+                    </DefaultTable>
+                  </div>
+
+                  <div v-if="filteredGroupedCustomSchedules.length === 0" class="text-center q-pa-xl">
+                    <q-icon name="event_busy" size="64px" color="grey-5" />
+                    <div class="text-h6 text-grey-7 q-mt-md">
+                      {{ $t('opportunities.empty_state') }}
+                    </div>
+                  </div>
                 </div>
               </div>
-            </div>
-          </div>
+            </q-tab-panel>
+          </q-tab-panels>
         </q-tab-panel>
       </q-tab-panels>
     </div>
@@ -206,21 +461,29 @@
 </template>
 
 <script setup>
-import { onMounted, ref, computed } from 'vue'
+import { onMounted, ref, computed, watch } from 'vue'
 import { useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import DefaultHeaderPage from 'src/components/layout/DefaultHeaderPage.vue'
 import ViewScheduleDialog from 'src/pages/schedule/components/ViewScheduleDialog.vue'
-import { getSchedulesGroupedByClient, updateScheduleStatus } from 'src/api/schedule'
+import ViewCustomScheduleDialog from 'src/pages/opportunity/components/ViewCustomScheduleDialog.vue'
+import { getSchedulesGroupedByClient, getSchedulesGroupedByClientCustom, updateScheduleStatus } from 'src/api/schedule'
+import { getProvidersProposalsAndOpportunities } from 'src/api/customSchedule'
+import ProviderSelect from 'src/components/provider/ProviderSelect.vue'
+import DefaultTable from 'src/components/defaults/DefaultTable.vue'
 
 const $q = useQuasar()
 const { t } = useI18n()
 
 const isLoading = ref(true)
+const scheduleType = ref('default')
 const viewMode = ref('client')
 const statusFilter = ref(null)
-const groupedSchedules = ref([])
-
+const groupedDefaultSchedules = ref([])
+const groupedCustomSchedules = ref([])
+const selectedProvider = ref(null)
+const providerProposals = ref([])
+const availableOpportunities = ref([])
 const statusFilterOptions = computed(() => [
   { label: t('schedules.all_statuses'), value: null },
   { label: t('schedules.statuses.pending'), value: 'pending' },
@@ -232,21 +495,84 @@ const statusFilterOptions = computed(() => [
   { label: t('schedules.statuses.finished'), value: 'finished' }
 ])
 
-const filteredGroupedSchedules = computed(() => {
+const filteredGroupedDefaultSchedules = computed(() => {
   if (!statusFilter.value) {
-    return groupedSchedules.value
+    return groupedDefaultSchedules.value
   }
   
-  return groupedSchedules.value
+  return groupedDefaultSchedules.value
     .map(clientGroup => ({
       ...clientGroup,
-      schedules: clientGroup.schedules.filter(
+      schedules: clientGroup.schedules?.filter(
         schedule => schedule.status === statusFilter.value
       )
     }))
-    .filter(clientGroup => clientGroup.schedules.length > 0)
+    .filter(clientGroup => clientGroup.schedules?.length > 0)
 })
 
+const filteredGroupedCustomSchedules = computed(() => {
+  if (viewMode.value === 'provider' && selectedProvider.value) {
+    const opportunities = availableOpportunities.value
+    if (!statusFilter.value) {
+      return opportunities
+    }
+    return opportunities
+      .map(clientGroup => ({
+        ...clientGroup,
+        schedules: clientGroup.schedules?.filter(
+          schedule => schedule.status === statusFilter.value
+        )
+      }))
+      .filter(clientGroup => clientGroup.schedules?.length > 0)
+  }
+  
+  if (!statusFilter.value) {
+    return groupedCustomSchedules.value
+  }
+  
+  return groupedCustomSchedules.value
+    .map(clientGroup => ({
+      ...clientGroup,
+      schedules: clientGroup.schedules?.filter(
+        schedule => schedule.status === statusFilter.value
+      )
+    }))
+    .filter(clientGroup => clientGroup.schedules?.length > 0)
+})
+
+const proposalColumns = computed(() => [
+  {
+    name: 'id',
+    label: 'ID',
+    field: 'schedule_id',
+    align: 'left'
+  },
+  {
+    name: 'client',
+    label: t('opportunities.client'),
+    field: row => row.schedule?.client?.user?.name || '-',
+    align: 'left'
+  },
+  {
+    name: 'date',
+    label: t('opportunities.date'),
+    field: row => new Date(row.schedule?.date).toLocaleDateString('pt-BR'),
+    align: 'left'
+  },
+  {
+    name: 'period',
+    label: t('opportunities.period'),
+    field: row => row.schedule?.period === 'morning' ? t('provider_working_days.morning') : t('provider_working_days.afternoon'),
+    align: 'left'
+  },
+  {
+    name: 'status',
+    label: t('common.terms.status'),
+    field: row => row.schedule?.status,
+    align: 'left'
+  }
+])
+
 const formatCurrency = (value) => {
   if (!value) return 'R$ 0,00'
   return `R$ ${Number(value).toFixed(2).replace('.', ',')}`
@@ -268,8 +594,12 @@ const getStatusColor = (status) => {
 const loadSchedules = async () => {
   try {
     isLoading.value = true
-    const data = await getSchedulesGroupedByClient()
-    groupedSchedules.value = data
+    const [defaultData, customData] = await Promise.all([
+      getSchedulesGroupedByClient(),
+      getSchedulesGroupedByClientCustom()
+    ])
+    groupedDefaultSchedules.value = defaultData
+    groupedCustomSchedules.value = customData
   } catch (error) {
     $q.notify({
       type: 'negative',
@@ -281,6 +611,20 @@ const loadSchedules = async () => {
   }
 }
 
+const loadProviderProposalsAndOpportunities = async (providerId) => {
+  try {
+    const response = await getProvidersProposalsAndOpportunities(providerId)
+    providerProposals.value = response.proposals
+    availableOpportunities.value = response.opportunities
+  } catch (error) {
+    $q.notify({
+      type: 'negative',
+      message: error.message || t('common.ui.messages.error_loading_data'),
+      position: 'top'
+    })
+  }
+}
+
 const openScheduleDialog = (schedule) => {
   $q.dialog({
     component: ViewScheduleDialog,
@@ -293,6 +637,29 @@ const openScheduleDialog = (schedule) => {
       onCancel: handleCancel
     },
     persistent: true
+  }).onOk(async () => {
+    await loadSchedules();
+    if (selectedProvider.value?.value) {
+      await loadProviderProposalsAndOpportunities(selectedProvider.value.value);
+    }
+  })
+}
+
+const openCustomScheduleDialog = (schedule) => {
+  $q.dialog({
+    component: ViewCustomScheduleDialog,
+    componentProps: {
+      schedule,
+      viewMode: viewMode.value,
+      providerId: selectedProvider.value?.value || null,
+      onMarkAsPaid: handleMarkAsPaid,
+      onRefreshData: async () => {
+        await loadSchedules();
+        if (selectedProvider.value?.value) {
+          await loadProviderProposalsAndOpportunities(selectedProvider.value.value);
+        }
+      }
+    }
   })
 }
 
@@ -325,13 +692,23 @@ const handleCancel = async (scheduleId) => {
   await updateStatus(scheduleId, 'cancelled')
 }
 
+const onOpenProposal = ({row}) => {
+  openCustomScheduleDialog(row.schedule)
+}
+
+watch(selectedProvider, async (newProvider) => {
+  if (newProvider?.value && viewMode.value === 'provider') {
+    await loadProviderProposalsAndOpportunities(newProvider.value);
+  } else {
+    providerProposals.value = []
+    availableOpportunities.value = []
+  }
+})
+
 onMounted(async () => {
   await loadSchedules()
 })
 </script>
 
 <style scoped>
-.gap {
-  gap: 16px;
-}
 </style>

+ 15 - 4
src/pages/improvementType/components/AddEditImprovementTypeDialog.vue

@@ -35,10 +35,21 @@
             </div>
 
           </q-card-section>
-          <q-card-actions align="center">
-            <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-            <q-space />
-            <q-btn color="primary" label="OK" :type="'submit'" :loading="loading" :disable="!hasUpdatedFields" />
+          <q-card-actions align="right" class="q-px-md q-pb-md">
+            <q-btn
+              :label="$t('common.actions.close')"
+              flat
+              color="primary"
+              @click="onDialogCancel"
+            />
+            <q-btn 
+              color="secondary"
+              :label="$t('common.actions.save')"
+              unelevated
+              :type="'submit'"
+              :loading="loading"
+              :disable="!hasUpdatedFields"
+            />
           </q-card-actions>
         </q-form>
     </q-card>

+ 0 - 0
src/pages/opportunity/OpportunitiesPanel.vue → src/pages/opportunity/CustomSchedulesPage.vue


+ 76 - 104
src/pages/opportunity/components/AddEditCustomScheduleDialog.vue

@@ -17,7 +17,6 @@
           />
 
           <div class="col-12">
-            <div class="text-subtitle2 q-mb-sm">{{ $t('opportunities.address_type') }}</div>
             <q-option-group 
               v-model="formData.address_type"
               :options="addressTypeOptions"
@@ -53,15 +52,12 @@
           />
 
           <div class="col-12">
-            <div class="text-subtitle2 q-mb-sm col-12">
-              {{ $t('opportunities.specialities') }} 
-            </div>
-            <div class="row items-center">
+            <div class="row col-12">
               <SpecialitySelect
                 v-model="selectedSpeciality"
                 :label="$t('ui.navigation.speciality')"
                 :exclude-ids="excludedSpecialityIds"
-                class="col-md-8 col-12 q-mr-sm q-pb-none"
+                class="col-md-8 col-10 q-mr-sm q-pb-none"
               />
               <div class="col-auto">
                 <q-btn
@@ -103,17 +99,13 @@
             type="number"
             outlined
             min="1"
-            max="10"
-            :hint="$t('opportunities.quantity_hint')"
             :rules="[
               val => val >= 1 || $t('common.validation.min_value', { min: 1 }),
-              val => val <= 10 || $t('common.validation.max_value', { max: 10 })
             ]"
             class="col-12"
           />
 
           <div class="col-12">
-            <div class="text-subtitle2 q-mb-sm">{{ $t('schedules.period') }}</div>
             <q-option-group
               v-model="formData.period_type"
               :options="periodOptions"
@@ -133,10 +125,6 @@
             class="col-6"
           />
 
-          <div class="col-12 text-caption text-grey-7">
-            {{ $t('opportunities.price_note') }}
-          </div>
-
           <DefaultInputDatePicker
             v-model:untreated-date="formData.date"
             :label="$t('opportunities.date')"
@@ -148,7 +136,6 @@
           />
 
           <div v-show="formData.period_type && formData.date" class="col-12">
-            <div class="text-subtitle2 q-mb-sm">{{ $t('opportunities.select_time') }}</div>
             <q-option-group
               v-model="selectedTimeSlot"
               :options="availableTimeSlots"
@@ -169,18 +156,19 @@
           </div>
 
         </q-card-section>
-        <q-card-actions class="col-12 row q-gutter-sm justify-end q-mt-md">
+        <q-card-actions align="right" class="q-px-md q-pb-md">
           <q-btn 
-            :label="$t('common.actions.cancel')"
+            :label="$t('common.actions.close')"
             flat
-            color="grey-7"
+            color="primary"
             @click="onDialogCancel"
           />
           <q-btn
             type="submit"
             :label="$t('common.actions.save')"
-            :loading="isLoading"
-            color="primary"
+            :loading="loading"
+            :disable="props.opportunity && !hasUpdatedFields"
+            color="secondary"
             unelevated
           />
         </q-card-actions>
@@ -193,6 +181,8 @@
 import { ref, computed, onMounted, watch } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
+import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker'
+import { useSubmitHandler } from 'src/composables/useSubmitHandler'
 import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
 import ClientSelect from 'src/components/client/ClientSelect.vue'
 import AddressSelect from 'src/components/address/AddressSelect.vue'
@@ -200,7 +190,7 @@ import ServiceTypeSelect from 'src/components/serviceType/ServiceTypeSelect.vue'
 import SpecialitySelect from 'src/components/speciality/SpecialitySelect.vue'
 import DefaultInputDatePicker from 'src/components/defaults/DefaultInputDatePicker.vue'
 import DefaultCurrencyInput from 'src/components/defaults/DefaultCurrencyInput.vue'
-import { useInputRules } from 'src/composables/useInputRules';
+import { useInputRules } from 'src/composables/useInputRules'
 import { createCustomSchedule, updateCustomSchedule } from 'src/api/customSchedule'
 import { getSpecialities } from 'src/api/speciality'
 import { nextTick } from 'vue'
@@ -223,31 +213,41 @@ defineEmits([
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 const { t } = useI18n()
-const { inputRules } = useInputRules();
-const isLoading = ref(false)
-const serverErrors = ref({})
+const { inputRules } = useInputRules()
+const formRef = ref(null)
+
 const selectedClient = ref(null)
 const selectedAddress = ref(null)
 const selectedServiceType = ref(null)
 const selectedSpeciality = ref(null)
 const selectedTimeSlot = ref(null)
 const allSpecialities = ref([])
-const isMounted = ref(false);
-const formData = ref({
-  client_id: null,
-  address_id: null,
-  address_type: null,
-  service_type_id: null,
-  speciality_ids: [],
-  description: null,
+const isMounted = ref(false)
+
+const { form: formData, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+  client_id: props.opportunity?.schedule?.client_id || null,
+  address_id: props.opportunity?.schedule?.address_id || null,
+  address_type: props.opportunity?.address_type || null,
+  service_type_id: props.opportunity?.service_type_id || null,
+  speciality_ids: props.opportunity?.specialities?.map(s => s.id) || [],
+  description: props.opportunity?.description || null,
   quantity: 1,
-  period_type: null,
-  min_price: 0,
-  max_price: 0,
-  offers_meal: false,
-  date: null,
-  start_time: null,
-  end_time: null
+  period_type: props.opportunity?.schedule?.period_type ? String(props.opportunity.schedule.period_type) : null,
+  min_price: props.opportunity?.min_price ? parseFloat(props.opportunity.min_price) : 0,
+  max_price: props.opportunity?.max_price ? parseFloat(props.opportunity.max_price) : 0,
+  offers_meal: props.opportunity?.offers_meal || false,
+  date: props.opportunity?.schedule?.date || null,
+  start_time: props.opportunity?.schedule?.start_time || null,
+  end_time: props.opportunity?.schedule?.end_time || null
+})
+
+const {
+  loading,
+  serverErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
 })
 
 const addressTypeOptions = computed(() => [
@@ -268,13 +268,13 @@ const mealOptions = computed(() => [
 ])
 
 const excludedSpecialityIds = computed(() => {
-  return formData.value.speciality_ids
+  return formData.speciality_ids
 })
 
 const availableTimeSlots = computed(() => {
-  if (!formData.value.period_type) return []
+  if (!formData.period_type) return []
 
-  const period = parseInt(formData.value.period_type)
+  const period = parseInt(formData.period_type)
   const slots = []
 
   for (let hour = 7; hour <= 20 - period; hour++) {
@@ -289,7 +289,7 @@ const availableTimeSlots = computed(() => {
     })
   }
 
-  return slots;
+  return slots
 })
 
 const getSpecialityName = (specialityId) => {
@@ -298,35 +298,35 @@ const getSpecialityName = (specialityId) => {
 }
 
 const handleAddSpeciality = () => {
-  if (selectedSpeciality.value && !formData.value.speciality_ids.includes(selectedSpeciality.value.id)) {
-    formData.value.speciality_ids.push(selectedSpeciality.value.value)
+  if (selectedSpeciality.value && !formData.speciality_ids.includes(selectedSpeciality.value.id)) {
+    formData.speciality_ids.push(selectedSpeciality.value.value)
     selectedSpeciality.value = null
   }
 }
 
 const handleRemoveSpeciality = (index) => {
-  formData.value.speciality_ids.splice(index, 1)
+  formData.speciality_ids.splice(index, 1)
 }
 
 const onClientChange = (client) => {
-  formData.value.client_id = client?.value || null
-  formData.value.address_id = null
+  formData.client_id = client?.value || null
+  formData.address_id = null
   selectedAddress.value = null
-  serverErrors.value.client_id = null
+  serverErrors.client_id = null
 }
 
 const onAddressChange = (address) => {
-  formData.value.address_id = address?.id || null
-  serverErrors.value.address_id = null
+  formData.address_id = address?.value || null
+  serverErrors.address_id = null
 }
 
 const onServiceTypeChange = (serviceType) => {
-  formData.value.service_type_id = serviceType?.value || null
-  serverErrors.value.service_type_id = null
+  formData.service_type_id = serviceType?.value || null
+  serverErrors.service_type_id = null
 }
 
 const onAddressTypeChange = () => {
-  formData.value.address_id = null
+  formData.address_id = null
   selectedAddress.value = null
 }
 
@@ -335,15 +335,15 @@ const onTimeSlotChange = (slotValue) => {
 
   const [start, end] = slotValue.split('|')
 
-  formData.value.start_time = start
-  formData.value.end_time = end
+  formData.start_time = start
+  formData.end_time = end
 }
 
 const updateAvailableTimeSlots = () => {
-  if(!isMounted.value) return;
-  selectedTimeSlot.value = null;
-  formData.value.start_time = null;
-  formData.value.end_time = null;
+  if(!isMounted.value) return
+  selectedTimeSlot.value = null
+  formData.start_time = null
+  formData.end_time = null
 }
 
 const loadSpecialities = async () => {
@@ -358,35 +358,25 @@ const loadSpecialities = async () => {
 }
 
 const onSubmit = async () => {
-  isLoading.value = true
-  serverErrors.value = {}
-  try {
-    const payload = {
-      ...formData.value
-    }
-
-    if (props.opportunity) {
-      await updateCustomSchedule(props.opportunity.id, payload)
-    } else {
-      await createCustomSchedule(payload)
-    }
+  if (selectedClient.value?.value) {
+    formData.client_id = selectedClient.value.value
+  }
+  if (selectedAddress.value?.value) {
+    formData.address_id = selectedAddress.value.value
+  }
+  if (selectedServiceType.value?.value) {
+    formData.service_type_id = selectedServiceType.value.value
+  }
 
-    onDialogOK(true)
-  } catch (error) {
-    if (error.response?.data?.errors) {
-      serverErrors.value = error.response.data.errors
-    }
-    $q.notify({
-      type: 'negative',
-      message: error.message || t('common.ui.messages.error')
-    })
-  } finally {
-    isLoading.value = false
+  if (props.opportunity) {
+    await submitForm(() => updateCustomSchedule(props.opportunity.id, getUpdatedFields.value))
+  } else {
+    await submitForm(() => createCustomSchedule(formData))
   }
 }
 
 watch(
-  () => [availableTimeSlots.value, formData.value.start_time, formData.value.end_time],
+  () => [availableTimeSlots.value, formData.start_time, formData.end_time],
   ([slots, startTime, endTime]) => {
     if (!slots || !slots.length) return
     if (!startTime || !endTime) return
@@ -401,27 +391,9 @@ watch(
   },
   { immediate: true, deep: true }
 )
+
 onMounted(async () => {
   await loadSpecialities()
-
-  if (props.opportunity?.id ) {
-    formData.value = {
-      client_id: props.opportunity.schedule?.client_id,
-      address_id: props.opportunity.schedule?.address_id,
-      address_type: props.opportunity.address_type,
-      service_type_id: props.opportunity.service_type_id,
-      speciality_ids: props.opportunity.specialities?.map(s => s.id) || [],
-      description: props.opportunity.description,
-      quantity: 1,
-      period_type: String(props.opportunity.schedule?.period_type),
-      min_price: parseFloat(props.opportunity.min_price),
-      max_price: parseFloat(props.opportunity.max_price),
-      offers_meal: props.opportunity.offers_meal,
-      date: props.opportunity.schedule?.date,
-      start_time: props.opportunity.schedule?.start_time,
-      end_time: props.opportunity.schedule?.end_time
-    }
-  };
-  isMounted.value = true;
+  isMounted.value = true
 })
 </script>

+ 463 - 0
src/pages/opportunity/components/ViewCustomScheduleDialog.vue

@@ -0,0 +1,463 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin" style="min-width: 600px; max-width: 80vw">
+      <DefaultDialogHeader :title="() => $t('opportunities.view_details')" @close="onDialogCancel" />
+
+      <q-card-section class="scroll" style="max-height: 70vh">
+        <div class="row q-col-gutter-md">
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.client') }}</div>
+            <div class="text-body1">{{ schedule.client_name || schedule.client?.user?.name }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.provider') }}</div>
+            <div class="text-body1">{{ schedule.provider_name || schedule.provider?.user?.name || 'N/A' }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.date') }}</div>
+            <div class="text-body1">{{ schedule.date }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.period') }}</div>
+            <div class="text-body1">{{ schedule.period_type }} {{ $t('schedules.hours') }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.time') }}</div>
+            <div class="text-body1">
+              {{ schedule.start_time?.substring(0, 5) }} {{ $t('schedules.to') }} {{ schedule.end_time?.substring(0, 5) }}
+            </div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('opportunities.address_type') }}</div>
+            <div class="text-body1">
+              {{ $t(`address.types.${schedule.custom_schedule?.address_type}`) }}
+            </div>
+          </div>
+
+          <div class="col-12">
+            <div class="text-caption text-grey-7">{{ $t('schedules.address') }}</div>
+            <div class="text-body1">
+              {{ formatAddress(schedule.address) }}
+            </div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('opportunities.service_type') }}</div>
+            <div class="text-body1">
+              {{ schedule.custom_schedule?.service_type_name || schedule.custom_schedule?.service_type?.description || 'N/A' }}
+            </div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('opportunities.offers_meal') }}</div>
+            <div class="text-body1">
+              {{ schedule.custom_schedule?.offers_meal ? $t('opportunities.offers_meal_yes') : $t('opportunities.offers_meal_no') }}
+            </div>
+          </div>
+
+          <div class="col-12">
+            <div class="text-caption text-grey-7">{{ $t('opportunities.price_range') }}</div>
+            <div class="text-body1">
+              {{ formatCurrency(schedule.custom_schedule?.min_price) }} {{ $t('schedules.to') }} {{ formatCurrency(schedule.custom_schedule?.max_price) }}
+            </div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.total_amount') }}</div>
+            <div class="text-h6 text-positive">
+              {{ formatCurrency(schedule.total_amount) }}
+            </div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.status') }}</div>
+            <q-badge :color="getStatusColor(schedule.status)" class="q-pa-sm">
+              {{ $t(`schedules.statuses.${schedule.status}`) }}
+            </q-badge>
+          </div>
+
+          <div v-if="schedule.custom_schedule?.specialities?.length > 0" class="col-12">
+            <div class="text-caption text-grey-7 q-mb-sm">{{ $t('opportunities.specialities') }}</div>
+            <div class="q-gutter-sm">
+              <q-chip
+                v-for="speciality in schedule.custom_schedule.specialities"
+                :key="speciality.id"
+                color="primary"
+                text-color="white"
+              >
+                {{ speciality.description }}
+              </q-chip>
+            </div>
+          </div>
+
+          <div v-if="schedule.custom_schedule?.description" class="col-12">
+            <div class="text-caption text-grey-7">{{ $t('opportunities.description_label') }}</div>
+            <div class="text-body1">{{ schedule.custom_schedule.description }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.code') }}</div>
+            <div class="text-body1">{{ schedule.code }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.code_verified') }}</div>
+            <q-icon
+              :name="schedule.code_verified ? 'check_circle' : 'cancel'"
+              :color="schedule.code_verified ? 'positive' : 'negative'"
+              size="sm"
+            />
+          </div>
+        </div>
+
+        <div v-if="isClientView && isPending && !hasProvider && activeProposals.length > 0" class="q-mt-lg">
+          <q-separator class="q-mb-md" />
+          <div class="text-h6 q-mb-md">{{ $t('opportunities.proposals_received') }}</div>
+          
+          <q-card v-for="proposal in activeProposals" :key="proposal.id" class="row col-12 q-pa-md" bordered>
+            <div class="col-12">
+              <div class="text-weight-bold">{{ proposal.provider?.user?.name }}</div>
+              <div caption class="text-grey-7">
+                {{ 'Proposta enviada em: ' + new Date(proposal.created_at).toLocaleString('pt-BR') }}
+              </div>
+            </div>
+            
+            <div class="row col-12 q-mt-md">
+              <q-btn
+                :loading="isLoading"
+                :label="$t('opportunities.refuse_provider')"
+                color="negative"
+                size="sm"
+                outline
+                @click="handleRefuseProposal(proposal.id)"
+              />
+              <q-space/>
+              <q-btn
+                :loading="isLoading"
+                :label="$t('opportunities.accept_provider')"
+                color="positive"
+                size="sm"
+                @click="handleAcceptProposal(proposal.id)"
+              />
+            </div>
+          </q-card>
+        </div>
+
+        <div v-if="isClientView && isPending && !hasProvider && activeProposals.length === 0" class="q-mt-lg text-center q-pa-md bg-grey-2 rounded-borders">
+          <q-icon name="inbox" size="48px" color="grey-5" />
+          <div class="text-body2 text-grey-7 q-mt-sm">
+            {{ $t('opportunities.no_proposals_received') }}
+          </div>
+        </div>
+
+        <div v-if="isProviderView" class="q-mt-lg">
+          <q-separator class="q-mb-md" />
+          
+          <q-banner v-if="showProposalAccepted" class="bg-positive text-white">
+            <template #avatar>
+              <q-icon name="check_circle" />
+            </template>
+            {{ $t('opportunities.proposal_accepted') }}
+          </q-banner>
+          
+          <q-banner v-else-if="showProposalSent" class="bg-warning text-white">
+            <template #avatar>
+              <q-icon name="schedule" />
+            </template>
+            {{ $t('opportunities.waiting_client') }}
+          </q-banner>
+          
+          <q-banner v-else-if="showProposalRefused" class="bg-negative text-white">
+            <template #avatar>
+              <q-icon name="cancel" />
+            </template>
+            {{ $t('opportunities.proposal_refused') }}
+          </q-banner>
+        </div>
+      </q-card-section>
+
+      <q-card-actions align="right" class="q-px-md q-pb-md">
+        <q-btn
+          :label="$t('common.actions.close')"
+          flat
+          color="primary"
+          @click="onDialogCancel"
+        />
+        <q-btn
+          v-if="schedule?.status === 'paid'"
+          unelevated
+          :label="$t('schedules.cancel_schedule')"
+          color="negative"
+          @click="handleCancel"
+        />
+        <q-btn
+          v-if="canPropose"
+          :loading="isLoading"
+          :label="$t('schedules.reject')"
+          color="negative"
+          @click="handleRefuseOpportunity"
+        />
+        <q-btn
+          v-if="canPropose"
+          :loading="isLoading"
+          :label="$t('schedules.accept')"
+          color="positive"
+          @click="handleProposeOpportunity"
+        />
+        <q-btn
+          v-if="schedule?.status === 'accepted' && viewMode == 'client'"
+          unelevated
+          :label="$t('schedules.mark_as_paid')"
+          color="positive"
+          @click="handleMarkAsPaid"
+        />
+        <q-btn
+          v-if="schedule?.status === 'paid' && viewMode == 'provider'"
+          unelevated
+          :label="$t('schedules.fill_code')"
+          color="secondary"
+          @click="fillCode"
+        />
+      </q-card-actions>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
+import { getOpportunityProposals, proposeOpportunity, acceptProposal, refuseProposal, verifyScheduleCode, refuseOpportunity } from 'src/api/customSchedule'
+import { updateScheduleStatus } from 'src/api/schedule'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  },
+  viewMode: {
+    type: String,
+    default: 'client'
+  },
+  providerId: {
+    type: Number,
+    default: null
+  }
+})
+
+const emit = defineEmits([
+  ...useDialogPluginComponent.emits,
+  'refreshData',
+  'mark-as-paid'
+])
+
+const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } = useDialogPluginComponent()
+const $q = useQuasar()
+const { t } = useI18n()
+
+const proposals = ref([])
+const isLoading = ref(false)
+
+const hasProvider = computed(() => !!props.schedule.provider_id)
+const isPending = computed(() => props.schedule.status === 'pending')
+const isProviderView = computed(() => props.viewMode === 'provider')
+const isClientView = computed(() => props.viewMode === 'client')
+
+const providerProposal = computed(() => {
+  if (!isProviderView.value || !props.providerId) return null
+  return proposals.value.find(p => p.provider_id === props.providerId && !p.deleted_at)
+})
+
+const providerWasRefused = computed(() => {
+  if (!isProviderView.value || !props.providerId) return false
+  return proposals.value.some(p => p.provider_id === props.providerId && p.deleted_at)
+})
+
+const canPropose = computed(() => {
+  return isProviderView.value && 
+         isPending.value && 
+         !hasProvider.value && 
+         !providerProposal.value && 
+         !providerWasRefused.value &&
+         props.providerId
+})
+
+const showProposalSent = computed(() => {
+  return isProviderView.value && providerProposal.value
+})
+
+const showProposalRefused = computed(() => {
+  return isProviderView.value && providerWasRefused.value
+})
+
+const showProposalAccepted = computed(() => {
+  return isProviderView.value && hasProvider.value && props.schedule.provider_id === props.providerId
+})
+
+const activeProposals = computed(() => {
+  return proposals.value.filter(p => !p.deleted_at)
+})
+
+const loadProposals = async () => {
+  if (!props.schedule.id) return
+  
+  try {
+    proposals.value = await getOpportunityProposals(props.schedule.id)
+  } catch (error) {
+    console.error('Error loading proposals:', error)
+  }
+}
+
+const handleProposeOpportunity = async () => {
+  if (!props.providerId) return
+  
+  isLoading.value = true
+  try {
+    await proposeOpportunity(props.schedule.id, props.providerId)
+    
+    emit('refreshData')
+    onDialogOK()
+  } catch (error) {
+    console.error('Error proposing opportunity:', error)
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const handleRefuseOpportunity = async () => {
+  if (!props.providerId) return
+  
+  isLoading.value = true
+  try {
+    await refuseOpportunity(props.schedule.id, props.providerId)
+    
+    emit('refreshData')
+    onDialogOK()
+  } catch (error) {
+    console.error('Error proposing opportunity:', error)
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const handleAcceptProposal = async (proposalId) => {
+  isLoading.value = true
+  try {
+    await acceptProposal(proposalId)
+    
+    emit('refreshData')
+    onDialogOK()
+  } catch (error) {
+    console.log(error);
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const handleRefuseProposal = async (proposalId) => {
+  isLoading.value = true
+  try {
+    await refuseProposal(proposalId)
+    
+    await loadProposals()
+    emit('refreshData')
+  } catch (error) {
+    console.log(error);
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const formatAddress = (address) => {
+  if (!address) return 'N/A'
+  return `${address.address}${address.complement ? ', ' + address.complement : ''}, ${address.city?.name ?? address.city} - ${address.state?.name ?? address.state}, CEP: ${address.zip_code}`
+}
+
+const formatCurrency = (value) => {
+  if (!value) return 'R$ 0,00'
+  return `R$ ${Number(value).toFixed(2).replace('.', ',')}`
+}
+
+const getStatusColor = (status) => {
+  const colors = {
+    pending: 'warning',
+    accepted: 'positive',
+    rejected: 'negative',
+    paid: 'info',
+    cancelled: 'dark',
+    started: 'primary',
+    finished: 'positive'
+  }
+  return colors[status] || 'grey'
+}
+
+const handleMarkAsPaid = () => {
+  $q.dialog({
+    title: t('schedules.mark_as_paid'),
+    message: t('common.ui.messages.confirm_action'),
+    cancel: true,
+    persistent: true
+  }).onOk(async () => {
+    onDialogOK();
+    emit('mark-as-paid', props.schedule.id);
+  })
+}
+
+const fillCode = () => {
+  $q.dialog({
+    title: t('schedules.fill_code'),
+    message: t('schedules.enter_code'),
+    prompt: {
+      model: '',
+      type: 'text'
+    },
+    cancel: true,
+    persistent: true
+  }).onOk(async (code) => {
+    const response = await verifyScheduleCode(props.schedule.id, code)
+    if(response.data.success) {
+      $q.notify({
+        type: 'positive',
+        message: t('schedules.code_verified_success'),
+        position: 'top'
+      })
+    } else {
+      $q.notify({
+        type: 'negative',
+        message: t('schedules.code_verified_failed'),
+        position: 'top'
+      })
+    }
+    emit('refreshData');
+    onDialogOK();
+  })
+}
+
+const updateStatus = async (scheduleId, newStatus) => {
+  try {
+    await updateScheduleStatus(scheduleId, newStatus);
+    emit('refreshData');
+    onDialogOK();
+  } catch (error) {
+    $q.notify({
+      type: 'negative',
+      message: error.message || t('common.ui.messages.error'),
+      position: 'top'
+    })
+  }
+}
+
+const handleCancel = async () => {
+  await updateStatus(props.schedule.id, 'cancelled')
+}
+
+onMounted(async () => {
+  await loadProposals()
+})
+</script>

+ 16 - 7
src/pages/provider/components/AddEditProviderDialog.vue

@@ -152,6 +152,22 @@
                   </div>
                 </div>
               </q-card-section>
+              <q-card-actions align="right">
+                <q-btn
+                  :label="$t('common.actions.close')"
+                  flat
+                  color="primary"
+                  @click="onDialogCancel"
+                />
+                <q-btn 
+                  color="secondary"
+                  :label="$t('common.actions.save')"
+                  type="submit"
+                  unelevated
+                  :loading="loading"
+                  :disable="!hasUpdatedFields"
+                />
+              </q-card-actions>
             </q-form>
           </q-tab-panel>
 
@@ -172,11 +188,6 @@
           </q-tab-panel>
         </q-tab-panels>
       </div>
-      <q-card-actions align="center" class="">
-        <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-        <q-space />
-        <q-btn color="primary" label="OK" :type="'submit'" :loading="loading" :disable="!hasUpdatedFields" />
-      </q-card-actions>
     </q-card>
   </q-dialog>
 </template>
@@ -236,8 +247,6 @@ const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
   daily_price_2h: provider ? Number(provider?.daily_price_2h) : null,
 });
 
-// const birthDate = ref(null);
-
 const {
   loading,
   serverErrors,

+ 7 - 14
src/pages/schedule/components/AddEditScheduleDialog.vue

@@ -1,13 +1,10 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="q-dialog-plugin" style="width: 900px; max-width: 90vw">
+    <q-card class="q-dialog-plugin column" style="width: 900px; max-width: 80vw; height: 90vh">
       <DefaultDialogHeader :title="title" @close="onDialogCancel" />
-      <q-form ref="formRef" @submit="onOKClick">
+      <q-form ref="formRef" class="col scroll" @submit="onOKClick">
         <q-card-section class="row q-col-gutter-md">
           <div class="col-12">
-            <div class="text-subtitle1 text-weight-bold q-mb-md">
-              {{ $t('schedules.schedule_info') }}
-            </div>
             <div class="row q-col-gutter-sm">
               <ClientSelect
                 v-model="selectedClient"
@@ -50,9 +47,6 @@
             <q-separator class="col-12" />
             
             <div class="col-12">
-              <div class="text-subtitle1 text-weight-bold q-mb-md">
-                {{ $t('schedules.schedule_details') }}
-              </div>
               <div class="row q-col-gutter-sm">
                 <DefaultInputDatePicker
                   v-model:untreated-date="form.date"
@@ -266,18 +260,18 @@
 
         <q-card-actions align="right" class="q-px-md q-pb-md">
           <q-btn
+            :label="$t('common.actions.close')"
             flat
-            :label="$t('common.actions.cancel')"
-            color="negative"
+            color="primary"
             @click="onDialogCancel"
           />
           <q-btn
-            type="submit"
+            color="secondary"
             :label="$t('common.actions.save')"
+            type="submit"
+            unelevated
             :loading="loading"
             :disable="isSaveDisabled"
-            color="primary"
-            unelevated
           />
         </q-card-actions>
       </q-form>
@@ -318,7 +312,6 @@ const { inputRules } = useInputRules();
 const { t } = useI18n();
 const formRef = ref(null);
 
-// Estados
 const selectedClient = ref(null);
 const selectedProvider = ref(null);
 const selectedAddress = ref(null);

+ 61 - 31
src/pages/schedule/components/ViewScheduleDialog.vue

@@ -1,13 +1,9 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card class="q-dialog-plugin" style="width: 700px; max-width: 90vw">
-      <q-card-section class="row items-center bg-primary text-white">
-        <div class="text-h6">{{ $t('schedules.schedule_details') }}</div>
-        <q-space />
-        <q-btn icon="close" flat round dense @click="onDialogCancel" />
-      </q-card-section>
+      <DefaultDialogHeader :title="() => $t('schedules.schedule_details')" @close="onDialogCancel" />
 
-      <q-card-section class="q-mt-md">
+      <q-card-section>
         <div class="row q-col-gutter-md">
           <div class="col-12 col-md-6">
             <div class="text-caption text-grey-7">{{ $t('schedules.client') }}</div>
@@ -31,7 +27,7 @@
 
           <div class="col-12 col-md-6">
             <div class="text-caption text-grey-7">{{ $t('schedules.period') }}</div>
-            <div class="text-body1">{{ schedule?.period_type }}{{ $t('schedules.hours') }}</div>
+            <div class="text-body1">{{ schedule?.period_type }} {{ $t('schedules.hours') }}</div>
           </div>
 
           <div class="col-12 col-md-6">
@@ -58,48 +54,59 @@
             </q-badge>
           </div>
 
-          <div class="col-12">
+          <div class="col-6">
             <div class="text-caption text-grey-7">{{ $t('schedules.code') }}</div>
             <div class="text-body1">
               {{ schedule?.code }}
-              <q-icon
-                v-if="schedule?.code_verified"
-                name="check_circle"
-                color="positive"
-                size="sm"
-              >
-                <q-tooltip>{{ $t('schedules.code_verified') }}</q-tooltip>
-              </q-icon>
             </div>
           </div>
+          <div class="col-6 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.code_verified') }}</div>
+            <q-icon
+              :name="schedule.code_verified ? 'check_circle' : 'cancel'"
+              :color="schedule.code_verified ? 'positive' : 'negative'"
+              size="sm"
+            />
+          </div>
         </div>
       </q-card-section>
 
       <q-card-actions align="right" class="q-px-md q-pb-md">
         <q-btn
+          :label="$t('common.actions.close')"
           flat
-          :label="$t('common.actions.cancel')"
-          color="grey-7"
+          color="primary"
           @click="onDialogCancel"
         />
-        
+        <q-btn
+          v-if="schedule?.status === 'paid'"
+          unelevated
+          :label="$t('schedules.cancel_schedule')"
+          color="negative"
+          @click="handleCancel"
+        />
         <template v-if="viewMode === 'provider'">
           <template v-if="schedule?.status === 'pending'">
             <q-btn
               unelevated
               :label="$t('schedules.reject')"
               color="negative"
-              icon="close"
               @click="handleReject"
             />
             <q-btn
               unelevated
               :label="$t('schedules.accept')"
               color="positive"
-              icon="check"
               @click="handleAccept"
             />
           </template>
+          <q-btn
+            v-if="schedule?.status === 'paid'"
+            unelevated
+            :label="$t('schedules.fill_code')"
+            color="secondary"
+            @click="fillCode"
+          />
         </template>
 
         <template v-if="viewMode === 'client'">
@@ -108,18 +115,9 @@
             unelevated
             :label="$t('schedules.mark_as_paid')"
             color="positive"
-            icon="attach_money"
             @click="handleMarkAsPaid"
           />
-          
-          <q-btn
-            v-if="schedule?.status === 'paid'"
-            unelevated
-            :label="$t('schedules.cancel_schedule')"
-            color="negative"
-            icon="cancel"
-            @click="handleCancel"
-          />
+        
         </template>
       </q-card-actions>
     </q-card>
@@ -128,7 +126,9 @@
 
 <script setup>
 import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { verifyScheduleCode } from 'src/api/customSchedule'
 import { useI18n } from 'vue-i18n'
+import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
 
 const props = defineProps({
   schedule: {
@@ -231,4 +231,34 @@ const handleCancel = () => {
     emit('cancel', props.schedule.id);
   })
 }
+
+const fillCode = () => {
+  $q.dialog({
+    title: t('schedules.fill_code'),
+    message: t('schedules.enter_code'),
+    prompt: {
+      model: '',
+      type: 'text'
+    },
+    cancel: true,
+    persistent: true
+  }).onOk(async (code) => {
+    const response = await verifyScheduleCode(props.schedule.id, code)
+    if(response.data.success) {
+      $q.notify({
+        type: 'positive',
+        message: t('schedules.code_verified_success'),
+        position: 'top'
+      })
+    } else {
+      $q.notify({
+        type: 'negative',
+        message: t('schedules.code_verified_failed'),
+        position: 'top'
+      })
+    }
+    emit('refreshData');
+    onDialogOK();
+  })
+}
 </script>

+ 15 - 4
src/pages/serviceType/components/AddEditServiceTypeDialog.vue

@@ -23,10 +23,21 @@
             </div>
 
           </q-card-section>
-          <q-card-actions align="center">
-            <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-            <q-space />
-            <q-btn color="primary" label="OK" :type="'submit'" :loading="loading" :disable="!hasUpdatedFields" />
+          <q-card-actions align="right" class="q-px-md q-pb-md">
+            <q-btn
+              :label="$t('common.actions.close')"
+              flat
+              color="primary"
+              @click="onDialogCancel"
+            />
+            <q-btn
+              color="secondary"
+              :label="$t('common.actions.save')"
+              type="submit"
+              unelevated
+              :loading="loading"
+              :disable="!hasUpdatedFields"
+            />
           </q-card-actions>
         </q-form>
     </q-card>

+ 15 - 4
src/pages/speciality/components/AddEditSpecialityDialog.vue

@@ -23,10 +23,21 @@
             </div>
 
           </q-card-section>
-          <q-card-actions align="center">
-            <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-            <q-space />
-            <q-btn color="primary" label="OK" :type="'submit'" :loading="loading" :disable="!hasUpdatedFields" />
+          <q-card-actions align="right" class="q-px-md q-pb-md">
+            <q-btn
+              :label="$t('common.actions.close')"
+              flat
+              color="primary"
+              @click="onDialogCancel"
+            />
+            <q-btn 
+              color="secondary"
+              :label="$t('common.actions.save')"
+              type="submit"
+              unelevated
+              :loading="loading"
+              :disable="!hasUpdatedFields"
+            />
           </q-card-actions>
         </q-form>
     </q-card>

+ 10 - 5
src/pages/state/components/AddEditStateDialog.vue

@@ -43,13 +43,18 @@
             @update:model-value="serverErrors.status = null"
           />
         </q-card-section>
-        <q-card-actions align="center">
-          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-          <q-space />
+        <q-card-actions align="right" class="q-px-md q-pb-md">
           <q-btn
+            :label="$t('common.actions.close')"
+            flat
             color="primary"
-            label="OK"
-            :type="'submit'"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="secondary"
+            :label="$t('common.actions.save')"
+            type="submit"
+            unelevated
             :loading="loading"
             :disable="!hasUpdatedFields"
           />

+ 9 - 4
src/pages/users/components/AddEditUserDialog.vue

@@ -55,12 +55,17 @@
             class="col-md-6 col-12"
           />
         </q-card-section>
-        <q-card-actions align="center">
-          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-          <q-space />
+        <q-card-actions align="right" class="q-px-md q-pb-md">
           <q-btn
+            :label="$t('common.actions.close')"
+            flat
             color="primary"
-            label="OK"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            color="secondary"
+            :label="$t('common.actions.save')"
+            unelevated
             :type="'submit'"
             :loading="loading"
             :disable="!hasUpdatedFields"

+ 4 - 4
src/router/routes/opportunities.route.js → src/router/routes/customSchedules.route.js

@@ -1,8 +1,8 @@
 export default [
   {
-    path: 'opportunities',
-    name: 'OpportunitiesPage',
-    component: () => import('src/pages/opportunity/OpportunitiesPanel.vue'),
+    path: 'custom-schedules',
+    name: 'CustomSchedulesPage',
+    component: () => import('src/pages/opportunity/CustomSchedulesPage.vue'),
     meta: {
       title: 'opportunities.title',
       requireAuth: true,
@@ -13,7 +13,7 @@ export default [
           title: 'ui.navigation.dashboard'
         },
         {
-          name: 'OpportunitiesPage',
+          name: 'CustomSchedulesPage',
           title: 'opportunities.title'
         }
       ]

+ 1 - 1
src/stores/navigation.js

@@ -25,7 +25,7 @@ export const navigationStore = defineStore("navigation", () => {
     {
       type: "single",
       title: "ui.navigation.opportunities",
-      name: "OpportunitiesPage",
+      name: "CustomSchedulesPage",
       icon: "mdi-bullseye-arrow",
       disable: false,
       permission: false,