AccountsPayablePage.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <template>
  2. <div>
  3. <DefaultHeaderPage
  4. :show-filter-icon="false"
  5. title="Contas a Pagar"
  6. />
  7. <div class="row q-pa-md q-gutter-md">
  8. <FinancialCard
  9. :financial-value="totals.paid"
  10. :integer="totals.paidCount"
  11. icon="mdi-check-circle-outline"
  12. integer-label="pagamentos"
  13. title="Pago"
  14. />
  15. <FinancialCard
  16. :financial-value="totals.pending"
  17. :integer="totals.pendingCount"
  18. icon="mdi-cash-minus"
  19. integer-label="pendentes"
  20. title="A Pagar"
  21. />
  22. <FinancialCard
  23. :financial-value="totals.overdue"
  24. :integer="totals.overdueCount"
  25. icon="mdi-alert-circle-outline"
  26. integer-label="em atraso"
  27. title="Vencidas"
  28. />
  29. </div>
  30. <div class="q-px-md">
  31. <DefaultTable
  32. v-model:rows="rows"
  33. :add-item="canAdd"
  34. :columns="columns"
  35. :female="true"
  36. description="contas"
  37. no-api-call
  38. title="Contas a Pagar"
  39. @on-add-item="openCreate"
  40. >
  41. <template #body-cell-origin="{ row }">
  42. <q-td class="text-left">
  43. <q-chip
  44. :color="
  45. row.origin === 'tbr'
  46. ? 'deep-purple-5'
  47. : 'blue-grey-5'
  48. "
  49. :label="row.origin === 'tbr' ? 'TBR' : 'Manual'"
  50. dense
  51. square
  52. text-color="white"
  53. />
  54. </q-td>
  55. </template>
  56. <template #body-cell-value="{ row }">
  57. <q-td class="text-left">
  58. {{ formatCurrency(row.value) }}
  59. </q-td>
  60. </template>
  61. <template #body-cell-status="{ row }">
  62. <q-td class="text-left">
  63. <q-btn
  64. :color="row.status === 'paid' ? 'positive' : 'secondary'"
  65. :disable="!canEdit || row.status === 'paid'"
  66. :label="row.status === 'paid' ? 'Pago' : 'Não pago'"
  67. dense
  68. no-caps
  69. unelevated
  70. @click.stop="changeStatus(row)"
  71. >
  72. <q-tooltip v-if="canEdit && row.status !== 'paid'">
  73. Dar baixa
  74. </q-tooltip>
  75. </q-btn>
  76. </q-td>
  77. </template>
  78. <template #body-cell-actions="{ row }">
  79. <q-td auto-width>
  80. <q-btn
  81. v-if="row.invoice_url"
  82. aria-label="Abrir cobrança"
  83. color="teal-7"
  84. dense
  85. flat
  86. icon="mdi-open-in-new"
  87. round
  88. @click="openInvoice(row)"
  89. >
  90. <q-tooltip>
  91. Abrir cobrança no Asaas
  92. </q-tooltip>
  93. </q-btn>
  94. </q-td>
  95. </template>
  96. </DefaultTable>
  97. </div>
  98. <q-dialog v-model="settleDialog">
  99. <q-card
  100. class="dialog-form-card"
  101. style="min-width: 360px; max-width: 460px"
  102. >
  103. <DefaultDialogHeader
  104. title="Dar baixa"
  105. @close="settleDialog = false"
  106. />
  107. <DefaultForm @submit="onSettle">
  108. <q-scroll-area class="dialog-form-scroll dialog-form-scroll--sm">
  109. <q-card-section class="q-gutter-sm">
  110. <div class="text-body2 text-grey-8">
  111. {{ selected?.history }}
  112. </div>
  113. <DefaultInputDatePicker
  114. v-model="settleForm.payment_date"
  115. v-model:untreated-date="settleForm.payment_date_iso"
  116. label="Data do pagamento"
  117. />
  118. <div class="row q-col-gutter-sm">
  119. <DefaultCurrencyInput
  120. v-model="settleForm.discount"
  121. class="col-6"
  122. label="Desconto"
  123. outlined
  124. />
  125. <DefaultCurrencyInput
  126. v-model="settleForm.fine"
  127. class="col-6"
  128. label="Multa/Juros"
  129. outlined
  130. />
  131. </div>
  132. <div class="text-caption text-grey-7">
  133. Valor da conta: {{ formatCurrency(selected?.value) }} ·
  134. <span class="text-weight-medium">
  135. Pago: {{ formatCurrency(settleNetValue) }}
  136. </span>
  137. </div>
  138. </q-card-section>
  139. </q-scroll-area>
  140. <q-card-actions
  141. align="right"
  142. class="q-px-md q-pb-md"
  143. >
  144. <q-btn
  145. color="primary"
  146. label="Cancelar"
  147. no-caps
  148. outline
  149. @click="settleDialog = false"
  150. />
  151. <q-btn
  152. color="primary"
  153. label="Confirmar baixa"
  154. no-caps
  155. type="submit"
  156. :loading="settling"
  157. />
  158. </q-card-actions>
  159. </DefaultForm>
  160. </q-card>
  161. </q-dialog>
  162. <q-dialog v-model="createDialog">
  163. <q-card
  164. class="dialog-form-card"
  165. style="min-width: 360px; max-width: 460px"
  166. >
  167. <DefaultDialogHeader
  168. title="Nova conta a pagar"
  169. @close="createDialog = false"
  170. />
  171. <DefaultForm
  172. ref="createFormRef"
  173. @submit="onCreate"
  174. >
  175. <q-scroll-area
  176. ref="createScrollAreaRef"
  177. class="dialog-form-scroll dialog-form-scroll--sm"
  178. >
  179. <q-card-section class="q-gutter-sm">
  180. <DefaultInput
  181. v-model="createForm.history"
  182. v-model:error="createValidationErrors.history"
  183. label="Descrição"
  184. outlined
  185. :rules="[inputRules.required]"
  186. />
  187. <div
  188. style="
  189. display: grid;
  190. grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  191. gap: 8px;
  192. "
  193. >
  194. <DefaultCurrencyInput
  195. v-model="createForm.value"
  196. v-model:error="createValidationErrors.value"
  197. label="Valor"
  198. outlined
  199. :rules="[inputRules.required, positiveAmountRule]"
  200. />
  201. <DefaultInputDatePicker
  202. v-model="createForm.due_date"
  203. v-model:error="createValidationErrors.due_date"
  204. v-model:untreated-date="createForm.due_date_iso"
  205. label="Vencimento"
  206. :rules="[inputRules.required]"
  207. />
  208. </div>
  209. <DefaultInput
  210. v-model="createForm.obs"
  211. label="Observação (opcional)"
  212. outlined
  213. type="textarea"
  214. />
  215. </q-card-section>
  216. </q-scroll-area>
  217. <q-card-actions
  218. align="right"
  219. class="q-px-md q-pb-md"
  220. >
  221. <q-btn
  222. color="primary"
  223. label="Cancelar"
  224. no-caps
  225. outline
  226. @click="createDialog = false"
  227. />
  228. <q-btn
  229. color="primary"
  230. label="Salvar"
  231. no-caps
  232. type="submit"
  233. :loading="creating"
  234. />
  235. </q-card-actions>
  236. </DefaultForm>
  237. </q-card>
  238. </q-dialog>
  239. </div>
  240. </template>
  241. <script setup>
  242. import {
  243. createPayableMe,
  244. getPayablesMe,
  245. settlePayableMe,
  246. } from "src/api/unit_payable";
  247. import { computed, onMounted, ref, useTemplateRef } from "vue";
  248. import { permissionStore } from "src/stores/permission";
  249. import { useForm } from "src/composables/useForm";
  250. import { useInputRules } from "src/composables/useInputRules";
  251. import { useScroll } from "src/composables/useScroll";
  252. import { useSubmitHandler } from "src/composables/useSubmitHandler";
  253. import DefaultCurrencyInput from "src/components/defaults/DefaultCurrencyInput.vue";
  254. import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
  255. import DefaultForm from "src/components/defaults/DefaultForm.vue";
  256. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  257. import DefaultInput from "src/components/defaults/DefaultInput.vue";
  258. import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
  259. import DefaultTable from "src/components/defaults/DefaultTable.vue";
  260. import FinancialCard from "src/components/financial/FinancialCard.vue";
  261. const { inputRules } = useInputRules();
  262. const { scrollToComponent } = useScroll();
  263. const permissions = permissionStore();
  264. const createFormRef = useTemplateRef("createFormRef");
  265. const createScrollAreaRef = useTemplateRef("createScrollAreaRef");
  266. const createDialog = ref(false);
  267. const rows = ref([]);
  268. const selected = ref(null);
  269. const settleDialog = ref(false);
  270. const settleForm = ref({
  271. discount: 0,
  272. fine: 0,
  273. payment_date: null,
  274. payment_date_iso: null,
  275. });
  276. const settling = ref(false);
  277. const canAdd = computed(() =>
  278. permissions.getAccess("franchisee_financial", "add"),
  279. );
  280. const canEdit = computed(() =>
  281. permissions.getAccess("franchisee_financial", "edit"),
  282. );
  283. const settleNetValue = computed(() => {
  284. const baseValue = Number(selected.value?.value ?? 0);
  285. const discount = Number(settleForm.value.discount ?? 0);
  286. const fine = Number(settleForm.value.fine ?? 0);
  287. return baseValue - discount + fine;
  288. });
  289. const totals = computed(() => {
  290. const accumulatedTotals = {
  291. overdue: 0,
  292. overdueCount: 0,
  293. paid: 0,
  294. paidCount: 0,
  295. pending: 0,
  296. pendingCount: 0,
  297. };
  298. for (const row of rows.value) {
  299. if (row.status === "paid") {
  300. accumulatedTotals.paid += Number(row.paid_value ?? 0);
  301. accumulatedTotals.paidCount += 1;
  302. continue;
  303. }
  304. if (row.status === "overdue") {
  305. accumulatedTotals.overdue += Number(row.value ?? 0);
  306. accumulatedTotals.overdueCount += 1;
  307. continue;
  308. }
  309. if (row.status === "pending") {
  310. accumulatedTotals.pending += Number(row.value ?? 0);
  311. accumulatedTotals.pendingCount += 1;
  312. }
  313. }
  314. return accumulatedTotals;
  315. });
  316. const columns = [
  317. {
  318. align: "left",
  319. field: "origin",
  320. label: "Origem",
  321. name: "origin",
  322. },
  323. {
  324. align: "left",
  325. field: "history",
  326. label: "Descrição",
  327. name: "history",
  328. },
  329. {
  330. align: "left",
  331. field: "value",
  332. label: "Valor",
  333. name: "value",
  334. },
  335. {
  336. align: "left",
  337. field: "due_date",
  338. label: "Vencimento",
  339. name: "due_date",
  340. },
  341. {
  342. align: "left",
  343. field: "status",
  344. label: "Status",
  345. name: "status",
  346. },
  347. {
  348. align: "right",
  349. field: "actions",
  350. label: "Ações",
  351. name: "actions",
  352. },
  353. ];
  354. const defaultCreateForm = () => {
  355. const today = new Date();
  356. return {
  357. due_date: today.toLocaleDateString("pt-BR"),
  358. due_date_iso: today.toISOString().slice(0, 10),
  359. history: "",
  360. obs: "",
  361. value: 0,
  362. };
  363. };
  364. const { form: createForm } = useForm(defaultCreateForm());
  365. const {
  366. loading: creating,
  367. validationErrors: createValidationErrors,
  368. execute: executeCreate,
  369. } = useSubmitHandler({
  370. containerRef: createScrollAreaRef,
  371. formRef: createFormRef,
  372. onSuccess: async () => {
  373. createDialog.value = false;
  374. await loadData();
  375. },
  376. scrollFn: scrollToComponent,
  377. });
  378. const changeStatus = (row) => {
  379. if (row.status === "paid") return;
  380. openSettle(row);
  381. };
  382. const formatCurrency = (value) =>
  383. new Intl.NumberFormat("pt-BR", {
  384. currency: "BRL",
  385. style: "currency",
  386. }).format(Number(value ?? 0));
  387. const loadData = async () => {
  388. try {
  389. rows.value = await getPayablesMe();
  390. } catch (error) {
  391. console.error("Erro ao buscar contas a pagar:", error);
  392. }
  393. };
  394. const onCreate = async () => {
  395. await executeCreate(() =>
  396. createPayableMe({
  397. due_date: createForm.due_date_iso,
  398. history: createForm.history,
  399. obs: createForm.obs || null,
  400. value: createForm.value,
  401. }),
  402. );
  403. };
  404. const onSettle = async () => {
  405. if (!selected.value) return;
  406. settling.value = true;
  407. try {
  408. await settlePayableMe(selected.value.id, {
  409. discount: settleForm.value.discount,
  410. fine: settleForm.value.fine,
  411. payment_date: settleForm.value.payment_date_iso,
  412. });
  413. settleDialog.value = false;
  414. await loadData();
  415. } catch (error) {
  416. console.error(error);
  417. } finally {
  418. settling.value = false;
  419. }
  420. };
  421. const openCreate = () => {
  422. Object.assign(createForm, defaultCreateForm());
  423. createDialog.value = true;
  424. };
  425. const openInvoice = (row) => {
  426. window.open(row.invoice_url, "_blank");
  427. };
  428. const openSettle = (row) => {
  429. const today = new Date();
  430. selected.value = row;
  431. settleForm.value = {
  432. discount: Number(row.discount ?? 0),
  433. fine: Number(row.fine ?? 0),
  434. payment_date: today.toLocaleDateString("pt-BR"),
  435. payment_date_iso: today.toISOString().slice(0, 10),
  436. };
  437. settleDialog.value = true;
  438. };
  439. const positiveAmountRule = (value) =>
  440. Number(value) > 0 || "Informe um valor maior que zero.";
  441. onMounted(loadData);
  442. </script>