Browse Source

crud agendamentos basico (sem datas e sem oportunidades)

Gustavo Zanatta 3 tuần trước cách đây
mục cha
commit
4c4d624612

+ 26 - 0
src/api/schedule.js

@@ -0,0 +1,26 @@
+import api from 'src/api'
+
+export const getSchedules = async () => {
+  const { data } = await api.get('/schedules')
+  return data.payload
+}
+
+export const getScheduleById = async (id) => {
+  const { data } = await api.get(`/schedule/${id}`)
+  return data.payload
+}
+
+export const createSchedule = async (scheduleData) => {
+  const { data } = await api.post('/schedule', scheduleData)
+  return data.payload
+}
+
+export const updateSchedule = async (id, scheduleData) => {
+  const { data } = await api.put(`/schedule/${id}`, scheduleData)
+  return data.payload
+}
+
+export const deleteSchedule = async (id) => {
+  const { data } = await api.delete(`/schedule/${id}`)
+  return data.payload
+}

+ 144 - 0
src/components/address/AddressSelect.vue

@@ -0,0 +1,144 @@
+<template>
+  <q-select
+    v-model="selectedAddress"
+    :options="filteredAddresses"
+    :label="label"
+    outlined
+    use-input
+    clearable
+    input-debounce="300"
+    option-label="label"
+    option-value="value"
+    :rules="rules"
+    :error="error"
+    :error-message="errorMessage"
+    :disabled="disabled"
+    :loading="loading"
+    @filter="filterFn"
+    @update:model-value="updateValue"
+  >
+    <template #no-option>
+      <q-item>
+        <q-item-section class="text-grey">
+          {{ disabled ? $t('common.messages.select_client_first') : $t('common.status.no_results') }}
+        </q-item-section>
+      </q-item>
+    </template>
+  </q-select>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+import { getAddresses } from 'src/api/address'
+import { normalizeString } from 'src/helpers/utils'
+
+const props = defineProps({
+  label: {
+    type: String,
+    default: 'Endereço'
+  },
+  rules: {
+    type: Array,
+    default: () => []
+  },
+  error: {
+    type: Boolean,
+    default: false
+  },
+  errorMessage: {
+    type: String,
+    default: ''
+  },
+  initialId: {
+    type: Number,
+    default: null
+  },
+  clientId: {
+    type: Number,
+    default: null
+  },
+  disabled: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const model = defineModel({
+  type: Object,
+  default: null
+})
+const emit = defineEmits(['update:modelValue'])
+
+const addresses = ref([])
+const filteredAddresses = ref([])
+const selectedAddress = ref(null)
+const loading = ref(false)
+
+const loadAddresses = async () => {
+  if (!props.clientId) {
+    addresses.value = []
+    filteredAddresses.value = []
+    return
+  }
+
+  loading.value = true
+  try {
+    const response = await getAddresses('client', props.clientId)
+    addresses.value = response.map((address) => ({
+      label: `${address.address_full}`,
+      value: address.id,
+      ...address
+    }))
+    filteredAddresses.value = addresses.value
+
+    if (props.initialId) {
+      selectAddressById(props.initialId)
+    }
+  } catch (error) {
+    console.error('Error loading addresses:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const filterFn = (val, update) => {
+  update(() => {
+    const needle = normalizeString(val)
+    filteredAddresses.value = addresses.value.filter((address) => {
+      const addressString = normalizeString(address.label)
+      return addressString.includes(needle)
+    })
+  })
+}
+
+const updateValue = (value) => {
+  model.value = value
+  emit('update:modelValue', value)
+}
+
+const selectAddressById = (id) => {
+  const address = addresses.value.find((a) => a.value === id)
+  if (address) {
+    selectedAddress.value = address
+    updateValue(address)
+  }
+}
+
+watch(() => props.clientId, (newClientId) => {
+  if (newClientId) {
+    loadAddresses()
+  } else {
+    addresses.value = []
+    filteredAddresses.value = []
+    selectedAddress.value = null
+    updateValue(null)
+  }
+}, { immediate: true })
+
+watch(() => props.initialId, (newId) => {
+  if (newId && addresses.value.length > 0) {
+    selectAddressById(newId)
+  }
+})
+
+</script>

+ 4 - 4
src/components/layout/LeftMenuLayout.vue

@@ -227,7 +227,7 @@
             </q-list>
           </q-menu>
         </q-item>
-        <q-item v-ripple clickable @click="openUrl('https://softpar.inf.br')">
+        <q-item v-ripple>
           <div class="flex full-width justify-center">
             <q-img
               :src="
@@ -304,9 +304,9 @@ const logoutFn = async () => {
   router.push({ name: "LoginPage" });
 };
 
-const openUrl = (url) => {
-  window.open(url, "_blank");
-};
+// const openUrl = (url) => {
+//   window.open(url, "_blank");
+// };
 
 watch(miniState, () => {
   Cookies.set("miniState", miniState.value);

+ 51 - 8
src/i18n/locales/en.json

@@ -137,7 +137,8 @@
       "confirm": "Confirm",
       "confirm_delete": "Are you sure you want to delete this item?",
       "deleted_successfully": "Deleted successfully",
-      "error_deleting": "Error deleting the item"
+      "error_deleting": "Error deleting the item",
+      "select_client_first": "Select a client first"
     }
   },
   "auth": {
@@ -358,13 +359,13 @@
     "morning": "Morning",
     "afternoon": "Afternoon",
     "days": {
-      "1": "Sunday",
-      "2": "Monday",
-      "3": "Tuesday",
-      "4": "Wednesday",
-      "5": "Thursday",
-      "6": "Friday",
-      "7": "Saturday"
+      "0": "Sunday",
+      "1": "Monday",
+      "2": "Tuesday",
+      "3": "Wednesday",
+      "4": "Thursday",
+      "5": "Friday",
+      "6": "Saturday"
     }
   },
   "provider_blocked_days": {
@@ -423,6 +424,47 @@
       "amex": "American Express"
     }
   },
+  "schedules": {
+    "singular": "Schedule",
+    "plural": "Schedules",
+    "header": "Schedules",
+    "add_button": "New Schedule",
+    "edit_button": "Edit Schedule",
+    "empty_state": "No schedules registered",
+    "client": "Client",
+    "provider": "Provider",
+    "address": "Address",
+    "date": "Date",
+    "period": "Period",
+    "period_type": "Period Type",
+    "schedule_type": "Schedule Type",
+    "start_time": "Start Time",
+    "end_time": "End Time",
+    "status": "Status",
+    "total_amount": "Total Amount",
+    "code": "Code",
+    "code_verified": "Code Verified",
+    "hours": "hours",
+    "period_types": {
+      "2": "2 hours",
+      "4": "4 hours",
+      "6": "6 hours",
+      "8": "8 hours"
+    },
+    "schedule_types": {
+      "default": "Default",
+      "custom": "Custom"
+    },
+    "statuses": {
+      "pending": "Pending",
+      "accepted": "Accepted",
+      "rejected": "Rejected",
+      "paid": "Paid",
+      "cancelled": "Cancelled",
+      "started": "Started",
+      "finished": "Finished"
+    }
+  },
   "orders": {
     "singular": "Order",
     "plural": "Orders",
@@ -474,6 +516,7 @@
       "profile": "Profile",
       "interests": "Interests",
       "registration": "Registration",
+      "schedules": "Schedules",
       "wallet": "Wallet",
       "settings": "Settings",
       "city": "City",

+ 51 - 8
src/i18n/locales/es.json

@@ -137,7 +137,8 @@
       "confirm": "Confirmar",
       "confirm_delete": "¿Estás seguro de que quieres eliminar este elemento?",
       "deleted_successfully": "Eliminado con éxito",
-      "error_deleting": "Error al eliminar el elemento"
+      "error_deleting": "Error al eliminar el elemento",
+      "select_client_first": "Selecciona un cliente primero"
     }
   },
   "auth": {
@@ -358,13 +359,13 @@
     "morning": "Mañana",
     "afternoon": "Tarde",
     "days": {
-      "1": "Domingo",
-      "2": "Lunes",
-      "3": "Martes",
-      "4": "Miércoles",
-      "5": "Jueves",
-      "6": "Viernes",
-      "7": "Sábado"
+      "0": "Domingo",
+      "1": "Lunes",
+      "2": "Martes",
+      "3": "Miércoles",
+      "4": "Jueves",
+      "5": "Viernes",
+      "6": "Sábado"
     }
   },
   "provider_blocked_days": {
@@ -423,6 +424,47 @@
       "amex": "American Express"
     }
   },
+  "schedules": {
+    "singular": "Agenda",
+    "plural": "Agendas",
+    "header": "Agendas",
+    "add_button": "Nueva Agenda",
+    "edit_button": "Editar Agenda",
+    "empty_state": "No hay agendas registradas",
+    "client": "Cliente",
+    "provider": "Proveedor",
+    "address": "Dirección",
+    "date": "Fecha",
+    "period": "Período",
+    "period_type": "Tipo de Período",
+    "schedule_type": "Tipo de Agenda",
+    "start_time": "Hora de Inicio",
+    "end_time": "Hora de Fin",
+    "status": "Estado",
+    "total_amount": "Monto Total",
+    "code": "Código",
+    "code_verified": "Código Verificado",
+    "hours": "horas",
+    "period_types": {
+      "2": "2 horas",
+      "4": "4 horas",
+      "6": "6 horas",
+      "8": "8 horas"
+    },
+    "schedule_types": {
+      "default": "Predeterminado",
+      "custom": "Personalizado"
+    },
+    "statuses": {
+      "pending": "Pendiente",
+      "accepted": "Aceptado",
+      "rejected": "Rechazado",
+      "paid": "Pagado",
+      "cancelled": "Cancelado",
+      "started": "Iniciado",
+      "finished": "Finalizado"
+    }
+  },
   "orders": {
     "singular": "Pedido",
     "plural": "Pedidos",
@@ -474,6 +516,7 @@
       "profile": "Perfil",
       "interests": "Intereses",
       "registration": "Registro",
+      "schedules": "Agendas",
       "wallet": "Billetera",
       "settings": "Configuración",
       "city": "Ciudad",

+ 51 - 8
src/i18n/locales/pt.json

@@ -137,7 +137,8 @@
       "confirm": "Confirmar",
       "confirm_delete": "Tem certeza de que deseja excluir este item?",
       "deleted_successfully": "Excluído com sucesso",
-      "error_deleting": "Erro ao excluir o item"
+      "error_deleting": "Erro ao excluir o item",
+      "select_client_first": "Selecione um cliente primeiro"
     }
   },
   "auth": {
@@ -358,13 +359,13 @@
     "morning": "Manhã",
     "afternoon": "Tarde",
     "days": {
-      "1": "Domingo",
-      "2": "Segunda",
-      "3": "Terça",
-      "4": "Quarta",
-      "5": "Quinta",
-      "6": "Sexta",
-      "7": "Sábado"
+      "0": "Domingo",
+      "1": "Segunda",
+      "2": "Terça",
+      "3": "Quarta",
+      "4": "Quinta",
+      "5": "Sexta",
+      "6": "Sábado"
     }
   },
   "provider_blocked_days": {
@@ -423,6 +424,47 @@
       "amex": "American Express"
     }
   },
+  "schedules": {
+    "singular": "Agendamento",
+    "plural": "Agendamentos",
+    "header": "Agendamentos",
+    "add_button": "Novo Agendamento",
+    "edit_button": "Editar Agendamento",
+    "empty_state": "Nenhum agendamento cadastrado",
+    "client": "Cliente",
+    "provider": "Prestador",
+    "address": "Endereço",
+    "date": "Data",
+    "period": "Período",
+    "period_type": "Tipo de Período",
+    "schedule_type": "Tipo de Agendamento",
+    "start_time": "Hora Início",
+    "end_time": "Hora Fim",
+    "status": "Status",
+    "total_amount": "Valor Total",
+    "code": "Código",
+    "code_verified": "Código Verificado",
+    "hours": "horas",
+    "period_types": {
+      "2": "2 horas",
+      "4": "4 horas",
+      "6": "6 horas",
+      "8": "8 horas"
+    },
+    "schedule_types": {
+      "default": "Padrão",
+      "custom": "Personalizado"
+    },
+    "statuses": {
+      "pending": "Pendente",
+      "accepted": "Aceito",
+      "rejected": "Rejeitado",
+      "paid": "Pago",
+      "cancelled": "Cancelado",
+      "started": "Iniciado",
+      "finished": "Finalizado"
+    }
+  },
   "orders": {
     "singular": "Pedido",
     "plural": "Pedidos",
@@ -474,6 +516,7 @@
       "profile": "Perfil",
       "interests": "Interesses",
       "registration": "Cadastro",
+      "schedules": "Agendamentos",
       "wallet": "Carteira",
       "settings": "Configurações",
       "city": "Cidade",

+ 7 - 7
src/pages/provider/components/ProviderWorkingDaysPanel.vue

@@ -75,13 +75,13 @@ const workingDays = ref([])
 const loading = ref(false)
 
 const daysOfWeek = [
-  { value: 1 },
-  { value: 2 },
-  { value: 3 },
-  { value: 4 },
-  { value: 5 },
-  { value: 6 },
-  { value: 7 }
+  { value: 0 },  // Sunday
+  { value: 1 },  // Monday
+  { value: 2 },  // Tuesday
+  { value: 3 },  // Wednesday
+  { value: 4 },  // Thursday
+  { value: 5 },  // Friday
+  { value: 6 }   // Saturday
 ]
 
 const isSelected = (day, period) => {

+ 9 - 0
src/pages/schedule/SchedulesPage.vue

@@ -0,0 +1,9 @@
+<template>
+  <q-page class="q-pa-md">
+    <SchedulesPanel />
+  </q-page>
+</template>
+
+<script setup>
+import SchedulesPanel from './components/SchedulesPanel.vue'
+</script>

+ 297 - 0
src/pages/schedule/components/AddEditScheduleDialog.vue

@@ -0,0 +1,297 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin" style="width: 800px; max-width: 90vw">
+      <DefaultDialogHeader :title="title" @close="onDialogCancel" />
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="row q-col-gutter-sm">
+          <ClientSelect
+            v-model="selectedClient"
+            :label="$t('schedules.client')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.client_id"
+            :error-message="serverErrors?.client_id"
+            :initial-id="schedule ? schedule.client_id : null"
+            class="col-12 col-md-6"
+            @update:model-value="onClientChange"
+          />
+
+          <ProviderSelect
+            v-model="selectedProvider"
+            :label="$t('schedules.provider')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.provider_id"
+            :error-message="serverErrors?.provider_id"
+            :initial-id="schedule ? schedule.provider_id : null"
+            class="col-12 col-md-6"
+            @update:model-value="onProviderChange"
+          />
+
+          <AddressSelect
+            v-model="selectedAddress"
+            :label="$t('schedules.address')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.address_id"
+            :error-message="serverErrors?.address_id"
+            :initial-id="schedule ? schedule.address_id : null"
+            :client-id="form.client_id"
+            :disabled="!form.client_id"
+            class="col-12"
+            @update:model-value="serverErrors.address_id = null"
+          />
+
+          <DefaultInputDatePicker
+            v-model:untreated-date="form.date"
+            :label="$t('schedules.date')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.date"
+            :error-message="serverErrors?.date"
+            class="col-12 col-md-6"
+            @update:untreated-date="serverErrors.date = null"
+          />
+
+          <q-select
+            v-model="form.period_type"
+            :label="$t('schedules.period_type')"
+            :options="periodOptions"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.period_type"
+            :error-message="serverErrors?.period_type"
+            emit-value
+            map-options
+            class="col-12 col-md-6"
+            @update:model-value="onPeriodChange"
+          />
+
+          <DefaultInputTimePicker
+            v-model:untreated-time="form.start_time"
+            :label="$t('schedules.start_time')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.start_time"
+            :error-message="serverErrors?.start_time"
+            class="col-12 col-md-6"
+            @update:untreated-time="onStartTimeChange"
+          />
+
+          <DefaultInputTimePicker
+            v-model:untreated-time="form.end_time"
+            :label="$t('schedules.end_time')"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.end_time"
+            :error-message="serverErrors?.end_time"
+            :disabled="true"
+            class="col-12 col-md-6"
+            @update:untreated-time="serverErrors.end_time = null"
+          />
+
+          <q-select
+            v-model="form.status"
+            :label="$t('schedules.status')"
+            :options="statusOptions"
+            :rules="[inputRules.required]"
+            :error="!!serverErrors?.status"
+            :error-message="serverErrors?.status"
+            :disable="!schedule"
+            emit-value
+            map-options
+            class="col-12 col-md-6"
+            @update:model-value="serverErrors.status = null"
+          />
+
+          <q-input
+            v-model="form.total_amount"
+            :label="$t('schedules.total_amount')"
+            :prefix="'R$'"
+            :readonly="true"
+            class="col-12 col-md-6"
+          />
+
+          <q-checkbox
+            v-if="schedule"
+            v-model="form.code_verified"
+            :label="$t('schedules.code_verified')"
+            class="col-12"
+          />
+        </q-card-section>
+
+        <q-card-actions align="right">
+          <q-btn
+            flat
+            :label="$t('common.actions.cancel')"
+            color="negative"
+            @click="onDialogCancel"
+          />
+          <q-btn
+            type="submit"
+            :label="$t('common.actions.save')"
+            :loading="loading"
+            :disable="!hasUpdatedFields"
+            color="primary"
+          />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useDialogPluginComponent } from 'quasar'
+import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker'
+import { useSubmitHandler } from 'src/composables/useSubmitHandler'
+import { createSchedule, updateSchedule } from 'src/api/schedule'
+import { getProvider } from 'src/api/provider'
+import DefaultDialogHeader from 'src/components/defaults/DefaultDialogHeader.vue'
+import ClientSelect from 'src/components/client/ClientSelect.vue'
+import ProviderSelect from 'src/components/provider/ProviderSelect.vue'
+import AddressSelect from 'src/components/address/AddressSelect.vue'
+import DefaultInputDatePicker from 'src/components/defaults/DefaultInputDatePicker.vue'
+import DefaultInputTimePicker from 'src/components/defaults/DefaultInputTimePicker.vue'
+import { useInputRules } from 'src/composables/useInputRules'
+import { useI18n } from 'vue-i18n'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    default: null
+  },
+  title: {
+    type: Function,
+    default: () => ''
+  }
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const { inputRules } = useInputRules()
+const { t } = useI18n()
+const formRef = ref(null)
+const selectedClient = ref(null)
+const selectedProvider = ref(null)
+const selectedAddress = ref(null)
+const providerData = ref(null)
+
+const { form, getUpdatedFields, hasUpdatedFields } = useFormUpdateTracker({
+  client_id: props.schedule ? props.schedule.client_id : null,
+  provider_id: props.schedule ? props.schedule.provider_id : null,
+  address_id: props.schedule ? props.schedule.address_id : null,
+  date: props.schedule ? props.schedule.date : null,
+  period_type: props.schedule ? props.schedule.period_type : '8',
+  schedule_type: props.schedule ? props.schedule.schedule_type : 'default',
+  start_time: props.schedule ? props.schedule.start_time : null,
+  end_time: props.schedule ? props.schedule.end_time : null,
+  status: props.schedule ? props.schedule.status : 'pending',
+  total_amount: props.schedule ? props.schedule.total_amount : 0,
+  code_verified: props.schedule ? props.schedule.code_verified : false
+})
+
+const {
+  loading,
+  serverErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () => onDialogOK(true),
+  formRef: formRef,
+})
+
+const periodOptions = computed(() => [
+  { label: t('schedules.period_types.2'), value: '2' },
+  { label: t('schedules.period_types.4'), value: '4' },
+  { label: t('schedules.period_types.6'), value: '6' },
+  { label: t('schedules.period_types.8'), value: '8' }
+])
+
+const statusOptions = computed(() => [
+  { 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 onClientChange = (client) => {
+  form.client_id = client?.value || null
+  if (!props.schedule) {
+    selectedAddress.value = null
+    form.address_id = null
+  }
+  serverErrors.client_id = null
+}
+
+const onProviderChange = async (provider) => {
+  form.provider_id = provider?.value || null
+  serverErrors.provider_id = null
+  
+  if (form.provider_id) {
+    try {
+      providerData.value = await getProvider(form.provider_id)
+      calculateTotalAmount()
+    } catch (error) {
+      console.error('Error loading provider:', error)
+    }
+  }
+}
+
+const onPeriodChange = () => {
+  serverErrors.period_type = null
+  calculateEndTime()
+  calculateTotalAmount()
+}
+
+const onStartTimeChange = () => {
+  serverErrors.start_time = null
+  calculateEndTime()
+}
+
+const calculateEndTime = () => {
+  if (!form.start_time || !form.period_type) return
+  
+  const [hours, minutes] = form.start_time.split(':').map(Number)
+  const periodHours = parseInt(form.period_type)
+  
+  const endHours = hours + periodHours
+  const endMinutes = minutes
+  
+  form.end_time = `${String(endHours).padStart(2, '0')}:${String(endMinutes).padStart(2, '0')}:00`
+}
+
+const calculateTotalAmount = () => {
+  if (!providerData.value || !form.period_type) return
+  const rateMap = {
+    '2': providerData.value.daily_price_2h,
+    '4': providerData.value.daily_price_4h,
+    '6': providerData.value.daily_price_6h,
+    '8': providerData.value.daily_price_8h
+  }
+  form.total_amount = rateMap[form.period_type] || 0
+}
+
+const onOKClick = async () => {
+  if (selectedClient.value?.value) {
+    form.client_id = selectedClient.value.value
+  }
+  if (selectedProvider.value?.value) {
+    form.provider_id = selectedProvider.value.value
+  }
+  if (selectedAddress.value?.value) {
+    form.address_id = selectedAddress.value.value
+  }
+
+  if (props.schedule) {
+    await submitForm(() => updateSchedule(props.schedule.id, getUpdatedFields.value))
+  } else {
+    await submitForm(() => createSchedule({ ...form }))
+  }
+}
+
+onMounted(async () => {
+  if (props.schedule?.provider_id) {
+    try {
+      providerData.value = await getProvider(props.schedule.provider_id)
+    } catch (error) {
+      console.error('Error loading provider:', error)
+    }
+  }
+})
+</script>

+ 155 - 0
src/pages/schedule/components/SchedulesPanel.vue

@@ -0,0 +1,155 @@
+<template>
+  <DefaultTable
+    ref="tableRef"
+    :columns="columns"
+    :loading="loading"
+    :api-call="getSchedules"
+    :add-button-label="$t('schedules.add_button')"
+    :empty-message="$t('schedules.empty_state')"
+    :delete-function="deleteSchedule"
+    :mostrar-selecao-de-colunas="false"
+    :mostrar-botao-fullscreen="false"
+    :mostrar-toggle-inativos="false"
+    :open-item="true"
+    @on-row-click="onRowClick"
+    @on-add-item="onAddItem"
+  >
+    <template #body-cell-status="template_props">
+      <q-td :props="template_props">
+        <q-chip
+          :label="$t(`schedules.statuses.${template_props.row.status}`)"
+          :color="getStatusColor(template_props.row.status)"
+          text-color="white"
+          size="sm"
+        />
+      </q-td>
+    </template>
+
+    <template #body-cell-period_type="template_props">
+      <q-td :props="template_props">
+        {{ template_props.row.period_type }} {{ $t('schedules.hours') }}
+      </q-td>
+    </template>
+
+    <template #body-cell-total_amount="template_props">
+      <q-td :props="template_props">
+        {{ formatCurrency(template_props.row.total_amount) }}
+      </q-td>
+    </template>
+  </DefaultTable>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useQuasar } from 'quasar'
+import { getSchedules, deleteSchedule } from 'src/api/schedule'
+import DefaultTable from 'src/components/defaults/DefaultTable.vue'
+import AddEditScheduleDialog from './AddEditScheduleDialog.vue'
+import { format, parseISO } from 'date-fns'
+
+const { t } = useI18n()
+const $q = useQuasar()
+const tableRef = ref(null)
+const loading = ref(false)
+
+const columns = computed(() => [
+  {
+    name: 'client_name',
+    label: t('schedules.client'),
+    align: 'left',
+    field: 'client_name',
+    sortable: true
+  },
+  {
+    name: 'provider_name',
+    label: t('schedules.provider'),
+    align: 'left',
+    field: 'provider_name',
+    sortable: true
+  },
+  {
+    name: 'date',
+    label: t('schedules.date'),
+    align: 'left',
+    field: 'date',
+    sortable: true,
+    format: (val) => val ? format(parseISO(val), 'dd/MM/yyyy') : ''
+  },
+  {
+    name: 'period_type',
+    label: t('schedules.period'),
+    align: 'center',
+    field: 'period_type',
+    sortable: true
+  },
+  {
+    name: 'status',
+    label: t('schedules.status'),
+    align: 'center',
+    field: 'status',
+    sortable: true
+  },
+  {
+    name: 'total_amount',
+    label: t('schedules.total_amount'),
+    align: 'right',
+    field: 'total_amount',
+    sortable: true
+  },
+  {
+    name: 'actions',
+    label: t('common.terms.actions'),
+    align: 'center',
+    field: 'actions'
+  }
+])
+
+const getStatusColor = (status) => {
+  const colors = {
+    pending: 'orange',
+    accepted: 'blue',
+    rejected: 'red',
+    paid: 'green',
+    cancelled: 'grey',
+    started: 'purple',
+    finished: 'teal'
+  }
+  return colors[status] || 'grey'
+}
+
+const formatCurrency = (value) => {
+  return new Intl.NumberFormat('pt-BR', {
+    style: 'currency',
+    currency: 'BRL'
+  }).format(value)
+}
+
+const onAddItem = () => {
+  $q.dialog({
+    component: AddEditScheduleDialog,
+    componentProps: {
+      title: () => t('schedules.add_button')
+    }
+  }).onOk((success) => {
+    if(success) {
+      tableRef.value.refresh()
+    }
+  })
+}
+
+const onRowClick = ({ row }) => {
+  $q.dialog({
+    component: AddEditScheduleDialog,
+    componentProps: {
+      schedule: row,
+      title: () => t('schedules.edit_button')
+    }
+  }).onOk((success) => {
+    if(success) {
+      tableRef.value.refresh()
+    }
+  })
+}
+
+</script>

+ 22 - 0
src/router/routes/schedule.route.js

@@ -0,0 +1,22 @@
+export default [
+  {
+    path: "/schedules",
+    name: "SchedulesPage",
+    component: () => import("pages/schedule/SchedulesPage.vue"),
+    meta: {
+      title: "ui.navigation.schedules",
+      requireAuth: true,
+      requiredPermission: "config.schedule",
+      breadcrumbs: [
+        {
+          name: "DashboardPage",
+          title: "ui.navigation.dashboard",
+        },
+        {
+          name: "SchedulesPage",
+          title: "ui.navigation.schedules",
+        },
+      ],
+    },
+  },
+];

+ 9 - 0
src/stores/navigation.js

@@ -13,6 +13,15 @@ export const navigationStore = defineStore("navigation", () => {
       permission: false,
       permissionScope: "dashboard",
     },
+    {
+      type: "single",
+      title: "ui.navigation.schedules",
+      name: "SchedulesPage",
+      icon: "mdi-calendar-clock",
+      disable: false,
+      permission: false,
+      permissionScope: "config.schedule",
+    },
     {
       type: "expansive",
       title: "ui.navigation.registration",