Procházet zdrojové kódy

feat(financial): add FinancialPlanAccountTreeNode component and integrate into ChartOfAccountsPage

ebagabee před 1 týdnem
rodič
revize
5d5eea0340

+ 100 - 0
src/components/financial/FinancialPlanAccountTreeNode.vue

@@ -0,0 +1,100 @@
+<template>
+  <q-expansion-item
+    v-if="hasChildren"
+    v-model="expanded"
+    dense
+    expand-separator
+    header-class="account-tree-row"
+    :style="{ '--account-depth': depth }"
+  >
+    <template #header>
+      <q-item-section class="account-code">{{ node.code }}</q-item-section>
+      <q-item-section class="account-name text-weight-medium">
+        {{ node.description }}
+      </q-item-section>
+      <q-item-section class="account-type">{{ node.chart_type }}</q-item-section>
+    </template>
+
+    <FinancialPlanAccountTreeNode
+      v-for="child in node.children"
+      :key="child.id"
+      :node="child"
+      :depth="depth + 1"
+      :force-expanded="forceExpanded"
+    />
+  </q-expansion-item>
+
+  <q-item
+    v-else
+    dense
+    class="account-tree-row account-tree-leaf"
+    :style="{ '--account-depth': depth }"
+  >
+    <q-item-section class="account-code">{{ node.code }}</q-item-section>
+    <q-item-section class="account-name">{{ node.description }}</q-item-section>
+    <q-item-section class="account-type">{{ node.chart_type }}</q-item-section>
+  </q-item>
+</template>
+
+<script setup>
+import { computed, ref, watch } from "vue";
+
+defineOptions({ name: "FinancialPlanAccountTreeNode" });
+
+const props = defineProps({
+  node: { type: Object, required: true },
+  depth: { type: Number, default: 0 },
+  forceExpanded: { type: Boolean, default: false },
+});
+
+const hasChildren = computed(() => props.node.children?.length > 0);
+const expanded = ref(props.depth === 0);
+
+watch(
+  () => props.forceExpanded,
+  (value) => {
+    if (value) expanded.value = true;
+  },
+  { immediate: true },
+);
+</script>
+
+<style scoped>
+:deep(.account-tree-row) {
+  min-height: 48px;
+  padding-left: calc(16px + (var(--account-depth) * 28px));
+  border-bottom: 1px solid #dedede;
+}
+
+:deep(.account-tree-leaf) {
+  padding-right: 56px;
+}
+
+:deep(.account-code) {
+  flex: 0 0 18%;
+  min-width: 80px;
+}
+
+:deep(.account-name) {
+  flex: 1 1 auto;
+  min-width: 0;
+}
+
+:deep(.account-type) {
+  flex: 0 0 18%;
+  min-width: 90px;
+  align-items: flex-start;
+}
+
+@media (max-width: 599px) {
+  :deep(.account-tree-row) {
+    padding-left: calc(8px + (var(--account-depth) * 16px));
+  }
+
+  :deep(.account-code),
+  :deep(.account-type) {
+    flex-basis: 25%;
+    min-width: 64px;
+  }
+}
+</style>

+ 2 - 0
src/components/layout/DefaultHeaderPage.vue

@@ -319,6 +319,8 @@ const NOTIFICATION_ICONS = {
   kanban_reply: "mdi-comment-text-outline",
   kanban_reply: "mdi-comment-text-outline",
   kanban_status_changed: "mdi-swap-horizontal",
   kanban_status_changed: "mdi-swap-horizontal",
   tbr_month_closed: "mdi-cash-multiple",
   tbr_month_closed: "mdi-cash-multiple",
+  account_payable_created: "mdi-file-document-plus-outline",
+  payment_credited: "mdi-cash-check",
 };
 };
 
 
 const iconFor = (type) => NOTIFICATION_ICONS[type] || "mdi-bell-outline";
 const iconFor = (type) => NOTIFICATION_ICONS[type] || "mdi-bell-outline";

+ 113 - 32
src/pages/financial/ChartOfAccountsPage.vue

@@ -3,19 +3,57 @@
     <DefaultHeaderPage title="Plano de Contas" :show-filter-icon="false" />
     <DefaultHeaderPage title="Plano de Contas" :show-filter-icon="false" />
 
 
     <div class="q-px-md">
     <div class="q-px-md">
-      <DefaultTable
-        v-model:rows="rows"
-        no-api-call
-        :add-item="canAdd"
-        add-item-label="Nova Conta"
-        dense-add
-        :loading="loading"
-        title="Plano de Contas"
-        description="contas"
-        :female="true"
-        :columns="columns"
-        @on-add-item="handleAddItem"
-      />
+      <q-card flat bordered class="q-pa-md q-mt-md">
+        <div class="row items-center q-gutter-md q-mb-md">
+          <div>
+            <div class="text-h6">Plano de Contas</div>
+            <div class="text-body2">{{ accounts.length }} contas cadastradas</div>
+          </div>
+          <q-space />
+          <q-btn
+            v-if="canAdd"
+            color="primary"
+            label="Nova Conta"
+            no-caps
+            unelevated
+            @click="handleAddItem"
+          />
+        </div>
+
+        <DefaultInput
+          v-model="search"
+          dense
+          clearable
+          debounce="250"
+          label="Buscar por código ou nome da conta"
+          class="q-mb-md"
+        >
+          <template #prepend>
+            <q-icon name="mdi-magnify" color="grey-6" />
+          </template>
+        </DefaultInput>
+
+        <div class="account-tree-header">
+          <div>Código</div>
+          <div>Nome da Conta</div>
+          <div>Tipo</div>
+        </div>
+
+        <q-list>
+          <FinancialPlanAccountTreeNode
+            v-for="node in filteredTree"
+            :key="node.id"
+            :node="node"
+            :force-expanded="!!normalizedSearch"
+          />
+        </q-list>
+
+        <div v-if="!loading && filteredTree.length === 0" class="text-center q-pa-lg">
+          Nenhuma conta encontrada.
+        </div>
+
+        <q-inner-loading :showing="loading" color="primary" />
+      </q-card>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
@@ -24,8 +62,9 @@
 import { ref, computed, onMounted } from "vue";
 import { ref, computed, onMounted } from "vue";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 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 AddFinancialPlanAccountDialog from "src/components/financial/AddFinancialPlanAccountDialog.vue";
 import AddFinancialPlanAccountDialog from "src/components/financial/AddFinancialPlanAccountDialog.vue";
+import FinancialPlanAccountTreeNode from "src/components/financial/FinancialPlanAccountTreeNode.vue";
 import { getFinancialPlanAccounts } from "src/api/financial_plan_account";
 import { getFinancialPlanAccounts } from "src/api/financial_plan_account";
 import { permissionStore } from "src/stores/permission";
 import { permissionStore } from "src/stores/permission";
 
 
@@ -34,25 +73,49 @@ const canAdd = permissionStore().getAccess("franchisor_financial", "add");
 
 
 const accounts = ref([]);
 const accounts = ref([]);
 const loading = ref(false);
 const loading = ref(false);
+const search = ref("");
+
+const accountTree = computed(() => {
+  const nodes = new Map(
+    accounts.value.map((account) => [account.id, { ...account, children: [] }]),
+  );
+  const roots = [];
+
+  for (const node of nodes.values()) {
+    const parent = node.parent_id ? nodes.get(node.parent_id) : null;
+    if (parent) parent.children.push(node);
+    else roots.push(node);
+  }
+
+  const sortNodes = (items) => {
+    items.sort((a, b) =>
+      String(a.code).localeCompare(String(b.code), "pt-BR", { numeric: true }),
+    );
+    items.forEach((item) => sortNodes(item.children));
+    return items;
+  };
 
 
-const rows = computed(() =>
-  accounts.value.map((account) => ({
-    id: account.id,
-    code: account.code,
-    name: account.description,
-    type: account.chart_type,
-    parent: account.parent
-      ? `${account.parent.code} — ${account.parent.description}`
-      : "—",
-  })),
-);
-
-const columns = [
-  { name: "code", label: "Código", field: "code", align: "left" },
-  { name: "name", label: "Nome da Conta", field: "name", align: "left" },
-  { name: "parent", label: "Conta Pai", field: "parent", align: "left" },
-  { name: "type", label: "Tipo", field: "type", align: "left" },
-];
+  return sortNodes(roots);
+});
+
+const normalizedSearch = computed(() => search.value?.trim().toLocaleLowerCase("pt-BR") ?? "");
+
+const filteredTree = computed(() => {
+  if (!normalizedSearch.value) return accountTree.value;
+
+  const filterNodes = (nodes) =>
+    nodes.reduce((result, node) => {
+      const children = filterNodes(node.children);
+      const matches = `${node.code} ${node.description}`
+        .toLocaleLowerCase("pt-BR")
+        .includes(normalizedSearch.value);
+
+      if (matches || children.length) result.push({ ...node, children });
+      return result;
+    }, []);
+
+  return filterNodes(accountTree.value);
+});
 
 
 const fetchAccounts = async () => {
 const fetchAccounts = async () => {
   loading.value = true;
   loading.value = true;
@@ -74,3 +137,21 @@ const handleAddItem = () => {
 
 
 onMounted(fetchAccounts);
 onMounted(fetchAccounts);
 </script>
 </script>
+
+<style scoped>
+.account-tree-header {
+  display: grid;
+  grid-template-columns: 18% 1fr 18%;
+  gap: 16px;
+  padding: 12px 56px 12px 16px;
+  border-bottom: 1px solid #c7c7c7;
+  font-weight: 600;
+}
+
+@media (max-width: 599px) {
+  .account-tree-header {
+    grid-template-columns: 25% 1fr 25%;
+    padding-left: 8px;
+  }
+}
+</style>