ebagabee 2 тижнів тому
батько
коміт
62bd863162

+ 21 - 0
src/api/student.js

@@ -0,0 +1,21 @@
+import api from "src/api";
+
+export const getStudents = async () => {
+  const { data } = await api.get("/student");
+  return data.payload;
+};
+
+export const createStudent = async (student) => {
+  const { data } = await api.post("/student", student);
+  return data.payload;
+};
+
+export const updateStudent = async (student, id) => {
+  const { data } = await api.put(`/student/${id}`, student);
+  return data.payload;
+};
+
+export const deleteStudent = async (id) => {
+  const { data } = await api.delete(`/student/${id}`);
+  return data.payload;
+};

+ 3 - 4
src/components/defaults/DefaultDialogHeader.vue

@@ -1,7 +1,7 @@
 <template>
   <q-bar class="bg-transparent q-px-md" v-bind="$attrs" style="height: 55px">
     <q-icon v-if="icon" :name="icon" />
-    <div>{{ title() }}</div>
+    <div>{{ title }}</div>
 
     <q-space />
 
@@ -19,14 +19,13 @@
 
 <script setup>
 import { ref, onMounted } from "vue";
-import { useI18n } from "vue-i18n";
 
 const emit = defineEmits(["maximized", "close"]);
 
 const { title, fullscreen, maximizable, icon } = defineProps({
   title: {
-    type: Function,
-    default: () => useI18n().t("common.terms.title"),
+    type: String,
+    default: "Titulo",
   },
   fullscreen: {
     type: Boolean,

+ 44 - 30
src/components/defaults/DefaultInput.vue

@@ -5,22 +5,23 @@
         ref="inputRef"
         v-model="model"
         v-bind="inputAttrs"
+        hide-bottom-space
+        label-color="secondary"
+        color="secondary"
         :label
-        label-color="dark"
         :error="!!error"
         :error-message="errorMessage"
         :rules
-        hide-bottom-space
+        :outlined
+        :bg-color
         :class="inputClass"
         :input-class="nativeInputClass"
         @update:model-value="error = null"
       >
-        <template v-for="(_, slotName) in $slots" #[slotName]>
-          <slot :name="slotName" />
-        </template>
-
         <template #append>
-          <q-icon v-if="icon" :name="icon" size="sm" color="secondary" />
+          <slot name="append">
+            <q-icon v-if="icon" :name="icon" size="sm" color="secondary" />
+          </slot>
         </template>
       </q-input>
     </div>
@@ -41,28 +42,37 @@ defineOptions({
   inheritAttrs: false,
 });
 
-const { label, nativeInputClass, inputClass, rules, icon } = defineProps({
-  label: {
-    type: String,
-    default: "",
-  },
-  icon: {
-    type: String,
-    default: "",
-  },
-  rules: {
-    type: Array,
-    default: () => [],
-  },
-  nativeInputClass: {
-    type: String,
-    default: null,
-  },
-  inputClass: {
-    type: String,
-    default: null,
-  },
-});
+const { label, nativeInputClass, inputClass, rules, icon, bgColor, outlined } =
+  defineProps({
+    label: {
+      type: String,
+      default: "",
+    },
+    icon: {
+      type: String,
+      default: "",
+    },
+    rules: {
+      type: Array,
+      default: () => [],
+    },
+    nativeInputClass: {
+      type: String,
+      default: null,
+    },
+    inputClass: {
+      type: String,
+      default: null,
+    },
+    bgColor: {
+      type: String,
+      default: "white",
+    },
+    outlined: {
+      type: Boolean,
+      default: false,
+    },
+  });
 
 const attrs = useAttrs();
 
@@ -111,4 +121,8 @@ defineExpose({
 });
 </script>
 
-<style scoped></style>
+<style scoped lang="scss">
+:deep(.q-field--outlined.q-field--rounded .q-field__control) {
+  border-radius: 8px;
+}
+</style>

+ 30 - 7
src/components/defaults/DefaultSelect.vue

@@ -1,27 +1,30 @@
 <template>
   <div class="column" :class="attrs.class" :style="attrs.style">
-    <div v-if="label || $slots.label" class="q-pl-xs">
-      <slot name="label">
-        <span>{{ label }}</span>
-      </slot>
-      <span v-if="required" class="text-negative q-ml-xs">*</span>
-    </div>
     <div class="col">
       <q-select
         ref="selectRef"
         v-model="model"
+        :label
+        label-color="secondary"
         v-bind="selectAttrs"
         :error="!!error"
         :error-message="errorMessage"
         :rules
+        :outlined
         hide-bottom-space
+        :bg-color
         :class="inputClass"
         :popup-content-class="popupContentClass"
+        hide-dropdown-icon
         @update:model-value="error = null"
       >
         <template v-for="(_, slotName) in $slots" #[slotName]="scope">
           <slot :name="slotName" v-bind="scope" />
         </template>
+
+        <template #append>
+          <q-icon :name="dropdownIcon" color="secondary" />
+        </template>
       </q-select>
     </div>
   </div>
@@ -34,7 +37,15 @@ defineOptions({
   inheritAttrs: false,
 });
 
-const { label, inputClass, popupContentClass, rules } = defineProps({
+const {
+  label,
+  inputClass,
+  popupContentClass,
+  rules,
+  bgColor,
+  outlined,
+  dropdownIcon,
+} = defineProps({
   label: {
     type: String,
     default: "",
@@ -51,6 +62,18 @@ const { label, inputClass, popupContentClass, rules } = defineProps({
     type: String,
     default: null,
   },
+  bgColor: {
+    type: String,
+    default: "white",
+  },
+  outlined: {
+    type: Boolean,
+    default: false,
+  },
+  dropdownIcon: {
+    type: String,
+    default: "mdi-chevron-down",
+  },
 });
 
 const attrs = useAttrs();

+ 19 - 14
src/pages/students/StudentPage.vue

@@ -4,7 +4,6 @@
 
     <div class="q-px-sm">
       <DefaultTable
-        ref="tableRef"
         v-model:rows="rows"
         title="Lista de alunos"
         :columns
@@ -12,6 +11,7 @@
         :feminino="false"
         no-api-call
         add-item
+        :loading="isLoading"
         @on-add-item="onAddStudent"
       >
         <template #body-cell-contato="{ row }">
@@ -56,17 +56,20 @@
 </template>
 
 <script setup>
-import { defineAsyncComponent, ref, useTemplateRef } from "vue";
+import { defineAsyncComponent, ref, onMounted } from "vue";
 import { useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import { getStudents } from "src/api/student";
 
 const AddEditStudentDialog = defineAsyncComponent(
   () => import("src/pages/students/components/AddEditStudentDialog.vue"),
 );
 
 const $q = useQuasar();
-const tableRef = useTemplateRef("tableRef");
+
+const rows = ref([]);
+const isLoading = ref(false);
 
 const columns = ref([
   {
@@ -95,22 +98,24 @@ const columns = ref([
   },
 ]);
 
+async function loadStudents() {
+  isLoading.value = true;
+  try {
+    rows.value = await getStudents();
+  } catch (error) {
+    console.error("Failed to load students:", error);
+  } finally {
+    isLoading.value = false;
+  }
+}
+
 function onAddStudent() {
   $q.dialog({
     component: AddEditStudentDialog,
   }).onOk(() => {
-    tableRef.value?.refresh();
+    loadStudents();
   });
 }
 
-const rows = ref([
-  { id: 1, name: "Heloisa Faria", phone: "(45)99999-9999", city: "Toledo-PR", status: "inactive" },
-  { id: 2, name: "Carol", phone: "(45)99999-9999", city: "Arapongas-PR", status: "inactive" },
-  { id: 3, name: "Marcelo Souza", phone: "(45)98888-8888", city: "Curitiba-PR", status: "active" },
-  { id: 4, name: "Ana Lucia", phone: "(45)97777-7777", city: "Londrina-PR", status: "active" },
-  { id: 5, name: "Ricardo Silva", phone: "(45)96666-6666", city: "Ponta Grossa-PR", status: "active" },
-  { id: 6, name: "Juliana Costa", phone: "(45)95555-5555", city: "Maringá-PR", status: "active" },
-  { id: 7, name: "Fernando Almeida", phone: "(45)94444-4444", city: "Cascavel-PR", status: "active" },
-  { id: 8, name: "Patricia Lima", phone: "(45)93333-3333", city: "Foz do Iguaçu-PR", status: "active" },
-]);
+onMounted(loadStudents);
 </script>

+ 56 - 15
src/pages/students/components/AddEditStudentDialog.vue

@@ -15,6 +15,7 @@
               v-model="form.name"
               label="Nome do aluno"
               class="col-md-5 col-12"
+              :rules="[inputRules.required]"
             />
 
             <DefaultInputDatePicker
@@ -27,7 +28,12 @@
               <div style="position: relative; display: inline-block">
                 <q-avatar size="72px" color="grey-3">
                   <img v-if="avatarPreview" :src="avatarPreview" />
-                  <q-icon v-else name="mdi-account" size="42px" color="grey-6" />
+                  <q-icon
+                    v-else
+                    name="mdi-account"
+                    size="42px"
+                    color="grey-6"
+                  />
                 </q-avatar>
                 <q-btn
                   round
@@ -52,6 +58,8 @@
               v-model="form.cpf"
               label="CPF / CNH"
               class="col-md-6 col-12"
+              :mask="masks.Brasil.cpf"
+              :rules="[inputRules.cpf]"
             />
 
             <DefaultSelect
@@ -68,18 +76,22 @@
               label="E-mail"
               class="col-md-6 col-12"
               type="email"
+              :rules="[inputRules.email]"
             />
 
             <DefaultInput
               v-model="form.phone"
               label="Celular com DDD"
               class="col-md-6 col-12"
+              :mask="masks.Brasil.celular"
             />
 
             <DefaultInput
               v-model="form.cep"
-              label="Cep"
+              label="CEP"
               class="col-md-3 col-12"
+              :mask="masks.Brasil.cep"
+              :rules="[inputRules.cep]"
             />
 
             <DefaultInput
@@ -90,7 +102,7 @@
 
             <DefaultInput
               v-model="form.address_number"
-              label="Numero"
+              label="Número"
               class="col-md-3 col-12"
             />
 
@@ -100,10 +112,11 @@
               class="col-md-6 col-12"
             />
 
-            <DefaultInput
-              v-model="form.city_state"
-              label="Cidade / Estado"
+            <StateSelect
+              v-model="form.state"
+              label="Estado"
               class="col-md-6 col-12"
+              outlined
             />
 
             <DefaultInput
@@ -164,15 +177,22 @@ import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
+import StateSelect from "src/components/selects/StateSelect.vue";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { createStudent } from "src/api/student";
+import masks from "src/helpers/masks";
+import { formatDateDMYtoYMD } from "src/helpers/utils";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
+const { inputRules } = useInputRules();
+
 const formRef = useTemplateRef("formRef");
 const fileInputRef = useTemplateRef("fileInputRef");
-const loading = ref(false);
 const avatarPreview = ref(null);
 
 const form = ref({
@@ -186,7 +206,7 @@ const form = ref({
   address: null,
   address_number: null,
   neighborhood: null,
-  city_state: null,
+  state: null,
   complement: null,
   payer: null,
   how_found: null,
@@ -207,6 +227,33 @@ const howFoundOptions = ref([
   { label: "Outro", value: "other" },
 ]);
 
+const { loading, execute } = useSubmitHandler({
+  formRef,
+  onSuccess: () => {
+    onDialogOK(true);
+  },
+});
+
+function buildPayload() {
+  return {
+    name: form.value.name,
+    birth_date: form.value.birthdate ? formatDateDMYtoYMD(form.value.birthdate) : null,
+    document_number: form.value.cpf,
+    gender: form.value.gender,
+    email: form.value.email || null,
+    phone: form.value.phone,
+    postal_code: form.value.cep,
+    street: form.value.address,
+    address_number: form.value.address_number,
+    neighborhood: form.value.neighborhood,
+    state_id: form.value.state?.value ?? null,
+    complement: form.value.complement,
+    payer_name: form.value.payer,
+    how_did_you_know_us: form.value.how_found,
+    notes: form.value.notes,
+  };
+}
+
 function triggerFileInput() {
   fileInputRef.value?.click();
 }
@@ -219,12 +266,6 @@ function onAvatarChange(event) {
 }
 
 async function onOKClick() {
-  loading.value = true;
-  try {
-    console.log("Saving student:", form.value);
-    onDialogOK(true);
-  } finally {
-    loading.value = false;
-  }
+  await execute(() => createStudent(buildPayload()));
 }
 </script>