Selaa lähdekoodia

feat: :sparkles: feat (agendamentos-associado) criado agendamentos no layout associado

foi criado o fluxo de agendamentos no layout associado, com possibilidade de criar e ver o historico de agendamentos

fase:dev | origin:escopo
Gustavo Zanatta 3 viikkoa sitten
vanhempi
commit
c0364410c7

+ 5 - 0
src/api/appointment.js

@@ -15,6 +15,11 @@ export const cancelAppointment = async (id) => {
   return data.payload;
   return data.payload;
 };
 };
 
 
+export const updateAppointment = async (id, payload) => {
+  const { data } = await api.put(`/appointment/${id}`, payload);
+  return data.payload;
+};
+
 export const getAdminCounters = async () => {
 export const getAdminCounters = async () => {
   const { data } = await api.get("/appointment/admin/counters");
   const { data } = await api.get("/appointment/admin/counters");
   return data.payload;
   return data.payload;

+ 1 - 1
src/components/defaults/DefaultInputDatePicker.vue

@@ -103,7 +103,7 @@ const unformatDate = (value) => {
 };
 };
 
 
 const inputMask = computed(() => {
 const inputMask = computed(() => {
-  if (!inputRef.value) return "";
+  // if (!inputRef.value) return "";
 
 
   if (time) {
   if (time) {
     return masks.Brasil.datetime;
     return masks.Brasil.datetime;

+ 5 - 0
src/components/selects/PartnerAgreementServiceSelect.vue

@@ -47,6 +47,7 @@ const serviceOptions = ref([]);
 watch(
 watch(
   () => partnerAgreementId,
   () => partnerAgreementId,
   async (id) => {
   async (id) => {
+    const savedSelection = selectedService.value;
     selectedService.value = null;
     selectedService.value = null;
     serviceOptions.value = [];
     serviceOptions.value = [];
     if (!id) return;
     if (!id) return;
@@ -58,6 +59,10 @@ watch(
         value: s.id,
         value: s.id,
         data: s,
         data: s,
       }));
       }));
+      if (savedSelection?.value) {
+        const match = serviceOptions.value.find((s) => s.value === savedSelection.value);
+        if (match) selectedService.value = match;
+      }
     } catch (e) {
     } catch (e) {
       console.error(e);
       console.error(e);
     } finally {
     } finally {

+ 1 - 0
src/i18n/locales/en.json

@@ -430,6 +430,7 @@
     }
     }
   },
   },
   "associado": {
   "associado": {
+    "associado": "Associate",
     "personal_data": "Personal Data",
     "personal_data": "Personal Data",
     "registration": "Registration",
     "registration": "Registration",
     "cracha": "Badge",
     "cracha": "Badge",

+ 1 - 0
src/i18n/locales/es.json

@@ -430,6 +430,7 @@
     }
     }
   },
   },
   "associado": {
   "associado": {
+    "associado": "Asociado",
     "personal_data": "Datos Personales",
     "personal_data": "Datos Personales",
     "registration": "Matrícula",
     "registration": "Matrícula",
     "cracha": "Identificador",
     "cracha": "Identificador",

+ 1 - 0
src/i18n/locales/pt.json

@@ -430,6 +430,7 @@
     }
     }
   },
   },
   "associado": {
   "associado": {
+    "associado": "Associado",
     "personal_data": "Dados Pessoais",
     "personal_data": "Dados Pessoais",
     "registration": "Matrícula",
     "registration": "Matrícula",
     "cracha": "Crachá",
     "cracha": "Crachá",

+ 182 - 83
src/pages/associado/agendamentos/AgendamentosPage.vue

@@ -3,32 +3,44 @@
     <DefaultHeaderPage />
     <DefaultHeaderPage />
 
 
     <div class="q-pa-md">
     <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">
       <div v-if="activeTab === 'novo'" class="q-mt-md">
-        <q-card flat bordered style="max-width: 600px">
+        <q-card flat bordered >
           <q-card-section>
           <q-card-section>
-            <div class="text-h6 q-mb-md">{{ $t("associado.new_appointment") }}</div>
             <q-form ref="appointmentFormRef" @submit="submitAppointment">
             <q-form ref="appointmentFormRef" @submit="submitAppointment">
               <div class="row q-col-gutter-sm">
               <div class="row q-col-gutter-sm">
                 <PartnerAgreementSelect
                 <PartnerAgreementSelect
                   v-model="selectedPartner"
                   v-model="selectedPartner"
                   :label="$t('ui.navigation.convenios')"
                   :label="$t('ui.navigation.convenios')"
                   :rules="[inputRules.required]"
                   :rules="[inputRules.required]"
-                  class="col-12"
+                  class="col-12 input-violet"
                 />
                 />
                 <PartnerAgreementServiceSelect
                 <PartnerAgreementServiceSelect
                   v-model="selectedService"
                   v-model="selectedService"
                   :partner-agreement-id="selectedPartner?.value"
                   :partner-agreement-id="selectedPartner?.value"
                   :label="$t('associado.service')"
                   :label="$t('associado.service')"
                   :rules="[inputRules.required]"
                   :rules="[inputRules.required]"
-                  class="col-12"
+                  class="col-12 input-violet"
                 />
                 />
                 <DefaultInputDatePicker
                 <DefaultInputDatePicker
-                  v-model="appointmentForm.date"
+                  v-model:untreated-date="appointmentForm.date"
                   :label="$t('common.terms.date')"
                   :label="$t('common.terms.date')"
                   :rules="[inputRules.required]"
                   :rules="[inputRules.required]"
-                  class="col-12 col-md-6"
+                  class="col-12 col-md-6 input-violet"
+                  placeholder="dd/mm/aaaa"
+                  lazy-rules
                 />
                 />
                 <DefaultInput
                 <DefaultInput
                   v-model="appointmentForm.time"
                   v-model="appointmentForm.time"
@@ -36,21 +48,22 @@
                   :rules="[inputRules.required]"
                   :rules="[inputRules.required]"
                   mask="##:##"
                   mask="##:##"
                   placeholder="HH:MM"
                   placeholder="HH:MM"
-                  class="col-12 col-md-6"
+                  class="col-12 col-md-6 input-violet"
                 />
                 />
                 <DefaultInput
                 <DefaultInput
-                  v-model="appointmentForm.notes"
+                  v-model="appointmentForm.observations"
                   :label="$t('associado.notes')"
                   :label="$t('associado.notes')"
                   type="textarea"
                   type="textarea"
                   autogrow
                   autogrow
-                  class="col-12"
+                  class="col-12 input-violet"
                 />
                 />
               </div>
               </div>
               <div class="q-mt-md flex justify-end">
               <div class="q-mt-md flex justify-end">
                 <q-btn
                 <q-btn
-                  color="primary"
+                  unelevated
                   type="submit"
                   type="submit"
-                  :label="$t('associado.schedule')"
+                  class="btn-gradient"
+                  :label="editingId ? $t('common.actions.save') : $t('associado.schedule')"
                   :loading="submitting"
                   :loading="submitting"
                 />
                 />
               </div>
               </div>
@@ -59,101 +72,151 @@
         </q-card>
         </q-card>
       </div>
       </div>
 
 
+      <!-- Tab: Meus Agendamentos -->
       <div v-else class="q-mt-md">
       <div v-else class="q-mt-md">
         <div v-if="loadingList" class="flex flex-center q-py-xl">
         <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>
 
 
         <div v-else-if="appointments.length === 0" class="text-center text-grey q-py-xl">
         <div v-else-if="appointments.length === 0" class="text-center text-grey q-py-xl">
           {{ $t("http.errors.no_records_found") }}
           {{ $t("http.errors.no_records_found") }}
         </div>
         </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
               <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"
                 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
               <q-btn
+                dense
+                round
                 flat
                 flat
+                icon="mdi-pencil"
+                color="violet-normal"
+                size="sm"
+                :disable="props.row.status !== 'pendente'"
+                @click="onEditAppointment(props.row)"
+              />
+              <q-btn
+                dense
                 round
                 round
-                icon="mdi-cancel"
-                color="negative"
+                flat
+                icon="mdi-calendar"
+                color="violet-normal"
                 size="sm"
                 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>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, reactive, computed, watch, onMounted, useTemplateRef } from "vue";
+import { ref, reactive, computed, watch, useTemplateRef } from "vue";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 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 { useInputRules } from "src/composables/useInputRules";
-
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 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 DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
 import PartnerAgreementSelect from "src/components/selects/PartnerAgreementSelect.vue";
 import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
 import PartnerAgreementServiceSelect from "src/components/selects/PartnerAgreementServiceSelect.vue";
-
+import { userStore } from "src/stores/user";
 const $q = useQuasar();
 const $q = useQuasar();
 const { t } = useI18n();
 const { t } = useI18n();
 const { inputRules } = useInputRules();
 const { inputRules } = useInputRules();
 const appointmentFormRef = useTemplateRef("appointmentFormRef");
 const appointmentFormRef = useTemplateRef("appointmentFormRef");
-
+const user = userStore();
 const activeTab = ref("novo");
 const activeTab = ref("novo");
+const PER_PAGE = 12;
+const currentPage = ref(1);
 const tabs = computed(() => [
 const tabs = computed(() => [
   { name: "novo", label: t("associado.new_appointment") },
   { name: "novo", label: t("associado.new_appointment") },
   { name: "meus", label: t("associado.my_appointments") },
   { 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 selectedPartner = ref(null);
 const selectedService = ref(null);
 const selectedService = ref(null);
-const appointmentForm = reactive({ date: "", time: "", notes: "" });
+const appointmentForm = reactive({ date: "", time: "", observations: "" });
 const submitting = ref(false);
 const submitting = ref(false);
+const editingId = ref(null);
 
 
 const appointments = ref([]);
 const appointments = ref([]);
 const loadingList = ref(false);
 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 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";
   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 () => {
 const loadAppointments = async () => {
   loadingList.value = true;
   loadingList.value = true;
+  currentPage.value = 1;
   try {
   try {
     appointments.value = await getMyAppointments();
     appointments.value = await getMyAppointments();
   } catch (e) {
   } catch (e) {
@@ -167,26 +230,35 @@ watch(activeTab, (val) => {
   if (val === "meus") loadAppointments();
   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 submitAppointment = async () => {
   const valid = await appointmentFormRef.value?.validate();
   const valid = await appointmentFormRef.value?.validate();
   if (!valid) return;
   if (!valid) return;
   submitting.value = true;
   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 {
   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") });
     $q.notify({ type: "positive", message: t("http.success") });
-    selectedPartner.value = null;
-    selectedService.value = null;
-    appointmentForm.date = "";
-    appointmentForm.time = "";
-    appointmentForm.notes = "";
+    resetForm();
     activeTab.value = "meus";
     activeTab.value = "meus";
   } catch {
   } catch {
     $q.notify({ type: "negative", message: t("http.errors.failed") });
     $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>
 </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>