Преглед изворни кода

feat(financial): tela de tesouraria com saldo, extrato e movimentação manual

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ebagabee пре 1 месец
родитељ
комит
c5a8da77d5
2 измењених фајлова са 381 додато и 0 уклоњено
  1. 367 0
      src/pages/financial/TreasuryPage.vue
  2. 14 0
      src/router/routes/financial.route.js

+ 367 - 0
src/pages/financial/TreasuryPage.vue

@@ -0,0 +1,367 @@
+<template>
+  <div>
+    <DefaultHeaderPage title="Tesouraria" :show-filter-icon="false" />
+
+    <div class="row items-center justify-between q-px-md q-mb-sm">
+      <div class="text-subtitle1 text-weight-medium">Bancos / Caixa</div>
+      <q-btn
+        color="primary"
+        label="Nova conta"
+        icon="mdi-plus"
+        unelevated
+        no-caps
+        @click="openAccountCreate"
+      />
+    </div>
+
+    <div class="row q-px-md q-col-gutter-md">
+      <div
+        v-for="acc in accounts"
+        :key="acc.id"
+        class="col-12 col-sm-6 col-md-4"
+      >
+        <q-card
+          flat
+          bordered
+          class="q-pa-md cursor-pointer"
+          :class="{ 'selected-card': acc.id === selectedId }"
+          @click="selectAccount(acc)"
+        >
+          <div class="row items-center no-wrap">
+            <q-icon name="mdi-bank-outline" size="sm" color="primary" class="q-mr-sm" />
+            <div class="text-subtitle2 ellipsis">{{ acc.name }}</div>
+            <q-space />
+            <q-btn
+              flat
+              round
+              dense
+              icon="mdi-pencil-outline"
+              size="sm"
+              @click.stop="openAccountEdit(acc)"
+            />
+          </div>
+          <div class="text-h6 q-mt-sm">{{ formatCurrency(acc.balance) }}</div>
+          <div class="text-caption text-grey-7">
+            {{ acc.bank_name || "Conta" }}
+            <span v-if="acc.bank_agency"> · Ag. {{ acc.bank_agency }}</span>
+            <span v-if="acc.bank_account"> · CC {{ acc.bank_account }}</span>
+          </div>
+        </q-card>
+      </div>
+
+      <div v-if="!accounts.length" class="col-12 text-grey-7 q-pa-md">
+        Nenhuma conta cadastrada. Clique em "Nova conta" para começar.
+      </div>
+    </div>
+
+    <!-- Extrato -->
+    <div v-if="selectedId" class="q-px-md q-mt-lg">
+      <div class="row items-center justify-between q-mb-sm">
+        <div class="text-subtitle1 text-weight-medium">
+          Extrato — {{ selectedAccount?.name }}
+        </div>
+        <q-btn
+          color="primary-2"
+          label="Nova movimentação"
+          icon="mdi-swap-vertical"
+          unelevated
+          no-caps
+          @click="openLaunchCreate"
+        />
+      </div>
+
+      <DefaultTable
+        v-model:rows="launches"
+        no-api-call
+        :add-item="false"
+        title="Movimentações"
+        descricao="movimentações"
+        :feminino="true"
+        :columns="launchColumns"
+      >
+        <template #body-cell-transaction_type="{ row }">
+          <q-td class="text-left">
+            <q-chip
+              :color="row.transaction_type === 'entrada' ? 'positive' : 'negative'"
+              text-color="white"
+              dense
+              square
+              :label="row.transaction_type === 'entrada' ? 'Entrada' : 'Saída'"
+            />
+          </q-td>
+        </template>
+        <template #body-cell-amount="{ row }">
+          <q-td
+            class="text-left text-weight-medium"
+            :class="row.transaction_type === 'entrada' ? 'text-positive' : 'text-negative'"
+          >
+            {{ row.transaction_type === "entrada" ? "+" : "-" }}
+            {{ formatCurrency(row.amount) }}
+          </q-td>
+        </template>
+      </DefaultTable>
+    </div>
+
+    <!-- Diálogo conta -->
+    <q-dialog v-model="accountDialog">
+      <q-card style="min-width: 360px; max-width: 480px">
+        <DefaultDialogHeader
+          :title="editingAccount ? 'Editar conta' : 'Nova conta'"
+          @close="accountDialog = false"
+        />
+        <q-form ref="accountFormRef">
+          <q-card-section class="q-gutter-sm">
+            <DefaultInput
+              v-model="accountForm.name"
+              label="Nome (ex.: Banco do Brasil, Caixa Loja)"
+              outlined
+              :rules="[(v) => !!v || 'Informe o nome']"
+            />
+            <div class="row q-col-gutter-sm">
+              <DefaultInput v-model="accountForm.bank_name" label="Banco" class="col-6" outlined />
+              <DefaultInput v-model="accountForm.bank_agency" label="Agência" class="col-6" outlined />
+              <DefaultInput v-model="accountForm.bank_account" label="Conta" class="col-6" outlined />
+              <DefaultSelect
+                v-model="accountForm.bank_type_account"
+                label="Tipo"
+                :options="accountTypeOptions"
+                class="col-6"
+                outlined
+                emit-value
+                map-options
+              />
+            </div>
+          </q-card-section>
+          <q-card-actions align="right" class="q-pa-md">
+            <q-btn flat label="Cancelar" color="grey-7" @click="accountDialog = false" />
+            <q-btn color="primary" label="Salvar" :loading="savingAccount" @click="saveAccount" />
+          </q-card-actions>
+        </q-form>
+      </q-card>
+    </q-dialog>
+
+    <!-- Diálogo movimentação -->
+    <q-dialog v-model="launchDialog">
+      <q-card style="min-width: 360px; max-width: 460px">
+        <DefaultDialogHeader title="Nova movimentação" @close="launchDialog = false" />
+        <q-form ref="launchFormRef">
+          <q-card-section class="q-gutter-sm">
+            <DefaultSelect
+              v-model="launchForm.transaction_type"
+              label="Tipo"
+              :options="transactionTypeOptions"
+              outlined
+              emit-value
+              map-options
+            />
+            <DefaultInput
+              v-model="launchForm.description"
+              label="Descrição"
+              outlined
+              :rules="[(v) => !!v || 'Informe a descrição']"
+            />
+            <div class="row q-col-gutter-sm">
+              <DefaultCurrencyInput
+                v-model="launchForm.amount"
+                label="Valor"
+                class="col-6"
+                outlined
+              />
+              <DefaultInputDatePicker
+                v-model="launchForm.launch_date"
+                label="Data"
+                class="col-6"
+              />
+            </div>
+          </q-card-section>
+          <q-card-actions align="right" class="q-pa-md">
+            <q-btn flat label="Cancelar" color="grey-7" @click="launchDialog = false" />
+            <q-btn color="primary-2" label="Salvar" :loading="savingLaunch" @click="saveLaunch" />
+          </q-card-actions>
+        </q-form>
+      </q-card>
+    </q-dialog>
+  </div>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import {
+  getTreasuryAccountsMe,
+  createTreasuryAccountMe,
+  updateTreasuryAccountMe,
+  getTreasuryLaunchesMe,
+  createTreasuryLaunchMe,
+} from "src/api/treasury";
+
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import DefaultSelect from "src/components/defaults/DefaultSelect.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";
+
+const accounts = ref([]);
+const launches = ref([]);
+const selectedId = ref(null);
+
+const accountDialog = ref(false);
+const savingAccount = ref(false);
+const editingAccount = ref(false);
+const editingAccountId = ref(null);
+const accountFormRef = ref(null);
+
+const launchDialog = ref(false);
+const savingLaunch = ref(false);
+const launchFormRef = ref(null);
+
+const accountForm = ref(defaultAccountForm());
+const launchForm = ref(defaultLaunchForm());
+
+function defaultAccountForm() {
+  return {
+    name: "",
+    treasury_type: "bank",
+    bank_name: "",
+    bank_agency: "",
+    bank_account: "",
+    bank_type_account: null,
+  };
+}
+
+function defaultLaunchForm() {
+  return {
+    transaction_type: "entrada",
+    description: "",
+    amount: 0,
+    launch_date: new Date().toISOString().slice(0, 10),
+  };
+}
+
+const accountTypeOptions = [
+  { label: "Corrente", value: "corrente" },
+  { label: "Poupança", value: "poupanca" },
+];
+
+const transactionTypeOptions = [
+  { label: "Entrada", value: "entrada" },
+  { label: "Saída", value: "saida" },
+];
+
+const launchColumns = [
+  { name: "launch_date", label: "Data", field: "launch_date", align: "left" },
+  { name: "description", label: "Descrição", field: "description", align: "left" },
+  { name: "transaction_type", label: "Tipo", field: "transaction_type", align: "left" },
+  { name: "amount", label: "Valor", field: "amount", align: "left" },
+];
+
+const selectedAccount = computed(() =>
+  accounts.value.find((a) => a.id === selectedId.value),
+);
+
+function formatCurrency(value) {
+  return new Intl.NumberFormat("pt-BR", {
+    style: "currency",
+    currency: "BRL",
+  }).format(Number(value ?? 0));
+}
+
+async function loadAccounts() {
+  try {
+    accounts.value = await getTreasuryAccountsMe();
+    if (!selectedId.value && accounts.value.length) {
+      selectAccount(accounts.value[0]);
+    }
+  } catch (e) {
+    console.error("Erro ao carregar contas:", e);
+  }
+}
+
+async function loadLaunches() {
+  if (!selectedId.value) return;
+  try {
+    launches.value = await getTreasuryLaunchesMe(selectedId.value);
+  } catch (e) {
+    console.error("Erro ao carregar extrato:", e);
+  }
+}
+
+function selectAccount(acc) {
+  selectedId.value = acc.id;
+  loadLaunches();
+}
+
+function openAccountCreate() {
+  editingAccount.value = false;
+  editingAccountId.value = null;
+  accountForm.value = defaultAccountForm();
+  accountDialog.value = true;
+}
+
+function openAccountEdit(acc) {
+  editingAccount.value = true;
+  editingAccountId.value = acc.id;
+  accountForm.value = {
+    name: acc.name,
+    treasury_type: "bank",
+    bank_name: acc.bank_name || "",
+    bank_agency: acc.bank_agency || "",
+    bank_account: acc.bank_account || "",
+    bank_type_account: acc.bank_type_account || null,
+  };
+  accountDialog.value = true;
+}
+
+async function saveAccount() {
+  const valid = await accountFormRef.value?.validate();
+  if (!valid) return;
+  savingAccount.value = true;
+  try {
+    if (editingAccount.value) {
+      await updateTreasuryAccountMe(editingAccountId.value, accountForm.value);
+    } else {
+      await createTreasuryAccountMe(accountForm.value);
+    }
+    accountDialog.value = false;
+    await loadAccounts();
+  } catch (e) {
+    console.error(e);
+  } finally {
+    savingAccount.value = false;
+  }
+}
+
+function openLaunchCreate() {
+  launchForm.value = defaultLaunchForm();
+  launchDialog.value = true;
+}
+
+async function saveLaunch() {
+  const valid = await launchFormRef.value?.validate();
+  if (!valid) return;
+  savingLaunch.value = true;
+  try {
+    await createTreasuryLaunchMe({
+      account_id: selectedId.value,
+      ...launchForm.value,
+    });
+    launchDialog.value = false;
+    await loadAccounts();
+    await loadLaunches();
+  } catch (e) {
+    console.error(e);
+  } finally {
+    savingLaunch.value = false;
+  }
+}
+
+onMounted(loadAccounts);
+</script>
+
+<style scoped>
+.selected-card {
+  border-color: var(--q-primary);
+  border-width: 2px;
+}
+</style>

+ 14 - 0
src/router/routes/financial.route.js

@@ -54,4 +54,18 @@ export default [
       ],
     },
   },
+  {
+    path: "/financial/treasury",
+    name: "TreasuryPage",
+    component: () => import("pages/financial/TreasuryPage.vue"),
+    meta: {
+      title: { value: "Tesouraria", translate: false },
+      requireAuth: true,
+      breadcrumbs: [
+        { name: "DashboardPage", title: "Dashboard" },
+        { name: "FinancialPage", title: "Financeiro" },
+        { name: "TreasuryPage", title: "Tesouraria" },
+      ],
+    },
+  },
 ];