| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- <template>
- <q-dialog ref="dialogRef" @hide="onDialogHide">
- <q-card
- class="q-dialog-plugin overflow-hidden"
- style="width: 100%; max-width: 1350px"
- >
- <DefaultDialogHeader
- :title="props.contract ? 'Editar Contrato' : 'Novo Contrato'"
- @close="onDialogCancel"
- />
- <template v-if="props.contract">
- <q-tabs
- v-model="activeTab"
- dense
- align="left"
- class="q-px-md text-grey-7"
- active-color="primary"
- indicator-color="primary"
- >
- <q-tab name="dados" label="Dados do Contrato" />
- <q-tab name="midias" label="Mídias do Contrato" />
- </q-tabs>
- <q-separator />
- </template>
- <q-card-section class="q-pt-sm" style="height: 65vh; overflow-y: auto">
- <div v-show="activeTab === 'dados'">
- <div class="text-subtitle1 q-mb-md">Dados do Aluno</div>
- <div class="row q-col-gutter-sm">
- <div class="col-12">
- <DefaultInput
- :model-value="props.student.name"
- label="Aluno"
- disable
- />
- </div>
- <div class="col-6">
- <DefaultInput
- :model-value="props.student.document_number"
- label="CPF"
- disable
- />
- </div>
- <div class="col-6">
- <DefaultInput
- :model-value="formattedBirthDate"
- label="Data de Nascimento"
- disable
- />
- </div>
- </div>
- <div class="text-subtitle1 q-mt-lg q-mb-md">Dados do Contrato</div>
- <div class="row q-col-gutter-sm">
- <div class="col-4">
- <DefaultInput v-model="form.protocol" label="Protocolo" />
- </div>
- <div class="col-4">
- <DefaultInputDatePicker
- v-model="form.signature_date"
- label="Data Assinatura"
- />
- </div>
- <div class="col-4">
- <DefaultInputDatePicker
- v-model="form.end_date"
- label="Data Encerramento"
- />
- </div>
- <div class="col-5">
- <DefaultSelect
- v-model="form.package_id"
- label="Pacote de Aulas"
- :options="packages"
- option-value="id"
- option-label="name"
- emit-value
- map-options
- />
- </div>
- <div class="col-7">
- <DefaultInput
- v-model="form.class_quantity"
- label="Qtd. Aulas"
- type="number"
- disable
- />
- </div>
- <div class="col-4">
- <DefaultSelect
- v-model="form.weekday"
- label="Dia da Semana"
- :options="weekdays"
- option-value="value"
- option-label="label"
- emit-value
- map-options
- />
- </div>
- <div class="col-4">
- <DefaultInput
- v-model="form.start_time"
- label="Hora de Início"
- mask="##:##"
- >
- <template #append>
- <q-icon name="mdi-clock-outline" />
- </template>
- </DefaultInput>
- </div>
- <div class="col-4">
- <DefaultInput
- v-model="form.end_time"
- label="Hora de Término"
- mask="##:##"
- >
- <template #append>
- <q-icon name="mdi-clock-outline" />
- </template>
- </DefaultInput>
- </div>
- <div class="col-4">
- <DefaultSelect
- v-model="form.second_weekday"
- label="2° Dia da Semana"
- :options="weekdays"
- option-value="value"
- option-label="label"
- emit-value
- map-options
- />
- </div>
- <div class="col-4">
- <DefaultInput
- v-model="form.second_start_time"
- label="Hora de Início"
- mask="##:##"
- >
- <template #append>
- <q-icon name="mdi-clock-outline" />
- </template>
- </DefaultInput>
- </div>
- <div class="col-4">
- <DefaultInput
- v-model="form.second_end_time"
- label="Hora de Término"
- mask="##:##"
- >
- <template #append>
- <q-icon name="mdi-clock-outline" />
- </template>
- </DefaultInput>
- </div>
- </div>
- <div class="text-subtitle1 q-mt-lg q-mb-md">Dados Financeiros</div>
- <div class="row q-col-gutter-sm">
- <div class="col-4">
- <DefaultInput
- v-model="form.due_day"
- label="Dia de Vencimento"
- type="number"
- />
- </div>
- </div>
- <div class="text-subtitle2 q-mt-md q-mb-sm text-grey-8">Matrícula</div>
- <div class="row q-col-gutter-sm">
- <div class="col-3">
- <DefaultCurrencyInput
- v-model="form.enrollment_fee"
- label="Valor da Matrícula"
- disable
- />
- </div>
- <div class="col-3">
- <DefaultSelect
- v-model="form.installments"
- label="Qtde Parcelas"
- :options="enrollmentInstallmentOptions"
- option-value="value"
- option-label="label"
- emit-value
- map-options
- />
- </div>
- <div class="col-3">
- <DefaultCurrencyInput
- :model-value="enrollmentInstallmentValue"
- label="Valor da Parcela"
- disable
- />
- </div>
- <div class="col-3">
- <DefaultInputDatePicker
- v-model="form.enrollment_due_date"
- label="Data Vencimento"
- />
- </div>
- </div>
- <div class="text-subtitle2 q-mt-md q-mb-sm text-grey-8">Pacote</div>
- <div class="row q-col-gutter-sm">
- <div class="col-3">
- <DefaultCurrencyInput
- v-model="form.package_value"
- label="Valor do Pacote"
- disable
- />
- </div>
- <div class="col-3">
- <DefaultSelect
- v-model="form.package_installments"
- label="Qtde Parcelas"
- :options="packageInstallmentOptions"
- option-value="value"
- option-label="label"
- emit-value
- map-options
- />
- </div>
- <div class="col-3">
- <DefaultCurrencyInput
- :model-value="packageInstallmentValue"
- label="Valor da Parcela"
- disable
- />
- </div>
- <div class="col-3">
- <DefaultInputDatePicker
- v-model="form.package_due_date"
- label="Data 1ª Parcela"
- />
- </div>
- </div>
- <div class="row q-col-gutter-sm q-mt-xs">
- <div class="col-6">
- <DefaultInput
- v-model="form.early_payment_discount"
- label="Desconto até o vencimento (%)"
- type="number"
- />
- </div>
- <div class="col-6">
- <DefaultSelect
- v-model="form.payment_method"
- label="Forma de Pagamento"
- :options="paymentMethods"
- option-value="value"
- option-label="label"
- emit-value
- map-options
- />
- </div>
- <div class="col-6">
- <DefaultInput
- v-model="form.interest_rate"
- label="Juros (%) a.m"
- type="number"
- />
- </div>
- <div class="col-6">
- <DefaultInput
- v-model="form.late_fee"
- label="Multa (%)"
- type="number"
- />
- </div>
- </div>
- </div>
- <div v-if="props.contract" v-show="activeTab === 'midias'">
- <DefaultTable
- v-model:rows="medias"
- title="Mídias"
- :columns="mediaColumns"
- descricao="mídias"
- :feminino="true"
- no-api-call
- :show-search-field="false"
- :loading="loadingMedias"
- >
- <template #body-cell-actions="{ row }">
- <q-td align="center">
- <q-item-section class="no-wrap" style="flex-direction: row; gap: 4px">
- <q-btn
- outline
- icon="mdi-eye-outline"
- style="width: 36px"
- @click.prevent.stop="openFile(row.file_url)"
- />
- <q-btn
- outline
- icon="mdi-trash-can-outline"
- style="width: 36px"
- color="negative"
- @click.prevent.stop="handleDeleteMedia(row)"
- />
- </q-item-section>
- </q-td>
- </template>
- </DefaultTable>
- </div>
- </q-card-section>
- <q-separator />
- <q-card-actions align="right">
- <q-btn
- outline
- color="primary"
- label="CANCELAR"
- @click="onDialogCancel"
- />
- <q-btn
- v-if="activeTab === 'dados'"
- color="primary"
- label="SALVAR"
- :loading="saving"
- @click="handleSave"
- />
- </q-card-actions>
- </q-card>
- </q-dialog>
- </template>
- <script setup>
- import { computed, ref, watch, onMounted, nextTick } from "vue";
- import { useDialogPluginComponent, useQuasar } from "quasar";
- 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 DefaultTable from "src/components/defaults/DefaultTable.vue";
- import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
- import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
- import { useSubmitHandler } from "src/composables/useSubmitHandler";
- import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
- import { formatDateYMDtoDMY, formatDateDMYtoYMD } from "src/helpers/utils";
- import { getUnitPackages } from "src/api/package";
- import {
- createStudentContract,
- updateStudentContract,
- } from "src/api/studentContract";
- import { getContractMedias, deleteStudentMedia } from "src/api/student_media";
- const props = defineProps({
- student: {
- type: Object,
- required: true,
- },
- contract: {
- type: Object,
- default: null,
- },
- });
- defineEmits([...useDialogPluginComponent.emits]);
- const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
- useDialogPluginComponent();
- const $q = useQuasar();
- const activeTab = ref("dados");
- const medias = ref([]);
- const loadingMedias = ref(false);
- const trimTime = (t) => (t ? t.slice(0, 5) : null);
- const { form } = useFormUpdateTracker({
- protocol: props.contract?.protocol ?? null,
- signature_date: props.contract?.signature_date ?? null,
- end_date: props.contract?.end_date ?? null,
- package_id: props.contract?.class_package_unit_id ?? null,
- class_quantity: props.contract?.class_quantity ?? null,
- weekday: props.contract?.weekday ?? null,
- start_time: trimTime(props.contract?.start_time),
- end_time: trimTime(props.contract?.end_time),
- second_weekday: props.contract?.second_weekday ?? null,
- second_start_time: trimTime(props.contract?.second_start_time),
- second_end_time: trimTime(props.contract?.second_end_time),
- due_day: props.contract?.recurring_day ?? null,
- enrollment_fee: props.contract?.tax_register ?? null,
- total_classes: props.contract?.class_quantity ?? null,
- installments: props.contract?.installments ?? null,
- enrollment_due_date: props.contract?.enrollment_due_date ?? null,
- package_value: props.contract?.package_value ?? null,
- package_installments: props.contract?.package_installments ?? null,
- package_due_date: props.contract?.package_due_date ?? null,
- early_payment_discount: props.contract?.early_payment_discount ?? null,
- interest_rate: props.contract?.interest_rate ?? null,
- payment_method: props.contract?.payment_method ?? null,
- late_fee: props.contract?.fine_cancelled ?? null,
- });
- const enrollmentInstallmentOptions = Array.from({ length: 12 }, (_, i) => ({
- value: i + 1,
- label: `${i + 1}x`,
- }));
- const packageInstallmentOptions = Array.from({ length: 13 }, (_, i) => ({
- value: i + 1,
- label: `${i + 1}x`,
- }));
- const enrollmentInstallmentValue = computed(() => {
- if (!form.enrollment_fee || !form.installments) return null;
- return parseFloat((form.enrollment_fee / form.installments).toFixed(2));
- });
- const packageInstallmentValue = computed(() => {
- if (!form.package_value || !form.package_installments) return null;
- return parseFloat((form.package_value / form.package_installments).toFixed(2));
- });
- // Two-way binding: due_day <-> package_due_date
- const _syncingFromDay = ref(false);
- const _syncingFromDate = ref(false);
- function buildDateFromDay(day) {
- const d = parseInt(day);
- if (!d || d < 1 || d > 31) return null;
- const now = new Date();
- const nextMonthIndex = (now.getMonth() + 1) % 12;
- const nextYear = now.getMonth() === 11 ? now.getFullYear() + 1 : now.getFullYear();
- const lastDayOfNextMonth = new Date(nextYear, nextMonthIndex + 1, 0).getDate();
- const safeDay = Math.min(d, lastDayOfNextMonth);
- return `${String(safeDay).padStart(2, '0')}/${String(nextMonthIndex + 1).padStart(2, '0')}/${nextYear}`;
- }
- watch(
- () => form.due_day,
- (day) => {
- if (_syncingFromDate.value) return;
- const dateStr = buildDateFromDay(day);
- if (!dateStr) return;
- _syncingFromDay.value = true;
- form.package_due_date = dateStr;
- nextTick(() => { _syncingFromDay.value = false; });
- },
- );
- watch(
- () => form.package_due_date,
- (dateStr) => {
- if (_syncingFromDay.value) return;
- if (!dateStr || dateStr.length < 8) return;
- const day = parseInt(dateStr.split('/')[0]);
- if (!isNaN(day) && day !== parseInt(form.due_day)) {
- _syncingFromDate.value = true;
- form.due_day = day;
- nextTick(() => { _syncingFromDate.value = false; });
- }
- },
- );
- const paymentMethods = [
- { value: "pix", label: "Pix" },
- { value: "credit_card", label: "Cartão de Crédito" },
- { value: "debit_card", label: "Cartão de Débito" },
- ];
- const weekdays = [
- { value: 1, label: "Segunda" },
- { value: 2, label: "Terça" },
- { value: 3, label: "Quarta" },
- { value: 4, label: "Quinta" },
- { value: 5, label: "Sexta" },
- { value: 6, label: "Sábado" },
- { value: 0, label: "Domingo" },
- ];
- const packages = ref([]);
- const mediaColumns = [
- { name: "created_at", label: "Data de Anexo", field: "created_at", align: "left" },
- { name: "actions", label: "Ações", field: null, align: "center" },
- ];
- async function fetchMedias() {
- if (!props.contract) return;
- loadingMedias.value = true;
- try {
- medias.value = await getContractMedias(props.contract.id);
- } finally {
- loadingMedias.value = false;
- }
- }
- watch(activeTab, (tab) => {
- if (tab === "midias") fetchMedias();
- });
- onMounted(async () => {
- packages.value = await getUnitPackages();
- });
- function openFile(url) {
- window.open(url, "_blank");
- }
- function handleDeleteMedia(media) {
- $q.dialog({
- title: "Excluir mídia",
- message: "Deseja excluir esta mídia permanentemente?",
- ok: { color: "negative", label: "Excluir" },
- cancel: { color: "primary", outline: true, label: "Cancelar" },
- }).onOk(async () => {
- try {
- await deleteStudentMedia(media.id);
- medias.value = medias.value.filter((m) => m.id !== media.id);
- } catch (e) {
- console.error(e);
- $q.notify({ type: "negative", message: "Erro ao excluir mídia." });
- }
- });
- }
- watch(
- () => form.package_id,
- (id) => {
- const pkg = packages.value.find((p) => p.id === id);
- if (!pkg) return;
- form.class_quantity = pkg.quantity_classes;
- form.total_classes = pkg.quantity_classes;
- form.enrollment_fee = pkg.contract_register_value;
- form.package_value = pkg.contract_value;
- },
- );
- const formattedBirthDate = computed(() =>
- props.student.birth_date ? formatDateYMDtoDMY(props.student.birth_date) : "",
- );
- const { loading: saving, execute } = useSubmitHandler({
- onSuccess: () => onDialogOK(true),
- });
- function buildPayload() {
- return {
- student_id: props.student.id,
- protocol: form.protocol,
- signature_date: form.signature_date
- ? formatDateDMYtoYMD(form.signature_date)
- : null,
- end_date: form.end_date ? formatDateDMYtoYMD(form.end_date) : null,
- class_package_unit_id: form.package_id,
- class_quantity: form.class_quantity,
- weekday: form.weekday,
- start_time: form.start_time,
- end_time: form.end_time,
- second_weekday: form.second_weekday,
- second_start_time: form.second_start_time,
- second_end_time: form.second_end_time,
- due_day: form.due_day ? parseInt(form.due_day) : null,
- tax_register: form.enrollment_fee,
- installments: form.installments,
- enrollment_due_date: form.enrollment_due_date
- ? formatDateDMYtoYMD(form.enrollment_due_date)
- : null,
- package_value: form.package_value,
- package_installments: form.package_installments,
- package_due_date: form.package_due_date
- ? formatDateDMYtoYMD(form.package_due_date)
- : null,
- early_payment_discount: form.early_payment_discount,
- interest_rate: form.interest_rate,
- payment_method: form.payment_method,
- fine_cancelled: form.late_fee,
- };
- }
- async function handleSave() {
- const payload = buildPayload();
- await execute(() =>
- props.contract
- ? updateStudentContract(props.contract.id, payload)
- : createStudentContract(payload),
- );
- }
- </script>
|