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

feat(financial): implement dialogs for creating and settling franchisee receivables

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

+ 15 - 0
src/api/franchisee_account_receive.js

@@ -4,3 +4,18 @@ export const getFranchiseeReceivables = async (params = {}) => {
   const { data } = await api.get("/franchisee-account-receive", { params });
   return data.payload;
 };
+
+export const createFranchiseeReceivable = async (payload) => {
+  const { data } = await api.post("/franchisee-account-receive", payload);
+  return data.payload;
+};
+
+export const settleFranchiseeReceivable = async (id, payload = {}) => {
+  const { data } = await api.post(`/franchisee-account-receive/${id}/settle`, payload);
+  return data.payload;
+};
+
+export const reopenFranchiseeReceivable = async (id) => {
+  const { data } = await api.post(`/franchisee-account-receive/${id}/reopen`);
+  return data.payload;
+};

+ 163 - 0
src/components/financial/AddAccountReceivableDialog.vue

@@ -0,0 +1,163 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 520px; max-width: 90vw">
+      <DefaultDialogHeader title="Nova conta a receber" @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']"
+          />
+
+          <div class="col-12">
+            <div class="text-caption text-grey-8 q-mb-xs">Tipo da conta</div>
+            <q-option-group
+              v-model="form.account_type"
+              :options="accountTypeOptions"
+              color="primary"
+              inline
+              type="radio"
+            />
+          </div>
+
+          <UnitSelect
+            v-if="form.account_type === 'unit'"
+            v-model="form.unit"
+            label="Unidade"
+            class="col-12"
+            :rules="[(value) => !!value || 'Selecione a unidade']"
+          />
+
+          <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="Data de 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"
+            :options="planAccountOptions"
+            class="col-12"
+            clearable
+            emit-value
+            map-options
+            :loading="loadingPlanAccounts"
+          />
+
+          <DefaultInput
+            v-model="form.obs"
+            label="Observação"
+            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, watch } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { createFranchiseeReceivable } from "src/api/franchisee_account_receive";
+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";
+import UnitSelect from "src/components/selects/UnitSelect.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 accountTypeOptions = [
+  { label: "Conta interna", value: "internal" },
+  { label: "Relacionada a uma unidade", value: "unit" },
+];
+
+const formRef = ref(null);
+const form = ref({
+  account_type: "internal",
+  unit: null,
+  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);
+
+watch(
+  () => form.value.account_type,
+  (type) => {
+    if (type === "internal") form.value.unit = null;
+  },
+);
+
+const { loading, execute: submitForm } = useSubmitHandler({
+  formRef,
+  onSuccess: (response) => onDialogOK(response),
+});
+
+onMounted(async () => {
+  loadingPlanAccounts.value = true;
+  try {
+    const accounts = await getFinancialPlanAccountsForSelect();
+    planAccountOptions.value = (accounts ?? []).map((account) => ({
+      label: `${account.code} — ${account.description}`,
+      value: account.id,
+    }));
+  } finally {
+    loadingPlanAccounts.value = false;
+  }
+});
+
+const onSubmit = async () => {
+  await submitForm(() =>
+    createFranchiseeReceivable({
+      account_type: form.value.account_type,
+      unit_id: form.value.account_type === "unit" ? form.value.unit?.value : null,
+      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>

+ 52 - 0
src/components/financial/SettleAccountReceivableDialog.vue

@@ -0,0 +1,52 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="q-dialog-plugin overflow-hidden" style="width: 420px; max-width: 90vw">
+      <DefaultDialogHeader title="Marcar como pago" @close="onDialogCancel" />
+      <q-form ref="formRef" @submit.prevent="onSubmit">
+        <q-card-section class="q-pt-none">
+          <div class="text-body2 q-mb-md">{{ receivable.history }}</div>
+          <DefaultInputDatePicker
+            v-model="paymentDate"
+            v-model:untreated-date="paymentDateIso"
+            label="Data do pagamento"
+            lazy-rules="ondemand"
+            :rules="[(value) => !!value || 'Informe a data do pagamento']"
+          />
+        </q-card-section>
+        <q-separator />
+        <q-card-actions align="right" class="q-pa-md q-gutter-sm">
+          <q-btn outline color="negative" label="Cancelar" no-caps @click="onDialogCancel" />
+          <q-btn color="primary" label="Confirmar" type="submit" no-caps :loading="loading" />
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { settleFranchiseeReceivable } from "src/api/franchisee_account_receive";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+const { receivable } = defineProps({ receivable: { type: Object, required: true } });
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const today = new Date();
+const paymentDate = ref(today.toLocaleDateString("pt-BR"));
+const paymentDateIso = ref([
+  today.getFullYear(),
+  String(today.getMonth() + 1).padStart(2, "0"),
+  String(today.getDate()).padStart(2, "0"),
+].join("-"));
+const formRef = ref(null);
+const { loading, execute: submitForm } = useSubmitHandler({
+  formRef,
+  onSuccess: (response) => onDialogOK(response),
+});
+const onSubmit = () => submitForm(() =>
+  settleFranchiseeReceivable(receivable.id, { payment_date: paymentDateIso.value }),
+);
+</script>

+ 16 - 40
src/pages/financial/AccountsPayablePage.vue

@@ -49,39 +49,18 @@
 
         <template #body-cell-status="{ row }">
           <q-td class="text-left">
-            <q-chip
-              :color="statusMeta(row.status).color"
-              text-color="white"
-              dense
-              square
-              :label="statusMeta(row.status).label"
-            />
-          </q-td>
-        </template>
-
-        <template #body-cell-actions="{ row }">
-          <q-td auto-width>
             <q-btn
-              v-if="canEdit && row.status !== 'paid' && row.status !== 'cancelled'"
-              color="primary"
-              label="Dar baixa"
+              :color="row.status === 'paid' ? 'positive' : 'orange-7'"
+              :label="row.status === 'paid' ? 'Pago' : 'Não pago'"
               dense
               no-caps
               unelevated
-              class="q-px-sm"
-              @click="openSettle(row)"
-            />
-            <q-btn
-              v-else-if="canEdit && row.status === 'paid'"
-              color="grey-7"
-              label="Reabrir"
-              dense
-              no-caps
-              outline
-              class="q-px-sm"
+              :disable="!canEdit"
               :loading="reopeningId === row.id"
-              @click="onReopen(row)"
-            />
+              @click.prevent.stop="changeStatus(row)"
+            >
+              <q-tooltip v-if="canEdit">Clique para alterar o status</q-tooltip>
+            </q-btn>
           </q-td>
         </template>
       </DefaultTable>
@@ -119,7 +98,6 @@ const columns = [
   { name: "value", label: "Valor", field: "value", align: "left" },
   { name: "due_date", label: "Vencimento", field: "due_date", align: "left" },
   { name: "status", label: "Status", field: "status", align: "left" },
-  { name: "actions", label: "Ações", field: "actions", align: "right" },
 ];
 
 function formatCurrency(value) {
@@ -129,17 +107,6 @@ function formatCurrency(value) {
   }).format(Number(value ?? 0));
 }
 
-const STATUS_META = {
-  pending: { label: "Pendente", color: "orange-7" },
-  paid: { label: "Pago", color: "positive" },
-  overdue: { label: "Vencido", color: "negative" },
-  cancelled: { label: "Cancelado", color: "grey-6" },
-};
-
-function statusMeta(status) {
-  return STATUS_META[status] ?? { label: status, color: "grey-6" };
-}
-
 const totals = computed(() => {
   const acc = {
     paid: 0,
@@ -179,6 +146,15 @@ function openSettle(row) {
   }).onOk(loadData);
 }
 
+function changeStatus(row) {
+  if (row.status === "paid") {
+    onReopen(row);
+    return;
+  }
+
+  openSettle(row);
+}
+
 async function onReopen(row) {
   reopeningId.value = row.id;
   try {

+ 67 - 20
src/pages/financial/AccountsReceivablePage.vue

@@ -24,35 +24,60 @@
       />
     </div>
 
-    <div class="row justify-end items-center q-px-md q-mb-sm q-gutter-sm">
-      <q-btn color="primary" label="Exportar Relatório" icon="mdi-download" unelevated no-caps />
-    </div>
-
     <div class="q-px-md">
       <DefaultTable
         v-model:rows="rows"
         no-api-call
-        :add-item="false"
+        :add-item="canAdd"
         :loading="loading"
         title="Contas a Receber"
         description="contas"
         :female="true"
         :columns="columns"
-      />
+        @on-add-item="openCreate"
+      >
+        <template #body-cell-status="{ row }">
+          <q-td class="text-left">
+            <q-btn
+              :color="row.status === 'paid' ? 'positive' : 'orange-7'"
+              :label="row.status === 'paid' ? 'Pago' : 'Não pago'"
+              dense
+              no-caps
+              unelevated
+              :disable="!canEdit"
+              :loading="changingStatusId === row.id"
+              @click.prevent.stop="changeStatus(row)"
+            >
+              <q-tooltip v-if="canEdit">Clique para alterar o status</q-tooltip>
+            </q-btn>
+          </q-td>
+        </template>
+      </DefaultTable>
     </div>
   </div>
 </template>
 
 <script setup>
 import { ref, computed, onMounted } from "vue";
-import { Notify } from "quasar";
+import { Notify, useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTable from "src/components/defaults/DefaultTable.vue";
 import FinancialCard from "src/components/financial/FinancialCard.vue";
-import { getFranchiseeReceivables } from "src/api/franchisee_account_receive";
+import {
+  getFranchiseeReceivables,
+  reopenFranchiseeReceivable,
+} from "src/api/franchisee_account_receive";
+import AddAccountReceivableDialog from "src/components/financial/AddAccountReceivableDialog.vue";
+import SettleAccountReceivableDialog from "src/components/financial/SettleAccountReceivableDialog.vue";
+import { permissionStore } from "src/stores/permission";
 
+const $q = useQuasar();
+const permissions = permissionStore();
+const canAdd = permissions.getAccess("franchisor_financial", "add");
+const canEdit = permissions.getAccess("franchisor_financial", "edit");
 const loading = ref(false);
 const items = ref([]);
+const changingStatusId = ref(null);
 
 const columns = [
   { name: "unit", label: "Unidade", field: "unit", align: "left" },
@@ -63,14 +88,6 @@ const columns = [
   { name: "status", label: "Status", field: "status", align: "left" },
 ];
 
-const STATUS_LABEL = {
-  pending: "Pendente",
-  awaiting_payment: "Aguardando",
-  paid: "Pago",
-  overdue: "Atrasado",
-  cancelled: "Cancelado",
-};
-
 const formatCurrency = (value) =>
   new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(value ?? 0);
 
@@ -82,13 +99,14 @@ const formatDate = (value) => {
 
 const rows = computed(() =>
   items.value.map((i) => ({
-    id: i.id,
-    unit: i.unit_name ?? `Unidade ${i.unit_id}`,
+    ...i,
+    unit: i.unit_id ? (i.unit_name ?? `Unidade ${i.unit_id}`) : "Conta interna",
     description: i.history,
-    category: "TBR",
+    category: i.plan_account
+      ? `${i.plan_account.code} — ${i.plan_account.description}`
+      : i.origin === "tbr" ? "TBR" : "—",
     value: formatCurrency(Number(i.value)),
     due_date: formatDate(i.due_date),
-    status: STATUS_LABEL[i.status] ?? i.status,
   })),
 );
 
@@ -121,5 +139,34 @@ const fetchReceivables = async () => {
   }
 };
 
+const openCreate = () => {
+  $q.dialog({ component: AddAccountReceivableDialog }).onOk(fetchReceivables);
+};
+
+const changeStatus = (row) => {
+  if (row.status !== "paid") {
+    $q.dialog({
+      component: SettleAccountReceivableDialog,
+      componentProps: { receivable: row },
+    }).onOk(fetchReceivables);
+    return;
+  }
+
+  $q.dialog({
+    title: "Marcar como não pago",
+    message: `Deseja reabrir a conta "${row.history}"?`,
+    ok: { color: "primary", label: "Confirmar" },
+    cancel: { color: "negative", outline: true, label: "Cancelar" },
+  }).onOk(async () => {
+    changingStatusId.value = row.id;
+    try {
+      await reopenFranchiseeReceivable(row.id);
+      await fetchReceivables();
+    } finally {
+      changingStatusId.value = null;
+    }
+  });
+};
+
 onMounted(fetchReceivables);
 </script>