3 Commits 830acfb269 ... e51d890cf5

Autore SHA1 Messaggio Data
  Gustavo Zanatta e51d890cf5 modal de alteracao de status + dashboard com agendamentos e acoes (fluxo de testes) 2 settimane fa
  Gustavo Zanatta ceea8d8917 aplicando cores do sistema PT1 2 settimane fa
  Gustavo Zanatta d77de59298 switch to light theme 2 settimane fa

+ 1 - 1
src/App.vue

@@ -21,7 +21,7 @@ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
 const theme = $q.cookies.get("theme") || systemTheme;
 const localeCookie = Cookies.get("locale") || window.navigator.language;
 console.log(theme, localeCookie);
-$q.dark.set(theme == "dark");
+$q.dark.set(theme == "light");
 
 watch(
   () => $q.dark.isActive,

+ 10 - 0
src/api/schedule.js

@@ -5,6 +5,11 @@ export const getSchedules = async () => {
   return data.payload
 }
 
+export const getSchedulesGroupedByClient = async () => {
+  const { data } = await api.get('/schedules/grouped-by-client')
+  return data.payload
+}
+
 export const getScheduleById = async (id) => {
   const { data } = await api.get(`/schedule/${id}`)
   return data.payload
@@ -20,6 +25,11 @@ export const updateSchedule = async (id, scheduleData) => {
   return data.payload
 }
 
+export const updateScheduleStatus = async (id, status) => {
+  const { data } = await api.patch(`/schedule/${id}/status`, { status })
+  return data.payload
+}
+
 export const deleteSchedule = async (id) => {
   const { data } = await api.delete(`/schedule/${id}`)
   return data.payload

+ 2 - 1
src/components/layout/DefaultHeaderPage.vue

@@ -6,10 +6,11 @@
         :key="breadcrumb?.name"
         :label="$t(breadcrumb?.title)"
         :to="{ name: breadcrumb?.name }"
+        class="gradient-diarista"
       />
     </q-breadcrumbs>
     <div class="flex items-center justify-between">
-      <span class="text-h5">{{ $t($route.meta?.title) }}</span>
+      <span class="text-h5 gradient-diarista">{{ $t($route.meta?.title) }}</span>
       <slot name="after" />
     </div>
     <q-separator class="q-my-sm" />

+ 14 - 0
src/css/app.scss

@@ -90,3 +90,17 @@ input[type="number"]::-webkit-outer-spin-button {
     display: none;
   }
 }
+
+.gradient-diarista {
+  background: linear-gradient(
+    90deg,
+    #6a11cb 0%,
+    #2575fc 100%
+  );
+
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+
+  background-clip: text;
+  color: transparent;
+}

+ 134 - 119
src/css/quasar.variables.scss

@@ -1,147 +1,162 @@
-// Quasar SCSS Variables with Material Design Color System
+// Quasar SCSS Variables with Custom Color System
 // --------------------------------------------------
 
 // Primary Theme Colors
-$primary: #1976d2; // Material Blue 700
-$secondary: #9c27b0; // Material Purple 500
-$accent: #e91e63; // Material Pink 500
+$primary: #8B5CF6; // Vibrant Purple (main brand color)
+$secondary: #EC4899; // Pink/Magenta (secondary brand color)
+$accent: #F59E0B; // Amber/Orange (accent highlights)
 
 // Dark Theme Base Colors
 $dark: #1d1d1d;
 
 // Status Colors
-$positive: #2e7d32; // Material Green 800
-$negative: #d32f2f; // Material Red 700
-$info: #0288d1; // Material Light Blue 700
-$warning: #ed6c02; // Material Orange 800
+$positive: #10B981; // Emerald Green (success, online status)
+$negative: #EF4444; // Red (errors, alerts)
+$info: #3B82F6; // Blue (informational)
+$warning: #F59E0B; // Amber (warnings)
 
 // Extended Color System with Light/Dark Variants
 $colors: (
-  // Primary Colors and Variants
-  "primary": #1976d2,
-  // Base - Blue 700
-  "primary-light": #42a5f5,
-  // Light - Blue 400
-  "primary-dark": #1565c0,
-
-  // Dark - Blue 800
-  // Secondary Colors and Variants
-  "secondary": #9c27b0,
-  // Base - Purple 500
-  "secondary-light": #ba68c8,
-  // Light - Purple 300
-  "secondary-dark": #7b1fa2,
-
-  // Terceary Colors and Variants
-  "terciary": #ff9800,
-  // Base - Orange 500
-  "terciary-light": #ffb74d,
-  // Light - Orange 300
-  "terciary-dark": #f57c00,
-
-  // Dark - Purple 700
+  // Primary Colors and Variants (Purple Theme)
+  "primary": #8B5CF6,
+  // Base - Vibrant Purple
+  "primary-light": #A78BFA,
+  // Light Purple
+  "primary-dark": #7C3AED,
+  // Dark Purple
+
+  // Secondary Colors and Variants (Pink/Magenta Theme)
+  "secondary": #EC4899,
+  // Base - Pink
+  "secondary-light": #F9A8D4,
+  // Light Pink
+  "secondary-dark": #DB2777,
+  // Dark Pink
+
+  // Tertiary Colors and Variants (Amber/Orange Accent)
+  "terciary": #F59E0B,
+  // Base - Amber
+  "terciary-light": #FCD34D,
+  // Light Amber
+  "terciary-dark": #D97706,
+  // Dark Amber
+
   // Background Colors
-  "page": #f1f1f1,
+  "page": #F9FAFB,
+  // Very Light Gray (App Background)
 
-  // Surface Colors
-  "surface": #ffffff,
-  "surface-light": #f5f5f5,
-  "surface-dark": #f1f1f1,
+  // Surface Colors (Cards, Panels)
+  "surface": #FFFFFF,
+  // Pure White
+  "surface-light": #F9FAFB,
+  // Very Light Gray
+  "surface-dark": #F3F4F6,
+  // Light Gray
 
-  //text color
-  "text": #000000,
+  // Text Colors
+  "text": #111827,
+  // Very Dark Gray (almost black)
 
   // Status Colors with Variants
-  "success": #2e7d32,
-  // Green 800
-  "success-light": #4caf50,
-  // Green 500
-  "success-dark": #1b5e20,
-
-  // Green 900
-  "error": #d32f2f,
-  // Red 700
-  "error-light": #ef5350,
-  // Red 400
-  "error-dark": #c62828,
-
-  // Red 800
-  "warning": #ed6c02,
-  // Orange 800
-  "warning-light": #ff9800,
-  // Orange 500
-  "warning-dark": #e65100,
-
-  // Orange 900
-  "info": #0288d1,
-  // Light Blue 700
-  "info-light": #03a9f4,
-  // Light Blue 500
-  "info-dark": #01579b
+  "success": #10B981,
+  // Emerald Green
+  "success-light": #34D399,
+  // Light Green
+  "success-dark": #059669,
+  // Dark Green
+
+  "error": #EF4444,
+  // Red
+  "error-light": #F87171,
+  // Light Red
+  "error-dark": #DC2626,
+  // Dark Red
+
+  "warning": #F59E0B,
+  // Amber
+  "warning-light": #FCD34D,
+  // Light Amber
+  "warning-dark": #D97706,
+  // Dark Amber
+
+  "info": #3B82F6,
+  // Blue
+  "info-light": #60A5FA,
+  // Light Blue
+  "info-dark": #2563EB
+  // Dark Blue
 );
 
 // Dark Theme Color Overrides
 $colors-dark: (
-  // Primary Colors and Variants
-  "primary": #1976d2,
-  // Base - Blue 700
-  "primary-light": #42a5f5,
-  // Blue 50
-  "primary-dark": #1565c0,
+  // Primary Colors and Variants (Purple - adjusted for dark mode)
+  "primary": #8B5CF6,
+  // Vibrant Purple (same as light)
+  "primary-light": #A78BFA,
+  // Light Purple
+  "primary-dark": #6D28D9,
+  // Darker Purple for dark mode
 
   "dark": #1d1d1d,
 
-  // Blue 400
-  // Secondary Colors - Lighter in Dark Mode
-  "secondary": #ce93d8,
-  // Purple 200
-  "secondary-light": #f3e5f5,
-  // Purple 50
-  "secondary-dark": #ab47bc,
-
-  //Terceary Colors - Lighter in Dark Mode
-  "terciary": #ffd191,
-  // Yellow 200
-  "terciary-light": #ffecb3,
-  // Yellow 50
-  "terciary-dark": #ffab40,
-
-  "page": #121212,
-
-  "surface": #1d1d1d,
-  "surface-light": #333333,
-  "surface-dark": #121212,
-
-  "text": #ffffff,
+  // Secondary Colors (Pink - adjusted for dark mode)
+  "secondary": #F472B6,
+  // Lighter Pink for dark mode
+  "secondary-light": #FBCFE8,
+  // Very Light Pink
+  "secondary-dark": #DB2777,
+  // Dark Pink
+
+  // Tertiary Colors (Amber - adjusted for dark mode)
+  "terciary": #FBBF24,
+  // Lighter Amber for dark mode
+  "terciary-light": #FDE68A,
+  // Very Light Amber
+  "terciary-dark": #F59E0B,
+  // Amber
+
+  // Dark Mode Backgrounds
+  "page": #0F172A,
+  // Very Dark Blue-Gray
+
+  "surface": #1E293B,
+  // Dark Blue-Gray (Cards)
+  "surface-light": #334155,
+  // Medium Blue-Gray
+  "surface-dark": #0F172A,
+  // Very Dark Blue-Gray
+
+  "text": #F1F5F9,
+  // Almost White
 
-  // Black Background
   // Status Colors - Adjusted for Dark Mode
-  "success": #66bb6a,
-  // Green 400
-  "success-light": #81c784,
-  // Green 300
-  "success-dark": #388e3c,
-
-  // Green 700
-  "error": #f44336,
-  // Red 500
-  "error-light": #e57373,
-  // Red 300
-  "error-dark": #d32f2f,
-
-  // Red 700
-  "warning": #ffa726,
-  // Orange 400
-  "warning-light": #ffb74d,
-  // Orange 300
-  "warning-dark": #f57c00,
-
-  // Orange 700
-  "info": #29b6f6,
-  // Light Blue 400
-  "info-light": #4fc3f7,
-  // Light Blue 300
-  "info-dark": #0288d1 // Light Blue 700
+  "success": #34D399,
+  // Lighter Emerald for visibility
+  "success-light": #6EE7B7,
+  // Very Light Green
+  "success-dark": #10B981,
+  // Emerald
+
+  "error": #F87171,
+  // Lighter Red for visibility
+  "error-light": #FCA5A5,
+  // Very Light Red
+  "error-dark": #EF4444,
+  // Red
+
+  "warning": #FBBF24,
+  // Lighter Amber for visibility
+  "warning-light": #FDE68A,
+  // Very Light Amber
+  "warning-dark": #F59E0B,
+  // Amber
+
+  "info": #60A5FA,
+  // Lighter Blue for visibility
+  "info-light": #93C5FD,
+  // Very Light Blue
+  "info-dark": #3B82F6
+  // Blue
 );
 
 // Generate color utility classes for light theme

+ 11 - 0
src/i18n/locales/en.json

@@ -118,6 +118,9 @@
       "messages": {
         "copied_to_clipboard": "Copied to clipboard",
         "confirm_action": "Are you sure?",
+        "error": "Error processing request",
+        "error_loading_data": "Error loading data",
+        "updated": "Successfully updated",
         "are_you_sure_delete": "Are you sure you want to delete this item?",
         "welcome": "Welcome",
         "enjoy_the_event": "Enjoy the event!"
@@ -458,6 +461,14 @@
     "no_dates_added": "No dates added",
     "date_already_added": "This date has already been added",
     "at_least_one_date_required": "Add at least one date",
+    "accept": "Accept",
+    "reject": "Reject",
+    "mark_as_paid": "Mark as Paid",
+    "cancel_schedule": "Cancel Schedule",
+    "view_as_client": "Client View",
+    "view_as_provider": "Provider View",
+    "filter_by_status": "Filter by Status",
+    "all_statuses": "All Statuses",
     "period_types": {
       "2": "2 hours",
       "4": "4 hours",

+ 11 - 0
src/i18n/locales/es.json

@@ -118,6 +118,9 @@
       "messages": {
         "copied_to_clipboard": "Copiado al portapapeles",
         "confirm_action": "¿Estás seguro?",
+        "error": "Error al procesar solicitud",
+        "error_loading_data": "Error al cargar datos",
+        "updated": "Actualizado con éxito",
         "are_you_sure_delete": "¿Estás seguro de que quieres eliminar este elemento?",
         "welcome": "Bienvenido",
         "enjoy_the_event": "¡Disfruta el evento!"
@@ -458,6 +461,14 @@
     "no_dates_added": "No se han agregado fechas",
     "date_already_added": "Esta fecha ya ha sido agregada",
     "at_least_one_date_required": "Agregue al menos una fecha",
+    "accept": "Aceptar",
+    "reject": "Rechazar",
+    "mark_as_paid": "Marcar como Pagado",
+    "cancel_schedule": "Cancelar Agenda",
+    "view_as_client": "Vista Cliente",
+    "view_as_provider": "Vista Proveedor",
+    "filter_by_status": "Filtrar por Estado",
+    "all_statuses": "Todos los Estados",
     "period_types": {
       "2": "2 horas",
       "4": "4 horas",

+ 11 - 0
src/i18n/locales/pt.json

@@ -118,6 +118,9 @@
       "messages": {
         "copied_to_clipboard": "Copiado para a área de transferência",
         "confirm_action": "Você tem certeza?",
+        "error": "Erro ao processar solicitação",
+        "error_loading_data": "Erro ao carregar dados",
+        "updated": "Atualizado com sucesso",
         "are_you_sure_delete": "Tem certeza de que deseja excluir este item?",
         "welcome": "Bem-vindo",
         "enjoy_the_event": "Aproveite o evento!"
@@ -458,6 +461,14 @@
     "no_dates_added": "Nenhuma data adicionada",
     "date_already_added": "Esta data já foi adicionada",
     "at_least_one_date_required": "Adicione pelo menos uma data",
+    "accept": "Aceitar",
+    "reject": "Recusar",
+    "mark_as_paid": "Marcar como Pago",
+    "cancel_schedule": "Cancelar Agendamento",
+    "view_as_client": "Visão Cliente",
+    "view_as_provider": "Visão Prestador",
+    "filter_by_status": "Filtrar por Status",
+    "all_statuses": "Todos os Status",
     "period_types": {
       "2": "2 horas",
       "4": "4 horas",

+ 312 - 39
src/pages/dashboard/DashboardPage.vue

@@ -1,30 +1,202 @@
 <template>
   <div>
-    <DefaultHeaderPage>
-      <template #after>
-        <q-btn
-          outline
-          icon="mdi-calendar"
-          color="primary"
-          @click="showFilter"
-        />
-      </template>
-    </DefaultHeaderPage>
-    <q-expansion-item
-      v-model="filter"
-      dense
-      hide-expand-icon
-      class="remove-header-expansion-item"
-    >
-      <DatePeriodSelector
-        v-model:selected-period="defaultPeriod"
-        v-model:selected-event-id="defaultEventId"
-        class="q-pa-sm"
-      />
-    </q-expansion-item>
-
-    <div v-if="!isLoading" class="column gap q-pa-sm">
-      
+    <DefaultHeaderPage />
+
+    <div v-if="!isLoading" class="q-pa-md">
+      <q-tabs
+        v-model="viewMode"
+        dense
+        class="text-grey"
+        active-color="primary"
+        indicator-color="primary"
+        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-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') }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </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') }}
+                </div>
+              </div>
+            </div>
+          </div>
+        </q-tab-panel>
+      </q-tab-panels>
     </div>
 
     <div v-else class="flex flex-center full-width q-pa-xl">
@@ -34,27 +206,128 @@
 </template>
 
 <script setup>
-import { onMounted, ref/*, watch, defineAsyncComponent*/ } from "vue";
-// import { useI18n } from "vue-i18n";
-import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
-import DatePeriodSelector from "./components/DatePeriodSelector.vue";
+import { onMounted, ref, computed } 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'
+
+const $q = useQuasar()
+const { t } = useI18n()
 
-// const { t } = useI18n();
+const isLoading = ref(true)
+const viewMode = ref('client')
+const statusFilter = ref(null)
+const groupedSchedules = ref([])
 
-const isLoading = ref(true);
-const filter = ref(false);
-const defaultPeriod = ref("month");
-const defaultEventId = ref(1);
+const statusFilterOptions = computed(() => [
+  { label: t('schedules.all_statuses'), value: null },
+  { label: t('schedules.statuses.pending'), value: 'pending' },
+  { label: t('schedules.statuses.accepted'), value: 'accepted' },
+  { label: t('schedules.statuses.rejected'), value: 'rejected' },
+  { label: t('schedules.statuses.paid'), value: 'paid' },
+  { label: t('schedules.statuses.cancelled'), value: 'cancelled' },
+  { label: t('schedules.statuses.started'), value: 'started' },
+  { label: t('schedules.statuses.finished'), value: 'finished' }
+])
 
+const filteredGroupedSchedules = computed(() => {
+  if (!statusFilter.value) {
+    return groupedSchedules.value
+  }
+  
+  return groupedSchedules.value
+    .map(clientGroup => ({
+      ...clientGroup,
+      schedules: clientGroup.schedules.filter(
+        schedule => schedule.status === statusFilter.value
+      )
+    }))
+    .filter(clientGroup => clientGroup.schedules.length > 0)
+})
 
-const showFilter = () => {
-  filter.value = !filter.value;
-};
+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 loadSchedules = async () => {
+  try {
+    isLoading.value = true
+    const data = await getSchedulesGroupedByClient()
+    groupedSchedules.value = data
+  } catch (error) {
+    $q.notify({
+      type: 'negative',
+      message: error.message || t('common.ui.messages.error_loading_data'),
+      position: 'top'
+    })
+  } finally {
+    isLoading.value = false
+  }
+}
+
+const openScheduleDialog = (schedule) => {
+  $q.dialog({
+    component: ViewScheduleDialog,
+    componentProps: {
+      schedule,
+      viewMode: viewMode.value,
+      onAccept: handleAccept,
+      onReject: handleReject,
+      onMarkAsPaid: handleMarkAsPaid,
+      onCancel: handleCancel
+    },
+    persistent: true
+  })
+}
+
+const updateStatus = async (scheduleId, newStatus) => {
+  try {
+    await updateScheduleStatus(scheduleId, newStatus);
+    await loadSchedules();
+  } catch (error) {
+    $q.notify({
+      type: 'negative',
+      message: error.message || t('common.ui.messages.error'),
+      position: 'top'
+    })
+  }
+}
+
+const handleAccept = async (scheduleId) => {
+  await updateStatus(scheduleId, 'accepted')
+}
+
+const handleReject = async (scheduleId) => {
+  await updateStatus(scheduleId, 'rejected')
+}
+
+const handleMarkAsPaid = async (scheduleId) => {
+  await updateStatus(scheduleId, 'paid')
+}
+
+const handleCancel = async (scheduleId) => {
+  await updateStatus(scheduleId, 'cancelled')
+}
 
 onMounted(async () => {
-  isLoading.value = false;
-});
+  await loadSchedules()
+})
 </script>
 
 <style scoped>

+ 234 - 0
src/pages/schedule/components/ViewScheduleDialog.vue

@@ -0,0 +1,234 @@
+<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>
+
+      <q-card-section class="q-mt-md">
+        <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 || 'N/A' }}</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 || 'N/A' }}</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('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.start_time') }}</div>
+            <div class="text-body1">{{ schedule?.start_time?.substring(0, 5) }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.end_time') }}</div>
+            <div class="text-body1">{{ schedule?.end_time?.substring(0, 5) }}</div>
+          </div>
+
+          <div class="col-12 col-md-6">
+            <div class="text-caption text-grey-7">{{ $t('schedules.total_amount') }}</div>
+            <div class="text-body1 text-weight-bold 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 class="col-12">
+            <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>
+      </q-card-section>
+
+      <q-card-actions align="right" class="q-px-md q-pb-md">
+        <q-btn
+          flat
+          :label="$t('common.actions.cancel')"
+          color="grey-7"
+          @click="onDialogCancel"
+        />
+        
+        <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>
+        </template>
+
+        <template v-if="viewMode === 'client'">
+          <q-btn
+            v-if="schedule?.status === 'accepted'"
+            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>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  },
+  viewMode: {
+    type: String,
+    required: true,
+    validator: (value) => ['client', 'provider'].includes(value)
+  }
+})
+
+const emit = defineEmits([
+  ...useDialogPluginComponent.emits,
+  // 'accept',
+  // 'reject',
+  // 'mark-as-paid',
+  // 'cancel'
+])
+
+const $q = useQuasar()
+const { t } = useI18n()
+const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } = useDialogPluginComponent()
+
+const formatCurrency = (value) => {
+  if (!value) return 'R$ 0,00'
+  return `R$ ${Number(value).toFixed(2).replace('.', ',')}`
+}
+
+const formatAddress = (address) => {
+  if (!address) return 'N/A'
+  const parts = [
+    address.address,
+    address.complement,
+    address.zip_code,
+    address.city,
+    address.state
+  ].filter(Boolean)
+  return parts.join(', ')
+}
+
+const getStatusColor = (status) => {
+  const colors = {
+    pending: 'warning',
+    accepted: 'positive',
+    rejected: 'negative',
+    paid: 'info',
+    cancelled: 'dark',
+    started: 'primary',
+    finished: 'positive'
+  }
+  return colors[status] || 'grey'
+}
+
+const handleAccept = () => {
+    $q.dialog({
+    title: t('schedules.accept'),
+    message: t('common.ui.messages.confirm_action'),
+    cancel: true,
+    persistent: true
+  }).onOk(async () => {
+    emit('accept', props.schedule.id);
+    onDialogOK();
+  })
+}
+
+const handleReject = () => {
+  $q.dialog({
+    title: t('schedules.reject'),
+    message: t('common.ui.messages.confirm_action'),
+    cancel: true,
+    persistent: true
+  }).onOk(async () => {
+    emit('reject', props.schedule.id);
+    onDialogOK();
+  })
+}
+
+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 handleCancel = () => {
+  $q.dialog({
+    title: t('schedules.cancel_schedule'),
+    message: t('common.ui.messages.confirm_action'),
+    cancel: true,
+    persistent: true
+  }).onOk(async () => {
+    onDialogOK();
+    emit('cancel', props.schedule.id);
+  })
+}
+</script>