|
@@ -4,119 +4,287 @@
|
|
|
|
|
|
|
|
<div class="row q-pa-md q-gutter-md">
|
|
<div class="row q-pa-md q-gutter-md">
|
|
|
<FinancialCard
|
|
<FinancialCard
|
|
|
- title="Saldo Bancário"
|
|
|
|
|
- icon="mdi-bank-outline"
|
|
|
|
|
- :financial-value="0"
|
|
|
|
|
- :integer="0"
|
|
|
|
|
- integer-label="pagamentos pendentes"
|
|
|
|
|
- />
|
|
|
|
|
- <FinancialCard
|
|
|
|
|
- title="Contas Recebidas"
|
|
|
|
|
|
|
+ title="Recebido"
|
|
|
icon="mdi-check-circle-outline"
|
|
icon="mdi-check-circle-outline"
|
|
|
- :financial-value="0"
|
|
|
|
|
- :integer="0"
|
|
|
|
|
- integer-label="recebimentos no mês"
|
|
|
|
|
|
|
+ :financial-value="totals.received"
|
|
|
|
|
+ :integer="totals.receivedCount"
|
|
|
|
|
+ integer-label="recebimentos"
|
|
|
/>
|
|
/>
|
|
|
<FinancialCard
|
|
<FinancialCard
|
|
|
- title="Contas a Receber"
|
|
|
|
|
|
|
+ title="A Receber"
|
|
|
icon="mdi-cash-plus"
|
|
icon="mdi-cash-plus"
|
|
|
- :financial-value="0"
|
|
|
|
|
- :integer="0"
|
|
|
|
|
- integer-label="pagamentos pendentes"
|
|
|
|
|
|
|
+ :financial-value="totals.pending"
|
|
|
|
|
+ :integer="totals.pendingCount"
|
|
|
|
|
+ integer-label="pendentes"
|
|
|
|
|
+ />
|
|
|
|
|
+ <FinancialCard
|
|
|
|
|
+ title="Vencidas"
|
|
|
|
|
+ icon="mdi-alert-circle-outline"
|
|
|
|
|
+ :financial-value="totals.overdue"
|
|
|
|
|
+ :integer="totals.overdueCount"
|
|
|
|
|
+ integer-label="em atraso"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="row justify-end items-center q-px-md q-mb-sm q-gutter-sm">
|
|
<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"
|
|
|
|
|
|
|
+ <DefaultSelect
|
|
|
|
|
+ v-model="statusFilter"
|
|
|
|
|
+ label="Status"
|
|
|
|
|
+ :options="statusFilterOptions"
|
|
|
|
|
+ outlined
|
|
|
|
|
+ dense
|
|
|
|
|
+ emit-value
|
|
|
|
|
+ map-options
|
|
|
|
|
+ style="min-width: 200px"
|
|
|
|
|
+ @update:model-value="loadData"
|
|
|
/>
|
|
/>
|
|
|
- <q-btn color="primary" label="Exportar Relatório" icon="mdi-download" unelevated no-caps />
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="q-px-md">
|
|
<div class="q-px-md">
|
|
|
<DefaultTable
|
|
<DefaultTable
|
|
|
- v-if="!showMovimentacoes"
|
|
|
|
|
v-model:rows="rows"
|
|
v-model:rows="rows"
|
|
|
no-api-call
|
|
no-api-call
|
|
|
- add-item
|
|
|
|
|
|
|
+ :add-item="false"
|
|
|
title="Contas a Receber"
|
|
title="Contas a Receber"
|
|
|
- description="contas"
|
|
|
|
|
- :female="true"
|
|
|
|
|
|
|
+ descricao="contas"
|
|
|
|
|
+ :feminino="true"
|
|
|
:columns="columns"
|
|
:columns="columns"
|
|
|
- :actions="tableActions"
|
|
|
|
|
- @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"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #body-cell-value="{ row }">
|
|
|
|
|
+ <q-td class="text-left">{{ formatCurrency(row.value) }}</q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #body-cell-status="{ row }">
|
|
|
|
|
+ <q-td class="text-left">
|
|
|
|
|
+ <q-chip
|
|
|
|
|
+ :color="statusMeta(row.status).color"
|
|
|
|
|
+ text-color="white"
|
|
|
|
|
+ dense
|
|
|
|
|
+ square
|
|
|
|
|
+ :label="statusMeta(row.status).label"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <template #body-cell-actions="{ row }">
|
|
|
|
|
+ <q-td auto-width>
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ v-if="row.status !== 'paid' && row.status !== 'cancelled'"
|
|
|
|
|
+ color="primary-2"
|
|
|
|
|
+ label="Dar baixa"
|
|
|
|
|
+ dense
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ unelevated
|
|
|
|
|
+ class="q-px-sm"
|
|
|
|
|
+ @click="openSettle(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ v-else-if="row.status === 'paid'"
|
|
|
|
|
+ color="grey-7"
|
|
|
|
|
+ label="Reabrir"
|
|
|
|
|
+ dense
|
|
|
|
|
+ no-caps
|
|
|
|
|
+ outline
|
|
|
|
|
+ class="q-px-sm"
|
|
|
|
|
+ :loading="reopeningId === row.id"
|
|
|
|
|
+ @click="onReopen(row)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </DefaultTable>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Diálogo de baixa manual -->
|
|
|
|
|
+ <q-dialog v-model="settleDialog">
|
|
|
|
|
+ <q-card style="min-width: 360px; max-width: 460px">
|
|
|
|
|
+ <DefaultDialogHeader title="Dar baixa" @close="settleDialog = false" />
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-section class="q-gutter-sm">
|
|
|
|
|
+ <div class="text-body2 text-grey-8">
|
|
|
|
|
+ {{ selected?.student_name }} — {{ selected?.history }}
|
|
|
|
|
+ ({{ selected?.order }})
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <DefaultInputDatePicker
|
|
|
|
|
+ v-model="settleForm.payment_date"
|
|
|
|
|
+ label="Data do pagamento"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div class="row q-col-gutter-sm">
|
|
|
|
|
+ <DefaultCurrencyInput
|
|
|
|
|
+ v-model="settleForm.discount"
|
|
|
|
|
+ label="Desconto"
|
|
|
|
|
+ class="col-6"
|
|
|
|
|
+ outlined
|
|
|
|
|
+ />
|
|
|
|
|
+ <DefaultCurrencyInput
|
|
|
|
|
+ v-model="settleForm.fine"
|
|
|
|
|
+ label="Multa/Juros"
|
|
|
|
|
+ class="col-6"
|
|
|
|
|
+ outlined
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="text-caption text-grey-7">
|
|
|
|
|
+ Valor da parcela: {{ formatCurrency(selected?.value) }} ·
|
|
|
|
|
+ <span class="text-weight-medium">
|
|
|
|
|
+ Recebido: {{ formatCurrency(settleNetValue) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+
|
|
|
|
|
+ <q-card-actions align="right" class="q-pa-md">
|
|
|
|
|
+ <q-btn flat label="Cancelar" color="grey-7" @click="settleDialog = false" />
|
|
|
|
|
+ <q-btn
|
|
|
|
|
+ color="primary-2"
|
|
|
|
|
+ label="Confirmar baixa"
|
|
|
|
|
+ :loading="settling"
|
|
|
|
|
+ @click="onSettle"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-card-actions>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </q-dialog>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, onMounted } from "vue";
|
|
|
|
|
-import api from "src/api";
|
|
|
|
|
|
|
+import { computed, onMounted, ref } from "vue";
|
|
|
|
|
+import {
|
|
|
|
|
+ getReceivablesMe,
|
|
|
|
|
+ settleReceivableMe,
|
|
|
|
|
+ reopenReceivableMe,
|
|
|
|
|
+} from "src/api/unit_receivable";
|
|
|
|
|
+
|
|
|
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 DefaultTable from "src/components/defaults/DefaultTable.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";
|
|
|
import FinancialCard from "src/components/financial/FinancialCard.vue";
|
|
import FinancialCard from "src/components/financial/FinancialCard.vue";
|
|
|
|
|
|
|
|
-const showMovimentacoes = ref(false);
|
|
|
|
|
const rows = ref([]);
|
|
const rows = ref([]);
|
|
|
-const movimentacoesRows = ref([]);
|
|
|
|
|
-
|
|
|
|
|
-// Para controlar as Actions e o Link do Asaas
|
|
|
|
|
-const tableActions = [
|
|
|
|
|
- {
|
|
|
|
|
- label: "Ver Boleto/Pix",
|
|
|
|
|
- icon: "mdi-barcode",
|
|
|
|
|
- color: "primary",
|
|
|
|
|
- onClick: (row) => {
|
|
|
|
|
- if (row.invoice_url) window.open(row.invoice_url, "_blank");
|
|
|
|
|
- else alert("Ainda não possui link de cobrança gerado no Asaas.");
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
|
|
+const statusFilter = ref(null);
|
|
|
|
|
+const settleDialog = ref(false);
|
|
|
|
|
+const selected = ref(null);
|
|
|
|
|
+const settling = ref(false);
|
|
|
|
|
+const reopeningId = ref(null);
|
|
|
|
|
+
|
|
|
|
|
+const settleForm = ref({ payment_date: null, discount: 0, fine: 0 });
|
|
|
|
|
+
|
|
|
|
|
+const statusFilterOptions = [
|
|
|
|
|
+ { label: "Todos", value: null },
|
|
|
|
|
+ { label: "Pendentes", value: "pending" },
|
|
|
|
|
+ { label: "Pagos", value: "paid" },
|
|
|
|
|
+ { label: "Vencidos", value: "overdue" },
|
|
|
|
|
+ { label: "Cancelados", value: "cancelled" },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
const columns = [
|
|
const columns = [
|
|
|
|
|
+ { name: "student_name", label: "Aluno", field: "student_name", align: "left" },
|
|
|
{ name: "history", label: "Descrição", field: "history", align: "left" },
|
|
{ name: "history", label: "Descrição", field: "history", align: "left" },
|
|
|
- { name: "type", label: "Categoria", field: "type", align: "left" },
|
|
|
|
|
- { name: "value", label: "Valor", field: "value", align: "left", format: val => `R$ ${val}` },
|
|
|
|
|
|
|
+ { name: "order", label: "Parcela", field: "order", align: "left" },
|
|
|
|
|
+ { name: "value", label: "Valor", field: "value", align: "left" },
|
|
|
{ name: "due_date", label: "Vencimento", field: "due_date", align: "left" },
|
|
{ name: "due_date", label: "Vencimento", field: "due_date", align: "left" },
|
|
|
{ name: "status", label: "Status", field: "status", align: "left" },
|
|
{ name: "status", label: "Status", field: "status", align: "left" },
|
|
|
- { name: "actions", label: "Ações", field: "actions", align: "right" }
|
|
|
|
|
|
|
+ { name: "actions", label: "Ações", field: "actions", align: "right" },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
-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" },
|
|
|
|
|
-];
|
|
|
|
|
|
|
+function formatCurrency(value) {
|
|
|
|
|
+ return new Intl.NumberFormat("pt-BR", {
|
|
|
|
|
+ style: "currency",
|
|
|
|
|
+ currency: "BRL",
|
|
|
|
|
+ }).format(Number(value ?? 0));
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-const loadData = async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await api.get('/franchisee-receivable');
|
|
|
|
|
- rows.value = res.data.payload;
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error("Erro ao buscar contas a receber:", error);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const STATUS_META = {
|
|
|
|
|
+ pending: { label: "Pendente", color: "orange-7" },
|
|
|
|
|
+ paid: { label: "Pago", color: "positive" },
|
|
|
|
|
+ overdue: { label: "Vencido", color: "negative" },
|
|
|
|
|
+ cancelled: { label: "Cancelado", color: "grey-6" },
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-onMounted(() => {
|
|
|
|
|
- loadData();
|
|
|
|
|
|
|
+function statusMeta(status) {
|
|
|
|
|
+ return STATUS_META[status] ?? { label: status, color: "grey-6" };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const settleNetValue = computed(() => {
|
|
|
|
|
+ const base = Number(selected.value?.value ?? 0);
|
|
|
|
|
+ return base - Number(settleForm.value.discount ?? 0) + Number(settleForm.value.fine ?? 0);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const totals = computed(() => {
|
|
|
|
|
+ const acc = {
|
|
|
|
|
+ received: 0,
|
|
|
|
|
+ receivedCount: 0,
|
|
|
|
|
+ pending: 0,
|
|
|
|
|
+ pendingCount: 0,
|
|
|
|
|
+ overdue: 0,
|
|
|
|
|
+ overdueCount: 0,
|
|
|
|
|
+ };
|
|
|
|
|
+ for (const r of rows.value) {
|
|
|
|
|
+ if (r.status === "paid") {
|
|
|
|
|
+ acc.received += Number(r.paid_value ?? 0);
|
|
|
|
|
+ acc.receivedCount++;
|
|
|
|
|
+ } else if (r.status === "overdue") {
|
|
|
|
|
+ acc.overdue += Number(r.value ?? 0);
|
|
|
|
|
+ acc.overdueCount++;
|
|
|
|
|
+ } else if (r.status === "pending") {
|
|
|
|
|
+ acc.pending += Number(r.value ?? 0);
|
|
|
|
|
+ acc.pendingCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return acc;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-const handleAddItem = () => {};
|
|
|
|
|
|
|
+async function loadData() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const params = statusFilter.value ? { status: statusFilter.value } : {};
|
|
|
|
|
+ rows.value = await getReceivablesMe(params);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error("Erro ao buscar contas a receber:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openSettle(row) {
|
|
|
|
|
+ selected.value = row;
|
|
|
|
|
+ settleForm.value = {
|
|
|
|
|
+ payment_date: new Date().toISOString().slice(0, 10),
|
|
|
|
|
+ discount: Number(row.discount ?? 0),
|
|
|
|
|
+ fine: Number(row.fine ?? 0),
|
|
|
|
|
+ };
|
|
|
|
|
+ settleDialog.value = true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onSettle() {
|
|
|
|
|
+ if (!selected.value) return;
|
|
|
|
|
+ settling.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await settleReceivableMe(selected.value.id, {
|
|
|
|
|
+ payment_date: settleForm.value.payment_date,
|
|
|
|
|
+ discount: settleForm.value.discount,
|
|
|
|
|
+ fine: settleForm.value.fine,
|
|
|
|
|
+ });
|
|
|
|
|
+ settleDialog.value = false;
|
|
|
|
|
+ await loadData();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ settling.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function onReopen(row) {
|
|
|
|
|
+ reopeningId.value = row.id;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await reopenReceivableMe(row.id);
|
|
|
|
|
+ await loadData();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error(e);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ reopeningId.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(loadData);
|
|
|
</script>
|
|
</script>
|