ChartOfAccountsPage.vue 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <template>
  2. <div>
  3. <DefaultHeaderPage title="Plano de Contas" :show-filter-icon="false" />
  4. <div class="q-px-md">
  5. <q-card flat bordered class="q-pa-md q-mt-md">
  6. <div class="row items-center q-gutter-md q-mb-md">
  7. <div>
  8. <div class="text-h6">Plano de Contas</div>
  9. <div class="text-body2">{{ accounts.length }} contas cadastradas</div>
  10. </div>
  11. <q-space />
  12. <q-btn
  13. v-if="canAdd"
  14. color="primary"
  15. label="Nova Conta"
  16. no-caps
  17. unelevated
  18. @click="handleAddItem"
  19. />
  20. </div>
  21. <DefaultInput
  22. v-model="search"
  23. dense
  24. clearable
  25. debounce="250"
  26. label="Buscar por código ou nome da conta"
  27. class="q-mb-md"
  28. >
  29. <template #prepend>
  30. <q-icon name="mdi-magnify" color="grey-6" />
  31. </template>
  32. </DefaultInput>
  33. <div class="account-tree-header">
  34. <div>Código</div>
  35. <div>Nome da Conta</div>
  36. <div>Tipo</div>
  37. </div>
  38. <q-list>
  39. <FinancialPlanAccountTreeNode
  40. v-for="node in filteredTree"
  41. :key="node.id"
  42. :node="node"
  43. :force-expanded="!!normalizedSearch"
  44. />
  45. </q-list>
  46. <div v-if="!loading && filteredTree.length === 0" class="text-center q-pa-lg">
  47. Nenhuma conta encontrada.
  48. </div>
  49. <q-inner-loading :showing="loading" color="primary" />
  50. </q-card>
  51. </div>
  52. </div>
  53. </template>
  54. <script setup>
  55. import { ref, computed, onMounted } from "vue";
  56. import { useQuasar } from "quasar";
  57. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  58. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  59. import AddFinancialPlanAccountDialog from "src/components/financial/AddFinancialPlanAccountDialog.vue";
  60. import FinancialPlanAccountTreeNode from "src/components/financial/FinancialPlanAccountTreeNode.vue";
  61. import { getFinancialPlanAccounts } from "src/api/financial_plan_account";
  62. import { permissionStore } from "src/stores/permission";
  63. const $q = useQuasar();
  64. const canAdd = permissionStore().getAccess("franchisor_financial", "add");
  65. const accounts = ref([]);
  66. const loading = ref(false);
  67. const search = ref("");
  68. const accountTree = computed(() => {
  69. const nodes = new Map(
  70. accounts.value.map((account) => [account.id, { ...account, children: [] }]),
  71. );
  72. const roots = [];
  73. for (const node of nodes.values()) {
  74. const parent = node.parent_id ? nodes.get(node.parent_id) : null;
  75. if (parent) parent.children.push(node);
  76. else roots.push(node);
  77. }
  78. const sortNodes = (items) => {
  79. items.sort((a, b) =>
  80. String(a.code).localeCompare(String(b.code), "pt-BR", { numeric: true }),
  81. );
  82. items.forEach((item) => sortNodes(item.children));
  83. return items;
  84. };
  85. return sortNodes(roots);
  86. });
  87. const normalizedSearch = computed(() => search.value?.trim().toLocaleLowerCase("pt-BR") ?? "");
  88. const filteredTree = computed(() => {
  89. if (!normalizedSearch.value) return accountTree.value;
  90. const filterNodes = (nodes) =>
  91. nodes.reduce((result, node) => {
  92. const children = filterNodes(node.children);
  93. const matches = `${node.code} ${node.description}`
  94. .toLocaleLowerCase("pt-BR")
  95. .includes(normalizedSearch.value);
  96. if (matches || children.length) result.push({ ...node, children });
  97. return result;
  98. }, []);
  99. return filterNodes(accountTree.value);
  100. });
  101. const fetchAccounts = async () => {
  102. loading.value = true;
  103. try {
  104. accounts.value = await getFinancialPlanAccounts();
  105. } catch {
  106. $q.notify({ type: "negative", message: "Erro ao carregar o plano de contas." });
  107. } finally {
  108. loading.value = false;
  109. }
  110. };
  111. const handleAddItem = () => {
  112. $q.dialog({ component: AddFinancialPlanAccountDialog }).onOk(() => {
  113. $q.notify({ type: "positive", message: "Conta cadastrada com sucesso." });
  114. fetchAccounts();
  115. });
  116. };
  117. onMounted(fetchAccounts);
  118. </script>
  119. <style scoped>
  120. .account-tree-header {
  121. display: grid;
  122. grid-template-columns: 18% 1fr 18%;
  123. gap: 16px;
  124. padding: 12px 56px 12px 16px;
  125. border-bottom: 1px solid #c7c7c7;
  126. font-weight: 600;
  127. }
  128. @media (max-width: 599px) {
  129. .account-tree-header {
  130. grid-template-columns: 25% 1fr 25%;
  131. padding-left: 8px;
  132. }
  133. }
  134. </style>