Просмотр исходного кода

feat(financial): add dialogs for creating and settling company payables

ebagabee 1 неделя назад
Родитель
Сommit
5b1f5cb5e9

+ 140 - 0
src/components/financial/AddCompanyPayableDialog.vue

@@ -0,0 +1,140 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 500px; max-width: 90vw">
+      <DefaultDialogHeader title="Nova conta a pagar" @close="onDialogCancel" />
+
+      <q-form ref="formRef" @submit.prevent="onSubmit">
+        <q-card-section class="row q-col-gutter-md q-pt-none">
+          <DefaultInput
+            v-model="form.history"
+            label="Descrição"
+            class="col-12"
+            lazy-rules="ondemand"
+            :rules="[(value) => !!value || 'Informe a descrição']"
+          />
+
+          <DefaultCurrencyInput
+            v-model="form.value"
+            label="Valor"
+            class="col-12 col-sm-6"
+            lazy-rules="ondemand"
+            :rules="[(value) => Number(value) > 0 || 'Informe o valor']"
+          />
+
+          <DefaultInputDatePicker
+            v-model="form.due_date"
+            v-model:untreated-date="form.due_date_iso"
+            label="Vencimento"
+            class="col-12 col-sm-6"
+            lazy-rules="ondemand"
+            :rules="[(value) => !!value || 'Informe o vencimento']"
+          />
+
+          <DefaultSelect
+            v-model="form.financial_plan_account_id"
+            label="Plano de contas (opcional)"
+            :options="planAccountOptions"
+            class="col-12"
+            clearable
+            emit-value
+            map-options
+            :loading="loadingPlanAccounts"
+          />
+
+          <DefaultInput
+            v-model="form.obs"
+            label="Observação (opcional)"
+            type="textarea"
+            class="col-12"
+            rows="3"
+          />
+        </q-card-section>
+
+        <q-separator />
+        <q-card-actions align="right" class="q-pa-md q-gutter-sm">
+          <q-btn outline label="Cancelar" color="negative" no-caps @click="onDialogCancel" />
+          <q-btn
+            color="primary"
+            label="Salvar"
+            type="submit"
+            no-caps
+            unelevated
+            :loading="loading"
+          />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { onMounted, ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { createCompanyPayable } from "src/api/company_payable";
+import { getFinancialPlanAccountsForSelect } from "src/api/financial_plan_account";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+
+import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const today = new Date();
+const dueDateIso = [
+  today.getFullYear(),
+  String(today.getMonth() + 1).padStart(2, "0"),
+  String(today.getDate()).padStart(2, "0"),
+].join("-");
+
+const formRef = ref(null);
+const form = ref({
+  history: "",
+  value: 0,
+  due_date: today.toLocaleDateString("pt-BR"),
+  due_date_iso: dueDateIso,
+  financial_plan_account_id: null,
+  obs: "",
+});
+
+const planAccountOptions = ref([]);
+const loadingPlanAccounts = ref(false);
+
+const {
+  loading,
+  execute: submitForm,
+} = useSubmitHandler({
+  formRef,
+  onSuccess: (response) => onDialogOK(response),
+});
+
+onMounted(async () => {
+  loadingPlanAccounts.value = true;
+  try {
+    const planAccounts = await getFinancialPlanAccountsForSelect();
+    planAccountOptions.value = (planAccounts ?? []).map((account) => ({
+      label: `${account.code} — ${account.description}`,
+      value: account.id,
+    }));
+  } finally {
+    loadingPlanAccounts.value = false;
+  }
+});
+
+const onSubmit = async () => {
+  await submitForm(() =>
+    createCompanyPayable({
+      history: form.value.history,
+      value: form.value.value,
+      due_date: form.value.due_date_iso,
+      financial_plan_account_id: form.value.financial_plan_account_id || null,
+      obs: form.value.obs || null,
+    }),
+  );
+};
+</script>

+ 120 - 0
src/components/financial/SettleCompanyPayableDialog.vue

@@ -0,0 +1,120 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 460px; max-width: 90vw">
+      <DefaultDialogHeader title="Dar baixa" @close="onDialogCancel" />
+
+      <q-form ref="formRef" @submit.prevent="onSubmit">
+        <q-card-section class="row q-col-gutter-md q-pt-none">
+          <div class="col-12 text-body2 text-grey-8">{{ payable.history }}</div>
+
+          <DefaultInputDatePicker
+            v-model="form.payment_date"
+            v-model:untreated-date="form.payment_date_iso"
+            label="Data do pagamento"
+            class="col-12"
+            lazy-rules="ondemand"
+            :rules="[(value) => !!value || 'Informe a data do pagamento']"
+          />
+
+          <DefaultCurrencyInput
+            v-model="form.discount"
+            label="Desconto"
+            class="col-12 col-sm-6"
+          />
+          <DefaultCurrencyInput
+            v-model="form.fine"
+            label="Multa/Juros"
+            class="col-12 col-sm-6"
+          />
+
+          <div class="col-12 text-caption text-grey-7">
+            Valor da conta: {{ formatCurrency(payable.value) }} ·
+            <span class="text-weight-medium">Pago: {{ formatCurrency(netValue) }}</span>
+          </div>
+        </q-card-section>
+
+        <q-separator />
+        <q-card-actions align="right" class="q-pa-md q-gutter-sm">
+          <q-btn outline label="Cancelar" color="negative" no-caps @click="onDialogCancel" />
+          <q-btn
+            color="primary"
+            label="Confirmar baixa"
+            type="submit"
+            no-caps
+            unelevated
+            :loading="loading"
+          />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed, ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { settleCompanyPayable } from "src/api/company_payable";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+
+import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { payable } = defineProps({
+  payable: {
+    type: Object,
+    required: true,
+  },
+});
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const today = new Date();
+const paymentDateIso = [
+  today.getFullYear(),
+  String(today.getMonth() + 1).padStart(2, "0"),
+  String(today.getDate()).padStart(2, "0"),
+].join("-");
+
+const formRef = ref(null);
+const form = ref({
+  payment_date: today.toLocaleDateString("pt-BR"),
+  payment_date_iso: paymentDateIso,
+  discount: Number(payable.discount ?? 0),
+  fine: Number(payable.fine ?? 0),
+});
+
+const netValue = computed(
+  () =>
+    Number(payable.value ?? 0) -
+    Number(form.value.discount ?? 0) +
+    Number(form.value.fine ?? 0),
+);
+
+const formatCurrency = (value) =>
+  new Intl.NumberFormat("pt-BR", {
+    style: "currency",
+    currency: "BRL",
+  }).format(Number(value ?? 0));
+
+const {
+  loading,
+  execute: submitForm,
+} = useSubmitHandler({
+  formRef,
+  onSuccess: (response) => onDialogOK(response),
+});
+
+const onSubmit = async () => {
+  await submitForm(() =>
+    settleCompanyPayable(payable.id, {
+      payment_date: form.value.payment_date_iso,
+      discount: form.value.discount,
+      fine: form.value.fine,
+    }),
+  );
+};
+</script>

+ 12 - 238
src/pages/financial/AccountsPayablePage.vue

@@ -26,38 +26,16 @@
       />
     </div>
 
-    <div class="row justify-end items-center q-px-md q-mb-sm q-gutter-sm">
-      <DefaultSelect
-        v-model="statusFilter"
-        label="Status"
-        :options="statusFilterOptions"
-        outlined
-        dense
-        emit-value
-        map-options
-        style="min-width: 180px"
-        @update:model-value="loadData"
-      />
-      <q-btn
-        v-if="canAdd"
-        color="primary"
-        label="Nova conta a pagar"
-        icon="mdi-plus"
-        unelevated
-        no-caps
-        @click="openCreate"
-      />
-    </div>
-
     <div class="q-px-md">
       <DefaultTable
         v-model:rows="rows"
         no-api-call
-        :add-item="false"
+        :add-item="canAdd"
         title="Contas a Pagar"
         description="contas"
         :female="true"
         :columns="columns"
+        @on-add-item="openCreate"
       >
         <template #body-cell-plan_account="{ row }">
           <q-td class="text-left">
@@ -109,169 +87,31 @@
       </DefaultTable>
     </div>
 
-    <!-- Diálogo de baixa manual -->
-    <q-dialog v-model="settleDialog">
-      <q-card style="min-width: 360px; max-width: 460px">
-        <DefaultDialogHeader title="Dar baixa" @close="settleDialog = false" />
-
-        <q-card-section class="q-gutter-sm">
-          <div class="text-body2 text-grey-8">{{ selected?.history }}</div>
-
-          <DefaultInputDatePicker
-            v-model="settleForm.payment_date"
-            label="Data do pagamento"
-          />
-
-          <div class="row q-col-gutter-sm">
-            <DefaultCurrencyInput
-              v-model="settleForm.discount"
-              label="Desconto"
-              class="col-6"
-              outlined
-            />
-            <DefaultCurrencyInput
-              v-model="settleForm.fine"
-              label="Multa/Juros"
-              class="col-6"
-              outlined
-            />
-          </div>
-
-          <div class="text-caption text-grey-7">
-            Valor da conta: {{ formatCurrency(selected?.value) }} ·
-            <span class="text-weight-medium">
-              Pago: {{ formatCurrency(settleNetValue) }}
-            </span>
-          </div>
-        </q-card-section>
-
-        <q-card-actions align="right" class="q-pa-md">
-          <q-btn flat label="Cancelar" color="grey-7" @click="settleDialog = false" />
-          <q-btn
-            color="primary"
-            label="Confirmar baixa"
-            :loading="settling"
-            @click="onSettle"
-          />
-        </q-card-actions>
-      </q-card>
-    </q-dialog>
-
-    <!-- Diálogo de nova conta manual -->
-    <q-dialog v-model="createDialog">
-      <q-card style="min-width: 380px; max-width: 480px">
-        <DefaultDialogHeader title="Nova conta a pagar" @close="createDialog = false" />
-
-        <q-form ref="createFormRef">
-          <q-card-section class="q-gutter-sm">
-            <DefaultInput
-              v-model="createForm.history"
-              label="Descrição"
-              outlined
-              :rules="[(v) => !!v || 'Informe a descrição']"
-            />
-            <div class="row q-col-gutter-sm">
-              <DefaultCurrencyInput
-                v-model="createForm.value"
-                label="Valor"
-                class="col-6"
-                outlined
-                :rules="[(v) => Number(v) > 0 || 'Informe o valor']"
-              />
-              <DefaultInputDatePicker
-                v-model="createForm.due_date"
-                label="Vencimento"
-                class="col-6"
-                :rules="[(v) => !!v || 'Informe o vencimento']"
-              />
-            </div>
-            <DefaultSelect
-              v-model="createForm.financial_plan_account_id"
-              label="Plano de contas (opcional)"
-              :options="planAccountOptions"
-              outlined
-              clearable
-              emit-value
-              map-options
-              :loading="loadingAux"
-            />
-            <DefaultInput
-              v-model="createForm.obs"
-              label="Observação (opcional)"
-              type="textarea"
-              autogrow
-              outlined
-            />
-          </q-card-section>
-
-          <q-card-actions align="right" class="q-pa-md">
-            <q-btn flat label="Cancelar" color="grey-7" @click="createDialog = false" />
-            <q-btn
-              color="primary"
-              label="Salvar"
-              :loading="creating"
-              @click="onCreate"
-            />
-          </q-card-actions>
-        </q-form>
-      </q-card>
-    </q-dialog>
   </div>
 </template>
 
 <script setup>
 import { computed, onMounted, ref } from "vue";
+import { useQuasar } from "quasar";
 import {
   getCompanyPayables,
-  createCompanyPayable,
-  settleCompanyPayable,
   reopenCompanyPayable,
 } from "src/api/company_payable";
-import { getFinancialPlanAccountsForSelect } from "src/api/financial_plan_account";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
-import DefaultSelect from "src/components/defaults/DefaultSelect.vue";
-import DefaultInput from "src/components/defaults/DefaultInput.vue";
-import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
-import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
-import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
 import FinancialCard from "src/components/financial/FinancialCard.vue";
+import AddCompanyPayableDialog from "src/components/financial/AddCompanyPayableDialog.vue";
+import SettleCompanyPayableDialog from "src/components/financial/SettleCompanyPayableDialog.vue";
 import { permissionStore } from "src/stores/permission";
 
+const $q = useQuasar();
 const rows = ref([]);
 const permissions = permissionStore();
 const canAdd = permissions.getAccess("franchisor_financial", "add");
 const canEdit = permissions.getAccess("franchisor_financial", "edit");
-const statusFilter = ref(null);
 
-const settleDialog = ref(false);
-const selected = ref(null);
-const settling = ref(false);
 const reopeningId = ref(null);
-const settleForm = ref({ payment_date: null, discount: 0, fine: 0 });
-
-const createDialog = ref(false);
-const createFormRef = ref(null);
-const creating = ref(false);
-const loadingAux = ref(false);
-const planAccountOptions = ref([]);
-const defaultCreateForm = () => ({
-  history: "",
-  value: 0,
-  due_date: new Date().toISOString().slice(0, 10),
-  financial_plan_account_id: null,
-  obs: "",
-});
-const createForm = ref(defaultCreateForm());
-
-const statusFilterOptions = [
-  { label: "Todos", value: null },
-  { label: "Pendentes", value: "pending" },
-  { label: "Pagos", value: "paid" },
-  { label: "Vencidos", value: "overdue" },
-  { label: "Cancelados", value: "cancelled" },
-];
 
 const columns = [
   { name: "history", label: "Descrição", field: "history", align: "left" },
@@ -300,11 +140,6 @@ function statusMeta(status) {
   return STATUS_META[status] ?? { label: status, color: "grey-6" };
 }
 
-const settleNetValue = computed(() => {
-  const base = Number(selected.value?.value ?? 0);
-  return base - Number(settleForm.value.discount ?? 0) + Number(settleForm.value.fine ?? 0);
-});
-
 const totals = computed(() => {
   const acc = {
     paid: 0,
@@ -331,55 +166,17 @@ const totals = computed(() => {
 
 async function loadData() {
   try {
-    const params = statusFilter.value ? { status: statusFilter.value } : {};
-    rows.value = await getCompanyPayables(params);
+    rows.value = await getCompanyPayables();
   } catch (e) {
     console.error("Erro ao buscar contas a pagar:", e);
   }
 }
 
-async function loadAux() {
-  if (planAccountOptions.value.length) return;
-  loadingAux.value = true;
-  try {
-    const planAccounts = await getFinancialPlanAccountsForSelect();
-    planAccountOptions.value = (planAccounts ?? []).map((a) => ({
-      label: `${a.code} — ${a.description}`,
-      value: a.id,
-    }));
-  } catch (e) {
-    console.error("Erro ao carregar plano de contas:", e);
-  } finally {
-    loadingAux.value = false;
-  }
-}
-
 function openSettle(row) {
-  selected.value = row;
-  settleForm.value = {
-    payment_date: new Date().toISOString().slice(0, 10),
-    discount: Number(row.discount ?? 0),
-    fine: Number(row.fine ?? 0),
-  };
-  settleDialog.value = true;
-}
-
-async function onSettle() {
-  if (!selected.value) return;
-  settling.value = true;
-  try {
-    await settleCompanyPayable(selected.value.id, {
-      payment_date: settleForm.value.payment_date,
-      discount: settleForm.value.discount,
-      fine: settleForm.value.fine,
-    });
-    settleDialog.value = false;
-    await loadData();
-  } catch (e) {
-    console.error(e);
-  } finally {
-    settling.value = false;
-  }
+  $q.dialog({
+    component: SettleCompanyPayableDialog,
+    componentProps: { payable: row },
+  }).onOk(loadData);
 }
 
 async function onReopen(row) {
@@ -395,30 +192,7 @@ async function onReopen(row) {
 }
 
 function openCreate() {
-  createForm.value = defaultCreateForm();
-  createDialog.value = true;
-  loadAux();
-}
-
-async function onCreate() {
-  const valid = await createFormRef.value?.validate();
-  if (!valid) return;
-  creating.value = true;
-  try {
-    await createCompanyPayable({
-      history: createForm.value.history,
-      value: createForm.value.value,
-      due_date: createForm.value.due_date,
-      financial_plan_account_id: createForm.value.financial_plan_account_id || null,
-      obs: createForm.value.obs || null,
-    });
-    createDialog.value = false;
-    await loadData();
-  } catch (e) {
-    console.error(e);
-  } finally {
-    creating.value = false;
-  }
+  $q.dialog({ component: AddCompanyPayableDialog }).onOk(loadData);
 }
 
 onMounted(loadData);