|
@@ -0,0 +1,447 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <DefaultHeaderPage />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="row q-gutter-xs q-mb-md flex-wrap">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="tab in tabs"
|
|
|
|
|
+ :key="tab.name"
|
|
|
|
|
+ :class="['cat-chip', activeTab === tab.name ? 'cat-chip--selected' : 'cat-chip--default']"
|
|
|
|
|
+ @click="selectTab(tab.name)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span v-if="tab.counterKey !== null" class="cat-chip-count q-mr-xs">
|
|
|
|
|
+ {{ counters[tab.counterKey] !== undefined ? counters[tab.counterKey] : '—' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ {{ tab.label }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="activeTab === 'nova-solicitacao'" class="q-mt-md">
|
|
|
|
|
+ <q-card flat bordered>
|
|
|
|
|
+ <q-card-section>
|
|
|
|
|
+ <q-form ref="formRef" @submit="submitAppointment">
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <AssociadoSelect
|
|
|
|
|
+ v-model="form.associado"
|
|
|
|
|
+ :label="$t('agendamento.associado')"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ class="col-12 input-violet"
|
|
|
|
|
+ />
|
|
|
|
|
+ <PartnerAgreementSelect
|
|
|
|
|
+ v-model="form.partner"
|
|
|
|
|
+ :label="$t('ui.navigation.convenios')"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ class="col-12 input-violet"
|
|
|
|
|
+ />
|
|
|
|
|
+ <PartnerAgreementServiceSelect
|
|
|
|
|
+ v-model="form.service"
|
|
|
|
|
+ :partner-agreement-id="form.partner?.value"
|
|
|
|
|
+ :label="$t('associado.service')"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ class="col-12 input-violet"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultInputDatePicker
|
|
|
|
|
+ v-model:untreated-date="form.date"
|
|
|
|
|
+ :label="$t('common.terms.date')"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ placeholder="dd/mm/aaaa"
|
|
|
|
|
+ lazy-rules
|
|
|
|
|
+ class="col-12 col-md-6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.time"
|
|
|
|
|
+ :label="$t('common.terms.hour2')"
|
|
|
|
|
+ :rules="[inputRules.required]"
|
|
|
|
|
+ mask="##:##"
|
|
|
|
|
+ placeholder="HH:MM"
|
|
|
|
|
+ class="col-12 col-md-6"
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultInput
|
|
|
|
|
+ v-model="form.observations"
|
|
|
|
|
+ :label="$t('associado.notes')"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ autogrow
|
|
|
|
|
+ class="col-12"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="q-mt-md flex justify-end">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ :label="$t('agendamento.solicitar')"
|
|
|
|
|
+ :loading="submitting"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-form>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else-if="activeTab === 'visao-geral'" class="q-mt-md">
|
|
|
|
|
+ <div class="counters-row q-mb-md">
|
|
|
|
|
+ <div class="counter-card text-primary">
|
|
|
|
|
+ <span class="counter-value">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ name="mdi-clock"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ />
|
|
|
|
|
+ {{ counters.pendentes !== undefined ? counters.pendentes : '—' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="counter-label">{{ $t("agendamento.status.pendente") }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="counter-card text-primary">
|
|
|
|
|
+ <span class="counter-value">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ name="mdi-check"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ />
|
|
|
|
|
+ {{ counters.aprovados !== undefined ? counters.aprovados : '—' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="counter-label">{{ $t("agendamento.status.confirmado") }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="counter-card text-primary">
|
|
|
|
|
+ <span class="counter-value">
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ name="mdi-close"
|
|
|
|
|
+ color="primary"
|
|
|
|
|
+ />
|
|
|
|
|
+ {{ counters.recusados !== undefined ? counters.recusados : '—' }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="counter-label">{{ $t("agendamento.status.recusado") }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultTableServerSide
|
|
|
|
|
+ :key="tableKey"
|
|
|
|
|
+ :columns="columnsVisaoGeral"
|
|
|
|
|
+ :api-call="apiFetchAll"
|
|
|
|
|
+ :add-item="false"
|
|
|
|
|
+ :show-search-field="true"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #body-cell-status="{ row }">
|
|
|
|
|
+ <q-td class="text-center">
|
|
|
|
|
+ <q-chip
|
|
|
|
|
+ outline
|
|
|
|
|
+ :color="statusColor(row.status)"
|
|
|
|
|
+ :label="$t(`agendamento.status.${row.status}`)"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <template #body-cell-acoes="{ row }">
|
|
|
|
|
+ <q-td auto-width>
|
|
|
|
|
+ <div class="row no-wrap items-center" style="gap: 4px">
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ dense
|
|
|
|
|
+ round
|
|
|
|
|
+ icon="mdi-check"
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :unelevated="row.status === 'confirmado'"
|
|
|
|
|
+ :outline="row.status !== 'confirmado'"
|
|
|
|
|
+ :loading="actionId === row.id && actionType === 'approve'"
|
|
|
|
|
+ @click.prevent.stop="onApprove(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ dense
|
|
|
|
|
+ round
|
|
|
|
|
+ icon="mdi-close"
|
|
|
|
|
+ color="negative"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :unelevated="row.status === 'recusado' || row.status === 'cancelado'"
|
|
|
|
|
+ :outline="row.status !== 'recusado' && row.status !== 'cancelado'"
|
|
|
|
|
+ :loading="actionId === row.id && actionType === 'reject'"
|
|
|
|
|
+ @click.prevent.stop="onReject(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </DefaultTableServerSide>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-else-if="activeTab === 'aprovados'" class="q-mt-md">
|
|
|
|
|
+ <DefaultTableServerSide
|
|
|
|
|
+ :key="tableKey"
|
|
|
|
|
+ :columns="columnsAprovados"
|
|
|
|
|
+ :api-call="apiFetchAprovados"
|
|
|
|
|
+ :add-item="false"
|
|
|
|
|
+ :show-search-field="true"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #body-cell-status>
|
|
|
|
|
+ <q-td class="text-center">
|
|
|
|
|
+ <q-chip
|
|
|
|
|
+ outline
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ :label="$t('agendamento.status.confirmado')"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </DefaultTableServerSide>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import { ref, computed, onMounted, useTemplateRef } from "vue";
|
|
|
|
|
+import { useQuasar } from "quasar";
|
|
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
|
|
+import { useInputRules } from "src/composables/useInputRules";
|
|
|
|
|
+
|
|
|
|
|
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
|
|
+import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
|
|
|
|
|
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
|
|
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
|
|
|
|
|
+import AssociadoSelect from "src/components/selects/AssociadoSelect.vue";
|
|
|
|
|
+import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
|
|
|
|
|
+import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
|
|
|
|
|
+
|
|
|
|
|
+import {
|
|
|
|
|
+ getAdminCounters,
|
|
|
|
|
+ getAdminAppointmentsPaginated,
|
|
|
|
|
+ createAppointment,
|
|
|
|
|
+ approveAppointment,
|
|
|
|
|
+ rejectAppointment,
|
|
|
|
|
+} from "src/api/appointment";
|
|
|
|
|
+
|
|
|
|
|
+const $q = useQuasar();
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+const { inputRules } = useInputRules();
|
|
|
|
|
+const formRef = useTemplateRef("formRef");
|
|
|
|
|
+
|
|
|
|
|
+const activeTab = ref("nova-solicitacao");
|
|
|
|
|
+const tableKey = ref(0);
|
|
|
|
|
+const submitting = ref(false);
|
|
|
|
|
+const actionId = ref(null);
|
|
|
|
|
+const actionType = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const counters = ref({
|
|
|
|
|
+ pendentes: undefined,
|
|
|
|
|
+ aprovados: undefined,
|
|
|
|
|
+ recusados: undefined,
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const form = ref({
|
|
|
|
|
+ associado: null,
|
|
|
|
|
+ partner: null,
|
|
|
|
|
+ service: null,
|
|
|
|
|
+ date: "",
|
|
|
|
|
+ time: "",
|
|
|
|
|
+ observations: "",
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const tabs = computed(() => [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "nova-solicitacao",
|
|
|
|
|
+ label: t("agendamento.nova_solicitacao"),
|
|
|
|
|
+ icon: "mdi-calendar-plus-outline",
|
|
|
|
|
+ counterKey: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "visao-geral",
|
|
|
|
|
+ label: t("agendamento.visao_geral"),
|
|
|
|
|
+ icon: "mdi-calendar-check-outline",
|
|
|
|
|
+ counterKey: "pendentes",
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: "aprovados",
|
|
|
|
|
+ label: t("agendamento.aprovados_automaticamente"),
|
|
|
|
|
+ icon: "mdi-calendar-star-outline",
|
|
|
|
|
+ counterKey: "aprovados",
|
|
|
|
|
+ },
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+const columnsVisaoGeral = computed(() => [
|
|
|
|
|
+ { name: "order_number", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
|
|
|
|
|
+ { name: "cracha", label: t("associado.cracha"), field: "registration", align: "left" },
|
|
|
|
|
+ { name: "user_name", label: t("common.terms.name"), field: "user_name", align: "left" },
|
|
|
|
|
+ { name: "partner_name", label: t("agendamento.col.parceiro"), field: "partner_name", align: "left" },
|
|
|
|
|
+ { name: "service_name", label: t("agendamento.col.servico"), field: "service_name", align: "left" },
|
|
|
|
|
+ { name: "requested_at", label: t("agendamento.col.solicitacao"), field: "requested_at", align: "left" },
|
|
|
|
|
+ { name: "acoes", label: t("common.terms.actions"), field: "acoes", align: "center" },
|
|
|
|
|
+ { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+const columnsAprovados = computed(() => [
|
|
|
|
|
+ { name: "order_number", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
|
|
|
|
|
+ { name: "user_name", label: t("common.terms.name"), field: "user_name", align: "left" },
|
|
|
|
|
+ { name: "partner_name", label: t("agendamento.col.parceiro"), field: "partner_name", align: "left" },
|
|
|
|
|
+ { name: "service_name", label: t("agendamento.col.servico"), field: "service_name", align: "left" },
|
|
|
|
|
+ { name: "requested_at", label: t("agendamento.col.solicitacao"), field: "requested_at", align: "left" },
|
|
|
|
|
+ { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
|
|
|
|
|
+]);
|
|
|
|
|
+
|
|
|
|
|
+const statusColor = (status) => {
|
|
|
|
|
+ const map = {
|
|
|
|
|
+ pendente: "warning",
|
|
|
|
|
+ confirmado: "positive",
|
|
|
|
|
+ recusado: "negative",
|
|
|
|
|
+ cancelado: "grey-6",
|
|
|
|
|
+ concluido: "info",
|
|
|
|
|
+ };
|
|
|
|
|
+ return map[status] ?? "grey-6";
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const apiFetchAll = (params) => getAdminAppointmentsPaginated(params);
|
|
|
|
|
+const apiFetchAprovados = (params) =>
|
|
|
|
|
+ getAdminAppointmentsPaginated({ ...params, status: "confirmado" });
|
|
|
|
|
+
|
|
|
|
|
+const selectTab = (name) => {
|
|
|
|
|
+ activeTab.value = name;
|
|
|
|
|
+ tableKey.value++;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const loadCounters = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ counters.value = await getAdminCounters();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // silent — counters show '—'
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const resetForm = () => {
|
|
|
|
|
+ form.value = {
|
|
|
|
|
+ associado: null,
|
|
|
|
|
+ partner: null,
|
|
|
|
|
+ service: null,
|
|
|
|
|
+ date: "",
|
|
|
|
|
+ time: "",
|
|
|
|
|
+ observations: "",
|
|
|
|
|
+ };
|
|
|
|
|
+ formRef.value?.resetValidation();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const submitAppointment = async () => {
|
|
|
|
|
+ const valid = await formRef.value?.validate();
|
|
|
|
|
+ if (!valid) return;
|
|
|
|
|
+ submitting.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await createAppointment({
|
|
|
|
|
+ user_id: form.value.associado.value,
|
|
|
|
|
+ partner_agreement_id: form.value.partner.value,
|
|
|
|
|
+ partner_agreement_service_id: form.value.service.value,
|
|
|
|
|
+ time: form.value.time,
|
|
|
|
|
+ date: form.value.date,
|
|
|
|
|
+ observations: form.value.observations || null,
|
|
|
|
|
+ });
|
|
|
|
|
+ $q.notify({ type: "positive", message: t("http.success") });
|
|
|
|
|
+ resetForm();
|
|
|
|
|
+ await loadCounters();
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitting.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onApprove = (row) => {
|
|
|
|
|
+ if (row.status !== "pendente") return;
|
|
|
|
|
+ $q.dialog({
|
|
|
|
|
+ title: t("common.ui.messages.confirm_action"),
|
|
|
|
|
+ message: t("agendamento.confirm_approve"),
|
|
|
|
|
+ cancel: true,
|
|
|
|
|
+ persistent: true,
|
|
|
|
|
+ }).onOk(async () => {
|
|
|
|
|
+ actionId.value = row.id;
|
|
|
|
|
+ actionType.value = "approve";
|
|
|
|
|
+ try {
|
|
|
|
|
+ await approveAppointment(row.id);
|
|
|
|
|
+ tableKey.value++;
|
|
|
|
|
+ await loadCounters();
|
|
|
|
|
+ $q.notify({ type: "positive", message: t("http.success") });
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ actionId.value = null;
|
|
|
|
|
+ actionType.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const onReject = (row) => {
|
|
|
|
|
+ if (row.status !== "pendente") return;
|
|
|
|
|
+ $q.dialog({
|
|
|
|
|
+ title: t("common.ui.messages.confirm_action"),
|
|
|
|
|
+ message: t("agendamento.confirm_reject"),
|
|
|
|
|
+ cancel: true,
|
|
|
|
|
+ persistent: true,
|
|
|
|
|
+ }).onOk(async () => {
|
|
|
|
|
+ actionId.value = row.id;
|
|
|
|
|
+ actionType.value = "reject";
|
|
|
|
|
+ try {
|
|
|
|
|
+ await rejectAppointment(row.id);
|
|
|
|
|
+ tableKey.value++;
|
|
|
|
|
+ await loadCounters();
|
|
|
|
|
+ $q.notify({ type: "positive", message: t("http.success") });
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ actionId.value = null;
|
|
|
|
|
+ actionType.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ loadCounters();
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss" scoped>
|
|
|
|
|
+@use "src/css/quasar.variables.scss" as vars;
|
|
|
|
|
+
|
|
|
|
|
+.cat-chip {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ padding: 0 12px;
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ transition: background 0.15s, color 0.15s;
|
|
|
|
|
+
|
|
|
|
|
+ &--default {
|
|
|
|
|
+ background: #c9a3dc;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--selected {
|
|
|
|
|
+ background: #4d1658;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cat-chip-count {
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.counters-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.counter-card {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border: 1.5px solid vars.$color-border;
|
|
|
|
|
+ background: vars.$surface;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.counter-value {
|
|
|
|
|
+ font-size: 1.75rem;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.counter-label {
|
|
|
|
|
+ font-size: 0.85rem;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|