|
|
@@ -3,32 +3,44 @@
|
|
|
<DefaultHeaderPage />
|
|
|
|
|
|
<div class="q-pa-md">
|
|
|
- <DefaultTabs v-model="activeTab" :tabs-items="tabs" />
|
|
|
|
|
|
+ <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="activeTab = tab.name"
|
|
|
+ >
|
|
|
+ {{ tab.label }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tab: Nova Solicitação -->
|
|
|
<div v-if="activeTab === 'novo'" class="q-mt-md">
|
|
|
- <q-card flat bordered style="max-width: 600px">
|
|
|
+ <q-card flat bordered >
|
|
|
<q-card-section>
|
|
|
- <div class="text-h6 q-mb-md">{{ $t("associado.new_appointment") }}</div>
|
|
|
<q-form ref="appointmentFormRef" @submit="submitAppointment">
|
|
|
<div class="row q-col-gutter-sm">
|
|
|
<PartnerAgreementSelect
|
|
|
v-model="selectedPartner"
|
|
|
:label="$t('ui.navigation.convenios')"
|
|
|
:rules="[inputRules.required]"
|
|
|
- class="col-12"
|
|
|
+ class="col-12 input-violet"
|
|
|
/>
|
|
|
<PartnerAgreementServiceSelect
|
|
|
v-model="selectedService"
|
|
|
:partner-agreement-id="selectedPartner?.value"
|
|
|
:label="$t('associado.service')"
|
|
|
:rules="[inputRules.required]"
|
|
|
- class="col-12"
|
|
|
+ class="col-12 input-violet"
|
|
|
/>
|
|
|
<DefaultInputDatePicker
|
|
|
- v-model="appointmentForm.date"
|
|
|
+ v-model:untreated-date="appointmentForm.date"
|
|
|
:label="$t('common.terms.date')"
|
|
|
:rules="[inputRules.required]"
|
|
|
- class="col-12 col-md-6"
|
|
|
+ class="col-12 col-md-6 input-violet"
|
|
|
+ placeholder="dd/mm/aaaa"
|
|
|
+ lazy-rules
|
|
|
/>
|
|
|
<DefaultInput
|
|
|
v-model="appointmentForm.time"
|
|
|
@@ -36,21 +48,22 @@
|
|
|
:rules="[inputRules.required]"
|
|
|
mask="##:##"
|
|
|
placeholder="HH:MM"
|
|
|
- class="col-12 col-md-6"
|
|
|
+ class="col-12 col-md-6 input-violet"
|
|
|
/>
|
|
|
<DefaultInput
|
|
|
- v-model="appointmentForm.notes"
|
|
|
+ v-model="appointmentForm.observations"
|
|
|
:label="$t('associado.notes')"
|
|
|
type="textarea"
|
|
|
autogrow
|
|
|
- class="col-12"
|
|
|
+ class="col-12 input-violet"
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="q-mt-md flex justify-end">
|
|
|
<q-btn
|
|
|
- color="primary"
|
|
|
+ unelevated
|
|
|
type="submit"
|
|
|
- :label="$t('associado.schedule')"
|
|
|
+ class="btn-gradient"
|
|
|
+ :label="editingId ? $t('common.actions.save') : $t('associado.schedule')"
|
|
|
:loading="submitting"
|
|
|
/>
|
|
|
</div>
|
|
|
@@ -59,101 +72,151 @@
|
|
|
</q-card>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- Tab: Meus Agendamentos -->
|
|
|
<div v-else class="q-mt-md">
|
|
|
<div v-if="loadingList" class="flex flex-center q-py-xl">
|
|
|
- <q-spinner color="primary" size="48px" />
|
|
|
+ <q-spinner color="violet-normal" size="48px" />
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="appointments.length === 0" class="text-center text-grey q-py-xl">
|
|
|
{{ $t("http.errors.no_records_found") }}
|
|
|
</div>
|
|
|
|
|
|
- <q-list v-else separator bordered class="rounded-borders">
|
|
|
- <q-item v-for="apt in appointments" :key="apt.id">
|
|
|
- <q-item-section>
|
|
|
- <q-item-label class="text-weight-medium">
|
|
|
- {{ apt.partner_agreement?.trade_name || apt.partner_agreement?.company_name }}
|
|
|
- </q-item-label>
|
|
|
- <q-item-label caption>
|
|
|
- {{ apt.partner_agreement_service?.name }}
|
|
|
- </q-item-label>
|
|
|
- <q-item-label caption>
|
|
|
- {{ formatDate(apt.date) }} {{ apt.time ? `- ${apt.time}` : "" }}
|
|
|
- </q-item-label>
|
|
|
- </q-item-section>
|
|
|
- <q-item-section side>
|
|
|
+ <q-table
|
|
|
+ v-else
|
|
|
+ class="softpar-table q-pa-sm"
|
|
|
+ :rows="pagedAppointments"
|
|
|
+ :columns="columns"
|
|
|
+ row-key="id"
|
|
|
+ hide-pagination
|
|
|
+ :rows-per-page-options="[0]"
|
|
|
+ >
|
|
|
+ <template #body-cell-status="props">
|
|
|
+ <q-td :props="props">
|
|
|
<q-chip
|
|
|
- :color="statusColor(apt.status)"
|
|
|
- text-color="white"
|
|
|
- :label="$t(`associado.appointment_status.${apt.status}`)"
|
|
|
+ outline
|
|
|
+ :color="statusColor(props.row.status)"
|
|
|
+ :label="$t(`agendamento.status.${props.row.status}`)"
|
|
|
size="sm"
|
|
|
/>
|
|
|
- </q-item-section>
|
|
|
- <q-item-section v-if="apt.status === 'pendente'" side>
|
|
|
+ </q-td>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #body-cell-acoes="props">
|
|
|
+ <q-td :props="props">
|
|
|
<q-btn
|
|
|
+ dense
|
|
|
+ round
|
|
|
flat
|
|
|
+ icon="mdi-pencil"
|
|
|
+ color="violet-normal"
|
|
|
+ size="sm"
|
|
|
+ :disable="props.row.status !== 'pendente'"
|
|
|
+ @click="onEditAppointment(props.row)"
|
|
|
+ />
|
|
|
+ <q-btn
|
|
|
+ dense
|
|
|
round
|
|
|
- icon="mdi-cancel"
|
|
|
- color="negative"
|
|
|
+ flat
|
|
|
+ icon="mdi-calendar"
|
|
|
+ color="violet-normal"
|
|
|
size="sm"
|
|
|
- :loading="cancellingId === apt.id"
|
|
|
- @click="onCancelAppointment(apt)"
|
|
|
+ disable
|
|
|
/>
|
|
|
- </q-item-section>
|
|
|
- </q-item>
|
|
|
- </q-list>
|
|
|
+ </q-td>
|
|
|
+ </template>
|
|
|
+ </q-table>
|
|
|
+
|
|
|
+ <div v-if="totalPages > 1" class="flex flex-center q-mt-lg">
|
|
|
+ <q-pagination
|
|
|
+ v-model="currentPage"
|
|
|
+ :max="totalPages"
|
|
|
+ boundary-numbers
|
|
|
+ color="violet-normal"
|
|
|
+ active-color="violet-normal"
|
|
|
+ direction-links
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
+
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, computed, watch, onMounted, useTemplateRef } from "vue";
|
|
|
+import { ref, reactive, computed, watch, useTemplateRef } from "vue";
|
|
|
import { useQuasar } from "quasar";
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
-import { createAppointment, getMyAppointments, cancelAppointment } from "src/api/appointment";
|
|
|
+import { createAppointment, getMyAppointments, updateAppointment } from "src/api/appointment";
|
|
|
import { useInputRules } from "src/composables/useInputRules";
|
|
|
-
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
-import DefaultTabs from "src/components/defaults/DefaultTabs.vue";
|
|
|
import DefaultInput from "src/components/defaults/DefaultInput.vue";
|
|
|
import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
|
|
|
import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
|
|
|
import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
|
|
|
-
|
|
|
+import { userStore } from "src/stores/user";
|
|
|
const $q = useQuasar();
|
|
|
const { t } = useI18n();
|
|
|
const { inputRules } = useInputRules();
|
|
|
const appointmentFormRef = useTemplateRef("appointmentFormRef");
|
|
|
-
|
|
|
+const user = userStore();
|
|
|
const activeTab = ref("novo");
|
|
|
+const PER_PAGE = 12;
|
|
|
+const currentPage = ref(1);
|
|
|
const tabs = computed(() => [
|
|
|
{ name: "novo", label: t("associado.new_appointment") },
|
|
|
{ name: "meus", label: t("associado.my_appointments") },
|
|
|
]);
|
|
|
|
|
|
+const columns = computed(() => [
|
|
|
+ { name: "pedido", label: t("agendamento.col.pedido"), field: "order_number", align: "left" },
|
|
|
+ { name: "parceiro", label: t("agendamento.col.parceiro"), field: (row) => row.partner_agreement?.trade_name || row.partner_agreement?.company_name || "—", align: "left" },
|
|
|
+ { name: "servico", label: t("agendamento.col.servico"), field: (row) => row.partner_agreement_service?.name || "—", align: "left" },
|
|
|
+ { name: "solicitacao", label: t("agendamento.col.solicitacao"), field: (row) => formatDate(row.created_at), align: "left" },
|
|
|
+ { name: "horario", label: t("common.terms.hour2"), field: (row) => formatDateTime(row.date, row.time), align: "left" },
|
|
|
+ { name: "acoes", label: t("common.terms.actions"), field: "id", align: "center" },
|
|
|
+ { name: "status", label: t("common.terms.status"), field: "status", align: "center" },
|
|
|
+]);
|
|
|
+
|
|
|
const selectedPartner = ref(null);
|
|
|
const selectedService = ref(null);
|
|
|
-const appointmentForm = reactive({ date: "", time: "", notes: "" });
|
|
|
+const appointmentForm = reactive({ date: "", time: "", observations: "" });
|
|
|
const submitting = ref(false);
|
|
|
+const editingId = ref(null);
|
|
|
|
|
|
const appointments = ref([]);
|
|
|
const loadingList = ref(false);
|
|
|
-const cancellingId = ref(null);
|
|
|
+
|
|
|
+const totalPages = computed(() => Math.max(1, Math.ceil(appointments.value.length / PER_PAGE)));
|
|
|
+
|
|
|
+const pagedAppointments = computed(() => {
|
|
|
+ const start = (currentPage.value - 1) * PER_PAGE;
|
|
|
+ return appointments.value.slice(start, start + PER_PAGE);
|
|
|
+});
|
|
|
|
|
|
const statusColor = (status) => {
|
|
|
- const map = { pendente: "warning", confirmado: "positive", cancelado: "negative", concluido: "grey" };
|
|
|
+ const map = { pendente: "warning", confirmado: "positive", cancelado: "negative", recusado: "negative", concluido: "grey" };
|
|
|
return map[status] ?? "grey";
|
|
|
};
|
|
|
|
|
|
-const formatDate = (date) => {
|
|
|
- if (!date) return "";
|
|
|
- const [y, m, d] = date.split("-");
|
|
|
- return `${d}/${m}/${y}`;
|
|
|
+const formatDate = (isoStr) => {
|
|
|
+ if (!isoStr) return "—";
|
|
|
+ const d = new Date(isoStr);
|
|
|
+ if (isNaN(d)) return "—";
|
|
|
+ return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
|
+};
|
|
|
+
|
|
|
+const formatDateTime = (date, time) => {
|
|
|
+ if (!date) return "—";
|
|
|
+ const d = new Date(date + "T00:00:00");
|
|
|
+ if (isNaN(d)) return "—";
|
|
|
+ const dateStr = d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" });
|
|
|
+ return time ? `${dateStr} ${time}` : dateStr;
|
|
|
};
|
|
|
|
|
|
const loadAppointments = async () => {
|
|
|
loadingList.value = true;
|
|
|
+ currentPage.value = 1;
|
|
|
try {
|
|
|
appointments.value = await getMyAppointments();
|
|
|
} catch (e) {
|
|
|
@@ -167,26 +230,35 @@ watch(activeTab, (val) => {
|
|
|
if (val === "meus") loadAppointments();
|
|
|
});
|
|
|
|
|
|
-onMounted(() => {});
|
|
|
+const resetForm = () => {
|
|
|
+ selectedPartner.value = null;
|
|
|
+ selectedService.value = null;
|
|
|
+ appointmentForm.date = "";
|
|
|
+ appointmentForm.time = "";
|
|
|
+ appointmentForm.observations = "";
|
|
|
+ editingId.value = null;
|
|
|
+};
|
|
|
|
|
|
const submitAppointment = async () => {
|
|
|
const valid = await appointmentFormRef.value?.validate();
|
|
|
if (!valid) return;
|
|
|
submitting.value = true;
|
|
|
+ const payload = {
|
|
|
+ partner_agreement_id: selectedPartner.value.value,
|
|
|
+ partner_agreement_service_id: selectedService.value.value,
|
|
|
+ date: appointmentForm.date,
|
|
|
+ time: appointmentForm.time,
|
|
|
+ observations: appointmentForm.observations,
|
|
|
+ };
|
|
|
try {
|
|
|
- await createAppointment({
|
|
|
- partner_agreement_id: selectedPartner.value.value,
|
|
|
- partner_agreement_service_id: selectedService.value.value,
|
|
|
- date: appointmentForm.date,
|
|
|
- time: appointmentForm.time,
|
|
|
- notes: appointmentForm.notes,
|
|
|
- });
|
|
|
+ if (editingId.value) {
|
|
|
+ await updateAppointment(editingId.value, payload);
|
|
|
+ } else {
|
|
|
+ payload.user_id = user.user.id;
|
|
|
+ await createAppointment(payload);
|
|
|
+ }
|
|
|
$q.notify({ type: "positive", message: t("http.success") });
|
|
|
- selectedPartner.value = null;
|
|
|
- selectedService.value = null;
|
|
|
- appointmentForm.date = "";
|
|
|
- appointmentForm.time = "";
|
|
|
- appointmentForm.notes = "";
|
|
|
+ resetForm();
|
|
|
activeTab.value = "meus";
|
|
|
} catch {
|
|
|
$q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
@@ -195,23 +267,50 @@ const submitAppointment = async () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-const onCancelAppointment = (apt) => {
|
|
|
- $q.dialog({
|
|
|
- title: t("common.ui.messages.confirm_action"),
|
|
|
- message: t("associado.confirm_cancel_appointment"),
|
|
|
- cancel: true,
|
|
|
- persistent: true,
|
|
|
- }).onOk(async () => {
|
|
|
- cancellingId.value = apt.id;
|
|
|
- try {
|
|
|
- await cancelAppointment(apt.id);
|
|
|
- await loadAppointments();
|
|
|
- $q.notify({ type: "positive", message: t("http.success") });
|
|
|
- } catch {
|
|
|
- $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
- } finally {
|
|
|
- cancellingId.value = null;
|
|
|
- }
|
|
|
- });
|
|
|
+const onEditAppointment = (apt) => {
|
|
|
+ editingId.value = apt.id;
|
|
|
+ selectedPartner.value = {
|
|
|
+ value: apt.partner_agreement_id,
|
|
|
+ label: apt.partner_agreement?.trade_name || apt.partner_agreement?.company_name || "",
|
|
|
+ };
|
|
|
+ selectedService.value = {
|
|
|
+ value: apt.partner_agreement_service_id,
|
|
|
+ label: apt.partner_agreement_service?.name || "",
|
|
|
+ };
|
|
|
+ appointmentForm.date = apt.date ?? "";
|
|
|
+ appointmentForm.time = apt.time ?? "";
|
|
|
+ appointmentForm.observations = apt.observations ?? "";
|
|
|
+ activeTab.value = "novo";
|
|
|
};
|
|
|
</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+@import "src/css/table.scss";
|
|
|
+</style>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.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; }
|
|
|
+}
|
|
|
+
|
|
|
+.btn-gradient {
|
|
|
+ background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
|
|
|
+ color: white !important;
|
|
|
+ border-radius: 8px !important;
|
|
|
+
|
|
|
+ :deep(.q-icon) { color: white !important; }
|
|
|
+}
|
|
|
+</style>
|