Ver Fonte

modal de alteracao de status + dashboard com agendamentos e acoes (fluxo de testes)

Gustavo Zanatta há 2 semanas atrás
pai
commit
e51d890cf5

+ 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

+ 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>