Explorar o código

feat: implement financial module with overview page, accounts payable/receivable pages, and reusable card component

ebagabee hai 1 semana
pai
achega
1739ab5e40

+ 110 - 0
src/components/financial/FinancialCard.vue

@@ -0,0 +1,110 @@
+<template>
+  <q-card
+    class="financial-card"
+    :class="{
+      'financial-card--selected': selected,
+      'financial-card--clickable': clickable,
+    }"
+    @click="clickable && emit('click')"
+  >
+    <div class="row justify-between items-start no-wrap">
+      <span class="text-subtitle1 text-dark card-title">{{ title }}</span>
+      <q-icon :name="icon" size="22px" color="dark" />
+    </div>
+
+    <div class="bottom-area">
+      <div class="text-h5 text-primary value-text">{{ formattedValue }}</div>
+
+      <template v-if="hasFooter">
+        <q-separator class="q-mt-sm q-mb-xs" />
+        <div class="row items-center justify-between no-wrap">
+          <div class="column" style="gap: 2px">
+            <span v-if="percentage !== null" class="text-caption text-foreground">
+              {{ hideValues ? "••" : percentage }}% vs o mês anterior
+            </span>
+            <span v-if="integer !== null" class="text-caption text-foreground">
+              {{ hideValues ? "•" : integer }} {{ integerLabel }}
+            </span>
+          </div>
+          <span v-if="description" class="text-caption text-foreground">
+            {{ description }}
+          </span>
+        </div>
+      </template>
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+  title: { type: String, required: true },
+  icon: { type: String, default: "mdi-cash-multiple" },
+  financialValue: { type: Number, default: 0 },
+  legend: { type: String, default: "" },
+  percentage: { type: Number, default: null },
+  integer: { type: Number, default: null },
+  integerLabel: { type: String, default: "pagamentos restantes" },
+  description: { type: String, default: "" },
+  hideValues: { type: Boolean, default: false },
+  clickable: { type: Boolean, default: false },
+  selected: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["click"]);
+
+const formattedValue = computed(() =>
+  props.hideValues
+    ? "R$ ••••"
+    : new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(
+        props.financialValue,
+      ),
+);
+
+const hasFooter = computed(
+  () => props.percentage !== null || props.integer !== null || !!props.description,
+);
+</script>
+
+<style scoped lang="scss">
+@import "src/css/quasar.variables.scss";
+
+.financial-card--clickable {
+  cursor: pointer;
+
+  &:hover {
+    box-shadow: 0 0 0 1.5px $primary !important;
+  }
+}
+
+.financial-card--selected {
+  box-shadow: 0 0 0 2px $primary !important;
+}
+
+.financial-card {
+  flex: 1 1 0;
+  min-width: 220px;
+  border-radius: 12px;
+  box-shadow: 0 0 0 1px #c0c0c0c0 !important;
+  padding: 16px 20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.card-title {
+  line-height: 1.3;
+}
+
+.bottom-area {
+  display: flex;
+  flex-direction: column;
+}
+
+.value-text {
+  font-weight: 600;
+  line-height: 1.2;
+}
+</style>

+ 93 - 0
src/pages/financial/AccountsPayablePage.vue

@@ -0,0 +1,93 @@
+<template>
+  <div>
+    <DefaultHeaderPage title="Contas a Pagar" :show-filter-icon="false" />
+
+    <div class="row q-pa-md q-gutter-md">
+      <FinancialCard
+        title="Saldo Bancário"
+        icon="mdi-bank-outline"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos pendentes"
+      />
+      <FinancialCard
+        title="Contas Quitadas"
+        icon="mdi-check-circle-outline"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos no mês"
+      />
+      <FinancialCard
+        title="Contas a Pagar"
+        icon="mdi-cash-minus"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos pendentes"
+      />
+    </div>
+
+    <div class="row justify-end items-center q-px-md q-mb-sm q-gutter-sm">
+      <q-btn
+        :color="showMovimentacoes ? 'secondary' : 'primary'"
+        label="Últimas Movimentações"
+        unelevated
+        no-caps
+        @click="showMovimentacoes = !showMovimentacoes"
+      />
+      <q-btn color="primary" label="Exportar Relatório" icon="mdi-download" unelevated no-caps />
+    </div>
+
+    <div class="q-px-md">
+      <DefaultTable
+        v-if="!showMovimentacoes"
+        v-model:rows="rows"
+        no-api-call
+        add-item
+        title="Contas a Pagar"
+        description="contas"
+        :female="true"
+        :columns="columns"
+        @on-add-item="handleAddItem"
+      />
+      <DefaultTable
+        v-else
+        v-model:rows="movimentacoesRows"
+        no-api-call
+        :add-item="false"
+        title="Últimas Movimentações"
+        description="movimentações"
+        :female="true"
+        :columns="movimentacoesColumns"
+        @on-add-item="handleAddItem"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import FinancialCard from "src/components/financial/FinancialCard.vue";
+
+const showMovimentacoes = ref(false);
+const rows = ref([]);
+const movimentacoesRows = ref([]);
+
+const columns = [
+  { name: "description", label: "Descrição", field: "description", align: "left" },
+  { name: "category", label: "Categoria", field: "category", align: "left" },
+  { 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" },
+];
+
+const movimentacoesColumns = [
+  { name: "description", label: "Descrição", field: "description", align: "left" },
+  { name: "date", label: "Data", field: "date", align: "left" },
+  { name: "value", label: "Valor", field: "value", align: "left" },
+  { name: "status", label: "Status", field: "status", align: "left" },
+];
+
+const handleAddItem = () => {};
+</script>

+ 93 - 0
src/pages/financial/AccountsReceivablePage.vue

@@ -0,0 +1,93 @@
+<template>
+  <div>
+    <DefaultHeaderPage title="Contas a Receber" :show-filter-icon="false" />
+
+    <div class="row q-pa-md q-gutter-md">
+      <FinancialCard
+        title="Saldo Bancário"
+        icon="mdi-bank-outline"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos pendentes"
+      />
+      <FinancialCard
+        title="Contas Recebidas"
+        icon="mdi-check-circle-outline"
+        :financial-value="0"
+        :integer="0"
+        integer-label="recebimentos no mês"
+      />
+      <FinancialCard
+        title="Contas a Receber"
+        icon="mdi-cash-plus"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos pendentes"
+      />
+    </div>
+
+    <div class="row justify-end items-center q-px-md q-mb-sm q-gutter-sm">
+      <q-btn
+        :color="showMovimentacoes ? 'secondary' : 'primary'"
+        label="Últimas Movimentações"
+        unelevated
+        no-caps
+        @click="showMovimentacoes = !showMovimentacoes"
+      />
+      <q-btn color="primary" label="Exportar Relatório" icon="mdi-download" unelevated no-caps />
+    </div>
+
+    <div class="q-px-md">
+      <DefaultTable
+        v-if="!showMovimentacoes"
+        v-model:rows="rows"
+        no-api-call
+        add-item
+        title="Contas a Receber"
+        description="contas"
+        :female="true"
+        :columns="columns"
+        @on-add-item="handleAddItem"
+      />
+      <DefaultTable
+        v-else
+        v-model:rows="movimentacoesRows"
+        no-api-call
+        :add-item="false"
+        title="Últimas Movimentações"
+        description="movimentações"
+        :female="true"
+        :columns="movimentacoesColumns"
+        @on-add-item="handleAddItem"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
+import FinancialCard from "src/components/financial/FinancialCard.vue";
+
+const showMovimentacoes = ref(false);
+const rows = ref([]);
+const movimentacoesRows = ref([]);
+
+const columns = [
+  { name: "description", label: "Descrição", field: "description", align: "left" },
+  { name: "category", label: "Categoria", field: "category", align: "left" },
+  { 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" },
+];
+
+const movimentacoesColumns = [
+  { name: "description", label: "Descrição", field: "description", align: "left" },
+  { name: "date", label: "Data", field: "date", align: "left" },
+  { name: "value", label: "Valor", field: "value", align: "left" },
+  { name: "status", label: "Status", field: "status", align: "left" },
+];
+
+const handleAddItem = () => {};
+</script>

+ 79 - 0
src/pages/financial/FinancialPage.vue

@@ -0,0 +1,79 @@
+<template>
+  <div>
+    <DefaultHeaderPage title="Financeiro" :show-filter-icon="false" />
+
+    <div class="row justify-end q-px-md q-mb-xs">
+      <q-btn
+        flat
+        round
+        :icon="showValues ? 'mdi-eye-outline' : 'mdi-eye-off-outline'"
+        color="primary"
+        @click="showValues = !showValues"
+      >
+        <q-tooltip anchor="center left" self="center right" :offset="[8, 0]">
+          {{ showValues ? 'Ocultar valores' : 'Exibir valores' }}
+        </q-tooltip>
+      </q-btn>
+    </div>
+
+    <div class="row q-pa-md q-gutter-md">
+      <FinancialCard
+        title="Saldo Bancário"
+        icon="mdi-bank-outline"
+        :financial-value="0"
+        description="Estável"
+        :hide-values="!showValues"
+      />
+      <FinancialCard
+        title="Receita do Mês"
+        icon="mdi-cash-multiple"
+        :financial-value="0"
+        :percentage="0"
+        description="Estável"
+        :hide-values="!showValues"
+      />
+      <FinancialCard
+        title="Despesas do Mês"
+        icon="mdi-cash-multiple"
+        :financial-value="0"
+        :percentage="0"
+        description="Estável"
+        :hide-values="!showValues"
+      />
+    </div>
+
+    <div class="row q-px-md q-gutter-md">
+      <FinancialCard
+        title="Contas a Pagar"
+        icon="mdi-cash-minus"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos pendentes"
+        :hide-values="!showValues"
+        clickable
+        @click="$router.push({ name: 'AccountsPayablePage' })"
+      />
+      <FinancialCard
+        title="Contas a Receber"
+        icon="mdi-cash-plus"
+        :financial-value="0"
+        :integer="0"
+        integer-label="pagamentos pendentes"
+        :hide-values="!showValues"
+        clickable
+        @click="$router.push({ name: 'AccountsReceivablePage' })"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch } from "vue";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import FinancialCard from "src/components/financial/FinancialCard.vue";
+
+const STORAGE_KEY = "franchisee_financial_show_values";
+const showValues = ref(localStorage.getItem(STORAGE_KEY) !== "false");
+
+watch(showValues, (val) => localStorage.setItem(STORAGE_KEY, String(val)));
+</script>

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

@@ -0,0 +1,43 @@
+export default [
+  {
+    path: "/financial",
+    name: "FinancialPage",
+    component: () => import("pages/financial/FinancialPage.vue"),
+    meta: {
+      title: { value: "Financeiro", translate: false },
+      requireAuth: true,
+      breadcrumbs: [
+        { name: "DashboardPage", title: "Dashboard" },
+        { name: "FinancialPage", title: "Financeiro" },
+      ],
+    },
+  },
+  {
+    path: "/financial/accounts-payable",
+    name: "AccountsPayablePage",
+    component: () => import("pages/financial/AccountsPayablePage.vue"),
+    meta: {
+      title: { value: "Contas a Pagar", translate: false },
+      requireAuth: true,
+      breadcrumbs: [
+        { name: "DashboardPage", title: "Dashboard" },
+        { name: "FinancialPage", title: "Financeiro" },
+        { name: "AccountsPayablePage", title: "Contas a Pagar" },
+      ],
+    },
+  },
+  {
+    path: "/financial/accounts-receivable",
+    name: "AccountsReceivablePage",
+    component: () => import("pages/financial/AccountsReceivablePage.vue"),
+    meta: {
+      title: { value: "Contas a Receber", translate: false },
+      requireAuth: true,
+      breadcrumbs: [
+        { name: "DashboardPage", title: "Dashboard" },
+        { name: "FinancialPage", title: "Financeiro" },
+        { name: "AccountsReceivablePage", title: "Contas a Receber" },
+      ],
+    },
+  },
+];

+ 28 - 0
src/stores/navigation.js

@@ -28,6 +28,34 @@ export const navigationStore = defineStore("navigation", () => {
       disable: false,
       permission: true,
     },
+    {
+      type: "expansive",
+      title: "Financeiro",
+      name: "FinancialPage",
+      icon: "mdi-cash-multiple",
+      disable: false,
+      permission: true,
+      childrens: [
+        {
+          title: "Visão Geral",
+          name: "FinancialPage",
+          icon: "mdi-chart-line",
+          permission: true,
+        },
+        {
+          title: "Contas a Pagar",
+          name: "AccountsPayablePage",
+          icon: "mdi-cash-minus",
+          permission: true,
+        },
+        {
+          title: "Contas a Receber",
+          name: "AccountsReceivablePage",
+          icon: "mdi-cash-plus",
+          permission: true,
+        },
+      ],
+    },
     {
       type: "single",
       title: "Pacotes",