|
|
@@ -0,0 +1,217 @@
|
|
|
+<template>
|
|
|
+ <q-dialog ref="dialogRef" @hide="onDialogHide">
|
|
|
+ <q-card class="q-dialog-plugin overflow-hidden" style="width: 100%; max-width: 860px">
|
|
|
+ <DefaultDialogHeader title="Trancar Contrato" @close="onDialogCancel" />
|
|
|
+
|
|
|
+ <q-card-section class="q-pt-sm" style="max-height: 70vh; overflow-y: auto">
|
|
|
+
|
|
|
+ <!-- Seleção de meses -->
|
|
|
+ <div class="row q-col-gutter-sm q-mb-md items-center">
|
|
|
+ <div class="col-12 col-md-4">
|
|
|
+ <DefaultSelect
|
|
|
+ v-model="selectedMonths"
|
|
|
+ label="Tempo de Trancamento"
|
|
|
+ :options="monthOptions"
|
|
|
+ option-value="value"
|
|
|
+ option-label="label"
|
|
|
+ :option-disable="opt => opt.disable"
|
|
|
+ emit-value
|
|
|
+ map-options
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="selectedMonths" class="col-12 col-md-8">
|
|
|
+ <q-banner class="bg-blue-1 text-blue-9" dense rounded>
|
|
|
+ <template #avatar>
|
|
|
+ <q-icon name="mdi-information-outline" color="blue-7" />
|
|
|
+ </template>
|
|
|
+ {{ pendingInstallments.length }} parcela(s) pendente(s) serão adiadas
|
|
|
+ <strong>{{ selectedMonths }} {{ selectedMonths === 1 ? 'mês' : 'meses' }}</strong>.
|
|
|
+ </q-banner>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tabela de parcelas / preview -->
|
|
|
+ <div v-if="loading" class="flex flex-center q-py-lg">
|
|
|
+ <q-spinner color="primary" size="32px" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else-if="!pendingInstallments.length" class="text-center text-grey-6 q-py-lg">
|
|
|
+ Nenhuma parcela pendente encontrada para este contrato.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <q-table
|
|
|
+ v-else
|
|
|
+ :rows="previewRows"
|
|
|
+ :columns="tableColumns"
|
|
|
+ flat
|
|
|
+ bordered
|
|
|
+ dense
|
|
|
+ hide-pagination
|
|
|
+ :rows-per-page-options="[0]"
|
|
|
+ class="q-mt-sm"
|
|
|
+ >
|
|
|
+ <template #body-cell-new_due_date="{ row }">
|
|
|
+ <q-td align="center">
|
|
|
+ <span
|
|
|
+ v-if="row.new_due_date"
|
|
|
+ class="text-positive text-weight-medium"
|
|
|
+ >
|
|
|
+ {{ row.new_due_date }}
|
|
|
+ </span>
|
|
|
+ <span v-else class="text-grey-5">—</span>
|
|
|
+ </q-td>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #body-cell-value="{ row }">
|
|
|
+ <q-td align="right">
|
|
|
+ {{ formatCurrency(row.value) }}
|
|
|
+ </q-td>
|
|
|
+ </template>
|
|
|
+ </q-table>
|
|
|
+
|
|
|
+ </q-card-section>
|
|
|
+
|
|
|
+ <q-separator />
|
|
|
+
|
|
|
+ <q-card-actions align="right">
|
|
|
+ <q-btn outline color="primary" label="CANCELAR" @click="onDialogCancel" />
|
|
|
+ <q-btn
|
|
|
+ color="primary"
|
|
|
+ label="CONFIRMAR TRANCAMENTO"
|
|
|
+ :loading="saving"
|
|
|
+ :disable="!selectedMonths || !pendingInstallments.length"
|
|
|
+ @click="handleConfirm"
|
|
|
+ />
|
|
|
+ </q-card-actions>
|
|
|
+ </q-card>
|
|
|
+ </q-dialog>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
+import { useDialogPluginComponent, useQuasar } from "quasar";
|
|
|
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
|
|
|
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
|
|
|
+import { getContractInstallments, freezeContract } from "src/api/studentContract";
|
|
|
+import { getFinancialMe } from "src/api/unit_financial";
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ contract: { type: Object, required: true },
|
|
|
+});
|
|
|
+
|
|
|
+defineEmits([...useDialogPluginComponent.emits]);
|
|
|
+
|
|
|
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
|
|
+ useDialogPluginComponent();
|
|
|
+
|
|
|
+const $q = useQuasar();
|
|
|
+
|
|
|
+const loading = ref(true);
|
|
|
+const saving = ref(false);
|
|
|
+const selectedMonths = ref(null);
|
|
|
+const pendingInstallments = ref([]);
|
|
|
+const maxFreezeCount = ref(null);
|
|
|
+
|
|
|
+// --- Opções de meses ---
|
|
|
+const monthOptions = computed(() => {
|
|
|
+ const max = maxFreezeCount.value;
|
|
|
+ const limit = max ?? 12;
|
|
|
+
|
|
|
+ const opts = Array.from({ length: limit }, (_, i) => ({
|
|
|
+ value: i + 1,
|
|
|
+ label: i + 1 === 1 ? '1 mês' : `${i + 1} meses`,
|
|
|
+ disable: false,
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (max) {
|
|
|
+ opts.push({
|
|
|
+ value: null,
|
|
|
+ label: `Acima de ${max} meses — não permitido`,
|
|
|
+ disable: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return opts;
|
|
|
+});
|
|
|
+
|
|
|
+// --- Data helpers ---
|
|
|
+function addMonthsToDMY(dateStr, months) {
|
|
|
+ if (!dateStr) return null;
|
|
|
+ const [day, month, year] = dateStr.split("/").map(Number);
|
|
|
+ const targetMonthStart = new Date(year, month - 1 + months, 1);
|
|
|
+ const lastDay = new Date(
|
|
|
+ targetMonthStart.getFullYear(),
|
|
|
+ targetMonthStart.getMonth() + 1,
|
|
|
+ 0,
|
|
|
+ ).getDate();
|
|
|
+ const safeDay = Math.min(day, lastDay);
|
|
|
+ return (
|
|
|
+ String(safeDay).padStart(2, "0") +
|
|
|
+ "/" +
|
|
|
+ String(targetMonthStart.getMonth() + 1).padStart(2, "0") +
|
|
|
+ "/" +
|
|
|
+ targetMonthStart.getFullYear()
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function formatCurrency(value) {
|
|
|
+ return new Intl.NumberFormat("pt-BR", {
|
|
|
+ style: "currency",
|
|
|
+ currency: "BRL",
|
|
|
+ }).format(value ?? 0);
|
|
|
+}
|
|
|
+
|
|
|
+// --- Preview ---
|
|
|
+const previewRows = computed(() =>
|
|
|
+ pendingInstallments.value.map((inst) => ({
|
|
|
+ ...inst,
|
|
|
+ new_due_date: selectedMonths.value
|
|
|
+ ? addMonthsToDMY(inst.due_date, selectedMonths.value)
|
|
|
+ : null,
|
|
|
+ })),
|
|
|
+);
|
|
|
+
|
|
|
+const tableColumns = [
|
|
|
+ { name: "history", label: "Histórico", field: "history", align: "left" },
|
|
|
+ { name: "order", label: "Ordem", field: "order", align: "center" },
|
|
|
+ { name: "value", label: "Valor", field: "value", align: "right" },
|
|
|
+ { name: "due_date", label: "Vencimento Atual", field: "due_date", align: "center" },
|
|
|
+ { name: "new_due_date", label: "Novo Vencimento", field: "new_due_date", align: "center" },
|
|
|
+];
|
|
|
+
|
|
|
+// --- Init ---
|
|
|
+onMounted(async () => {
|
|
|
+ try {
|
|
|
+ const [installments, financial] = await Promise.all([
|
|
|
+ getContractInstallments(props.contract.id),
|
|
|
+ getFinancialMe(),
|
|
|
+ ]);
|
|
|
+ pendingInstallments.value = installments ?? [];
|
|
|
+ maxFreezeCount.value = financial?.max_freeze_count ?? null;
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ $q.notify({ type: "negative", message: "Erro ao carregar parcelas." });
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// --- Confirmar ---
|
|
|
+async function handleConfirm() {
|
|
|
+ if (!selectedMonths.value) return;
|
|
|
+ saving.value = true;
|
|
|
+ try {
|
|
|
+ const updated = await freezeContract(props.contract.id, selectedMonths.value);
|
|
|
+ $q.notify({ type: "positive", message: "Contrato trancado com sucesso!" });
|
|
|
+ onDialogOK(updated);
|
|
|
+ } catch (e) {
|
|
|
+ const msg =
|
|
|
+ e?.response?.data?.message ??
|
|
|
+ "Erro ao trancar contrato.";
|
|
|
+ $q.notify({ type: "negative", message: msg });
|
|
|
+ } finally {
|
|
|
+ saving.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|