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