|
@@ -1,30 +1,202 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div>
|
|
<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>
|
|
|
|
|
|
|
|
<div v-else class="flex flex-center full-width q-pa-xl">
|
|
<div v-else class="flex flex-center full-width q-pa-xl">
|
|
@@ -34,27 +206,128 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<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 () => {
|
|
onMounted(async () => {
|
|
|
- isLoading.value = false;
|
|
|
|
|
-});
|
|
|
|
|
|
|
+ await loadSchedules()
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|