@fayz-ai/plugin-financial 0.14.1 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/PaymentModal.d.ts.map +1 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/registries.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +2 -0
- package/dist/data/tables.d.ts.map +1 -1
- package/dist/data/types.d.ts +2 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1894 -439
- package/dist/index.js.map +1 -1
- package/dist/lib/idempotency.d.ts +2 -0
- package/dist/lib/idempotency.d.ts.map +1 -0
- package/dist/lib/setup-guides.d.ts.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/store.d.ts +2 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CashRegistersView.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/NotasFiscaisView.d.ts.map +1 -1
- package/dist/views/ReconciliationView.d.ts.map +1 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/document-memory.d.ts +43 -0
- package/dist/views/document-memory.d.ts.map +1 -0
- package/dist/views/payable-agent.d.ts +58 -0
- package/dist/views/payable-agent.d.ts.map +1 -0
- package/dist/views/payable-lines.d.ts +32 -0
- package/dist/views/payable-lines.d.ts.map +1 -0
- package/dist/views/receipt-agent.d.ts +9 -0
- package/dist/views/receipt-agent.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import
|
|
3
|
-
import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, createSafeDataProvider, registerMessageDefaults, useTranslation, countByTenant, getSupabaseClientOptional, getActiveTenantId, exportCSV } from '@fayz-ai/core';
|
|
4
|
-
import { toast, CurrencyInput, DatePicker, Button, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, PageTransition, ModulePage, SubpageHeader, Skeleton, Badge, TooltipProvider, ConfirmDialog, MultiSelect, Tooltip, TooltipTrigger, TooltipContent, Input, Checkbox, ModalFooter, DashboardCanvas, PageHeaderActions, Card, FilterChip, useSaveBar, SearchSelect, ListView, KpiCard, ChartWidget, CardHeader, CardTitle, CardContent, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
|
|
5
|
-
import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, PluginSettingsPanel, dedup, useModuleNavigation, usePermissionOptional, useModuleNavAccess, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, TeachMeLink, parseViewId, EntitlementGate, UpgradePrompt, GetStarted, useAuth, PermissionGate, PersonLink, useLimitGuard, useLookupCreate, OrderFormSkeleton, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, OrderFormAddItemStep, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, invalidateLimit, askAI, requestChatUpload, toInlineFile
|
|
1
|
+
import * as React15 from 'react';
|
|
2
|
+
import React15__default, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
3
|
+
import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, createSafeDataProvider, registerMessageDefaults, useTranslation, createPendingCommand, countByTenant, getSupabaseClientOptional, getActiveTenantId, exportCSV, useDataChanged } from '@fayz-ai/core';
|
|
4
|
+
import { toast, CurrencyInput, DatePicker, Button, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, PageTransition, ModulePage, SubpageHeader, Skeleton, Badge, TooltipProvider, ConfirmDialog, MultiSelect, Tooltip, TooltipTrigger, TooltipContent, Input, Checkbox, ModalFooter, DashboardCanvas, PageHeaderActions, Card, FilterChip, useSaveBar, tourAnchor, SearchSelect, ListView, KpiCard, ChartWidget, CardHeader, CardTitle, CardContent, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
|
|
5
|
+
import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, PluginSettingsPanel, dedup, useModuleNavigation, usePermissionOptional, useModuleNavAccess, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, TeachMeLink, useAgentSurface, parseViewId, EntitlementGate, UpgradePrompt, createArchetypeLookup, useChatStore, adminNavigateTo, revealPage, GetStarted, useAuth, resolveEntityRoute, PermissionGate, PersonLink, useLimitGuard, useLookupCreate, OrderFormSkeleton, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, OrderFormAddItemStep, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, invalidateLimit, askAI, requestChatUpload, toInlineFile } from '@fayz-ai/admin';
|
|
6
6
|
import { CircleDollarSign, FileText, X, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Pencil, Check, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, DollarSign, Trash2, Landmark, ChevronDown, Clock, BookOpen, ChevronRight, Info, Calculator, StickyNote, Play, Square, ChevronUp, ClipboardCheck, FileCheck2, ExternalLink, FileX2, RefreshCw, Send, Ban, CalendarClock, Gift, Loader2, BadgeCheck, Target, Settings, ArrowRight, Receipt, Users, ArrowDownCircle, ArrowUpCircle, Sparkles, Link2, Search, Download, Paperclip, CheckCheck, MoreVertical, CircleCheckBig, CircleDashed, Camera, CircleAlert, CircleEllipsis, CalendarDays, LayoutList, Layers, Undo2, Building2, ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell, Music, ShoppingBag, ArrowUpRight, ArrowDownRight, EyeOff, Eye } from 'lucide-react';
|
|
7
7
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
8
|
import { createPortal } from 'react-dom';
|
|
@@ -162,6 +162,48 @@ function MobileSummary() {
|
|
|
162
162
|
] });
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
// src/data/tables.ts
|
|
166
|
+
var T = {
|
|
167
|
+
movements: "plg_financial_movements",
|
|
168
|
+
cashMovements: "v_financial_cash_movements",
|
|
169
|
+
accountBalances: "v_financial_account_balances",
|
|
170
|
+
chartOfAccounts: "plg_financial_chart_of_accounts",
|
|
171
|
+
bankAccounts: "plg_financial_bank_accounts",
|
|
172
|
+
cardBrands: "plg_financial_card_brands",
|
|
173
|
+
costCenters: "plg_financial_cost_centers",
|
|
174
|
+
cashRegisterSessions: "plg_financial_cash_register_sessions",
|
|
175
|
+
paymentMethods: "plg_financial_payment_methods",
|
|
176
|
+
paymentMethodTypes: "plg_financial_payment_method_types",
|
|
177
|
+
notas: "plg_financial_nfe",
|
|
178
|
+
// A contabilidade da LINHA. public.items é a linha; o que só o financeiro
|
|
179
|
+
// sabe dela — plano de contas, centro de custo, imposto, CFOP — mora aqui,
|
|
180
|
+
// com item_id de chave. Ver 038_the_invoice_gets_a_line.sql.
|
|
181
|
+
invoiceItems: "plg_financial_invoice_items",
|
|
182
|
+
// A regra de comissão. O registry apontava para `commission_rules`, sem
|
|
183
|
+
// prefixo — nome que a 028 aposentou e que não existe em pool nenhum.
|
|
184
|
+
commissionRules: "plg_financial_commission_rules",
|
|
185
|
+
commissionRuleCriteria: "plg_financial_commission_rule_criteria",
|
|
186
|
+
commissionFunctions: "plg_financial_commission_functions",
|
|
187
|
+
// The legacy `movement_kind='bill'` SHAPE, derived from the instalment
|
|
188
|
+
// balances (financial 040). Reading it instead of the mirror is what let the
|
|
189
|
+
// mirror stop being written: the columns are the same, the source is one.
|
|
190
|
+
// The instalment IS the obligation; the `movement_kind='bill'` shape and
|
|
191
|
+
// its compat view left in financial 044.
|
|
192
|
+
bills: "v_financial_installment_balances",
|
|
193
|
+
/** A fatura com o que já foi pago somado — de onde toda lista de contas lê. */
|
|
194
|
+
invoiceBalances: "v_financial_invoice_balances",
|
|
195
|
+
commissions: "v_financial_commissions",
|
|
196
|
+
commissionGenerations: "plg_financial_commission_generations",
|
|
197
|
+
periods: "plg_financial_periods",
|
|
198
|
+
trialBalance: "v_financial_trial_balance",
|
|
199
|
+
aging: "v_financial_aging",
|
|
200
|
+
agingSummary: "v_financial_aging_summary",
|
|
201
|
+
lineMargin: "v_financial_line_margin",
|
|
202
|
+
marginByMonth: "v_financial_margin_by_month",
|
|
203
|
+
recurringBills: "plg_financial_recurring_bills",
|
|
204
|
+
discountsGiven: "v_financial_discounts_given"
|
|
205
|
+
};
|
|
206
|
+
|
|
165
207
|
// src/lib/accounts.ts
|
|
166
208
|
function postableAccounts(accounts) {
|
|
167
209
|
const parentIds = new Set(accounts.map((a) => a.parentId).filter(Boolean));
|
|
@@ -234,8 +276,8 @@ var SCALES = {
|
|
|
234
276
|
bilhao: 1e9,
|
|
235
277
|
bilhoes: 1e9
|
|
236
278
|
};
|
|
237
|
-
function normalize(
|
|
238
|
-
return
|
|
279
|
+
function normalize(text2) {
|
|
280
|
+
return text2.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
239
281
|
}
|
|
240
282
|
function sumGroup(words) {
|
|
241
283
|
let total = 0;
|
|
@@ -280,6 +322,111 @@ function lastCurrencyIndex(words, before) {
|
|
|
280
322
|
}
|
|
281
323
|
return before;
|
|
282
324
|
}
|
|
325
|
+
|
|
326
|
+
// src/views/payable-lines.ts
|
|
327
|
+
function round2(n) {
|
|
328
|
+
return Math.round(n * 100) / 100;
|
|
329
|
+
}
|
|
330
|
+
function parseLineItems(raw) {
|
|
331
|
+
if (raw === void 0 || raw === null) return { items: [] };
|
|
332
|
+
if (!Array.isArray(raw)) return { items: [], error: "`items` must be an array of table rows." };
|
|
333
|
+
const items = [];
|
|
334
|
+
for (const entry of raw) {
|
|
335
|
+
if (!entry || typeof entry !== "object") continue;
|
|
336
|
+
const row = entry;
|
|
337
|
+
const description = typeof row.description === "string" ? row.description.trim() : "";
|
|
338
|
+
const unitPrice = parseReceiptAmount(row.unitPrice);
|
|
339
|
+
if (!description || unitPrice === null) {
|
|
340
|
+
return {
|
|
341
|
+
items: [],
|
|
342
|
+
error: 'Every row needs a `description` and a readable `unitPrice` copied from the paper ("47,90"). Re-read the table and resend all rows.'
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const quantityRaw = typeof row.quantity === "number" ? row.quantity : Number(row.quantity);
|
|
346
|
+
const quantity = Number.isFinite(quantityRaw) && quantityRaw > 0 ? quantityRaw : 1;
|
|
347
|
+
items.push({ description, quantity, unitPrice });
|
|
348
|
+
}
|
|
349
|
+
return { items };
|
|
350
|
+
}
|
|
351
|
+
function text(value) {
|
|
352
|
+
const s = typeof value === "string" ? value.trim() : "";
|
|
353
|
+
return s || void 0;
|
|
354
|
+
}
|
|
355
|
+
function today() {
|
|
356
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
357
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
358
|
+
return `${now2.getFullYear()}-${pad(now2.getMonth() + 1)}-${pad(now2.getDate())}`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/views/document-memory.ts
|
|
362
|
+
var lastDocument = null;
|
|
363
|
+
var DOCUMENT_TTL_MS = 15 * 60 * 1e3;
|
|
364
|
+
var STORAGE_KEY = "fayz:financial:last-document-read";
|
|
365
|
+
function readStored() {
|
|
366
|
+
try {
|
|
367
|
+
const raw = globalThis.sessionStorage?.getItem(STORAGE_KEY);
|
|
368
|
+
if (!raw) return null;
|
|
369
|
+
const parsed = JSON.parse(raw);
|
|
370
|
+
return parsed && typeof parsed.at === "number" && parsed.doc ? parsed : null;
|
|
371
|
+
} catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function writeStored(entry) {
|
|
376
|
+
try {
|
|
377
|
+
if (entry) globalThis.sessionStorage?.setItem(STORAGE_KEY, JSON.stringify(entry));
|
|
378
|
+
else globalThis.sessionStorage?.removeItem(STORAGE_KEY);
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function rememberedDocument() {
|
|
383
|
+
const entry = lastDocument ?? readStored();
|
|
384
|
+
if (!entry) return null;
|
|
385
|
+
if (Date.now() - entry.at > DOCUMENT_TTL_MS) {
|
|
386
|
+
clearRememberedDocument();
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
lastDocument = entry;
|
|
390
|
+
return entry.doc;
|
|
391
|
+
}
|
|
392
|
+
function clearRememberedDocument() {
|
|
393
|
+
lastDocument = null;
|
|
394
|
+
writeStored(null);
|
|
395
|
+
}
|
|
396
|
+
function normalizeDocument(args) {
|
|
397
|
+
const parsed = parseLineItems(args.items);
|
|
398
|
+
const items = parsed.error ? [] : parsed.items;
|
|
399
|
+
const shipping = parseReceiptAmount(args.shipping) ?? void 0;
|
|
400
|
+
const subtotal = parseReceiptAmount(args.subtotal) ?? void 0;
|
|
401
|
+
const total = parseReceiptAmount(args.total) ?? void 0;
|
|
402
|
+
const lineSum = items.length ? round2(items.reduce((s, i) => s + i.quantity * i.unitPrice, 0)) : void 0;
|
|
403
|
+
const computed = lineSum === void 0 ? void 0 : round2(lineSum + (shipping ?? 0));
|
|
404
|
+
const reconciles = computed === void 0 || total === void 0 ? false : Math.abs(computed - total) <= 0.02;
|
|
405
|
+
return {
|
|
406
|
+
items,
|
|
407
|
+
...subtotal === void 0 ? {} : { subtotal },
|
|
408
|
+
...shipping === void 0 ? {} : { shipping },
|
|
409
|
+
...total === void 0 ? {} : { total },
|
|
410
|
+
...lineSum === void 0 ? {} : { lineSum },
|
|
411
|
+
reconciles,
|
|
412
|
+
...parseReceiptDate(args.documentDate) ? { documentDate: parseReceiptDate(args.documentDate) } : {},
|
|
413
|
+
// `null` é uma RESPOSTA: "o papel não diz". Diferente de "ainda não li".
|
|
414
|
+
dueDate: parseReceiptDate(args.dueDate) ?? null,
|
|
415
|
+
...parseReceiptDate(args.paymentDate) ? { paymentDate: parseReceiptDate(args.paymentDate) } : {},
|
|
416
|
+
...text(args.orderRef) ? { orderRef: text(args.orderRef) } : {},
|
|
417
|
+
...parseReceiptDate(args.deliveryDate) ? { deliveryDate: parseReceiptDate(args.deliveryDate) } : {},
|
|
418
|
+
...text(args.paymentMethod) ? { method: text(args.paymentMethod) } : {}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function rememberDocument(doc, meta) {
|
|
422
|
+
lastDocument = {
|
|
423
|
+
at: Date.now(),
|
|
424
|
+
doc,
|
|
425
|
+
...meta?.source ? { source: meta.source } : {},
|
|
426
|
+
...meta?.conversationId ? { conversationId: meta.conversationId } : {}
|
|
427
|
+
};
|
|
428
|
+
writeStored(lastDocument);
|
|
429
|
+
}
|
|
283
430
|
var RECEIPT_INTRO = {
|
|
284
431
|
role: "finance",
|
|
285
432
|
persona: "Leitor de recibos",
|
|
@@ -356,35 +503,35 @@ var OPEN_DRAFT = {
|
|
|
356
503
|
function parseReceiptAmount(raw) {
|
|
357
504
|
if (typeof raw === "number") return Number.isFinite(raw) && raw > 0 ? raw : null;
|
|
358
505
|
if (typeof raw !== "string") return null;
|
|
359
|
-
const
|
|
360
|
-
if (!
|
|
361
|
-
const lastComma =
|
|
362
|
-
const lastDot =
|
|
506
|
+
const text2 = raw.replace(/[^\d.,-]/g, "").trim();
|
|
507
|
+
if (!text2 || !/\d/.test(text2)) return null;
|
|
508
|
+
const lastComma = text2.lastIndexOf(",");
|
|
509
|
+
const lastDot = text2.lastIndexOf(".");
|
|
363
510
|
const cut = Math.max(lastComma, lastDot);
|
|
364
511
|
let normalized;
|
|
365
512
|
if (cut < 0) {
|
|
366
|
-
normalized =
|
|
513
|
+
normalized = text2;
|
|
367
514
|
} else {
|
|
368
|
-
const decimals =
|
|
369
|
-
normalized = decimals === 3 ?
|
|
515
|
+
const decimals = text2.length - cut - 1;
|
|
516
|
+
normalized = decimals === 3 ? text2.replace(/[.,]/g, "") : `${text2.slice(0, cut).replace(/[.,]/g, "")}.${text2.slice(cut + 1)}`;
|
|
370
517
|
}
|
|
371
518
|
const value = Number(normalized);
|
|
372
519
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
373
520
|
}
|
|
374
521
|
function parseReceiptDate(raw) {
|
|
375
|
-
const
|
|
376
|
-
if (!
|
|
377
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(
|
|
378
|
-
const br =
|
|
522
|
+
const text2 = typeof raw === "string" ? raw.trim() : "";
|
|
523
|
+
if (!text2) return void 0;
|
|
524
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text2)) return isRealDate(text2) ? text2 : void 0;
|
|
525
|
+
const br = text2.match(/^(\d{1,2})[/.-](\d{1,2})[/.-](\d{2}|\d{4})$/);
|
|
379
526
|
if (!br) return void 0;
|
|
380
527
|
const [, d, m, y] = br;
|
|
381
528
|
const year = y.length === 2 ? `20${y}` : y;
|
|
382
529
|
const iso = `${year}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`;
|
|
383
530
|
return isRealDate(iso) ? iso : void 0;
|
|
384
531
|
}
|
|
385
|
-
function findDateIn(
|
|
386
|
-
if (typeof
|
|
387
|
-
return
|
|
532
|
+
function findDateIn(text2) {
|
|
533
|
+
if (typeof text2 !== "string") return void 0;
|
|
534
|
+
return text2.match(/\b\d{1,2}[/.-]\d{1,2}[/.-](?:\d{4}|\d{2})\b/)?.[0];
|
|
388
535
|
}
|
|
389
536
|
function isRealDate(iso) {
|
|
390
537
|
const [y, m, d] = iso.split("-").map(Number);
|
|
@@ -409,6 +556,33 @@ function createReceiptTools({ onDraft, getReceiptUrl = lastReceiptPhoto }) {
|
|
|
409
556
|
{
|
|
410
557
|
tool: OPEN_DRAFT,
|
|
411
558
|
execute: async (args) => {
|
|
559
|
+
const read = rememberedDocument();
|
|
560
|
+
if (read && read.total !== void 0) {
|
|
561
|
+
pending = {
|
|
562
|
+
amount: read.total,
|
|
563
|
+
...read.items.length ? { items: read.items } : {},
|
|
564
|
+
...read.shipping === void 0 ? {} : { shipping: read.shipping },
|
|
565
|
+
supplier: text(args.supplier) ?? void 0,
|
|
566
|
+
description: text(args.description) ?? void 0,
|
|
567
|
+
notes: [
|
|
568
|
+
text(args.notes),
|
|
569
|
+
read.orderRef ? `Pedido: ${read.orderRef}` : null,
|
|
570
|
+
read.deliveryDate ? `Entrega: ${read.deliveryDate}` : null
|
|
571
|
+
].filter(Boolean).join(" \xB7 ") || void 0,
|
|
572
|
+
date: read.documentDate ?? parseReceiptDate(args.date),
|
|
573
|
+
receiptUrl: getReceiptUrl()
|
|
574
|
+
};
|
|
575
|
+
onDraft();
|
|
576
|
+
revealPage();
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
opened: true,
|
|
580
|
+
fromDocument: true,
|
|
581
|
+
items: read.items.length,
|
|
582
|
+
total: read.total,
|
|
583
|
+
message: `O cadastro abriu com o que foi LIDO do documento \u2014 ${read.items.length || 1} linha(s), total ${read.total}. Diga isso em UMA frase e pare. N\xE3o repita os valores campo a campo e N\xC3O os recalcule: a pessoa est\xE1 olhando para eles.`
|
|
584
|
+
};
|
|
585
|
+
}
|
|
412
586
|
if (typeof args.amount !== "string") {
|
|
413
587
|
return {
|
|
414
588
|
ok: false,
|
|
@@ -426,16 +600,16 @@ function createReceiptTools({ onDraft, getReceiptUrl = lastReceiptPhoto }) {
|
|
|
426
600
|
message: "That is not a readable amount. If the total is illegible on the photo, ask the person."
|
|
427
601
|
};
|
|
428
602
|
}
|
|
429
|
-
const
|
|
603
|
+
const text2 = (value) => {
|
|
430
604
|
const s = typeof value === "string" ? value.trim() : "";
|
|
431
605
|
return s || void 0;
|
|
432
606
|
};
|
|
433
607
|
const isoDate2 = parseReceiptDate(args.date) ?? parseReceiptDate(findDateIn(args.notes));
|
|
434
608
|
pending = {
|
|
435
609
|
amount,
|
|
436
|
-
supplier:
|
|
437
|
-
description:
|
|
438
|
-
notes:
|
|
610
|
+
supplier: text2(args.supplier),
|
|
611
|
+
description: text2(args.description),
|
|
612
|
+
notes: text2(args.notes),
|
|
439
613
|
date: isoDate2,
|
|
440
614
|
receiptUrl: getReceiptUrl()
|
|
441
615
|
};
|
|
@@ -477,9 +651,9 @@ function startReceiptCapture(labels) {
|
|
|
477
651
|
});
|
|
478
652
|
}
|
|
479
653
|
function ReceiptAgentSurface({ onOpenDraft }) {
|
|
480
|
-
const live =
|
|
654
|
+
const live = React15.useRef(onOpenDraft);
|
|
481
655
|
live.current = onOpenDraft;
|
|
482
|
-
const contribution =
|
|
656
|
+
const contribution = React15.useMemo(() => ({
|
|
483
657
|
id: "financial.payables.receipt",
|
|
484
658
|
pluginId: "financial",
|
|
485
659
|
role: "finance",
|
|
@@ -493,6 +667,17 @@ function ReceiptAgentSurface({ onOpenDraft }) {
|
|
|
493
667
|
useAgentSurface(contribution);
|
|
494
668
|
return null;
|
|
495
669
|
}
|
|
670
|
+
|
|
671
|
+
// src/lib/idempotency.ts
|
|
672
|
+
function createIdempotencyKey(scope) {
|
|
673
|
+
let suffix;
|
|
674
|
+
try {
|
|
675
|
+
suffix = typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
676
|
+
} catch {
|
|
677
|
+
suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
678
|
+
}
|
|
679
|
+
return `${scope}:${suffix}`;
|
|
680
|
+
}
|
|
496
681
|
var STATUS_OPTIONS = [
|
|
497
682
|
{ value: "open", labelKey: "financial.invoice.statusOpen", color: "bg-info-soft text-info-soft-foreground", icon: CircleDashed },
|
|
498
683
|
{ value: "partial", labelKey: "financial.invoice.statusPartial", color: "bg-warning-soft text-warning-soft-foreground", icon: CircleEllipsis },
|
|
@@ -644,6 +829,10 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
644
829
|
useEffect(() => {
|
|
645
830
|
fetchInvoices(query);
|
|
646
831
|
}, [query]);
|
|
832
|
+
useDataChanged({ table: T.invoiceBalances }, () => {
|
|
833
|
+
void fetchInvoices(query);
|
|
834
|
+
void fetchSummary();
|
|
835
|
+
}, [query]);
|
|
647
836
|
const filtered = invoices.filter((inv) => inv.direction === direction);
|
|
648
837
|
const columns = useInvoiceColumns(currency);
|
|
649
838
|
const tags = useMemo(
|
|
@@ -664,6 +853,13 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
664
853
|
}, [filtered]);
|
|
665
854
|
const [cancelTarget, setCancelTarget] = useState(null);
|
|
666
855
|
const [bulkBusy, setBulkBusy] = useState(false);
|
|
856
|
+
const [cancelReason, setCancelReason] = useState("");
|
|
857
|
+
const cancelRequestKey = useRef(createIdempotencyKey("financial:invoice-bulk-cancel"));
|
|
858
|
+
const closeBulkCancel = useCallback(() => {
|
|
859
|
+
setCancelTarget(null);
|
|
860
|
+
setCancelReason("");
|
|
861
|
+
cancelRequestKey.current = createIdempotencyKey("financial:invoice-bulk-cancel");
|
|
862
|
+
}, []);
|
|
667
863
|
const confirmBulkCancel = useCallback(async () => {
|
|
668
864
|
const targets = (cancelTarget ?? []).filter((inv) => inv.status !== "cancelled");
|
|
669
865
|
setBulkBusy(true);
|
|
@@ -671,7 +867,11 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
671
867
|
let failed = 0;
|
|
672
868
|
for (const invoice of targets) {
|
|
673
869
|
try {
|
|
674
|
-
await provider.cancelInvoice(
|
|
870
|
+
await provider.cancelInvoice({
|
|
871
|
+
invoiceId: invoice.id,
|
|
872
|
+
reason: cancelReason,
|
|
873
|
+
idempotencyKey: `${cancelRequestKey.current}:${invoice.id}`
|
|
874
|
+
});
|
|
675
875
|
done++;
|
|
676
876
|
} catch (err) {
|
|
677
877
|
failed++;
|
|
@@ -679,11 +879,11 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
679
879
|
}
|
|
680
880
|
}
|
|
681
881
|
setBulkBusy(false);
|
|
682
|
-
|
|
882
|
+
closeBulkCancel();
|
|
683
883
|
await Promise.all([fetchInvoices(query), fetchSummary()]);
|
|
684
884
|
if (done > 0) toast.success(t2("financial.invoice.bulkCancelDone", { count: String(done) }));
|
|
685
885
|
if (failed > 0) toast.error(t2("financial.invoice.bulkCancelFailed", { count: String(failed) }));
|
|
686
|
-
}, [cancelTarget, provider, fetchInvoices, fetchSummary, query, t2]);
|
|
886
|
+
}, [cancelTarget, cancelReason, closeBulkCancel, provider, fetchInvoices, fetchSummary, query, t2]);
|
|
687
887
|
const bulkActions = useMemo(() => {
|
|
688
888
|
if (!canEdit) return void 0;
|
|
689
889
|
return [{
|
|
@@ -864,21 +1064,1169 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
|
|
|
864
1064
|
/* @__PURE__ */ jsx("span", { className: "font-semibold text-warning", children: formatCurrency(summary.openAmount, currency) })
|
|
865
1065
|
] })
|
|
866
1066
|
] }),
|
|
867
|
-
|
|
868
|
-
|
|
1067
|
+
cancelTarget !== null && createPortal(
|
|
1068
|
+
/* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-[60] flex items-center justify-center", children: [
|
|
1069
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-black/50 backdrop-blur-[2px]", onClick: closeBulkCancel }),
|
|
1070
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-10 w-full max-w-sm rounded-modal border bg-card p-6 shadow-lg mx-4", role: "alertdialog", "aria-modal": "true", children: [
|
|
1071
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
1072
|
+
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 shrink-0", children: /* @__PURE__ */ jsx(Ban, { className: "h-5 w-5 text-destructive" }) }),
|
|
1073
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1074
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.invoice.bulkCancelTitle", { count: String(cancelTarget.length) }) }),
|
|
1075
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoice.cancelDesc") })
|
|
1076
|
+
] })
|
|
1077
|
+
] }),
|
|
1078
|
+
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-bulk-cancel-reason", children: t2("financial.invoice.cancelReason") }),
|
|
1079
|
+
/* @__PURE__ */ jsx(
|
|
1080
|
+
"textarea",
|
|
1081
|
+
{
|
|
1082
|
+
id: "invoice-bulk-cancel-reason",
|
|
1083
|
+
value: cancelReason,
|
|
1084
|
+
onChange: (event) => setCancelReason(event.target.value),
|
|
1085
|
+
placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
|
|
1086
|
+
className: "mb-5 min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
|
|
1087
|
+
autoFocus: true
|
|
1088
|
+
}
|
|
1089
|
+
),
|
|
1090
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
1091
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeBulkCancel, disabled: bulkBusy, children: t2("financial.invoice.keepInvoice") }),
|
|
1092
|
+
/* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: () => void confirmBulkCancel(), disabled: bulkBusy || !cancelReason.trim(), children: t2("financial.invoice.yesCancel") })
|
|
1093
|
+
] })
|
|
1094
|
+
] })
|
|
1095
|
+
] }),
|
|
1096
|
+
document.body
|
|
1097
|
+
)
|
|
1098
|
+
] });
|
|
1099
|
+
}
|
|
1100
|
+
var PAYABLE_INSTRUCTIONS = [
|
|
1101
|
+
'Uma "conta a pagar" \xE9 uma DESPESA com um FORNECEDOR \u2014 quem a empresa deve. Para registrar uma,',
|
|
1102
|
+
"chame `createPayable`. NUNCA use `createRecord` em `financial:payment-methods` para isso: aquele",
|
|
1103
|
+
"registro configura como os CLIENTES pagam a empresa (bandeira, taxa, parcelamento), e um",
|
|
1104
|
+
"fornecedor cadastrado l\xE1 n\xE3o vira conta nenhuma \u2014 vira lixo na configura\xE7\xE3o da loja.",
|
|
1105
|
+
"Se a pessoa mandar a FOTO de um recibo ou de uma nota, leia a imagem e use o que est\xE1 escrito",
|
|
1106
|
+
"nela. Uma foto n\xE3o tem colunas: nunca proponha importa\xE7\xE3o para ela.",
|
|
1107
|
+
"Num pedido ou nota com TABELA, leia a tabela LINHA A LINHA e mande todas em `items` \u2014 c\xF3digo,",
|
|
1108
|
+
'descri\xE7\xE3o, quantidade e pre\xE7o unit\xE1rio de cada uma. Uma linha gen\xE9rica de "servi\xE7o prestado" com',
|
|
1109
|
+
"o total dentro perde o pedido inteiro. O FRETE \xE9 campo pr\xF3prio (`shipping`), n\xE3o some ao produto.",
|
|
1110
|
+
"`amount` \xE9 sempre o TOTAL do rodap\xE9 (com frete), nunca o subtotal e nunca o total de uma linha.",
|
|
1111
|
+
'Capture tamb\xE9m "M\xE9todo de pagamento:" quando impresso, o N\xDAMERO DO PEDIDO ("Pedido: #164223") e a',
|
|
1112
|
+
"data de ENTREGA \u2014 pedido e entrega v\xE3o em `notes`, que \xE9 onde a pessoa procura o que o papel dizia.",
|
|
1113
|
+
'Se o texto trouxer uma linha como "[c\xF3digo Pix copia-e-cola \u2014 valor embutido: X]", aquilo \xE9 o',
|
|
1114
|
+
"c\xF3digo do pagamento: X confirma o total, e os d\xEDgitos em volta N\xC3O s\xE3o pre\xE7os.",
|
|
1115
|
+
'"Expira em" de um c\xF3digo Pix \xE9 a validade do C\xD3DIGO, n\xE3o o vencimento da conta. Um pedido pago \xE0',
|
|
1116
|
+
"vista n\xE3o tem vencimento: deixe `dueDate` de fora e o formul\xE1rio pergunta. Data chutada custa uma",
|
|
1117
|
+
"cobran\xE7a; campo vazio custa um toque.",
|
|
1118
|
+
"LEU UM PAPEL? PASSO 0 \xE9 `resolvePayableContext`, ANTES de escrever qualquer coisa. Extraia TUDO o",
|
|
1119
|
+
"que est\xE1 escrito \u2014 fornecedor, CNPJ, o que foi comprado, forma de pagamento, bandeira e final do",
|
|
1120
|
+
"cart\xE3o, datas, valores \u2014 e mande para conferir contra o que este neg\xF3cio j\xE1 tem. Ele responde o",
|
|
1121
|
+
"que existe e o que falta, sem gravar nada. Diga o resultado em uma linha curta por depend\xEAncia e",
|
|
1122
|
+
"resolva as que faltam NA ORDEM que ele devolve: fornecedor, forma de pagamento, depois a conta,",
|
|
1123
|
+
"depois a baixa. Se voltar um candidato parecido, PERGUNTE se \xE9 o mesmo \u2014 n\xE3o cadastre de novo.",
|
|
1124
|
+
'Valor vai COPIADO como est\xE1 escrito ("50,00", "1.234,56"), nunca convertido \u2014 a v\xEDrgula \xE9',
|
|
1125
|
+
"decimal em portugu\xEAs, e convert\xEA-la voc\xEA mesmo j\xE1 transformou cinquenta reais em cinco mil.",
|
|
1126
|
+
"N\xE3o entreviste a pessoa campo a campo. Mande o que ela disse e chame a ferramenta: o que abre",
|
|
1127
|
+
"\xE9 um FORMUL\xC1RIO edit\xE1vel, e o que faltar ela preenche ali \u2014 em um toque, olhando os campos.",
|
|
1128
|
+
"Uma conta a pagar tem TR\xCAS passos, cada um confirmado pela pessoa: (1) o fornecedor, (2) o t\xEDtulo,",
|
|
1129
|
+
"(3) o pagamento \u2014 e o terceiro s\xF3 existe se a conta J\xC1 foi paga. Se o recibo ou a pessoa disser",
|
|
1130
|
+
'que j\xE1 pagou ("pago hoje", "paguei no Pix"), mande `paid: true`; a ferramenta cuida da ordem.',
|
|
1131
|
+
"Quando faltar saber COMO foi pago, a ferramenta PERGUNTA com as op\xE7\xF5es do pr\xF3prio neg\xF3cio na tela.",
|
|
1132
|
+
"N\xE3o repita as op\xE7\xF5es em texto e n\xE3o escolha por ela: espere a resposta e ent\xE3o chame",
|
|
1133
|
+
"`registerPayablePayment` com o id que ela tocou.",
|
|
1134
|
+
"REGRA DE PRECED\xCANCIA: se o cadastro de conta a pagar estiver ABERTO na tela com um rascunho (veja",
|
|
1135
|
+
'"Tela aberta agora"), ele \xE9 a conta em curso. Complete-o com `fillPayableDraft` e salve com',
|
|
1136
|
+
"`submitPayableDraft`. `createPayable` \xE9 s\xF3 para quando N\xC3O h\xE1 rascunho aberto \u2014 cham\xE1-la com o",
|
|
1137
|
+
"formul\xE1rio na tela cria uma segunda conta e deixa a pessoa num rascunho \xF3rf\xE3o.",
|
|
1138
|
+
"Depois de lan\xE7ar, a tela da conta abre sozinha \u2014 \xE9 o normal e n\xE3o precisa ser anunciado. Mande",
|
|
1139
|
+
"`redirect: false` s\xF3 quando levar a pessoa embora atrapalharia: v\xE1rias contas em sequ\xEAncia, ou",
|
|
1140
|
+
"algu\xE9m no meio de outra tarefa que s\xF3 passou a conta de lado.",
|
|
1141
|
+
'"Marca como pago", "paguei", "foi pago" sobre uma conta que VOC\xCA acabou de lan\xE7ar \xE9 sempre',
|
|
1142
|
+
"`registerPayablePayment` com o invoiceId daquela conta \u2014 NUNCA `createPayable` de novo. Lan\xE7ar a",
|
|
1143
|
+
"mesma despesa duas vezes \xE9 dinheiro contado em dobro, e desfazer isso \xE9 trabalho de concilia\xE7\xE3o."
|
|
1144
|
+
].join(" ");
|
|
1145
|
+
var CREATE_PAYABLE = {
|
|
1146
|
+
id: "financial.create-payable",
|
|
1147
|
+
name: "createPayable",
|
|
1148
|
+
description: 'Records a BILL THE BUSINESS OWES a supplier (conta a pagar, despesa, boleto, invoice from a vendor). This is the tool for "lan\xE7ar/salvar uma conta a pagar", "paguei o fornecedor X", "recibo de despesa". It walks a CHECKLIST, one confirmable step at a time: (1) the supplier \u2014 found by name or created as a supplier contact, (2) the bill itself. Each step opens an EDITABLE FORM pre-filled with what you send and writes nothing until the person confirms it \u2014 so call it as soon as you have an amount and let the forms ask for the rest. If the receipt says it was ALREADY PAID ("pago hoje", "paguei via Pix"), set `paid: true`: step (3) is the payment, and this tool will either ask HOW it was paid or hand you off to `registerPayablePayment`. Send ONLY what you were actually told or could read; omit anything you would be guessing at. When the bill came off a receipt/photo/PDF, call `resolvePayableContext` FIRST and settle the missing dependencies it reports before calling this. A document with a TABLE goes in row by row through `items` (plus `shipping` for frete): the lines are reconciled against `amount`, and a mismatch comes back to be asked about rather than silently trusted. DO NOT use this when a payable form is already open on screen (a draft read from a receipt or PDF): that draft is the bill in progress \u2014 use `fillPayableDraft` and `submitPayableDraft` instead, or this writes a second, duplicate bill and strands the person on a draft that no longer means anything.',
|
|
1149
|
+
icon: "ReceiptText",
|
|
1150
|
+
mode: "persist",
|
|
1151
|
+
execution: { plane: "client" },
|
|
1152
|
+
// O guard é desta ferramenta: ela abre o próprio cartão de confirmação, que é
|
|
1153
|
+
// um formulário. O cartão genérico do loop seria um segundo "tem certeza?"
|
|
1154
|
+
// antes do formulário — duas perguntas para uma decisão.
|
|
1155
|
+
requiresConfirmation: false,
|
|
1156
|
+
category: "Finance",
|
|
1157
|
+
role: "finance",
|
|
1158
|
+
permission: { feature: "financial", action: "create" },
|
|
1159
|
+
limitKey: "movements_month",
|
|
1160
|
+
parameters: {
|
|
1161
|
+
type: "object",
|
|
1162
|
+
properties: {
|
|
1163
|
+
supplier: {
|
|
1164
|
+
type: "string",
|
|
1165
|
+
description: 'Quem RECEBE o dinheiro \u2014 o fornecedor, a empresa, o prestador. \xC9 um NOME de quem foi pago ("Printi", "Padaria do Z\xE9"). N\xE3o \xE9 o que foi comprado nem o valor por extenso.'
|
|
1166
|
+
},
|
|
1167
|
+
amount: {
|
|
1168
|
+
type: "string",
|
|
1169
|
+
description: 'O valor TOTAL da conta \u2014 o que vai ser pago, incluindo frete. Num pedido \xE9 o "Total" do rodap\xE9, nunca o subtotal e nunca o total de uma linha. COPIADO LETRA POR LETRA, com a pontua\xE7\xE3o que estiver escrita: "50,00", "1.234,56", "R$ 89,90". N\xC3O converta para n\xFAmero e n\xE3o tire a v\xEDrgula \u2014 quem l\xEA a pontua\xE7\xE3o \xE9 o aplicativo, que sabe a moeda do pa\xEDs.'
|
|
1170
|
+
},
|
|
1171
|
+
amountInWords: {
|
|
1172
|
+
type: "string",
|
|
1173
|
+
description: 'O valor POR EXTENSO, quando o papel escrever ("CINQUENTA REAIS"). Num recibo manuscrito \xE9 ele que desempata os algarismos. Copie as palavras como est\xE3o.'
|
|
1174
|
+
},
|
|
1175
|
+
dueDate: {
|
|
1176
|
+
type: "string",
|
|
1177
|
+
description: 'O VENCIMENTO, como est\xE1 escrito ("20/01/2026", "2026-01-20"). Omita se ningu\xE9m disse \u2014 o formul\xE1rio pergunta. Uma data de vencimento chutada custa uma confer\xEAncia inteira.'
|
|
1178
|
+
},
|
|
1179
|
+
date: {
|
|
1180
|
+
type: "string",
|
|
1181
|
+
description: 'A data do DOCUMENTO (quando a compra aconteceu), se souber. "hoje" \xE9 hoje.'
|
|
1182
|
+
},
|
|
1183
|
+
description: {
|
|
1184
|
+
type: "string",
|
|
1185
|
+
description: 'O que foi comprado ou pago, em poucas palavras \u2014 \xE9 o nome da linha da conta ("cart\xF5es de visita", "insumos de padaria"). Sem ela a conta nasce sem dizer o que \xE9.'
|
|
1186
|
+
},
|
|
1187
|
+
items: {
|
|
1188
|
+
type: "array",
|
|
1189
|
+
description: 'As LINHAS da nota, uma por linha da tabela \u2014 leia a tabela linha a linha e copie todas. Um pedido de tr\xEAs produtos vira tr\xEAs linhas aqui, n\xE3o uma linha gen\xE9rica de "servi\xE7o prestado": \xE9 assim que a conta diz depois o que foi comprado. Omita quando o papel n\xE3o tem tabela (um recibo de t\xE1xi \xE9 uma linha s\xF3, e a\xED basta `description`).',
|
|
1190
|
+
items: {
|
|
1191
|
+
type: "object",
|
|
1192
|
+
properties: {
|
|
1193
|
+
description: { type: "string", description: "O que \xE9 a linha, como est\xE1 escrito (c\xF3digo + nome)." },
|
|
1194
|
+
quantity: { type: "number", description: "A quantidade da linha. 1 quando a tabela n\xE3o diz." },
|
|
1195
|
+
unitPrice: {
|
|
1196
|
+
type: "string",
|
|
1197
|
+
description: 'O pre\xE7o UNIT\xC1RIO copiado letra por letra ("47,90"), n\xE3o o total da linha e n\xE3o convertido.'
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
required: ["description", "unitPrice"]
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
shipping: {
|
|
1204
|
+
type: "string",
|
|
1205
|
+
description: 'O FRETE, copiado como est\xE1 escrito ("52,19"), quando o papel o cobra em separado. Vira uma linha pr\xF3pria na conta \u2014 somado a um produto ele desapareceria dentro do pre\xE7o dele.'
|
|
1206
|
+
},
|
|
1207
|
+
notes: {
|
|
1208
|
+
type: "string",
|
|
1209
|
+
description: 'Todo o RESTO que foi dito e n\xE3o coube nos campos acima: N\xDAMERO DO PEDIDO ou do documento ("Pedido: #164223"), data de entrega, condi\xE7\xF5es, para qual projeto. \xC9 onde a pessoa vai procurar o que o papel dizia. N\xC3O repita aqui o que j\xE1 foi para os outros campos.'
|
|
1210
|
+
},
|
|
1211
|
+
paid: {
|
|
1212
|
+
type: "boolean",
|
|
1213
|
+
description: 'true quando a conta J\xC1 FOI PAGA \u2014 "pago hoje", "paguei via Pix", um recibo quitado. Uma conta paga tem um terceiro passo: registrar o pagamento. N\xE3o marque true por um boleto ou uma nota que ainda vai vencer.'
|
|
1214
|
+
},
|
|
1215
|
+
paymentMethod: {
|
|
1216
|
+
type: "string",
|
|
1217
|
+
description: 'COMO foi pago, se a pessoa disse ("Pix", "dinheiro", "cart\xE3o de cr\xE9dito"). N\xE3o chute: sem isto a ferramenta PERGUNTA, oferecendo as formas que este neg\xF3cio aceita.'
|
|
1218
|
+
},
|
|
1219
|
+
paymentDate: {
|
|
1220
|
+
type: "string",
|
|
1221
|
+
description: 'QUANDO foi pago, se disserem. "pago hoje" \xE9 hoje.'
|
|
1222
|
+
},
|
|
1223
|
+
redirect: {
|
|
1224
|
+
type: "boolean",
|
|
1225
|
+
description: "Levar a pessoa at\xE9 a conta rec\xE9m-criada (default: true). Deixe true no caso normal \u2014 quem acabou de lan\xE7ar uma conta quer v\xEA-la. Mande FALSE quando levar embora atrapalharia: ela est\xE1 lan\xE7ando V\xC1RIAS contas em sequ\xEAncia, est\xE1 no meio de outra tarefa (uma tela de cadastro, uma concilia\xE7\xE3o) e s\xF3 passou a conta de lado, ou pediu explicitamente para ficar onde est\xE1. Com false a conta \xE9 criada do mesmo jeito e o link volta na resposta."
|
|
1226
|
+
}
|
|
1227
|
+
},
|
|
1228
|
+
// `amount` deixa de ser obrigatório SÓ porque o passo 0 pode tê-lo lido: sem
|
|
1229
|
+
// leitura guardada a ferramenta recusa do mesmo jeito, com a mesma mensagem.
|
|
1230
|
+
required: []
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
var REGISTER_PAYMENT = {
|
|
1234
|
+
id: "financial.register-payable-payment",
|
|
1235
|
+
name: "registerPayablePayment",
|
|
1236
|
+
description: "Step 3 of the payable checklist: registers that a bill WAS PAID \u2014 the money leaving, with its method and date. Call it with the `invoiceId` a previous `createPayable` returned, and the `method` the person chose (send the option id when they tapped one). Opens an editable confirmation before writing. Use it whenever a bill already recorded turns out to be settled; never to create the bill itself.",
|
|
1237
|
+
icon: "BadgeDollarSign",
|
|
1238
|
+
mode: "persist",
|
|
1239
|
+
execution: { plane: "client" },
|
|
1240
|
+
requiresConfirmation: false,
|
|
1241
|
+
category: "Finance",
|
|
1242
|
+
role: "finance",
|
|
1243
|
+
permission: { feature: "financial", action: "update" },
|
|
1244
|
+
parameters: {
|
|
1245
|
+
type: "object",
|
|
1246
|
+
properties: {
|
|
1247
|
+
invoiceId: { type: "string", description: "Id da conta a pagar, devolvido por createPayable." },
|
|
1248
|
+
method: {
|
|
1249
|
+
type: "string",
|
|
1250
|
+
description: 'A forma de pagamento \u2014 o id da op\xE7\xE3o que a pessoa tocou, ou o nome dela ("Pix", "Dinheiro").'
|
|
1251
|
+
},
|
|
1252
|
+
paymentDate: { type: "string", description: "A data do pagamento. Omita para hoje." }
|
|
1253
|
+
},
|
|
1254
|
+
required: ["invoiceId"]
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
var OPEN_FULL_FORM = {
|
|
1258
|
+
id: "financial.open-payable-form",
|
|
1259
|
+
name: "openPayableForm",
|
|
1260
|
+
description: 'Opens the full "new payable" screen, empty or pre-filled, for a bill that needs more than a one-line entry: several installments, a receipt to attach, a ledger account or cost centre. Writes nothing. Prefer `createPayable` for an ordinary single bill \u2014 this one is the long form.',
|
|
1261
|
+
icon: "FilePlus",
|
|
1262
|
+
mode: "read",
|
|
1263
|
+
execution: { plane: "client" },
|
|
1264
|
+
category: "Finance",
|
|
1265
|
+
role: "finance",
|
|
1266
|
+
permission: { feature: "financial", action: "create" },
|
|
1267
|
+
parameters: { type: "object", properties: {} }
|
|
1268
|
+
};
|
|
1269
|
+
function supplierRef(id, label) {
|
|
1270
|
+
if (!resolveEntityRoute("person", "supplier")) return {};
|
|
1271
|
+
return { ref: { id, label, archetype: "person:supplier" } };
|
|
1272
|
+
}
|
|
1273
|
+
async function resolveSupplier(lookup, name) {
|
|
1274
|
+
const wanted = name.trim().toLowerCase();
|
|
1275
|
+
const hits = await lookup.search(name).catch(() => []);
|
|
1276
|
+
const exact = hits.find((r) => r.label.trim().toLowerCase() === wanted);
|
|
1277
|
+
if (exact) return { ok: true, id: exact.id, name: exact.label, created: false };
|
|
1278
|
+
if (!lookup.create) return { ok: true, name, created: false };
|
|
1279
|
+
const decision = await useChatStore.getState().requestConfirmation({
|
|
1280
|
+
id: `supplier-${Date.now()}`,
|
|
1281
|
+
toolName: "createPayable",
|
|
1282
|
+
title: "Novo fornecedor",
|
|
1283
|
+
params: { name },
|
|
1284
|
+
plane: "client",
|
|
1285
|
+
scope: "financial:supplier",
|
|
1286
|
+
scopeLabel: "Cadastrar fornecedor",
|
|
1287
|
+
fields: [
|
|
1288
|
+
{ key: "name", label: "Nome", type: "text", required: true },
|
|
1289
|
+
{ key: "document_number", label: "CNPJ / CPF", type: "text" },
|
|
1290
|
+
{ key: "phone", label: "Telefone", type: "text" },
|
|
1291
|
+
{ key: "email", label: "E-mail", type: "text" }
|
|
1292
|
+
]
|
|
1293
|
+
});
|
|
1294
|
+
if (!decision.approved) return { ok: false, cancelled: true };
|
|
1295
|
+
const draft = { ...{ name }, ...decision.values ?? {} };
|
|
1296
|
+
const finalName = text(draft.name) ?? name;
|
|
1297
|
+
const made = await lookup.create({
|
|
1298
|
+
...draft,
|
|
1299
|
+
name: finalName
|
|
1300
|
+
});
|
|
1301
|
+
return { ok: true, id: made.id, name: made.label, created: true };
|
|
1302
|
+
}
|
|
1303
|
+
async function paymentOptions(provider) {
|
|
1304
|
+
const types = await provider.getPaymentMethodTypes().catch(() => []);
|
|
1305
|
+
return types.filter((t2) => t2.isActive !== false).map((t2) => ({
|
|
1306
|
+
id: t2.id,
|
|
1307
|
+
label: t2.name,
|
|
1308
|
+
transactionType: t2.transactionType,
|
|
1309
|
+
allowedAccountTypes: t2.allowedAccountTypes
|
|
1310
|
+
}));
|
|
1311
|
+
}
|
|
1312
|
+
var ACCOUNT_TYPES_FOR_TX = {
|
|
1313
|
+
cash: ["cash_physical", "cash_session"],
|
|
1314
|
+
credit_card: ["card_receivable_credit"],
|
|
1315
|
+
debit_card: ["card_receivable_debit"],
|
|
1316
|
+
pix: ["bank"],
|
|
1317
|
+
bank_transfer: ["bank"],
|
|
1318
|
+
boleto: ["bank"],
|
|
1319
|
+
check: ["bank"],
|
|
1320
|
+
voucher: ["bank"]
|
|
1321
|
+
};
|
|
1322
|
+
async function eligibleAccounts(provider, method) {
|
|
1323
|
+
const accounts = (await provider.getBankAccounts().catch(() => [])).filter((a) => a.isActive !== false);
|
|
1324
|
+
const allowed = method.allowedAccountTypes?.length ? method.allowedAccountTypes : ACCOUNT_TYPES_FOR_TX[method.transactionType ?? ""];
|
|
1325
|
+
if (!allowed) return accounts;
|
|
1326
|
+
return accounts.filter((a) => allowed.includes(a.accountType));
|
|
1327
|
+
}
|
|
1328
|
+
function matchPaymentOption(options, raw) {
|
|
1329
|
+
const wanted = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
1330
|
+
if (!wanted) return null;
|
|
1331
|
+
return options.find((o) => o.id.toLowerCase() === wanted) ?? options.find((o) => o.label.toLowerCase() === wanted) ?? options.find((o) => o.label.toLowerCase().includes(wanted) || wanted.includes(o.label.toLowerCase())) ?? null;
|
|
1332
|
+
}
|
|
1333
|
+
async function settlePayable(provider, input) {
|
|
1334
|
+
const schedule = await provider.getMovements({
|
|
1335
|
+
invoiceId: input.invoiceId,
|
|
1336
|
+
direction: "debit",
|
|
1337
|
+
status: "pending"
|
|
1338
|
+
});
|
|
1339
|
+
const bill = schedule.data.find((m) => m.movementKind === "bill");
|
|
1340
|
+
if (!bill) {
|
|
1341
|
+
return {
|
|
1342
|
+
ok: false,
|
|
1343
|
+
error: "no_open_bill",
|
|
1344
|
+
message: "That bill has no open installment \u2014 it may already be settled. Say so and stop."
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
const invoice = await provider.getInvoiceById(input.invoiceId).catch(() => null);
|
|
1348
|
+
const money3 = (n) => n.toLocaleString("pt-BR", { style: "currency", currency: input.currency ?? "BRL" });
|
|
1349
|
+
const invoiceLabel = [
|
|
1350
|
+
invoice?.fiscalNumber ? `#${invoice.fiscalNumber}` : null,
|
|
1351
|
+
invoice?.contactName ?? null,
|
|
1352
|
+
money3(bill.amount)
|
|
1353
|
+
].filter(Boolean).join(" \u2014 ") || money3(bill.amount);
|
|
1354
|
+
const needsAccount = input.method.transactionType !== "cash";
|
|
1355
|
+
const accounts = needsAccount ? await eligibleAccounts(provider, input.method) : [];
|
|
1356
|
+
if (needsAccount && !accounts.length) {
|
|
1357
|
+
return {
|
|
1358
|
+
ok: false,
|
|
1359
|
+
error: "no_eligible_account",
|
|
1360
|
+
message: `The bill is recorded but NOT settled: this business has no account that can settle a ${input.method.label} payment. Say that, and that it has to be registered from the payables screen.`
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const onlyAccount = accounts.length === 1 ? accounts[0] : void 0;
|
|
1364
|
+
const paymentDate = parseReceiptDate(input.paymentDate) ?? today();
|
|
1365
|
+
const decision = await useChatStore.getState().requestConfirmation({
|
|
1366
|
+
id: `payment-${bill.id}`,
|
|
1367
|
+
toolName: "registerPayablePayment",
|
|
1368
|
+
title: "Registrar pagamento",
|
|
1369
|
+
params: {
|
|
1370
|
+
invoice: invoiceLabel,
|
|
1371
|
+
method: input.method.label,
|
|
1372
|
+
accountId: onlyAccount?.id,
|
|
1373
|
+
paymentDate,
|
|
1374
|
+
amount: bill.amount
|
|
1375
|
+
},
|
|
1376
|
+
plane: "client",
|
|
1377
|
+
scope: "financial:payment",
|
|
1378
|
+
scopeLabel: "Registrar pagamento",
|
|
1379
|
+
fields: [
|
|
1380
|
+
// Qual conta está sendo quitada, por extenso e primeiro. Só de leitura:
|
|
1381
|
+
// é a identidade do que se assina, não um campo a editar.
|
|
1382
|
+
{ key: "invoice", label: "Conta a pagar", type: "text", readOnly: true },
|
|
1383
|
+
{ key: "method", label: "Forma de pagamento", type: "text", required: true, readOnly: true },
|
|
1384
|
+
...needsAccount ? [{
|
|
1385
|
+
key: "accountId",
|
|
1386
|
+
label: "Conta de sa\xEDda",
|
|
1387
|
+
type: "select",
|
|
1388
|
+
required: true,
|
|
1389
|
+
options: accounts.map((a) => ({ value: a.id, label: a.name }))
|
|
1390
|
+
}] : [],
|
|
1391
|
+
{ key: "paymentDate", label: "Data do pagamento", type: "date", required: true },
|
|
1392
|
+
{ key: "amount", label: "Valor pago", type: "number", required: true, min: 0 }
|
|
1393
|
+
]
|
|
1394
|
+
});
|
|
1395
|
+
if (!decision.approved) {
|
|
1396
|
+
return {
|
|
1397
|
+
ok: false,
|
|
1398
|
+
cancelled: true,
|
|
1399
|
+
reason: "user_declined",
|
|
1400
|
+
message: "The bill is recorded but NOT settled \u2014 the payment step was declined. Say exactly that."
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
const values = { paymentDate, amount: bill.amount, accountId: onlyAccount?.id, ...decision.values ?? {} };
|
|
1404
|
+
const accountId = typeof values.accountId === "string" && values.accountId ? values.accountId : void 0;
|
|
1405
|
+
if (needsAccount && !accountId) {
|
|
1406
|
+
return {
|
|
1407
|
+
ok: false,
|
|
1408
|
+
error: "no_account",
|
|
1409
|
+
message: "NOTHING WAS SETTLED. The form came back with no outgoing account \u2014 do not say it was paid."
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
await provider.payMovement({
|
|
1413
|
+
movementId: bill.id,
|
|
1414
|
+
amount: parseReceiptAmount(values.amount) ?? bill.amount,
|
|
1415
|
+
paymentDate: parseReceiptDate(values.paymentDate) ?? paymentDate,
|
|
1416
|
+
paymentMethodTypeId: input.method.id,
|
|
1417
|
+
bankAccountId: accountId
|
|
1418
|
+
});
|
|
1419
|
+
return {
|
|
1420
|
+
ok: true,
|
|
1421
|
+
settled: true,
|
|
1422
|
+
invoiceId: input.invoiceId,
|
|
1423
|
+
movementId: bill.id,
|
|
1424
|
+
method: input.method.label,
|
|
1425
|
+
account: accounts.find((a) => a.id === accountId)?.name,
|
|
1426
|
+
paymentDate: parseReceiptDate(values.paymentDate) ?? paymentDate,
|
|
1427
|
+
// O botão que abre a conta quitada. `path` existe para exatamente isto —
|
|
1428
|
+
// uma tela de plugin que o mapa de rotas por arquétipo não sabe resolver.
|
|
1429
|
+
ref: {
|
|
1430
|
+
id: input.invoiceId,
|
|
1431
|
+
label: invoiceLabel,
|
|
1432
|
+
path: `/financial/payables/${input.invoiceId}`
|
|
1433
|
+
},
|
|
1434
|
+
message: `Pago via ${input.method.label}. Diga em UMA frase que a conta foi lan\xE7ada e quitada, e pare.`
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
var recentPayables = [];
|
|
1438
|
+
var RECENT_WINDOW_MS = 10 * 60 * 1e3;
|
|
1439
|
+
function rememberPayable(entry) {
|
|
1440
|
+
recentPayables = [...recentPayables.filter((p) => p.invoiceId !== entry.invoiceId), entry].slice(-10);
|
|
1441
|
+
}
|
|
1442
|
+
function findRecentPayable(supplier, amount) {
|
|
1443
|
+
const now2 = Date.now();
|
|
1444
|
+
recentPayables = recentPayables.filter((p) => now2 - p.at < RECENT_WINDOW_MS);
|
|
1445
|
+
const wanted = supplier.trim().toLowerCase();
|
|
1446
|
+
return recentPayables.find(
|
|
1447
|
+
(p) => p.supplier.trim().toLowerCase() === wanted && Math.abs(p.amount - amount) < 5e-3
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
function asChoiceCards(choices) {
|
|
1451
|
+
const usable = choices.filter((c) => c.options.length >= 2);
|
|
1452
|
+
return usable.length ? { kind: "record_choice", choices: usable } : {};
|
|
1453
|
+
}
|
|
1454
|
+
function gapChoices(input) {
|
|
1455
|
+
const questions = [];
|
|
1456
|
+
const { supplierName, supplier, method, account } = input;
|
|
1457
|
+
if (supplier.status !== "found" && supplierName) {
|
|
1458
|
+
const candidates = supplier.candidates ?? [];
|
|
1459
|
+
questions.push({
|
|
1460
|
+
id: "supplier",
|
|
1461
|
+
question: candidates.length ? `"${supplierName}" n\xE3o est\xE1 cadastrado. \xC9 algum destes?` : `"${supplierName}" ainda n\xE3o est\xE1 cadastrado. Cadastrar agora?`,
|
|
1462
|
+
options: [
|
|
1463
|
+
// Os parecidos PRIMEIRO: a razão de eles existirem é evitar o
|
|
1464
|
+
// duplicado, então a opção de criar não pode ser a mais fácil de tocar.
|
|
1465
|
+
...candidates.slice(0, 4).map((c) => ({
|
|
1466
|
+
id: `supplier:${c.id}`,
|
|
1467
|
+
label: c.name,
|
|
1468
|
+
meta: c.why
|
|
1469
|
+
})),
|
|
1470
|
+
{ id: "supplier:create", label: `Criar fornecedor "${supplierName}"` },
|
|
1471
|
+
{ id: "supplier:cancel", label: "Cancelar" }
|
|
1472
|
+
]
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
if (method.status !== "found") {
|
|
1476
|
+
const available = method.available ?? [];
|
|
1477
|
+
questions.push({
|
|
1478
|
+
id: "method",
|
|
1479
|
+
question: method.spoken ? `O papel diz "${String(method.spoken)}", que n\xE3o est\xE1 cadastrado. Qual forma corresponde?` : "Como esta conta foi (ou vai ser) paga?",
|
|
1480
|
+
options: [
|
|
1481
|
+
...available.slice(0, 5).map((o) => ({ id: `method:${o.id}`, label: o.label })),
|
|
1482
|
+
{ id: "method:none", label: "Deixar em aberto" }
|
|
1483
|
+
]
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
if (account.status === "ambiguous") {
|
|
1487
|
+
const options = account.options ?? [];
|
|
1488
|
+
questions.push({
|
|
1489
|
+
id: "account",
|
|
1490
|
+
question: account.cardLast4 ? `Nenhuma conta cadastrada tem o final ${String(account.cardLast4)}. De qual conta saiu?` : "De qual conta o dinheiro saiu?",
|
|
1491
|
+
options: [
|
|
1492
|
+
...options.slice(0, 5).map((o) => ({ id: `account:${o.id}`, label: o.name })),
|
|
1493
|
+
{ id: "account:skip", label: "Decidir na hora de pagar" }
|
|
1494
|
+
]
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return questions;
|
|
1498
|
+
}
|
|
1499
|
+
function askHowItWasPaid(invoiceId, options) {
|
|
1500
|
+
return {
|
|
1501
|
+
kind: "record_choice",
|
|
1502
|
+
choices: [{
|
|
1503
|
+
id: `payment-method-${invoiceId}`,
|
|
1504
|
+
question: "Foi pago como?",
|
|
1505
|
+
options
|
|
1506
|
+
}],
|
|
1507
|
+
invoiceId,
|
|
1508
|
+
nextStep: "registerPayablePayment",
|
|
1509
|
+
message: `The bill is recorded but NOT yet settled. The person is being asked HOW it was paid \u2014 the options are on screen. Do not list them again in prose and do not guess one. When they answer, call registerPayablePayment with invoiceId "${invoiceId}" and the option id they chose.`
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
function createPayableTools({ config, provider }) {
|
|
1513
|
+
const lookup = config.contactLookup ?? createArchetypeLookup({ archetype: "person", kind: "supplier" });
|
|
1514
|
+
return [
|
|
1515
|
+
{
|
|
1516
|
+
tool: RESOLVE_CONTEXT,
|
|
1517
|
+
execute: async (args) => {
|
|
1518
|
+
consumePayableDraft();
|
|
1519
|
+
const supplierName = text(args.supplier);
|
|
1520
|
+
const supplier = supplierName ? await findSupplier(lookup, supplierName, args.supplierDocument) : { status: "missing", candidates: [] };
|
|
1521
|
+
const method = await findMethod(provider, args);
|
|
1522
|
+
const account = await findAccount(provider, method, args.cardLast4);
|
|
1523
|
+
const gaps = [
|
|
1524
|
+
supplier.status !== "found" ? "supplier" : null,
|
|
1525
|
+
method.status !== "found" ? "method" : null,
|
|
1526
|
+
account.status === "ambiguous" || account.status === "missing" ? "account" : null
|
|
1527
|
+
].filter(Boolean);
|
|
1528
|
+
const choices = gapChoices({ supplierName, supplier, method, account });
|
|
1529
|
+
const document2 = normalizeDocument(args);
|
|
1530
|
+
rememberDocument(document2, {
|
|
1531
|
+
...text(args.supplier) ? { source: text(args.supplier) } : {},
|
|
1532
|
+
...useChatStore.getState().conversationId ? { conversationId: useChatStore.getState().conversationId } : {}
|
|
1533
|
+
});
|
|
1534
|
+
return {
|
|
1535
|
+
ok: true,
|
|
1536
|
+
read: {
|
|
1537
|
+
supplier: supplierName,
|
|
1538
|
+
supplierDocument: onlyDigits(args.supplierDocument) || void 0,
|
|
1539
|
+
paymentMethod: text(args.paymentMethod),
|
|
1540
|
+
cardBrand: text(args.cardBrand),
|
|
1541
|
+
cardLast4: lastFour(args.cardLast4) || void 0
|
|
1542
|
+
},
|
|
1543
|
+
supplier,
|
|
1544
|
+
method,
|
|
1545
|
+
account,
|
|
1546
|
+
gaps,
|
|
1547
|
+
document: document2,
|
|
1548
|
+
...asChoiceCards(choices),
|
|
1549
|
+
message: 'NOTHING WAS WRITTEN \u2014 this was a check. Tell the person the result as ONE short line per dependency, saying which already exist and which are missing (e.g. "Fornecedor Printi: ja existe - Pix: cadastrado - cartao final 4242: nao encontrei"). FIRST say what you READ, in one line, from `document`: how many items, the total, the method, the order ref, and the dates \u2014 and when `document.dueDate` is null say plainly there is no due date on the paper (\xE0 vista) and that the form will ask. If `document.reconciles` is false and there are items and a total, say the numbers do not add up and ask, instead of choosing one. The extraction is SAVED: when you call createPayable next, you may omit items/shipping/total/dates entirely \u2014 it reuses exactly what was read here. Do NOT re-read the document to fill them in again; re-reading from memory is where wrong numbers come from. ' + (choices.length ? "The open questions are ALREADY ON SCREEN as tappable buttons \u2014 do NOT repeat the options in prose and do NOT ask the person to type an answer. End your reply after the checklist and WAIT for the tap. When they answer, the option id says what to do: `supplier:create` means create the supplier under the name in `read.supplier` (call createPayable and its supplier step will confirm it), `supplier:<uuid>` means use that existing one, `method:<id>` is the payment method, `account:<id>` the outgoing account, and anything `:cancel` means stop. " : "Then continue with createPayable for the bill, and the payment. ") + "Never invent a value the paper did not state."
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
tool: OPEN_FULL_FORM,
|
|
1555
|
+
execute: async () => {
|
|
1556
|
+
adminNavigateTo("/financial/payables/new");
|
|
1557
|
+
return { ok: true, opened: "/financial/payables/new" };
|
|
1558
|
+
}
|
|
1559
|
+
},
|
|
1560
|
+
{
|
|
1561
|
+
tool: CREATE_PAYABLE,
|
|
1562
|
+
execute: async (args) => {
|
|
1563
|
+
if (hasLivePayableDraft()) {
|
|
1564
|
+
return {
|
|
1565
|
+
ok: false,
|
|
1566
|
+
error: "draft_open",
|
|
1567
|
+
message: "NOTHING WAS WRITTEN, and no second bill was created. The payable form is OPEN ON SCREEN with the draft this conversation has been building \u2014 that draft IS this bill. Continue in it: use `fillPayableDraft` for what is missing and `submitPayableDraft` to save. Do not call createPayable again while that form is open."
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
const remembered = rememberedDocument();
|
|
1571
|
+
if (args.amount === void 0 && remembered?.total !== void 0) {
|
|
1572
|
+
args = { ...args, amount: String(remembered.total) };
|
|
1573
|
+
}
|
|
1574
|
+
if (typeof args.amount !== "string") {
|
|
1575
|
+
return {
|
|
1576
|
+
ok: false,
|
|
1577
|
+
error: "amount_must_be_literal",
|
|
1578
|
+
message: 'Send `amount` as the TEXT that was written, punctuation included ("50,00", "1.234,56") \u2014 never as a number. A decimal comma is a thousands separator in English, so converting it yourself turns fifty into five thousand. Resend the literal.'
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
const figures = parseReceiptAmount(args.amount);
|
|
1582
|
+
const spelled = parseAmountInWords(args.amountInWords);
|
|
1583
|
+
const amount = spelled ?? figures;
|
|
1584
|
+
if (amount === null) {
|
|
1585
|
+
return {
|
|
1586
|
+
ok: false,
|
|
1587
|
+
error: "invalid_amount",
|
|
1588
|
+
message: "That is not a readable amount. If the total is illegible or was never said, ask the person."
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const lines = args.items === void 0 && remembered?.items.length ? { items: remembered.items, error: void 0 } : parseLineItems(args.items);
|
|
1592
|
+
if (lines.error) return { ok: false, error: "invalid_items", message: lines.error };
|
|
1593
|
+
const shipping = args.shipping === void 0 ? remembered?.shipping ?? null : parseReceiptAmount(args.shipping);
|
|
1594
|
+
const lineSum = round2(lines.items.reduce((s, i) => s + i.quantity * i.unitPrice, 0));
|
|
1595
|
+
const computed = round2(lineSum + (shipping ?? 0));
|
|
1596
|
+
const candidates = [
|
|
1597
|
+
{ value: amount, from: "argumento" },
|
|
1598
|
+
...remembered?.total !== void 0 ? [{ value: remembered.total, from: "leitura" }] : []
|
|
1599
|
+
];
|
|
1600
|
+
const agreed = lines.items.length ? candidates.find((c) => Math.abs(computed - c.value) <= 0.02) : candidates[0];
|
|
1601
|
+
if (lines.items.length && !agreed) {
|
|
1602
|
+
return {
|
|
1603
|
+
ok: false,
|
|
1604
|
+
error: "total_mismatch",
|
|
1605
|
+
lineSum,
|
|
1606
|
+
shipping: shipping ?? void 0,
|
|
1607
|
+
computed,
|
|
1608
|
+
total: amount,
|
|
1609
|
+
...remembered?.total !== void 0 ? { totalFromDocument: remembered.total } : {},
|
|
1610
|
+
message: `NOTHING WAS WRITTEN. The lines do not add up to the total: the item(s) sum to ${lineSum}${shipping ? ` plus ${shipping} of shipping` : ""} = ${computed}, but the total given is ${amount}. Do NOT pick one of them and do not adjust a line to make it fit. Re-read the document \u2014 a line may have been missed, a quantity misread, or a discount or shipping line not captured. If the paper genuinely disagrees with itself, ask the person which is right.`
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
const reconciledTotal = agreed?.value ?? amount;
|
|
1614
|
+
const correctedFromRead = agreed?.from === "leitura" && Math.abs(agreed.value - amount) > 5e-3;
|
|
1615
|
+
const dueDate = parseReceiptDate(args.dueDate) ?? remembered?.dueDate ?? void 0;
|
|
1616
|
+
const issueDate = parseReceiptDate(args.date) ?? remembered?.documentDate ?? parseReceiptDate(findDateIn(args.notes));
|
|
1617
|
+
const params = { amount };
|
|
1618
|
+
const supplierName = text(args.supplier);
|
|
1619
|
+
if (supplierName) params.supplier = supplierName;
|
|
1620
|
+
const description = text(args.description);
|
|
1621
|
+
if (description) params.description = description;
|
|
1622
|
+
if (dueDate) params.dueDate = dueDate;
|
|
1623
|
+
if (issueDate) params.issueDate = issueDate;
|
|
1624
|
+
const carried = [
|
|
1625
|
+
remembered?.orderRef ? `Pedido: ${remembered.orderRef}` : null,
|
|
1626
|
+
remembered?.deliveryDate ? `Entrega: ${remembered.deliveryDate}` : null
|
|
1627
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1628
|
+
const notes = text(args.notes) ?? (carried || void 0);
|
|
1629
|
+
if (notes) params.notes = notes;
|
|
1630
|
+
if (lines.items.length) {
|
|
1631
|
+
params.items = lines.items.map((i) => `${i.quantity}\xD7 ${i.description} @ ${i.unitPrice}`).join(" \xB7 ");
|
|
1632
|
+
params.amount = computed;
|
|
1633
|
+
}
|
|
1634
|
+
if (shipping) params.shipping = shipping;
|
|
1635
|
+
if (supplierName) {
|
|
1636
|
+
const already = findRecentPayable(supplierName, amount);
|
|
1637
|
+
if (already) {
|
|
1638
|
+
return {
|
|
1639
|
+
ok: false,
|
|
1640
|
+
error: "already_recorded",
|
|
1641
|
+
invoiceId: already.invoiceId,
|
|
1642
|
+
message: `NOTHING WAS WRITTEN and no second bill was created. The bill for ${supplierName} of ${amount} was ALREADY recorded in this conversation \u2014 its invoiceId is "${already.invoiceId}". If the person is saying it was PAID ("marca como pago", "paguei no Pix"), call registerPayablePayment with invoiceId "${already.invoiceId}". Only call createPayable again if this is genuinely a DIFFERENT bill, with a different amount or supplier.`
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
let supplier = null;
|
|
1647
|
+
if (supplierName) {
|
|
1648
|
+
const step = await resolveSupplier(lookup, supplierName);
|
|
1649
|
+
if (!step.ok) {
|
|
1650
|
+
return {
|
|
1651
|
+
ok: false,
|
|
1652
|
+
cancelled: true,
|
|
1653
|
+
reason: "user_declined_supplier",
|
|
1654
|
+
message: "NOTHING WAS WRITTEN. The person declined creating the supplier, so the bill has nobody to point at. Ask which existing supplier it belongs to \u2014 do not retry the same name."
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
supplier = { id: step.id, name: step.name, created: step.created };
|
|
1658
|
+
params.supplier = step.name;
|
|
1659
|
+
}
|
|
1660
|
+
const decision = await useChatStore.getState().requestConfirmation({
|
|
1661
|
+
id: `payable-${Date.now()}`,
|
|
1662
|
+
toolName: "createPayable",
|
|
1663
|
+
// Nomeia o que está sendo criado. Era `config.labels.payablesNew`,
|
|
1664
|
+
// que é o rótulo da AÇÃO na sub-navegação ("New") — no topo de um
|
|
1665
|
+
// cartão de confirmação ele dizia só "New" e não nomeava nada.
|
|
1666
|
+
title: "Nova conta a pagar",
|
|
1667
|
+
params,
|
|
1668
|
+
plane: "client",
|
|
1669
|
+
scope: "financial:payable",
|
|
1670
|
+
scopeLabel: "Lan\xE7ar conta a pagar",
|
|
1671
|
+
fields: [
|
|
1672
|
+
{ key: "supplier", label: "Fornecedor", type: "text", required: true },
|
|
1673
|
+
{ key: "description", label: "Descri\xE7\xE3o", type: "text", required: true },
|
|
1674
|
+
// As linhas lidas da tabela, à vista e só de leitura: é o que a
|
|
1675
|
+
// pessoa confere para saber se a leitura do papel bateu.
|
|
1676
|
+
...lines.items.length ? [{ key: "items", label: "Itens", type: "text", readOnly: true }] : [],
|
|
1677
|
+
...shipping ? [{ key: "shipping", label: "Frete", type: "number", readOnly: true }] : [],
|
|
1678
|
+
// Com tabela lida, o valor É a soma conferida — editá-lo aqui o
|
|
1679
|
+
// separaria das linhas, e o banco recusa uma fatura cujas parcelas
|
|
1680
|
+
// não fecham com os itens. Sem tabela, ele continua editável.
|
|
1681
|
+
{
|
|
1682
|
+
key: "amount",
|
|
1683
|
+
label: "Valor total",
|
|
1684
|
+
type: "number",
|
|
1685
|
+
required: true,
|
|
1686
|
+
min: 0,
|
|
1687
|
+
...lines.items.length ? { readOnly: true } : {}
|
|
1688
|
+
},
|
|
1689
|
+
{ key: "dueDate", label: "Vencimento", type: "date", required: true },
|
|
1690
|
+
{ key: "issueDate", label: "Data do documento", type: "date", default: today() },
|
|
1691
|
+
{ key: "notes", label: "Observa\xE7\xF5es", type: "text" }
|
|
1692
|
+
]
|
|
1693
|
+
});
|
|
1694
|
+
if (!decision.approved) return { ok: false, cancelled: true, reason: "user_declined" };
|
|
1695
|
+
const values = { ...params, ...decision.values ?? {} };
|
|
1696
|
+
const finalAmount = lines.items.length ? computed : parseReceiptAmount(values.amount) ?? amount;
|
|
1697
|
+
const finalSupplier = text(values.supplier);
|
|
1698
|
+
const finalDescription = text(values.description) ?? finalSupplier ?? "";
|
|
1699
|
+
const finalDue = parseReceiptDate(values.dueDate);
|
|
1700
|
+
const finalIssue = parseReceiptDate(values.issueDate) ?? today();
|
|
1701
|
+
if (!finalSupplier || !finalDue) {
|
|
1702
|
+
return {
|
|
1703
|
+
ok: false,
|
|
1704
|
+
error: "incomplete",
|
|
1705
|
+
message: "NOTHING WAS WRITTEN. The form came back without a supplier or a due date \u2014 do not tell the user it worked."
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
let resolvedSupplier = supplier;
|
|
1709
|
+
if (!resolvedSupplier || resolvedSupplier.name !== finalSupplier) {
|
|
1710
|
+
const again = await resolveSupplier(lookup, finalSupplier);
|
|
1711
|
+
if (!again.ok) {
|
|
1712
|
+
return {
|
|
1713
|
+
ok: false,
|
|
1714
|
+
cancelled: true,
|
|
1715
|
+
reason: "user_declined_supplier",
|
|
1716
|
+
message: "NOTHING WAS WRITTEN. The supplier step was declined \u2014 ask which supplier this bill belongs to."
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
resolvedSupplier = { id: again.id, name: again.name, created: again.created };
|
|
1720
|
+
}
|
|
1721
|
+
const invoice = await provider.createInvoice({
|
|
1722
|
+
direction: "debit",
|
|
1723
|
+
invoiceDate: finalIssue,
|
|
1724
|
+
contactId: resolvedSupplier.id,
|
|
1725
|
+
contactName: resolvedSupplier.name,
|
|
1726
|
+
observations: text(values.notes),
|
|
1727
|
+
// A nota que o papel descreve, linha por linha. Sem tabela, a conta
|
|
1728
|
+
// nasce com uma linha só — que é o que um recibo de táxi é. O FRETE é
|
|
1729
|
+
// linha PRÓPRIA: somado ao preço de um produto ele sumiria dentro
|
|
1730
|
+
// dele, e a conciliação com a transportadora não teria onde olhar.
|
|
1731
|
+
items: [
|
|
1732
|
+
...lines.items.length ? lines.items.map((line) => ({
|
|
1733
|
+
itemKind: "other",
|
|
1734
|
+
description: line.description,
|
|
1735
|
+
quantity: line.quantity,
|
|
1736
|
+
unitPrice: line.unitPrice
|
|
1737
|
+
})) : [{
|
|
1738
|
+
itemKind: "other",
|
|
1739
|
+
description: finalDescription,
|
|
1740
|
+
quantity: 1,
|
|
1741
|
+
unitPrice: round2(finalAmount - (shipping ?? 0))
|
|
1742
|
+
}],
|
|
1743
|
+
...shipping ? [{ itemKind: "other", description: "Frete", quantity: 1, unitPrice: shipping }] : []
|
|
1744
|
+
],
|
|
1745
|
+
installments: [{
|
|
1746
|
+
direction: "debit",
|
|
1747
|
+
movementKind: "bill",
|
|
1748
|
+
amount: finalAmount,
|
|
1749
|
+
dueDate: finalDue,
|
|
1750
|
+
installmentNumber: 1
|
|
1751
|
+
}]
|
|
1752
|
+
});
|
|
1753
|
+
const id = invoice?.id;
|
|
1754
|
+
if (id) {
|
|
1755
|
+
rememberPayable({ invoiceId: id, supplier: resolvedSupplier.name, amount: finalAmount, at: Date.now() });
|
|
1756
|
+
}
|
|
1757
|
+
consumePayableDraft();
|
|
1758
|
+
const drift = spelled !== null && figures !== null && Math.abs(spelled - figures) > 5e-3 ? { note: `Os algarismos pareciam ${figures} e o extenso diz ${spelled}. Usei o extenso.` } : {};
|
|
1759
|
+
const invoicePath = id ? `/financial/payables/${id}` : null;
|
|
1760
|
+
const redirected = invoicePath !== null && args.redirect !== false;
|
|
1761
|
+
if (redirected) {
|
|
1762
|
+
adminNavigateTo(invoicePath);
|
|
1763
|
+
revealPage();
|
|
1764
|
+
}
|
|
1765
|
+
const invoiceLink = invoicePath ? { ref: { id, label: `${resolvedSupplier.name} \u2014 ${finalDescription}`, path: invoicePath } } : {};
|
|
1766
|
+
if (args.paid === true && id) {
|
|
1767
|
+
const options = await paymentOptions(provider);
|
|
1768
|
+
const chosen = matchPaymentOption(options, args.paymentMethod);
|
|
1769
|
+
if (!chosen) {
|
|
1770
|
+
if (options.length >= 2) {
|
|
1771
|
+
return {
|
|
1772
|
+
...askHowItWasPaid(id, options),
|
|
1773
|
+
supplier: resolvedSupplier.name,
|
|
1774
|
+
supplierCreated: resolvedSupplier.created,
|
|
1775
|
+
amount: finalAmount,
|
|
1776
|
+
openedInvoice: redirected,
|
|
1777
|
+
...drift
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
ok: true,
|
|
1782
|
+
invoiceId: id,
|
|
1783
|
+
settled: false,
|
|
1784
|
+
supplier: resolvedSupplier.name,
|
|
1785
|
+
amount: finalAmount,
|
|
1786
|
+
dueDate: finalDue,
|
|
1787
|
+
openedInvoice: redirected,
|
|
1788
|
+
...invoiceLink,
|
|
1789
|
+
message: "A conta foi lan\xE7ada, mas o pagamento N\xC3O foi registrado: este neg\xF3cio n\xE3o tem formas de pagamento cadastradas para escolher. Diga isso e ofere\xE7a registrar a baixa pela tela.",
|
|
1790
|
+
...drift
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
const settled = await settlePayable(provider, {
|
|
1794
|
+
invoiceId: id,
|
|
1795
|
+
method: chosen,
|
|
1796
|
+
paymentDate: typeof args.paymentDate === "string" ? args.paymentDate : void 0,
|
|
1797
|
+
currency: config.currency?.code
|
|
1798
|
+
});
|
|
1799
|
+
return {
|
|
1800
|
+
...settled,
|
|
1801
|
+
invoiceId: id,
|
|
1802
|
+
supplier: resolvedSupplier.name,
|
|
1803
|
+
supplierCreated: resolvedSupplier.created,
|
|
1804
|
+
amount: finalAmount,
|
|
1805
|
+
openedInvoice: redirected,
|
|
1806
|
+
...drift
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
return {
|
|
1810
|
+
ok: true,
|
|
1811
|
+
invoiceId: id,
|
|
1812
|
+
settled: false,
|
|
1813
|
+
supplier: resolvedSupplier.name,
|
|
1814
|
+
supplierCreated: resolvedSupplier.created,
|
|
1815
|
+
amount: finalAmount,
|
|
1816
|
+
dueDate: finalDue,
|
|
1817
|
+
openedInvoice: redirected,
|
|
1818
|
+
...lines.items.length ? { itemsWritten: lines.items.length } : {},
|
|
1819
|
+
// Dito, nunca silencioso: se o total que veio na chamada perdeu os
|
|
1820
|
+
// centavos e o da leitura fechou com as linhas, quem escreveu foi a
|
|
1821
|
+
// leitura — e quem confere merece saber disso.
|
|
1822
|
+
...correctedFromRead ? {
|
|
1823
|
+
correctedTotal: {
|
|
1824
|
+
sent: amount,
|
|
1825
|
+
written: reconciledTotal,
|
|
1826
|
+
why: "o total enviado n\xE3o fechava com as linhas; o lido do documento fechou"
|
|
1827
|
+
}
|
|
1828
|
+
} : {},
|
|
1829
|
+
...shipping ? { shipping } : {},
|
|
1830
|
+
// A CONTA é o destino principal — é o que a pessoa acabou de criar.
|
|
1831
|
+
...invoiceLink,
|
|
1832
|
+
// O fornecedor vai como segundo botão, e só onde existe tela para ele
|
|
1833
|
+
// (ver supplierRef): `extractRecordLinks` lê `ref` e `record.ref`.
|
|
1834
|
+
...resolvedSupplier.id ? { record: supplierRef(resolvedSupplier.id, resolvedSupplier.name) } : {},
|
|
1835
|
+
// O que fazer DEPOIS, dito com o id dentro. Sem esta linha, "marca
|
|
1836
|
+
// como pago no pix hoje" na mensagem seguinte virou uma SEGUNDA conta
|
|
1837
|
+
// a pagar: o modelo tinha a ferramenta de baixa e nenhuma frase
|
|
1838
|
+
// ligando-a a esta fatura.
|
|
1839
|
+
message: `A conta a pagar de ${resolvedSupplier.name} foi lan\xE7ada (invoiceId "${id}"), em aberto. ` + (resolvedSupplier.created ? "O fornecedor foi cadastrado agora. " : "") + (redirected ? "A tela da conta J\xC1 foi aberta \u2014 n\xE3o ofere\xE7a abri-la. " : "") + `Diga isso em UMA frase e pare \u2014 n\xE3o repita os campos, a pessoa acabou de conferi-los. IMPORTANTE: se a pessoa disser depois que esta conta foi paga ("marca como pago", "paguei no Pix", "foi pago hoje"), chame registerPayablePayment com invoiceId "${id}". N\xC3O chame createPayable de novo \u2014 isso criaria uma segunda conta para a mesma despesa.`,
|
|
1840
|
+
...drift
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
tool: REGISTER_PAYMENT,
|
|
1846
|
+
execute: async (args) => {
|
|
1847
|
+
const invoiceId = typeof args.invoiceId === "string" ? args.invoiceId : "";
|
|
1848
|
+
if (!invoiceId) {
|
|
1849
|
+
return {
|
|
1850
|
+
ok: false,
|
|
1851
|
+
error: "missing_invoice",
|
|
1852
|
+
message: "Which bill? Pass the `invoiceId` that createPayable returned."
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
const options = await paymentOptions(provider);
|
|
1856
|
+
const chosen = matchPaymentOption(options, args.method);
|
|
1857
|
+
if (!chosen) {
|
|
1858
|
+
if (options.length < 2) {
|
|
1859
|
+
return {
|
|
1860
|
+
ok: false,
|
|
1861
|
+
error: "no_payment_methods",
|
|
1862
|
+
message: "This business has no payment methods to choose from. Say so instead of guessing one."
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
return askHowItWasPaid(invoiceId, options);
|
|
1866
|
+
}
|
|
1867
|
+
return settlePayable(provider, {
|
|
1868
|
+
invoiceId,
|
|
1869
|
+
method: chosen,
|
|
1870
|
+
paymentDate: typeof args.paymentDate === "string" ? args.paymentDate : void 0,
|
|
1871
|
+
currency: config.currency?.code
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
];
|
|
1876
|
+
}
|
|
1877
|
+
var liveDraft = null;
|
|
1878
|
+
function hasLivePayableDraft() {
|
|
1879
|
+
return liveDraft !== null;
|
|
1880
|
+
}
|
|
1881
|
+
var COMPANY_NOISE = /* @__PURE__ */ new Set([
|
|
1882
|
+
"ltda",
|
|
1883
|
+
"ltd",
|
|
1884
|
+
"me",
|
|
1885
|
+
"epp",
|
|
1886
|
+
"eireli",
|
|
1887
|
+
"sa",
|
|
1888
|
+
"mei",
|
|
1889
|
+
"cia",
|
|
1890
|
+
"ei",
|
|
1891
|
+
"inc",
|
|
1892
|
+
"comercio",
|
|
1893
|
+
"servicos",
|
|
1894
|
+
"industria",
|
|
1895
|
+
"do",
|
|
1896
|
+
"da",
|
|
1897
|
+
"de",
|
|
1898
|
+
"dos",
|
|
1899
|
+
"das",
|
|
1900
|
+
"e"
|
|
1901
|
+
]);
|
|
1902
|
+
function normalizeCompanyName(raw) {
|
|
1903
|
+
if (typeof raw !== "string") return "";
|
|
1904
|
+
return raw.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w && !COMPANY_NOISE.has(w)).join(" ").trim();
|
|
1905
|
+
}
|
|
1906
|
+
function onlyDigits(raw) {
|
|
1907
|
+
return typeof raw === "string" ? raw.replace(/\D/g, "") : "";
|
|
1908
|
+
}
|
|
1909
|
+
function lastFour(raw) {
|
|
1910
|
+
const digits = onlyDigits(raw);
|
|
1911
|
+
return digits.length >= 4 ? digits.slice(-4) : "";
|
|
1912
|
+
}
|
|
1913
|
+
async function findSupplier(lookup, rawName, rawDocument) {
|
|
1914
|
+
const wanted = normalizeCompanyName(rawName);
|
|
1915
|
+
const doc = onlyDigits(rawDocument);
|
|
1916
|
+
if (!wanted) return { status: "missing", candidates: [] };
|
|
1917
|
+
const core = wanted.split(" ")[0] ?? wanted;
|
|
1918
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1919
|
+
for (const query of core === wanted ? [core] : [core, wanted]) {
|
|
1920
|
+
for (const hit of await lookup.search(query).catch(() => [])) {
|
|
1921
|
+
const row = hit.data ?? {};
|
|
1922
|
+
seen.set(hit.id, {
|
|
1923
|
+
id: hit.id,
|
|
1924
|
+
name: hit.label,
|
|
1925
|
+
document: onlyDigits(row.document_number) || void 0,
|
|
1926
|
+
why: ""
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const all = [...seen.values()];
|
|
1931
|
+
if (doc) {
|
|
1932
|
+
const byDoc = all.find((c) => c.document && c.document === doc);
|
|
1933
|
+
if (byDoc) return { status: "found", match: { ...byDoc, why: "mesmo CNPJ/CPF" }, candidates: [] };
|
|
1934
|
+
}
|
|
1935
|
+
const exact = all.filter((c) => normalizeCompanyName(c.name) === wanted);
|
|
1936
|
+
if (exact.length === 1) {
|
|
1937
|
+
const only = exact[0];
|
|
1938
|
+
if (doc && only.document && only.document !== doc) {
|
|
1939
|
+
return {
|
|
1940
|
+
status: "ambiguous",
|
|
1941
|
+
candidates: [{ ...only, why: "mesmo nome, mas o CNPJ do papel \xE9 outro" }]
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
return { status: "found", match: { ...only, why: "nome id\xEAntico" }, candidates: [] };
|
|
1945
|
+
}
|
|
1946
|
+
const near = all.map((c) => {
|
|
1947
|
+
const n = normalizeCompanyName(c.name);
|
|
1948
|
+
if (!n) return null;
|
|
1949
|
+
if (n === wanted) return { ...c, why: "nome id\xEAntico" };
|
|
1950
|
+
if (n.startsWith(wanted) || wanted.startsWith(n)) return { ...c, why: "nome contido" };
|
|
1951
|
+
const shared = n.split(" ").filter((w) => wanted.split(" ").includes(w));
|
|
1952
|
+
return shared.length ? { ...c, why: `parte do nome em comum (${shared.join(" ")})` } : null;
|
|
1953
|
+
}).filter((c) => c !== null).slice(0, 5);
|
|
1954
|
+
return { status: near.length ? "ambiguous" : "missing", candidates: near };
|
|
1955
|
+
}
|
|
1956
|
+
async function findMethod(provider, args) {
|
|
1957
|
+
const types = await paymentOptions(provider);
|
|
1958
|
+
const spoken = [args.paymentMethod, args.cardBrand].filter((v) => typeof v === "string" && !!v.trim());
|
|
1959
|
+
const card = lastFour(args.cardLast4);
|
|
1960
|
+
const looksLikeCard = !!(card || args.cardBrand);
|
|
1961
|
+
let match = matchPaymentOption(types, args.paymentMethod);
|
|
1962
|
+
if (!match && looksLikeCard) {
|
|
1963
|
+
match = types.find((t2) => t2.transactionType === "credit_card") ?? null;
|
|
1964
|
+
}
|
|
1965
|
+
const configured = (await provider.getPaymentMethods().catch(() => [])).filter((m) => m.isActive !== false);
|
|
1966
|
+
const namedMethod = configured.find((m) => {
|
|
1967
|
+
const n = normalizeCompanyName(m.name);
|
|
1968
|
+
return !!n && spoken.some((s) => {
|
|
1969
|
+
const w = normalizeCompanyName(s);
|
|
1970
|
+
return !!w && (n === w || n.includes(w) || w.includes(n));
|
|
1971
|
+
});
|
|
1972
|
+
});
|
|
1973
|
+
if (!match) {
|
|
1974
|
+
return {
|
|
1975
|
+
status: "missing",
|
|
1976
|
+
spoken: spoken.join(" ") || void 0,
|
|
1977
|
+
cardLast4: card || void 0,
|
|
1978
|
+
available: types.map((t2) => ({ id: t2.id, label: t2.label })),
|
|
1979
|
+
hint: "This business has no payment method matching what the paper says. ASK which of the available ones it maps to, or offer to add it in Settings (createRecord on financial:payment-methods is the right tool for THAT, and only that). Never silently pick one."
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
status: "found",
|
|
1984
|
+
id: match.id,
|
|
1985
|
+
label: match.label,
|
|
1986
|
+
transactionType: match.transactionType,
|
|
1987
|
+
...namedMethod ? { configuredMethod: { id: namedMethod.id, name: namedMethod.name } } : {},
|
|
1988
|
+
...card ? { cardLast4: card } : {}
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
async function findAccount(provider, method, rawLast4) {
|
|
1992
|
+
if (method.status !== "found") {
|
|
1993
|
+
return { status: "blocked", hint: "Resolve the payment method first \u2014 the eligible accounts depend on it." };
|
|
1994
|
+
}
|
|
1995
|
+
if (method.transactionType === "cash") {
|
|
1996
|
+
return {
|
|
1997
|
+
status: "not_applicable",
|
|
1998
|
+
hint: "Cash settles through the open cash session, not an account \u2014 nothing to choose."
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
const accounts = await eligibleAccounts(provider, {
|
|
2002
|
+
id: String(method.id),
|
|
2003
|
+
label: String(method.label),
|
|
2004
|
+
transactionType: method.transactionType
|
|
2005
|
+
});
|
|
2006
|
+
const card = lastFour(rawLast4);
|
|
2007
|
+
const byCard = card ? accounts.find((a) => lastFour(a.accountNumber) === card || a.name.includes(card)) : void 0;
|
|
2008
|
+
if (byCard) return { status: "found", id: byCard.id, name: byCard.name, why: `final ${card}` };
|
|
2009
|
+
if (!accounts.length) {
|
|
2010
|
+
return {
|
|
2011
|
+
status: "missing",
|
|
2012
|
+
hint: `No account can settle a ${String(method.label)} payment. Say so \u2014 it has to be created in Settings.`
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
if (card) {
|
|
2016
|
+
return {
|
|
2017
|
+
status: "ambiguous",
|
|
2018
|
+
cardLast4: card,
|
|
2019
|
+
note: `o papel diz final ${card} e nenhuma conta cadastrada tem esse final`,
|
|
2020
|
+
options: accounts.map((a) => ({ id: a.id, name: a.name })),
|
|
2021
|
+
hint: `ASK whether the card ending ${card} is one of these accounts, or whether it still has to be registered. Do not assume it is the only eligible one.`
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
if (accounts.length === 1) {
|
|
2025
|
+
return { status: "found", id: accounts[0].id, name: accounts[0].name, why: "\xFAnica conta eleg\xEDvel" };
|
|
2026
|
+
}
|
|
2027
|
+
return {
|
|
2028
|
+
status: "ambiguous",
|
|
2029
|
+
...card ? { cardLast4: card, note: `nenhuma conta bate com o final ${card}` } : {},
|
|
2030
|
+
options: accounts.map((a) => ({ id: a.id, name: a.name })),
|
|
2031
|
+
hint: "Ask which account the money left. Do not pick one."
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
var RESOLVE_CONTEXT = {
|
|
2035
|
+
id: "financial.resolve-payable-context",
|
|
2036
|
+
name: "resolvePayableContext",
|
|
2037
|
+
description: "STEP 0, BEFORE writing anything: checks what a receipt names against what this business actually has. Call it as soon as you have read a receipt/photo/PDF, with EVERYTHING the paper says \u2014 supplier, its CNPJ/CPF if printed, the payment method, the card brand and last 4 digits if printed. Returns, for each dependency, whether it already exists here (fuzzy-matched on name and CNPJ, so the same supplier is never created twice), is ambiguous, or is missing \u2014 plus the candidates to choose from. Writes NOTHING. Report the result as a short checklist and then resolve the gaps IN ORDER: supplier first, then the payment method, then the bill (createPayable), then the payment.",
|
|
2038
|
+
icon: "ListChecks",
|
|
2039
|
+
mode: "read",
|
|
2040
|
+
execution: { plane: "client" },
|
|
2041
|
+
category: "Finance",
|
|
2042
|
+
role: "finance",
|
|
2043
|
+
permission: { feature: "financial", action: "read" },
|
|
2044
|
+
parameters: {
|
|
2045
|
+
type: "object",
|
|
2046
|
+
properties: {
|
|
2047
|
+
supplier: { type: "string", description: "O nome do fornecedor EXATAMENTE como est\xE1 escrito no papel." },
|
|
2048
|
+
supplierDocument: { type: "string", description: "O CNPJ ou CPF impresso, se houver. Copie os d\xEDgitos." },
|
|
2049
|
+
paymentMethod: {
|
|
2050
|
+
type: "string",
|
|
2051
|
+
description: 'Como o papel diz que foi pago ("PIX", "cart\xE3o de cr\xE9dito", "dinheiro", "boleto").'
|
|
2052
|
+
},
|
|
2053
|
+
cardBrand: { type: "string", description: 'A bandeira, se impressa ("Visa", "Mastercard", "Nubank").' },
|
|
2054
|
+
cardLast4: { type: "string", description: 'Os quatro \xFAltimos d\xEDgitos do cart\xE3o, se impressos ("4242").' },
|
|
2055
|
+
// O DOCUMENTO INTEIRO, lido UMA VEZ.
|
|
2056
|
+
//
|
|
2057
|
+
// Não é zelo: é onde a fabricação entrava. Extrair no passo 0 e extrair de
|
|
2058
|
+
// novo no passo da conta são duas leituras do mesmo papel, e a segunda não
|
|
2059
|
+
// tem o papel na frente — ela tem a memória da primeira, que é onde os
|
|
2060
|
+
// números começam a escorregar. Lido aqui, normalizado aqui, conferido
|
|
2061
|
+
// aqui, e as ferramentas seguintes CONSOMEM o que ficou guardado.
|
|
2062
|
+
items: {
|
|
2063
|
+
type: "array",
|
|
2064
|
+
description: "As LINHAS da tabela do documento, uma por linha \u2014 leia linha a linha e copie TODAS.",
|
|
2065
|
+
items: {
|
|
2066
|
+
type: "object",
|
|
2067
|
+
properties: {
|
|
2068
|
+
description: { type: "string", description: "O que \xE9 a linha, como est\xE1 escrito." },
|
|
2069
|
+
quantity: { type: "number", description: "A quantidade. 1 quando a tabela n\xE3o diz." },
|
|
2070
|
+
unitPrice: { type: "string", description: 'Pre\xE7o UNIT\xC1RIO copiado ("47,90").' }
|
|
2071
|
+
},
|
|
2072
|
+
required: ["description", "unitPrice"]
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
2075
|
+
shipping: { type: "string", description: 'O FRETE cobrado \xE0 parte, copiado ("52,19").' },
|
|
2076
|
+
subtotal: { type: "string", description: 'O subtotal impresso, se houver ("1.437,00").' },
|
|
2077
|
+
total: {
|
|
2078
|
+
type: "string",
|
|
2079
|
+
description: 'O TOTAL do rodap\xE9, copiado letra por letra ("1.489,19"). Com frete, sem converter.'
|
|
2080
|
+
},
|
|
2081
|
+
documentDate: { type: "string", description: 'A data do documento ("09/09/2026").' },
|
|
2082
|
+
dueDate: {
|
|
2083
|
+
type: "string",
|
|
2084
|
+
description: 'O VENCIMENTO, s\xF3 se o papel disser. "Expira em" de um c\xF3digo Pix \xE9 a validade do C\xD3DIGO, n\xE3o vencimento: um pedido pago \xE0 vista n\xE3o tem, e a\xED se omite.'
|
|
2085
|
+
},
|
|
2086
|
+
paymentDate: { type: "string", description: "Quando foi pago, se o papel disser." },
|
|
2087
|
+
orderRef: { type: "string", description: 'O n\xFAmero do pedido ou documento ("#164223").' },
|
|
2088
|
+
deliveryDate: { type: "string", description: 'A data de entrega, se impressa ("11/09/2026").' }
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
var FILL_DRAFT = {
|
|
2093
|
+
id: "financial.fill-payable-draft",
|
|
2094
|
+
name: "fillPayableDraft",
|
|
2095
|
+
description: "Fills a field of the payable form ALREADY OPEN on screen (the draft the person is looking at). Use this \u2014 not createPayable \u2014 whenever that form is open: it is the bill in progress, and writing a new one instead would leave the person staring at a draft that means nothing. Looks the supplier up by name and offers to create it when it does not exist yet. Saves nothing.",
|
|
2096
|
+
icon: "PenLine",
|
|
2097
|
+
mode: "read",
|
|
2098
|
+
execution: { plane: "client" },
|
|
2099
|
+
category: "Finance",
|
|
2100
|
+
role: "finance",
|
|
2101
|
+
parameters: {
|
|
2102
|
+
type: "object",
|
|
2103
|
+
properties: {
|
|
2104
|
+
supplier: { type: "string", description: "O nome do fornecedor a colocar no rascunho." },
|
|
2105
|
+
dueDate: { type: "string", description: 'O vencimento, como foi dito ("20/01/2026").' }
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
var SUBMIT_DRAFT = {
|
|
2110
|
+
id: "financial.submit-payable-draft",
|
|
2111
|
+
name: "submitPayableDraft",
|
|
2112
|
+
description: `SAVES the payable form open on screen \u2014 the same thing the screen's save button does. This is how "salva", "pode salvar", "confirma" is answered while that form is open. Never call createPayable for a draft that is already on screen: that writes a second, duplicate bill.`,
|
|
2113
|
+
icon: "Check",
|
|
2114
|
+
mode: "persist",
|
|
2115
|
+
execution: { plane: "client" },
|
|
2116
|
+
requiresConfirmation: false,
|
|
2117
|
+
category: "Finance",
|
|
2118
|
+
role: "finance",
|
|
2119
|
+
permission: { feature: "financial", action: "create" },
|
|
2120
|
+
parameters: { type: "object", properties: {} }
|
|
2121
|
+
};
|
|
2122
|
+
function PayableDraftSurface({ config, draft }) {
|
|
2123
|
+
const live = React15.useRef(draft);
|
|
2124
|
+
live.current = draft;
|
|
2125
|
+
const lookup = React15.useMemo(
|
|
2126
|
+
() => config.contactLookup ?? createArchetypeLookup({ archetype: "person", kind: "supplier" }),
|
|
2127
|
+
[config.contactLookup]
|
|
2128
|
+
);
|
|
2129
|
+
React15.useEffect(() => {
|
|
2130
|
+
liveDraft = {
|
|
2131
|
+
describe: () => live.current.describe(),
|
|
2132
|
+
setSupplier: (name, id) => live.current.setSupplier(name, id),
|
|
2133
|
+
setDueDate: (iso) => live.current.setDueDate(iso),
|
|
2134
|
+
submit: () => live.current.submit()
|
|
2135
|
+
};
|
|
2136
|
+
return () => {
|
|
2137
|
+
liveDraft = null;
|
|
2138
|
+
};
|
|
2139
|
+
}, []);
|
|
2140
|
+
const contribution = React15.useMemo(() => ({
|
|
2141
|
+
id: "financial.payables.draft",
|
|
2142
|
+
pluginId: "financial",
|
|
2143
|
+
role: "finance",
|
|
2144
|
+
instructions: 'O cadastro de uma CONTA A PAGAR est\xE1 aberto na tela, com um rascunho que a pessoa est\xE1 conferindo. Enquanto ele estiver a\xED, ele \xC9 a conta em curso: use `fillPayableDraft` para completar o que falta e `submitPayableDraft` para salvar. N\xC3O chame `createPayable` \u2014 ela criaria uma segunda conta e deixaria a pessoa num rascunho \xF3rf\xE3o. O estado do rascunho est\xE1 em "Tela aberta agora".',
|
|
2145
|
+
describe: () => {
|
|
2146
|
+
const state = live.current.describe();
|
|
2147
|
+
return {
|
|
2148
|
+
title: "Cadastro \u2014 Conta a pagar (rascunho n\xE3o salvo)",
|
|
2149
|
+
state: { screen: "/financial/payables/new", draft: state }
|
|
2150
|
+
};
|
|
2151
|
+
},
|
|
2152
|
+
tools: [
|
|
869
2153
|
{
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
2154
|
+
tool: FILL_DRAFT,
|
|
2155
|
+
execute: async (args) => {
|
|
2156
|
+
const filled = [];
|
|
2157
|
+
const dueDate = parseReceiptDate(args.dueDate);
|
|
2158
|
+
if (dueDate) {
|
|
2159
|
+
live.current.setDueDate(dueDate);
|
|
2160
|
+
filled.push("dueDate");
|
|
2161
|
+
}
|
|
2162
|
+
const name = text(args.supplier);
|
|
2163
|
+
if (name) {
|
|
2164
|
+
const step = await resolveSupplier(lookup, name);
|
|
2165
|
+
if (!step.ok) {
|
|
2166
|
+
return {
|
|
2167
|
+
ok: false,
|
|
2168
|
+
cancelled: true,
|
|
2169
|
+
reason: "user_declined_supplier",
|
|
2170
|
+
message: "The supplier was not created, so the draft still has none. Ask which supplier it is."
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
live.current.setSupplier(step.name, step.id);
|
|
2174
|
+
filled.push("supplier");
|
|
2175
|
+
}
|
|
2176
|
+
if (!filled.length) {
|
|
2177
|
+
return { ok: false, error: "nothing_to_fill", message: "Send a supplier or a dueDate to fill in." };
|
|
2178
|
+
}
|
|
2179
|
+
return {
|
|
2180
|
+
ok: true,
|
|
2181
|
+
filled,
|
|
2182
|
+
draft: live.current.describe(),
|
|
2183
|
+
message: "O rascunho na tela foi preenchido \u2014 nada foi salvo ainda. Diga em UMA frase o que entrou e que basta confirmar. Quando a pessoa mandar salvar, chame submitPayableDraft."
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
tool: SUBMIT_DRAFT,
|
|
2189
|
+
execute: async () => {
|
|
2190
|
+
const before = live.current.describe();
|
|
2191
|
+
if (!before.itemCount) {
|
|
2192
|
+
return {
|
|
2193
|
+
ok: false,
|
|
2194
|
+
error: "empty_draft",
|
|
2195
|
+
message: "NOTHING WAS SAVED. The draft has no line to bill. Say what is missing."
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
await live.current.submit();
|
|
2199
|
+
return {
|
|
2200
|
+
ok: true,
|
|
2201
|
+
saved: true,
|
|
2202
|
+
supplier: before.supplier,
|
|
2203
|
+
total: before.total,
|
|
2204
|
+
message: "A conta a pagar foi salva a partir do rascunho que estava na tela, e a ficha dela j\xE1 abriu. Diga isso em UMA frase e pare."
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
879
2207
|
}
|
|
880
|
-
|
|
881
|
-
|
|
2208
|
+
]
|
|
2209
|
+
}), [lookup]);
|
|
2210
|
+
useAgentSurface(contribution);
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
function createFinancialAgentMount(deps) {
|
|
2214
|
+
const FinancialAgentMount = () => {
|
|
2215
|
+
const contribution = React15.useMemo(() => ({
|
|
2216
|
+
id: "financial.payables.global",
|
|
2217
|
+
pluginId: "financial",
|
|
2218
|
+
role: "finance",
|
|
2219
|
+
instructions: PAYABLE_INSTRUCTIONS,
|
|
2220
|
+
// Sem `describe`: esta superfície não é uma tela. O snapshot responde "o
|
|
2221
|
+
// que está na tela agora", e um mount de shell que dissesse "Contas a
|
|
2222
|
+
// pagar" mentiria em todas as outras rotas do app.
|
|
2223
|
+
tools: createPayableTools(deps)
|
|
2224
|
+
}), []);
|
|
2225
|
+
useAgentSurface(contribution);
|
|
2226
|
+
return null;
|
|
2227
|
+
};
|
|
2228
|
+
FinancialAgentMount.displayName = "FinancialAgentMount";
|
|
2229
|
+
return FinancialAgentMount;
|
|
882
2230
|
}
|
|
883
2231
|
function useReceiptPicker(onPick) {
|
|
884
2232
|
const t2 = useTranslation();
|
|
@@ -910,7 +2258,7 @@ function useReceiptPicker(onPick) {
|
|
|
910
2258
|
);
|
|
911
2259
|
return { open: () => ref.current?.click(), input };
|
|
912
2260
|
}
|
|
913
|
-
var
|
|
2261
|
+
var ACCOUNT_TYPES_FOR_TX2 = {
|
|
914
2262
|
cash: ["cash_physical", "cash_session"],
|
|
915
2263
|
credit_card: ["card_receivable_credit"],
|
|
916
2264
|
debit_card: ["card_receivable_debit"],
|
|
@@ -966,6 +2314,8 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
966
2314
|
const [cardInstallments, setCardInstallments] = useState(1);
|
|
967
2315
|
const [notes, setNotes] = useState("");
|
|
968
2316
|
const [saving, setSaving] = useState(false);
|
|
2317
|
+
const command = React15__default.useRef(createPendingCommand(() => createIdempotencyKey("financial:payment"), (input) => input.movementId));
|
|
2318
|
+
const [pending2, setPending] = useState(false);
|
|
969
2319
|
const [visible, setVisible] = useState(false);
|
|
970
2320
|
const [expandMethodPicker, setExpandMethodPicker] = useState(true);
|
|
971
2321
|
useEffect(() => {
|
|
@@ -984,16 +2334,16 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
984
2334
|
);
|
|
985
2335
|
const needsCard = txType === "credit_card" || txType === "debit_card";
|
|
986
2336
|
const needsCardInstallments = txType === "credit_card";
|
|
987
|
-
const
|
|
988
|
-
const allowed = selectedType?.allowedAccountTypes && selectedType.allowedAccountTypes.length ? selectedType.allowedAccountTypes :
|
|
2337
|
+
const eligibleAccounts2 = useMemo(() => {
|
|
2338
|
+
const allowed = selectedType?.allowedAccountTypes && selectedType.allowedAccountTypes.length ? selectedType.allowedAccountTypes : ACCOUNT_TYPES_FOR_TX2[txType];
|
|
989
2339
|
if (!allowed || !allowed.length) return bankAccounts;
|
|
990
2340
|
return bankAccounts.filter((a) => allowed.includes(a.accountType));
|
|
991
2341
|
}, [bankAccounts, selectedType, txType]);
|
|
992
2342
|
useEffect(() => {
|
|
993
|
-
if (selectedTypeId && !bankAccountId &&
|
|
994
|
-
setBankAccountId(
|
|
2343
|
+
if (selectedTypeId && !bankAccountId && eligibleAccounts2.length) {
|
|
2344
|
+
setBankAccountId(eligibleAccounts2[0].id);
|
|
995
2345
|
}
|
|
996
|
-
}, [selectedTypeId,
|
|
2346
|
+
}, [selectedTypeId, eligibleAccounts2]);
|
|
997
2347
|
function handleSelectType(typeId) {
|
|
998
2348
|
setSelectedTypeId(typeId);
|
|
999
2349
|
setExpandMethodPicker(false);
|
|
@@ -1003,6 +2353,7 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1003
2353
|
setCardInstallments(1);
|
|
1004
2354
|
}
|
|
1005
2355
|
function handleClose() {
|
|
2356
|
+
if (command.current.pending || command.current.running) return;
|
|
1006
2357
|
setVisible(false);
|
|
1007
2358
|
setTimeout(onClose, 200);
|
|
1008
2359
|
}
|
|
@@ -1012,15 +2363,17 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1012
2363
|
e.preventDefault();
|
|
1013
2364
|
if (amount > 0 && selectedTypeId && bankAccountId && !saving) void handlePay();
|
|
1014
2365
|
}
|
|
1015
|
-
|
|
2366
|
+
React15__default.useEffect(() => {
|
|
1016
2367
|
void fetchCashSessions();
|
|
1017
2368
|
}, [fetchCashSessions]);
|
|
1018
2369
|
const needsOpenCashSession = txType === "cash" && !openSession;
|
|
1019
2370
|
async function handlePay() {
|
|
1020
|
-
if (
|
|
2371
|
+
if (command.current.running) return;
|
|
2372
|
+
if (!command.current.pending && (amount <= 0 || !selectedTypeId || !bankAccountId || needsOpenCashSession)) return;
|
|
1021
2373
|
setSaving(true);
|
|
2374
|
+
setPending(true);
|
|
1022
2375
|
try {
|
|
1023
|
-
await
|
|
2376
|
+
await command.current.run({
|
|
1024
2377
|
// O comprovante viaja com o pagamento — é dele que ele é prova.
|
|
1025
2378
|
metadata: receiptUrl ? { receiptUrl } : void 0,
|
|
1026
2379
|
movementId: movement.id,
|
|
@@ -1035,12 +2388,13 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1035
2388
|
cardBrand: needsCard ? cardBrand || void 0 : void 0,
|
|
1036
2389
|
cardInstallments: needsCardInstallments ? cardInstallments : void 0,
|
|
1037
2390
|
notes: notes.trim() || void 0
|
|
1038
|
-
});
|
|
2391
|
+
}, payMovement);
|
|
1039
2392
|
setVisible(false);
|
|
1040
2393
|
setTimeout(onPaid, 200);
|
|
1041
2394
|
} catch {
|
|
1042
2395
|
} finally {
|
|
1043
2396
|
setSaving(false);
|
|
2397
|
+
setPending(command.current.pending);
|
|
1044
2398
|
}
|
|
1045
2399
|
}
|
|
1046
2400
|
const SelectedIcon = selectedType ? TYPE_ICONS[txType] ?? Banknote : null;
|
|
@@ -1073,7 +2427,8 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1073
2427
|
] }),
|
|
1074
2428
|
/* @__PURE__ */ jsx("button", { onClick: handleClose, className: "p-1 text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
|
|
1075
2429
|
] }),
|
|
1076
|
-
/* @__PURE__ */
|
|
2430
|
+
pending2 && /* @__PURE__ */ jsx("p", { role: "status", className: "px-5 pt-4 text-sm text-warning", children: t2("financial.payment.pendingRetry") }),
|
|
2431
|
+
/* @__PURE__ */ jsxs("fieldset", { disabled: pending2, className: "px-5 py-4 space-y-4", children: [
|
|
1077
2432
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
1078
2433
|
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.method") }),
|
|
1079
2434
|
/* @__PURE__ */ jsx(
|
|
@@ -1179,7 +2534,7 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1179
2534
|
] }),
|
|
1180
2535
|
/* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "100ms" }, children: [
|
|
1181
2536
|
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.bankAccount") }),
|
|
1182
|
-
|
|
2537
|
+
eligibleAccounts2.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-1 rounded-lg border border-dashed border-input bg-muted/30 px-3 py-2.5 text-center", children: [
|
|
1183
2538
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: bankAccounts.length > 0 && selectedType ? t2("financial.payment.noAccountForMethod", { method: selectedType.name }) : t2("financial.payment.noAccounts") }),
|
|
1184
2539
|
/* @__PURE__ */ jsxs("a", { href: "#/settings/financial/bank-accounts", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
|
|
1185
2540
|
t2("financial.payment.configureAccounts"),
|
|
@@ -1187,7 +2542,7 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1187
2542
|
] })
|
|
1188
2543
|
] }) : /* @__PURE__ */ jsxs("select", { value: bankAccountId, onChange: (e) => setBankAccountId(e.target.value), className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm", children: [
|
|
1189
2544
|
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectAccount") }),
|
|
1190
|
-
|
|
2545
|
+
eligibleAccounts2.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
1191
2546
|
] })
|
|
1192
2547
|
] }),
|
|
1193
2548
|
/* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "150ms" }, children: [
|
|
@@ -1206,14 +2561,14 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
|
|
|
1206
2561
|
}
|
|
1207
2562
|
` }),
|
|
1208
2563
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-4 border-t sticky bottom-0 bg-card", children: [
|
|
1209
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, children: t2("financial.payment.cancel") }),
|
|
2564
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, disabled: pending2 || saving, children: t2("financial.payment.cancel") }),
|
|
1210
2565
|
/* @__PURE__ */ jsxs(
|
|
1211
2566
|
Button,
|
|
1212
2567
|
{
|
|
1213
2568
|
variant: "default",
|
|
1214
2569
|
size: "sm",
|
|
1215
2570
|
onClick: handlePay,
|
|
1216
|
-
disabled: amount <= 0 || !selectedTypeId || !bankAccountId || needsOpenCashSession
|
|
2571
|
+
disabled: saving || !pending2 && (amount <= 0 || !selectedTypeId || !bankAccountId || needsOpenCashSession),
|
|
1217
2572
|
children: [
|
|
1218
2573
|
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
|
|
1219
2574
|
saving ? t2("financial.payment.processing") : t2("financial.payment.payAmount", { amount: formatCurrency(amount, currency) })
|
|
@@ -1318,7 +2673,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1318
2673
|
const t2 = useTranslation();
|
|
1319
2674
|
const config = useFinancialConfig();
|
|
1320
2675
|
const { currency, itemTypes: itemTypesRaw, locations } = config;
|
|
1321
|
-
const itemTypes =
|
|
2676
|
+
const itemTypes = React15__default.useMemo(
|
|
1322
2677
|
() => itemTypesRaw.map((tp) => {
|
|
1323
2678
|
const traduzido = t2(`financial.itemType.${tp.value}`);
|
|
1324
2679
|
return traduzido === `financial.itemType.${tp.value}` ? tp : { ...tp, label: traduzido };
|
|
@@ -1355,6 +2710,14 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1355
2710
|
cancelled = true;
|
|
1356
2711
|
};
|
|
1357
2712
|
}, [provider]);
|
|
2713
|
+
const contactLookup = useMemo(
|
|
2714
|
+
() => config.contactLookup ?? createArchetypeLookup({
|
|
2715
|
+
archetype: "person",
|
|
2716
|
+
kind: direction === "debit" ? "supplier" : "customer",
|
|
2717
|
+
kindLabels: { supplier: "Fornecedor", customer: "Cliente" }
|
|
2718
|
+
}),
|
|
2719
|
+
[config.contactLookup, direction]
|
|
2720
|
+
);
|
|
1358
2721
|
const [loaded, setLoaded] = useState(!editId);
|
|
1359
2722
|
const [loading, setLoading] = useState(!!editId);
|
|
1360
2723
|
const [contactName, setContactName] = useState("");
|
|
@@ -1373,7 +2736,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1373
2736
|
const [savedMovements, setSavedMovements] = useState([]);
|
|
1374
2737
|
const [payingMovement, setPayingMovement] = useState(null);
|
|
1375
2738
|
const [pendingReceipt, setPendingReceipt] = useState();
|
|
1376
|
-
const contactCreate = useLookupCreate(
|
|
2739
|
+
const contactCreate = useLookupCreate(contactLookup, (created) => {
|
|
1377
2740
|
setContactId(created.id);
|
|
1378
2741
|
setContactName(created.label);
|
|
1379
2742
|
setPendingContact(null);
|
|
@@ -1397,8 +2760,15 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1397
2760
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
1398
2761
|
const [confirmCancel, setConfirmCancel] = useState(false);
|
|
1399
2762
|
const [cancelling, setCancelling] = useState(false);
|
|
2763
|
+
const [cancelReason, setCancelReason] = useState("");
|
|
2764
|
+
const cancelRequestKey = useRef(createIdempotencyKey("financial:invoice-cancel"));
|
|
1400
2765
|
const menuRef = useRef(null);
|
|
1401
2766
|
const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
|
|
2767
|
+
function closeCancelDialog() {
|
|
2768
|
+
setConfirmCancel(false);
|
|
2769
|
+
setCancelReason("");
|
|
2770
|
+
cancelRequestKey.current = createIdempotencyKey("financial:invoice-cancel");
|
|
2771
|
+
}
|
|
1402
2772
|
useEffect(() => {
|
|
1403
2773
|
if (!menuOpen) return;
|
|
1404
2774
|
function handleClick(e) {
|
|
@@ -1411,7 +2781,11 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1411
2781
|
if (!editId) return;
|
|
1412
2782
|
setCancelling(true);
|
|
1413
2783
|
try {
|
|
1414
|
-
await cancelInvoice(
|
|
2784
|
+
await cancelInvoice({
|
|
2785
|
+
invoiceId: editId,
|
|
2786
|
+
reason: cancelReason,
|
|
2787
|
+
idempotencyKey: cancelRequestKey.current
|
|
2788
|
+
});
|
|
1415
2789
|
onSaved?.();
|
|
1416
2790
|
} catch {
|
|
1417
2791
|
} finally {
|
|
@@ -1505,13 +2879,13 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1505
2879
|
if (isEdit || totalAmount <= 0 || installments.length > 0) return;
|
|
1506
2880
|
generateInstallments(1);
|
|
1507
2881
|
}, [isEdit, totalAmount, installments.length]);
|
|
1508
|
-
const fromCard =
|
|
2882
|
+
const fromCard = React15__default.useMemo(
|
|
1509
2883
|
() => fromCardId ? bankAccounts.find((a) => a.id === fromCardId) : void 0,
|
|
1510
2884
|
[fromCardId, bankAccounts]
|
|
1511
2885
|
);
|
|
1512
2886
|
const cardLast4 = (fromCard?.accountNumber ?? "").replace(/\D/g, "").slice(-4);
|
|
1513
2887
|
const nomeJaTemOsDigitos = !!cardLast4 && (fromCard?.name ?? "").replace(/\D/g, "").endsWith(cardLast4);
|
|
1514
|
-
const cardMethodName =
|
|
2888
|
+
const cardMethodName = React15__default.useMemo(() => {
|
|
1515
2889
|
const id = installments[0]?.payMethodTypeId;
|
|
1516
2890
|
const escolhida = id ? paymentMethodTypes.find((tp) => tp.id === id) : void 0;
|
|
1517
2891
|
return (escolhida ?? paymentMethodTypes.find((tp) => tp.transactionType === "credit_card"))?.name;
|
|
@@ -1535,21 +2909,31 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1535
2909
|
if (draft.supplier) {
|
|
1536
2910
|
setContactName(draft.supplier);
|
|
1537
2911
|
void (async () => {
|
|
1538
|
-
const hit = (await
|
|
2912
|
+
const hit = (await contactLookup.search(draft.supplier).catch(() => []))?.find((r) => r.label.trim().toLowerCase() === draft.supplier.trim().toLowerCase());
|
|
1539
2913
|
if (hit) {
|
|
1540
2914
|
setContactId(hit.id);
|
|
1541
2915
|
setContactName(hit.label);
|
|
1542
2916
|
} else setPendingContact(draft.supplier);
|
|
1543
2917
|
})();
|
|
1544
2918
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
2919
|
+
setItems(
|
|
2920
|
+
draft.items?.length ? [
|
|
2921
|
+
...draft.items.map((line) => ({
|
|
2922
|
+
_id: fid(),
|
|
2923
|
+
itemKind: "other",
|
|
2924
|
+
description: line.description,
|
|
2925
|
+
quantity: line.quantity,
|
|
2926
|
+
unitPrice: line.unitPrice
|
|
2927
|
+
})),
|
|
2928
|
+
...draft.shipping ? [{ _id: fid(), itemKind: "other", description: "Frete", quantity: 1, unitPrice: draft.shipping }] : []
|
|
2929
|
+
] : [{
|
|
2930
|
+
_id: fid(),
|
|
2931
|
+
itemKind: "other",
|
|
2932
|
+
description: draft.description ?? draft.supplier ?? "",
|
|
2933
|
+
quantity: 1,
|
|
2934
|
+
unitPrice: draft.amount - (draft.shipping ?? 0)
|
|
2935
|
+
}]
|
|
2936
|
+
);
|
|
1553
2937
|
setInstallments([{ _id: fid(), amount: draft.amount, dueDate: draft.date ?? invoiceDate }]);
|
|
1554
2938
|
if (draft.receiptUrl) setPendingReceipt(draft.receiptUrl);
|
|
1555
2939
|
}, [isEdit]);
|
|
@@ -1651,7 +3035,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1651
3035
|
amount: bill.amount,
|
|
1652
3036
|
paymentDate: inst.dueDate,
|
|
1653
3037
|
paymentMethodTypeId: inst.payMethodTypeId || void 0,
|
|
1654
|
-
bankAccountId: inst.payAccountId || void 0
|
|
3038
|
+
bankAccountId: inst.payAccountId || void 0,
|
|
3039
|
+
idempotencyKey: `financial:invoice:${invoice.id}:installment:${bill.id}:receipt`
|
|
1655
3040
|
});
|
|
1656
3041
|
}
|
|
1657
3042
|
}
|
|
@@ -1712,130 +3097,133 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1712
3097
|
] }) : null;
|
|
1713
3098
|
const paidSoFar = savedMovements.reduce((sum, m) => sum + (m.paidAmount ?? 0), 0);
|
|
1714
3099
|
const hasPayments = paidSoFar > 0;
|
|
1715
|
-
const scheduleSection =
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
/* @__PURE__ */ jsx("
|
|
1719
|
-
|
|
1720
|
-
{
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
n
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
/* @__PURE__ */ jsx(
|
|
1753
|
-
|
|
3100
|
+
const scheduleSection = (
|
|
3101
|
+
// Tour anchor: the guided setup points here for the installment schedule.
|
|
3102
|
+
/* @__PURE__ */ jsx("div", { ...tourAnchor("financial.installments"), children: /* @__PURE__ */ jsx(OrderFormBlock, { title: t2("financial.invoice.paymentSchedule"), children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
3103
|
+
hasPayments ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoiceForm.scheduleLocked") }) : (
|
|
3104
|
+
// Atalhos de parcelamento — 1x, 2x, 3x, 6x, 12x cobrem quase tudo.
|
|
3105
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: [1, 2, 3, 6, 12].map((n) => /* @__PURE__ */ jsxs(
|
|
3106
|
+
"button",
|
|
3107
|
+
{
|
|
3108
|
+
type: "button",
|
|
3109
|
+
onClick: () => generateInstallments(n),
|
|
3110
|
+
disabled: totalAmount <= 0,
|
|
3111
|
+
className: `rounded-full px-2.5 py-1 text-[10px] font-medium transition-colors disabled:opacity-30 ${installments.length === n ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:bg-muted/80"}`,
|
|
3112
|
+
children: [
|
|
3113
|
+
n,
|
|
3114
|
+
"x"
|
|
3115
|
+
]
|
|
3116
|
+
},
|
|
3117
|
+
n
|
|
3118
|
+
)) })
|
|
3119
|
+
),
|
|
3120
|
+
installments.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-4 text-center text-xs text-muted-foreground", children: totalAmount <= 0 ? t2("financial.invoiceForm.addItemsFirst") : t2("financial.invoiceForm.selectInstallments") }) : /* @__PURE__ */ jsx("div", { className: "divide-y rounded border", children: installments.map((inst, i) => {
|
|
3121
|
+
const patch = (next) => {
|
|
3122
|
+
const updated = [...installments];
|
|
3123
|
+
updated[i] = { ...inst, ...next };
|
|
3124
|
+
setInstallments(updated);
|
|
3125
|
+
};
|
|
3126
|
+
const saved = savedMovements[i];
|
|
3127
|
+
const isPaid = saved?.status === "paid";
|
|
3128
|
+
const isPartial = saved?.status === "partial";
|
|
3129
|
+
const StatusIcon = isPaid ? CircleCheckBig : CircleDashed;
|
|
3130
|
+
const canAttach = isEdit ? !!saved && !isPaid : true;
|
|
3131
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3132
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 px-3 py-2", children: [
|
|
3133
|
+
/* @__PURE__ */ jsxs("span", { className: "w-6 shrink-0 text-xs font-medium text-muted-foreground", children: [
|
|
3134
|
+
"#",
|
|
3135
|
+
i + 1
|
|
3136
|
+
] }),
|
|
3137
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold tabular-nums", children: money3(inst.amount) }),
|
|
3138
|
+
saved && /* @__PURE__ */ jsxs("span", { className: `inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium ${isPaid ? "bg-success-soft text-success-soft-foreground" : isPartial ? "bg-warning-soft text-warning-soft-foreground" : "bg-muted text-muted-foreground"}`, children: [
|
|
3139
|
+
/* @__PURE__ */ jsx(StatusIcon, { className: "h-2.5 w-2.5" }),
|
|
3140
|
+
t2(isPaid ? "financial.invoice.statusPaid" : isPartial ? "financial.invoice.statusPartial" : "financial.invoice.statusPending")
|
|
3141
|
+
] }),
|
|
3142
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1.5", children: [
|
|
3143
|
+
!hasPayments ? /* @__PURE__ */ jsx(
|
|
3144
|
+
DatePicker,
|
|
3145
|
+
{
|
|
3146
|
+
value: inst.dueDate,
|
|
3147
|
+
variant: "chip",
|
|
3148
|
+
onChange: (v) => patch({ dueDate: v })
|
|
3149
|
+
}
|
|
3150
|
+
) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: inst.dueDate }),
|
|
3151
|
+
canAttach && /* @__PURE__ */ jsx(
|
|
3152
|
+
"button",
|
|
3153
|
+
{
|
|
3154
|
+
type: "button",
|
|
3155
|
+
onClick: () => {
|
|
3156
|
+
if (!inst.receiptUrl && pendingReceipt) {
|
|
3157
|
+
patch({ receiptUrl: pendingReceipt });
|
|
3158
|
+
setPendingReceipt(void 0);
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
setReceiptTarget(i);
|
|
3162
|
+
receiptPicker.open();
|
|
3163
|
+
},
|
|
3164
|
+
title: !inst.receiptUrl && pendingReceipt ? t2("financial.receipt.attachRead") : t2("financial.quickTx.receiptCapture"),
|
|
3165
|
+
"aria-label": !inst.receiptUrl && pendingReceipt ? t2("financial.receipt.attachRead") : t2("financial.quickTx.receiptCapture"),
|
|
3166
|
+
className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${inst.receiptUrl ? "text-success hover:bg-success-soft" : pendingReceipt ? "text-primary hover:bg-primary/10" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
3167
|
+
children: inst.receiptUrl ? /* @__PURE__ */ jsx(Paperclip, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Camera, { className: "h-3.5 w-3.5" })
|
|
3168
|
+
}
|
|
3169
|
+
),
|
|
3170
|
+
saved && !isPaid && /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: () => setPayingMovement(saved), children: [
|
|
3171
|
+
/* @__PURE__ */ jsx(DollarSign, { className: "mr-1 h-3.5 w-3.5" }),
|
|
3172
|
+
t2("financial.invoice.pay")
|
|
3173
|
+
] })
|
|
3174
|
+
] })
|
|
1754
3175
|
] }),
|
|
1755
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
1756
|
-
|
|
1757
|
-
|
|
3176
|
+
!isEdit && inst.receiptUrl && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 border-t bg-muted/20 px-3 py-2", children: [
|
|
3177
|
+
/* @__PURE__ */ jsx(
|
|
3178
|
+
"img",
|
|
3179
|
+
{
|
|
3180
|
+
src: inst.receiptUrl,
|
|
3181
|
+
alt: t2("financial.quickTx.receipt"),
|
|
3182
|
+
className: "h-8 w-8 shrink-0 rounded border object-cover"
|
|
3183
|
+
}
|
|
3184
|
+
),
|
|
3185
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] font-medium text-success", children: t2("financial.invoiceForm.willBePaid") }),
|
|
3186
|
+
/* @__PURE__ */ jsxs(
|
|
3187
|
+
"select",
|
|
3188
|
+
{
|
|
3189
|
+
value: inst.payMethodTypeId ?? "",
|
|
3190
|
+
onChange: (e) => patch({ payMethodTypeId: e.target.value }),
|
|
3191
|
+
className: "min-w-[8rem] flex-1 rounded-input border border-input bg-card px-2 py-1 text-xs shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
|
|
3192
|
+
children: [
|
|
3193
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.invoiceForm.selectMethod") }),
|
|
3194
|
+
paymentMethodTypes.map((tp) => /* @__PURE__ */ jsx("option", { value: tp.id, children: tp.name }, tp.id))
|
|
3195
|
+
]
|
|
3196
|
+
}
|
|
3197
|
+
),
|
|
3198
|
+
/* @__PURE__ */ jsxs(
|
|
3199
|
+
"select",
|
|
1758
3200
|
{
|
|
1759
|
-
value: inst.
|
|
1760
|
-
|
|
1761
|
-
|
|
3201
|
+
value: inst.payAccountId ?? (bankAccounts.length === 1 ? bankAccounts[0].id : ""),
|
|
3202
|
+
onChange: (e) => patch({ payAccountId: e.target.value }),
|
|
3203
|
+
className: "min-w-[8rem] flex-1 rounded-input border border-input bg-card px-2 py-1 text-xs shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
|
|
3204
|
+
children: [
|
|
3205
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.invoiceForm.selectAccount") }),
|
|
3206
|
+
bankAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
3207
|
+
]
|
|
1762
3208
|
}
|
|
1763
|
-
)
|
|
1764
|
-
|
|
3209
|
+
),
|
|
3210
|
+
/* @__PURE__ */ jsx(
|
|
1765
3211
|
"button",
|
|
1766
3212
|
{
|
|
1767
3213
|
type: "button",
|
|
1768
|
-
onClick: () => {
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
setReceiptTarget(i);
|
|
1775
|
-
receiptPicker.open();
|
|
1776
|
-
},
|
|
1777
|
-
title: !inst.receiptUrl && pendingReceipt ? t2("financial.receipt.attachRead") : t2("financial.quickTx.receiptCapture"),
|
|
1778
|
-
"aria-label": !inst.receiptUrl && pendingReceipt ? t2("financial.receipt.attachRead") : t2("financial.quickTx.receiptCapture"),
|
|
1779
|
-
className: `flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors ${inst.receiptUrl ? "text-success hover:bg-success-soft" : pendingReceipt ? "text-primary hover:bg-primary/10" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
|
|
1780
|
-
children: inst.receiptUrl ? /* @__PURE__ */ jsx(Paperclip, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx(Camera, { className: "h-3.5 w-3.5" })
|
|
3214
|
+
onClick: () => patch({ receiptUrl: void 0, payMethodTypeId: void 0, payAccountId: void 0 }),
|
|
3215
|
+
"aria-label": t2("financial.quickTx.receiptRemove"),
|
|
3216
|
+
className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
3217
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
1781
3218
|
}
|
|
1782
|
-
)
|
|
1783
|
-
saved && !isPaid && /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: () => setPayingMovement(saved), children: [
|
|
1784
|
-
/* @__PURE__ */ jsx(DollarSign, { className: "mr-1 h-3.5 w-3.5" }),
|
|
1785
|
-
t2("financial.invoice.pay")
|
|
1786
|
-
] })
|
|
3219
|
+
)
|
|
1787
3220
|
] })
|
|
1788
|
-
] })
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
{
|
|
1793
|
-
src: inst.receiptUrl,
|
|
1794
|
-
alt: t2("financial.quickTx.receipt"),
|
|
1795
|
-
className: "h-8 w-8 shrink-0 rounded border object-cover"
|
|
1796
|
-
}
|
|
1797
|
-
),
|
|
1798
|
-
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] font-medium text-success", children: t2("financial.invoiceForm.willBePaid") }),
|
|
1799
|
-
/* @__PURE__ */ jsxs(
|
|
1800
|
-
"select",
|
|
1801
|
-
{
|
|
1802
|
-
value: inst.payMethodTypeId ?? "",
|
|
1803
|
-
onChange: (e) => patch({ payMethodTypeId: e.target.value }),
|
|
1804
|
-
className: "min-w-[8rem] flex-1 rounded-input border border-input bg-card px-2 py-1 text-xs shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
|
|
1805
|
-
children: [
|
|
1806
|
-
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.invoiceForm.selectMethod") }),
|
|
1807
|
-
paymentMethodTypes.map((tp) => /* @__PURE__ */ jsx("option", { value: tp.id, children: tp.name }, tp.id))
|
|
1808
|
-
]
|
|
1809
|
-
}
|
|
1810
|
-
),
|
|
1811
|
-
/* @__PURE__ */ jsxs(
|
|
1812
|
-
"select",
|
|
1813
|
-
{
|
|
1814
|
-
value: inst.payAccountId ?? (bankAccounts.length === 1 ? bankAccounts[0].id : ""),
|
|
1815
|
-
onChange: (e) => patch({ payAccountId: e.target.value }),
|
|
1816
|
-
className: "min-w-[8rem] flex-1 rounded-input border border-input bg-card px-2 py-1 text-xs shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring",
|
|
1817
|
-
children: [
|
|
1818
|
-
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.invoiceForm.selectAccount") }),
|
|
1819
|
-
bankAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
1820
|
-
]
|
|
1821
|
-
}
|
|
1822
|
-
),
|
|
1823
|
-
/* @__PURE__ */ jsx(
|
|
1824
|
-
"button",
|
|
1825
|
-
{
|
|
1826
|
-
type: "button",
|
|
1827
|
-
onClick: () => patch({ receiptUrl: void 0, payMethodTypeId: void 0, payAccountId: void 0 }),
|
|
1828
|
-
"aria-label": t2("financial.quickTx.receiptRemove"),
|
|
1829
|
-
className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
|
|
1830
|
-
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
1831
|
-
}
|
|
1832
|
-
)
|
|
1833
|
-
] })
|
|
1834
|
-
] }, inst._id);
|
|
1835
|
-
}) })
|
|
1836
|
-
] }) });
|
|
3221
|
+
] }, inst._id);
|
|
3222
|
+
}) })
|
|
3223
|
+
] }) }) })
|
|
3224
|
+
);
|
|
1837
3225
|
const sideSections = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1838
|
-
/* @__PURE__ */ jsxs(OrderFormBlock, { title: direction === "debit" ? t2("financial.invoiceForm.payTo") : t2("financial.invoiceForm.receiveFrom"), children: [
|
|
3226
|
+
/* @__PURE__ */ jsx("div", { ...tourAnchor("financial.contact"), children: /* @__PURE__ */ jsxs(OrderFormBlock, { title: direction === "debit" ? t2("financial.invoiceForm.payTo") : t2("financial.invoiceForm.receiveFrom"), children: [
|
|
1839
3227
|
/* @__PURE__ */ jsx(
|
|
1840
3228
|
SearchSelect,
|
|
1841
3229
|
{
|
|
@@ -1848,8 +3236,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1848
3236
|
setPendingContact(null);
|
|
1849
3237
|
},
|
|
1850
3238
|
onSearch: async (q) => {
|
|
1851
|
-
|
|
1852
|
-
const results = await config.contactLookup.search(q);
|
|
3239
|
+
const results = await contactLookup.search(q);
|
|
1853
3240
|
return results.map((r) => ({ id: r.id, label: r.label, subtitle: r.subtitle, group: r.group, data: r }));
|
|
1854
3241
|
},
|
|
1855
3242
|
createLabel: t2("financial.invoiceForm.createContact"),
|
|
@@ -1878,7 +3265,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1878
3265
|
)
|
|
1879
3266
|
] }),
|
|
1880
3267
|
contactCreate.modal
|
|
1881
|
-
] }),
|
|
3268
|
+
] }) }),
|
|
1882
3269
|
/* @__PURE__ */ jsx(OrderFormBlock, { title: t2("financial.invoice.details"), children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1883
3270
|
hasLocations && /* @__PURE__ */ jsx(LocationSelector, { locations, value: unitId, onChange: setUnitId }),
|
|
1884
3271
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -1911,6 +3298,29 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1911
3298
|
) })
|
|
1912
3299
|
] });
|
|
1913
3300
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3301
|
+
!isEdit && direction === "debit" && /* @__PURE__ */ jsx(
|
|
3302
|
+
PayableDraftSurface,
|
|
3303
|
+
{
|
|
3304
|
+
config,
|
|
3305
|
+
draft: {
|
|
3306
|
+
describe: () => ({
|
|
3307
|
+
supplier: contactName || void 0,
|
|
3308
|
+
supplierResolved: !!contactId,
|
|
3309
|
+
description: items.map((i) => i.description).filter(Boolean).join(", ") || void 0,
|
|
3310
|
+
total: totalAmount,
|
|
3311
|
+
dueDate: installments[0]?.dueDate,
|
|
3312
|
+
itemCount: items.length
|
|
3313
|
+
}),
|
|
3314
|
+
setSupplier: (name, id) => {
|
|
3315
|
+
setContactName(name);
|
|
3316
|
+
setContactId(id);
|
|
3317
|
+
setPendingContact(id ? null : name);
|
|
3318
|
+
},
|
|
3319
|
+
setDueDate: (iso) => setInstallments((prev) => prev.map((inst, i) => i === 0 ? { ...inst, dueDate: iso } : inst)),
|
|
3320
|
+
submit: handleSave
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
),
|
|
1914
3324
|
fromCard && /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-center gap-3 rounded-card border bg-card px-4 py-3", children: [
|
|
1915
3325
|
/* @__PURE__ */ jsx(
|
|
1916
3326
|
"span",
|
|
@@ -1944,6 +3354,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
1944
3354
|
/* @__PURE__ */ jsx(
|
|
1945
3355
|
OrderFormItemsCard,
|
|
1946
3356
|
{
|
|
3357
|
+
"data-tour": "financial.items",
|
|
1947
3358
|
label: t2("financial.invoiceForm.items"),
|
|
1948
3359
|
addLabel: t2("financial.invoiceForm.addItem"),
|
|
1949
3360
|
onAdd: startAddItem,
|
|
@@ -2085,7 +3496,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
2085
3496
|
}
|
|
2086
3497
|
),
|
|
2087
3498
|
confirmCancel && createPortal(
|
|
2088
|
-
/* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick:
|
|
3499
|
+
/* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick: closeCancelDialog, children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-modal border bg-card shadow-lg mx-4 p-5", onClick: (e) => e.stopPropagation(), style: { animation: "field-slide-in 200ms ease-out" }, children: [
|
|
2089
3500
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
2090
3501
|
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 shrink-0", children: /* @__PURE__ */ jsx(Ban, { className: "h-5 w-5 text-destructive" }) }),
|
|
2091
3502
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -2094,9 +3505,21 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
|
|
|
2094
3505
|
] })
|
|
2095
3506
|
] }),
|
|
2096
3507
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-4", children: t2("financial.invoiceForm.cancelDesc") }),
|
|
3508
|
+
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-form-cancel-reason", children: t2("financial.invoice.cancelReason") }),
|
|
3509
|
+
/* @__PURE__ */ jsx(
|
|
3510
|
+
"textarea",
|
|
3511
|
+
{
|
|
3512
|
+
id: "invoice-form-cancel-reason",
|
|
3513
|
+
value: cancelReason,
|
|
3514
|
+
onChange: (event) => setCancelReason(event.target.value),
|
|
3515
|
+
placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
|
|
3516
|
+
className: "mb-4 min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
|
|
3517
|
+
autoFocus: true
|
|
3518
|
+
}
|
|
3519
|
+
),
|
|
2097
3520
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
2098
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick:
|
|
2099
|
-
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancelInvoice, disabled: cancelling, children: [
|
|
3521
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeCancelDialog, children: t2("financial.invoiceForm.keepInvoice") }),
|
|
3522
|
+
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancelInvoice, disabled: cancelling || !cancelReason.trim(), children: [
|
|
2100
3523
|
/* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
|
|
2101
3524
|
cancelling ? t2("financial.invoiceForm.cancelling") : t2("financial.invoiceForm.yesCancel")
|
|
2102
3525
|
] })
|
|
@@ -2180,8 +3603,15 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
2180
3603
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2181
3604
|
const [confirmCancel, setConfirmCancel] = useState(false);
|
|
2182
3605
|
const [cancelling, setCancelling] = useState(false);
|
|
3606
|
+
const [cancelReason, setCancelReason] = useState("");
|
|
3607
|
+
const cancelRequestKey = useRef(createIdempotencyKey("financial:invoice-cancel"));
|
|
2183
3608
|
const menuRef = useRef(null);
|
|
2184
3609
|
const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
|
|
3610
|
+
function closeCancelDialog() {
|
|
3611
|
+
setConfirmCancel(false);
|
|
3612
|
+
setCancelReason("");
|
|
3613
|
+
cancelRequestKey.current = createIdempotencyKey("financial:invoice-cancel");
|
|
3614
|
+
}
|
|
2185
3615
|
async function loadData() {
|
|
2186
3616
|
setLoading(true);
|
|
2187
3617
|
const [inv, invItems, movResult] = await Promise.all([
|
|
@@ -2204,6 +3634,9 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
2204
3634
|
useEffect(() => {
|
|
2205
3635
|
loadData();
|
|
2206
3636
|
}, [invoiceId]);
|
|
3637
|
+
useDataChanged({ table: T.movements }, () => {
|
|
3638
|
+
void loadData();
|
|
3639
|
+
}, [invoiceId, direction]);
|
|
2207
3640
|
useEffect(() => {
|
|
2208
3641
|
if (!menuOpen) return;
|
|
2209
3642
|
function handleClick(e) {
|
|
@@ -2227,9 +3660,13 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
2227
3660
|
async function handleCancel() {
|
|
2228
3661
|
setCancelling(true);
|
|
2229
3662
|
try {
|
|
2230
|
-
await cancelInvoice(
|
|
3663
|
+
await cancelInvoice({
|
|
3664
|
+
invoiceId,
|
|
3665
|
+
reason: cancelReason,
|
|
3666
|
+
idempotencyKey: cancelRequestKey.current
|
|
3667
|
+
});
|
|
2231
3668
|
await loadData();
|
|
2232
|
-
|
|
3669
|
+
closeCancelDialog();
|
|
2233
3670
|
setMenuOpen(false);
|
|
2234
3671
|
} catch {
|
|
2235
3672
|
} finally {
|
|
@@ -2317,6 +3754,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
2317
3754
|
badges: [{ label: t2(statusConfig.labelKey), tone: statusConfig.tone }],
|
|
2318
3755
|
onBack,
|
|
2319
3756
|
backLabel: parentLabel,
|
|
3757
|
+
meta: invoice,
|
|
2320
3758
|
actions,
|
|
2321
3759
|
stats: [
|
|
2322
3760
|
{ id: "total", label: t2("financial.invoice.total"), value: formatCurrency(invoice.totalAmount, currency), icon: DollarSign },
|
|
@@ -2577,7 +4015,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
2577
4015
|
}
|
|
2578
4016
|
),
|
|
2579
4017
|
confirmCancel && createPortal(
|
|
2580
|
-
/* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick:
|
|
4018
|
+
/* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick: closeCancelDialog, children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-modal border bg-card shadow-lg mx-4 p-5", onClick: (e) => e.stopPropagation(), style: { animation: "field-slide-in 200ms ease-out" }, children: [
|
|
2581
4019
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
2582
4020
|
/* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 shrink-0", children: /* @__PURE__ */ jsx(Ban, { className: "h-5 w-5 text-destructive" }) }),
|
|
2583
4021
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -2586,9 +4024,21 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
2586
4024
|
] })
|
|
2587
4025
|
] }),
|
|
2588
4026
|
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-4", children: t2("financial.invoice.cancelDesc") }),
|
|
4027
|
+
/* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-cancel-reason", children: t2("financial.invoice.cancelReason") }),
|
|
4028
|
+
/* @__PURE__ */ jsx(
|
|
4029
|
+
"textarea",
|
|
4030
|
+
{
|
|
4031
|
+
id: "invoice-cancel-reason",
|
|
4032
|
+
value: cancelReason,
|
|
4033
|
+
onChange: (event) => setCancelReason(event.target.value),
|
|
4034
|
+
placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
|
|
4035
|
+
className: "mb-4 min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
|
|
4036
|
+
autoFocus: true
|
|
4037
|
+
}
|
|
4038
|
+
),
|
|
2589
4039
|
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
2590
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick:
|
|
2591
|
-
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancel, disabled: cancelling, children: [
|
|
4040
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeCancelDialog, children: t2("financial.invoice.keepInvoice") }),
|
|
4041
|
+
/* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancel, disabled: cancelling || !cancelReason.trim(), children: [
|
|
2592
4042
|
/* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
|
|
2593
4043
|
cancelling ? t2("financial.invoice.cancelling") : t2("financial.invoice.yesCancel")
|
|
2594
4044
|
] })
|
|
@@ -2948,6 +4398,7 @@ function CashOpeningPanel({
|
|
|
2948
4398
|
/* @__PURE__ */ jsxs(
|
|
2949
4399
|
"button",
|
|
2950
4400
|
{
|
|
4401
|
+
"data-tour": "financial.cash-open",
|
|
2951
4402
|
onClick: () => setConfirming(true),
|
|
2952
4403
|
disabled: effectiveBalance < 0,
|
|
2953
4404
|
className: "inline-flex items-center gap-1.5 rounded-button bg-primary border border-primary px-4 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors disabled:opacity-50",
|
|
@@ -3094,7 +4545,7 @@ function ReviewQueue({ variant }) {
|
|
|
3094
4545
|
const me = user?.id ?? null;
|
|
3095
4546
|
const [busy, setBusy] = useState(null);
|
|
3096
4547
|
const [error, setError] = useState(null);
|
|
3097
|
-
const load =
|
|
4548
|
+
const load = React15__default.useCallback(async () => {
|
|
3098
4549
|
try {
|
|
3099
4550
|
setError(null);
|
|
3100
4551
|
setRows(await provider.getCashSessionReviews?.() ?? []);
|
|
@@ -3252,6 +4703,7 @@ function CashRegistersView({ variant = "page" } = {}) {
|
|
|
3252
4703
|
expandedClosing === session.id ? /* @__PURE__ */ jsx(CashClosingPanel, { session, currency, onClose: handleClose }) : /* @__PURE__ */ jsxs(
|
|
3253
4704
|
"button",
|
|
3254
4705
|
{
|
|
4706
|
+
"data-tour": "financial.cash-close",
|
|
3255
4707
|
onClick: () => setExpandedClosing(session.id),
|
|
3256
4708
|
className: "inline-flex items-center gap-1.5 text-xs text-destructive hover:text-destructive font-medium transition-colors",
|
|
3257
4709
|
children: [
|
|
@@ -3706,7 +5158,7 @@ function StatementsView({ onNavigate }) {
|
|
|
3706
5158
|
about: "A movimenta\xE7\xE3o de uma conta por vez: cr\xE9ditos, d\xE9bitos e transfer\xEAncias no per\xEDodo. \xC9 tamb\xE9m de onde sai a transfer\xEAncia entre duas contas."
|
|
3707
5159
|
}
|
|
3708
5160
|
),
|
|
3709
|
-
/* @__PURE__ */ jsxs(Button, {
|
|
5161
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => setShowTransfer(true), disabled: bankAccounts.length < 2, children: [
|
|
3710
5162
|
/* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5" }),
|
|
3711
5163
|
t2("financial.statements.newTransfer")
|
|
3712
5164
|
] })
|
|
@@ -3797,7 +5249,7 @@ function firstOfMonth() {
|
|
|
3797
5249
|
d.setDate(1);
|
|
3798
5250
|
return d.toISOString().slice(0, 10);
|
|
3799
5251
|
}
|
|
3800
|
-
function
|
|
5252
|
+
function today2() {
|
|
3801
5253
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3802
5254
|
}
|
|
3803
5255
|
function money(v) {
|
|
@@ -3817,14 +5269,14 @@ function CommissionsView() {
|
|
|
3817
5269
|
const fetchCommissions = useFinancialStore((s) => s.fetchCommissions);
|
|
3818
5270
|
const generateCommissions = useFinancialStore((s) => s.generateCommissions);
|
|
3819
5271
|
const provider = useFinancialProvider();
|
|
3820
|
-
const [from, setFrom] =
|
|
3821
|
-
const [to, setTo] =
|
|
3822
|
-
const [running, setRunning] =
|
|
3823
|
-
const [lastRun, setLastRun] =
|
|
3824
|
-
const [generation, setGeneration] =
|
|
3825
|
-
const [busy, setBusy] =
|
|
3826
|
-
const [error, setError] =
|
|
3827
|
-
const loadGeneration =
|
|
5272
|
+
const [from, setFrom] = React15__default.useState(firstOfMonth());
|
|
5273
|
+
const [to, setTo] = React15__default.useState(today2());
|
|
5274
|
+
const [running, setRunning] = React15__default.useState(false);
|
|
5275
|
+
const [lastRun, setLastRun] = React15__default.useState(null);
|
|
5276
|
+
const [generation, setGeneration] = React15__default.useState(null);
|
|
5277
|
+
const [busy, setBusy] = React15__default.useState(null);
|
|
5278
|
+
const [error, setError] = React15__default.useState(null);
|
|
5279
|
+
const loadGeneration = React15__default.useCallback(async () => {
|
|
3828
5280
|
try {
|
|
3829
5281
|
const gens = await provider.getCommissionGenerations?.() ?? [];
|
|
3830
5282
|
const doPeriodo = gens.filter((g) => g.periodStart === from && g.periodEnd === to);
|
|
@@ -3832,11 +5284,11 @@ function CommissionsView() {
|
|
|
3832
5284
|
} catch {
|
|
3833
5285
|
}
|
|
3834
5286
|
}, [provider, from, to]);
|
|
3835
|
-
|
|
5287
|
+
React15__default.useEffect(() => {
|
|
3836
5288
|
void fetchCommissions({ periodStart: from, periodEnd: to });
|
|
3837
5289
|
void loadGeneration();
|
|
3838
5290
|
}, [fetchCommissions, loadGeneration, from, to]);
|
|
3839
|
-
const byPerson =
|
|
5291
|
+
const byPerson = React15__default.useMemo(() => {
|
|
3840
5292
|
const map = /* @__PURE__ */ new Map();
|
|
3841
5293
|
for (const c of commissions2) {
|
|
3842
5294
|
const key = c.personId;
|
|
@@ -4365,13 +5817,13 @@ function money2(v) {
|
|
|
4365
5817
|
function GoalsView() {
|
|
4366
5818
|
const { labels } = useFinancialConfig();
|
|
4367
5819
|
const provider = useFinancialProvider();
|
|
4368
|
-
const [units, setUnits] =
|
|
4369
|
-
const [unitId, setUnitId] =
|
|
4370
|
-
const [year, setYear] =
|
|
4371
|
-
const [rows, setRows] =
|
|
4372
|
-
const [editing, setEditing] =
|
|
4373
|
-
const [error, setError] =
|
|
4374
|
-
|
|
5820
|
+
const [units, setUnits] = React15__default.useState([]);
|
|
5821
|
+
const [unitId, setUnitId] = React15__default.useState("");
|
|
5822
|
+
const [year, setYear] = React15__default.useState((/* @__PURE__ */ new Date()).getFullYear());
|
|
5823
|
+
const [rows, setRows] = React15__default.useState(null);
|
|
5824
|
+
const [editing, setEditing] = React15__default.useState(null);
|
|
5825
|
+
const [error, setError] = React15__default.useState(null);
|
|
5826
|
+
React15__default.useEffect(() => {
|
|
4375
5827
|
void (async () => {
|
|
4376
5828
|
const cat = await provider.getCommissionCatalogs?.();
|
|
4377
5829
|
const us = cat?.units ?? [];
|
|
@@ -4379,7 +5831,7 @@ function GoalsView() {
|
|
|
4379
5831
|
setUnitId((cur) => cur || us[0]?.id || "");
|
|
4380
5832
|
})();
|
|
4381
5833
|
}, [provider]);
|
|
4382
|
-
const load =
|
|
5834
|
+
const load = React15__default.useCallback(async () => {
|
|
4383
5835
|
if (!unitId) return;
|
|
4384
5836
|
setRows(null);
|
|
4385
5837
|
try {
|
|
@@ -4390,7 +5842,7 @@ function GoalsView() {
|
|
|
4390
5842
|
setRows([]);
|
|
4391
5843
|
}
|
|
4392
5844
|
}, [provider, unitId, year]);
|
|
4393
|
-
|
|
5845
|
+
React15__default.useEffect(() => {
|
|
4394
5846
|
void load();
|
|
4395
5847
|
}, [load]);
|
|
4396
5848
|
const totalRealizado = (rows ?? []).reduce((s, r) => s + r.achievedAmount, 0);
|
|
@@ -4531,11 +5983,11 @@ function GoalDialog({
|
|
|
4531
5983
|
onSaved
|
|
4532
5984
|
}) {
|
|
4533
5985
|
const provider = useFinancialProvider();
|
|
4534
|
-
const [target, setTarget] =
|
|
4535
|
-
const [sup, setSup] =
|
|
4536
|
-
const [hyper, setHyper] =
|
|
4537
|
-
const [saving, setSaving] =
|
|
4538
|
-
const [error, setError] =
|
|
5986
|
+
const [target, setTarget] = React15__default.useState(String(goal.targetAmount || ""));
|
|
5987
|
+
const [sup, setSup] = React15__default.useState(String(goal.superAmount || ""));
|
|
5988
|
+
const [hyper, setHyper] = React15__default.useState(String(goal.hyperAmount || ""));
|
|
5989
|
+
const [saving, setSaving] = React15__default.useState(false);
|
|
5990
|
+
const [error, setError] = React15__default.useState(null);
|
|
4539
5991
|
const t2 = Number(target) || 0;
|
|
4540
5992
|
const s = Number(sup) || 0;
|
|
4541
5993
|
const h = Number(hyper) || 0;
|
|
@@ -4625,21 +6077,21 @@ function Dash() {
|
|
|
4625
6077
|
function GiftCardsView() {
|
|
4626
6078
|
const provider = useFinancialProvider();
|
|
4627
6079
|
const { currency } = useFinancialConfig();
|
|
4628
|
-
const [enabled, setEnabled] =
|
|
4629
|
-
const [cards, setCards] =
|
|
4630
|
-
const [error, setError] =
|
|
4631
|
-
const [issuing, setIssuing] =
|
|
4632
|
-
const [issued, setIssued] =
|
|
4633
|
-
const [activating, setActivating] =
|
|
4634
|
-
const [search, setSearch] =
|
|
4635
|
-
const [state, setState] =
|
|
4636
|
-
const [minValue, setMinValue] =
|
|
4637
|
-
const [maxValue, setMaxValue] =
|
|
4638
|
-
const [from, setFrom] =
|
|
4639
|
-
const [to, setTo] =
|
|
4640
|
-
const money3 =
|
|
4641
|
-
const seq =
|
|
4642
|
-
const load =
|
|
6080
|
+
const [enabled, setEnabled] = React15__default.useState(null);
|
|
6081
|
+
const [cards, setCards] = React15__default.useState(null);
|
|
6082
|
+
const [error, setError] = React15__default.useState(null);
|
|
6083
|
+
const [issuing, setIssuing] = React15__default.useState(false);
|
|
6084
|
+
const [issued, setIssued] = React15__default.useState(null);
|
|
6085
|
+
const [activating, setActivating] = React15__default.useState(null);
|
|
6086
|
+
const [search, setSearch] = React15__default.useState("");
|
|
6087
|
+
const [state, setState] = React15__default.useState("");
|
|
6088
|
+
const [minValue, setMinValue] = React15__default.useState("");
|
|
6089
|
+
const [maxValue, setMaxValue] = React15__default.useState("");
|
|
6090
|
+
const [from, setFrom] = React15__default.useState("");
|
|
6091
|
+
const [to, setTo] = React15__default.useState("");
|
|
6092
|
+
const money3 = React15__default.useCallback((v) => formatCurrency(v, currency), [currency]);
|
|
6093
|
+
const seq = React15__default.useRef(0);
|
|
6094
|
+
const load = React15__default.useCallback(async () => {
|
|
4643
6095
|
const mine = ++seq.current;
|
|
4644
6096
|
setCards(null);
|
|
4645
6097
|
setError(null);
|
|
@@ -4660,7 +6112,7 @@ function GiftCardsView() {
|
|
|
4660
6112
|
setCards([]);
|
|
4661
6113
|
}
|
|
4662
6114
|
}, [provider, search, state, minValue, maxValue, from, to]);
|
|
4663
|
-
|
|
6115
|
+
React15__default.useEffect(() => {
|
|
4664
6116
|
void (async () => {
|
|
4665
6117
|
try {
|
|
4666
6118
|
setEnabled(await provider.giftCardsEnabled?.() === true);
|
|
@@ -4669,7 +6121,7 @@ function GiftCardsView() {
|
|
|
4669
6121
|
}
|
|
4670
6122
|
})();
|
|
4671
6123
|
}, [provider]);
|
|
4672
|
-
|
|
6124
|
+
React15__default.useEffect(() => {
|
|
4673
6125
|
if (enabled) void load();
|
|
4674
6126
|
}, [enabled, load]);
|
|
4675
6127
|
const outstanding = (cards ?? []).filter((c) => c.state === "active").reduce((s, c) => s + (c.balance ?? 0), 0);
|
|
@@ -4884,12 +6336,12 @@ function IssueDialog({
|
|
|
4884
6336
|
onIssued
|
|
4885
6337
|
}) {
|
|
4886
6338
|
const provider = useFinancialProvider();
|
|
4887
|
-
const [quantity, setQuantity] =
|
|
4888
|
-
const [amount, setAmount] =
|
|
4889
|
-
const [expiresAt, setExpiresAt] =
|
|
4890
|
-
const [note, setNote] =
|
|
4891
|
-
const [saving, setSaving] =
|
|
4892
|
-
const [error, setError] =
|
|
6339
|
+
const [quantity, setQuantity] = React15__default.useState("10");
|
|
6340
|
+
const [amount, setAmount] = React15__default.useState("100");
|
|
6341
|
+
const [expiresAt, setExpiresAt] = React15__default.useState("");
|
|
6342
|
+
const [note, setNote] = React15__default.useState("");
|
|
6343
|
+
const [saving, setSaving] = React15__default.useState(false);
|
|
6344
|
+
const [error, setError] = React15__default.useState(null);
|
|
4893
6345
|
const q = Math.trunc(Number(quantity)) || 0;
|
|
4894
6346
|
const a = Number(amount) || 0;
|
|
4895
6347
|
const total = q * a;
|
|
@@ -5058,25 +6510,19 @@ function ReconciliationView() {
|
|
|
5058
6510
|
}
|
|
5059
6511
|
const selected = lines.find((l) => l.id === selectedId) ?? null;
|
|
5060
6512
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
5061
|
-
/* @__PURE__ */ jsxs(
|
|
5062
|
-
/* @__PURE__ */
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
),
|
|
5075
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: lines.length > 0 ? t2("financial.reconciliation.pending", { count: lines.length }) : t2("financial.reconciliation.allDone") }),
|
|
5076
|
-
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading || busy, children: [
|
|
5077
|
-
/* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }),
|
|
5078
|
-
t2("financial.reconciliation.refresh")
|
|
5079
|
-
] })
|
|
6513
|
+
/* @__PURE__ */ jsxs(PageHeaderActions, { children: [
|
|
6514
|
+
/* @__PURE__ */ jsx(
|
|
6515
|
+
TeachMeLink,
|
|
6516
|
+
{
|
|
6517
|
+
feature: "financial.conciliacao",
|
|
6518
|
+
title: "Concilia\xE7\xE3o Banc\xE1ria",
|
|
6519
|
+
about: "O extrato que vem do BANCO, n\xE3o do sistema, e a compara\xE7\xE3o entre os dois. Pendente aqui n\xE3o \xE9 conta a pagar: \xE9 transa\xE7\xE3o que o banco tem e ainda n\xE3o foi casada com um lan\xE7amento."
|
|
6520
|
+
}
|
|
6521
|
+
),
|
|
6522
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: lines.length > 0 ? t2("financial.reconciliation.pending", { count: lines.length }) : t2("financial.reconciliation.allDone") }),
|
|
6523
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", onClick: () => void load(), disabled: loading || busy, children: [
|
|
6524
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }),
|
|
6525
|
+
t2("financial.reconciliation.refresh")
|
|
5080
6526
|
] })
|
|
5081
6527
|
] }),
|
|
5082
6528
|
lines.length === 0 && !loading ? /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card shadow-sm py-16 text-center", children: [
|
|
@@ -5241,7 +6687,7 @@ function NotasFiscaisView() {
|
|
|
5241
6687
|
};
|
|
5242
6688
|
if (!supported) {
|
|
5243
6689
|
return /* @__PURE__ */ jsxs("div", { className: "p-1", children: [
|
|
5244
|
-
/* @__PURE__ */ jsx(SubpageHeader, { title: "Notas Fiscais", subtitle: "Emiss\xE3o e acompanhamento de NF-e / NFC-e / NFS-e"
|
|
6690
|
+
/* @__PURE__ */ jsx(SubpageHeader, { title: "Notas Fiscais", subtitle: "Emiss\xE3o e acompanhamento de NF-e / NFC-e / NFS-e" }),
|
|
5245
6691
|
/* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-muted-foreground", children: "Este provedor financeiro ainda n\xE3o exp\xF5e notas fiscais." })
|
|
5246
6692
|
] });
|
|
5247
6693
|
}
|
|
@@ -5251,7 +6697,6 @@ function NotasFiscaisView() {
|
|
|
5251
6697
|
{
|
|
5252
6698
|
title: "Notas Fiscais",
|
|
5253
6699
|
subtitle: "Emiss\xE3o e acompanhamento de NF-e / NFC-e / NFS-e",
|
|
5254
|
-
icon: "FileText",
|
|
5255
6700
|
actions: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
5256
6701
|
/* @__PURE__ */ jsx(
|
|
5257
6702
|
TeachMeLink,
|
|
@@ -5262,8 +6707,8 @@ function NotasFiscaisView() {
|
|
|
5262
6707
|
}
|
|
5263
6708
|
),
|
|
5264
6709
|
canCreateManually && /* @__PURE__ */ jsx(PermissionGate, { feature: "financial", action: "create", children: /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => setCreating(true), children: [
|
|
5265
|
-
/* @__PURE__ */ jsx(Plus, { className: "
|
|
5266
|
-
"
|
|
6710
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
|
|
6711
|
+
"Nova nota"
|
|
5267
6712
|
] }) })
|
|
5268
6713
|
] })
|
|
5269
6714
|
}
|
|
@@ -5841,10 +7286,10 @@ function RecentTransactions() {
|
|
|
5841
7286
|
function useDueLabel() {
|
|
5842
7287
|
const t2 = useTranslation();
|
|
5843
7288
|
return (dueDate) => {
|
|
5844
|
-
const
|
|
5845
|
-
|
|
7289
|
+
const today4 = /* @__PURE__ */ new Date();
|
|
7290
|
+
today4.setHours(0, 0, 0, 0);
|
|
5846
7291
|
const due = /* @__PURE__ */ new Date(`${dueDate}T00:00:00`);
|
|
5847
|
-
const days = Math.round((due.getTime() -
|
|
7292
|
+
const days = Math.round((due.getTime() - today4.getTime()) / 864e5);
|
|
5848
7293
|
if (days < 0) return t2("financial.home.overdueDays", { count: String(Math.abs(days)) });
|
|
5849
7294
|
if (days === 0) return t2("financial.home.dueToday");
|
|
5850
7295
|
if (days === 1) return t2("financial.home.dueTomorrow");
|
|
@@ -5857,7 +7302,7 @@ function BalanceHero() {
|
|
|
5857
7302
|
const summary = useFinancialStore((s) => s.summary);
|
|
5858
7303
|
const navigate = useDashboardNavigate();
|
|
5859
7304
|
useEnsureSummary();
|
|
5860
|
-
const [hidden, setHidden] =
|
|
7305
|
+
const [hidden, setHidden] = React15__default.useState(false);
|
|
5861
7306
|
const money3 = (v) => hidden ? "\u2022\u2022\u2022\u2022\u2022\u2022" : formatCurrency(v ?? 0, currency);
|
|
5862
7307
|
const inflow = summary?.monthlyInflow ?? 0;
|
|
5863
7308
|
const outflow = summary?.monthlyOutflow ?? 0;
|
|
@@ -5930,7 +7375,7 @@ function categorizeSpend(notes) {
|
|
|
5930
7375
|
function SpendByCategoryChart() {
|
|
5931
7376
|
const t2 = useTranslation();
|
|
5932
7377
|
const provider = useFinancialProvider();
|
|
5933
|
-
const [rows, setRows] =
|
|
7378
|
+
const [rows, setRows] = React15__default.useState([]);
|
|
5934
7379
|
useEffect(() => {
|
|
5935
7380
|
let alive = true;
|
|
5936
7381
|
void (async () => {
|
|
@@ -5973,7 +7418,7 @@ function UpcomingBills() {
|
|
|
5973
7418
|
const { currency } = useFinancialConfig();
|
|
5974
7419
|
const provider = useFinancialProvider();
|
|
5975
7420
|
const dueLabel = useDueLabel();
|
|
5976
|
-
const [bills, setBills] =
|
|
7421
|
+
const [bills, setBills] = React15__default.useState([]);
|
|
5977
7422
|
useEffect(() => {
|
|
5978
7423
|
let alive = true;
|
|
5979
7424
|
void (async () => {
|
|
@@ -6074,10 +7519,10 @@ function uid() {
|
|
|
6074
7519
|
function now() {
|
|
6075
7520
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
6076
7521
|
}
|
|
6077
|
-
function
|
|
7522
|
+
function today3() {
|
|
6078
7523
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6079
7524
|
}
|
|
6080
|
-
function
|
|
7525
|
+
function round22(n) {
|
|
6081
7526
|
return Math.round(n * 100) / 100;
|
|
6082
7527
|
}
|
|
6083
7528
|
function matchesDateRange(dateStr, range) {
|
|
@@ -6165,7 +7610,7 @@ function createMockFinancialProvider(options) {
|
|
|
6165
7610
|
invoice.status = "partial";
|
|
6166
7611
|
} else {
|
|
6167
7612
|
const movements = store6.movements.filter((m) => m.invoiceId === invoice.id && m.movementKind === "bill");
|
|
6168
|
-
const hasOverdue = movements.some((m) => m.status === "overdue" || m.status === "pending" && m.dueDate <
|
|
7613
|
+
const hasOverdue = movements.some((m) => m.status === "overdue" || m.status === "pending" && m.dueDate < today3());
|
|
6169
7614
|
invoice.status = hasOverdue ? "overdue" : "open";
|
|
6170
7615
|
}
|
|
6171
7616
|
invoice.updatedAt = now();
|
|
@@ -6278,12 +7723,13 @@ function createMockFinancialProvider(options) {
|
|
|
6278
7723
|
Object.assign(invoice, data, { updatedAt: now() });
|
|
6279
7724
|
return invoice;
|
|
6280
7725
|
},
|
|
6281
|
-
async cancelInvoice(
|
|
6282
|
-
|
|
6283
|
-
|
|
7726
|
+
async cancelInvoice(input) {
|
|
7727
|
+
if (!input.reason.trim()) throw new Error("A cancellation reason is required");
|
|
7728
|
+
const invoice = store6.invoices.find((i) => i.id === input.invoiceId);
|
|
7729
|
+
if (!invoice) throw new Error(`Invoice ${input.invoiceId} not found`);
|
|
6284
7730
|
invoice.status = "cancelled";
|
|
6285
7731
|
invoice.updatedAt = now();
|
|
6286
|
-
store6.movements.filter((m) => m.invoiceId ===
|
|
7732
|
+
store6.movements.filter((m) => m.invoiceId === input.invoiceId && m.status === "pending").forEach((m) => {
|
|
6287
7733
|
m.status = "cancelled";
|
|
6288
7734
|
m.updatedAt = now();
|
|
6289
7735
|
});
|
|
@@ -6319,7 +7765,7 @@ function createMockFinancialProvider(options) {
|
|
|
6319
7765
|
if (!bill) throw new Error(`Movement ${input.movementId} not found`);
|
|
6320
7766
|
const method = input.paymentMethodId ? store6.paymentMethods.find((pm) => pm.id === input.paymentMethodId) : void 0;
|
|
6321
7767
|
const feePct = bill.direction === "credit" ? method?.discountValue ?? 0 : 0;
|
|
6322
|
-
const thisFee =
|
|
7768
|
+
const thisFee = round22(input.amount * (feePct / 100));
|
|
6323
7769
|
const payment = {
|
|
6324
7770
|
id: uid(),
|
|
6325
7771
|
invoiceId: bill.invoiceId,
|
|
@@ -6346,7 +7792,7 @@ function createMockFinancialProvider(options) {
|
|
|
6346
7792
|
updatedAt: now()
|
|
6347
7793
|
};
|
|
6348
7794
|
store6.movements.push(payment);
|
|
6349
|
-
bill.paidAmount =
|
|
7795
|
+
bill.paidAmount = round22(bill.paidAmount + input.amount);
|
|
6350
7796
|
bill.status = bill.paidAmount >= bill.amount ? "paid" : "partial";
|
|
6351
7797
|
bill.paymentDate = void 0;
|
|
6352
7798
|
bill.bankAccountId = void 0;
|
|
@@ -6355,9 +7801,9 @@ function createMockFinancialProvider(options) {
|
|
|
6355
7801
|
const account = store6.bankAccounts.find((a) => a.id === input.bankAccountId);
|
|
6356
7802
|
if (account) {
|
|
6357
7803
|
if (bill.direction === "credit") {
|
|
6358
|
-
account.currentBalance =
|
|
7804
|
+
account.currentBalance = round22(account.currentBalance + (input.amount - thisFee));
|
|
6359
7805
|
} else {
|
|
6360
|
-
account.currentBalance =
|
|
7806
|
+
account.currentBalance = round22(account.currentBalance - input.amount);
|
|
6361
7807
|
}
|
|
6362
7808
|
account.updatedAt = now();
|
|
6363
7809
|
}
|
|
@@ -6417,11 +7863,11 @@ function createMockFinancialProvider(options) {
|
|
|
6417
7863
|
const from = store6.bankAccounts.find((a) => a.id === input.fromAccountId);
|
|
6418
7864
|
const to = store6.bankAccounts.find((a) => a.id === input.toAccountId);
|
|
6419
7865
|
if (from) {
|
|
6420
|
-
from.currentBalance =
|
|
7866
|
+
from.currentBalance = round22(from.currentBalance - input.amount);
|
|
6421
7867
|
from.updatedAt = now();
|
|
6422
7868
|
}
|
|
6423
7869
|
if (to) {
|
|
6424
|
-
to.currentBalance =
|
|
7870
|
+
to.currentBalance = round22(to.currentBalance + input.amount);
|
|
6425
7871
|
to.updatedAt = now();
|
|
6426
7872
|
}
|
|
6427
7873
|
return { transferId, debitMovementId, creditMovementId };
|
|
@@ -6535,7 +7981,7 @@ function createMockFinancialProvider(options) {
|
|
|
6535
7981
|
},
|
|
6536
7982
|
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
6537
7983
|
async getStatement(query) {
|
|
6538
|
-
const todayStr =
|
|
7984
|
+
const todayStr = today3();
|
|
6539
7985
|
const isRealized = (m) => m.status !== "cancelled" && !!m.paymentDate && affectsCash(m);
|
|
6540
7986
|
const consolidated = !query.bankAccountId;
|
|
6541
7987
|
const scopeAccounts = consolidated ? store6.bankAccounts.filter((a) => a.isActive) : store6.bankAccounts.filter((a) => a.id === query.bankAccountId);
|
|
@@ -6547,7 +7993,7 @@ function createMockFinancialProvider(options) {
|
|
|
6547
7993
|
}
|
|
6548
7994
|
const netOf = (m) => m.direction === "credit" ? m.paidAmount - (m.feeAmount ?? 0) : -m.paidAmount;
|
|
6549
7995
|
const inScope = store6.movements.filter((m) => isRealized(m) && (consolidated || m.bankAccountId && accountIds.has(m.bankAccountId)));
|
|
6550
|
-
const openingBalance =
|
|
7996
|
+
const openingBalance = round22(initialSum + inScope.filter((m) => (m.paymentDate ?? "") < query.dateRange.from).reduce((s, m) => s + netOf(m), 0));
|
|
6551
7997
|
const rangeMovs = inScope.filter((m) => matchesDateRange(m.paymentDate, query.dateRange)).sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? "") || a.createdAt.localeCompare(b.createdAt));
|
|
6552
7998
|
let balance = openingBalance;
|
|
6553
7999
|
let totalCredits = 0, totalDebits = 0, totalFees = 0;
|
|
@@ -6555,16 +8001,16 @@ function createMockFinancialProvider(options) {
|
|
|
6555
8001
|
const gross = movement.paidAmount;
|
|
6556
8002
|
const isCredit = movement.direction === "credit";
|
|
6557
8003
|
const fee = isCredit ? movement.feeAmount ?? 0 : 0;
|
|
6558
|
-
const net = isCredit ?
|
|
8004
|
+
const net = isCredit ? round22(gross - fee) : gross;
|
|
6559
8005
|
const isTransfer = movement.movementKind === "transfer";
|
|
6560
8006
|
const meta = movement.metadata ?? {};
|
|
6561
8007
|
const entryKind = isTransfer ? meta.transferRole === "in" ? "transfer-in" : "transfer-out" : "movement";
|
|
6562
8008
|
const counterAccountId = isTransfer ? meta.counterAccountId : void 0;
|
|
6563
|
-
balance =
|
|
8009
|
+
balance = round22(isCredit ? balance + net : balance - net);
|
|
6564
8010
|
if (!(consolidated && isTransfer)) {
|
|
6565
|
-
if (isCredit) totalCredits =
|
|
6566
|
-
else totalDebits =
|
|
6567
|
-
totalFees =
|
|
8011
|
+
if (isCredit) totalCredits = round22(totalCredits + net);
|
|
8012
|
+
else totalDebits = round22(totalDebits + net);
|
|
8013
|
+
totalFees = round22(totalFees + fee);
|
|
6568
8014
|
}
|
|
6569
8015
|
return {
|
|
6570
8016
|
movement,
|
|
@@ -6591,7 +8037,7 @@ function createMockFinancialProvider(options) {
|
|
|
6591
8037
|
contactName: inv?.contactName
|
|
6592
8038
|
};
|
|
6593
8039
|
}
|
|
6594
|
-
const accountCurrentBalance =
|
|
8040
|
+
const accountCurrentBalance = round22(initialSum + inScope.filter((m) => (m.paymentDate ?? "") <= todayStr).reduce((s, m) => s + netOf(m), 0));
|
|
6595
8041
|
return {
|
|
6596
8042
|
entries,
|
|
6597
8043
|
openingBalance,
|
|
@@ -6599,7 +8045,7 @@ function createMockFinancialProvider(options) {
|
|
|
6599
8045
|
totalCredits,
|
|
6600
8046
|
totalDebits,
|
|
6601
8047
|
totalFees,
|
|
6602
|
-
net:
|
|
8048
|
+
net: round22(totalCredits - totalDebits),
|
|
6603
8049
|
accountId: query.bankAccountId,
|
|
6604
8050
|
accountCurrentBalance
|
|
6605
8051
|
};
|
|
@@ -6613,7 +8059,7 @@ function createMockFinancialProvider(options) {
|
|
|
6613
8059
|
const pendingPayable = store6.movements.filter(
|
|
6614
8060
|
(m) => m.direction === "debit" && m.movementKind === "bill" && ["pending", "partial"].includes(m.status)
|
|
6615
8061
|
);
|
|
6616
|
-
const todayStr =
|
|
8062
|
+
const todayStr = today3();
|
|
6617
8063
|
const monthAgo = /* @__PURE__ */ new Date();
|
|
6618
8064
|
monthAgo.setDate(monthAgo.getDate() - 30);
|
|
6619
8065
|
const monthStart = monthAgo.toISOString().slice(0, 10);
|
|
@@ -6675,7 +8121,7 @@ function createMockFinancialProvider(options) {
|
|
|
6675
8121
|
const line = store6.movements.find((m) => m.id === bankMovementId);
|
|
6676
8122
|
if (!line) return [];
|
|
6677
8123
|
const amount = line.amount;
|
|
6678
|
-
const tol = Math.max(0.01,
|
|
8124
|
+
const tol = Math.max(0.01, round22(amount * 0.02));
|
|
6679
8125
|
const lineDate = line.paymentDate ?? line.dueDate;
|
|
6680
8126
|
return store6.movements.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction && m.id !== bankMovementId && Math.abs(m.amount - amount) <= tol).map((m) => {
|
|
6681
8127
|
const amtScore = amount > 0 ? Math.max(0, 1 - Math.abs(m.amount - amount) / amount) : 0;
|
|
@@ -6685,7 +8131,7 @@ function createMockFinancialProvider(options) {
|
|
|
6685
8131
|
return {
|
|
6686
8132
|
movement: m,
|
|
6687
8133
|
invoice: m.invoiceId ? store6.invoices.find((i) => i.id === m.invoiceId) : void 0,
|
|
6688
|
-
score:
|
|
8134
|
+
score: round22(0.7 * amtScore + 0.3 * dateScore)
|
|
6689
8135
|
};
|
|
6690
8136
|
}).sort((a, b) => b.score - a.score).slice(0, 25);
|
|
6691
8137
|
},
|
|
@@ -6789,46 +8235,6 @@ function createMockFinancialProvider(options) {
|
|
|
6789
8235
|
return provider;
|
|
6790
8236
|
}
|
|
6791
8237
|
|
|
6792
|
-
// src/data/tables.ts
|
|
6793
|
-
var T = {
|
|
6794
|
-
movements: "plg_financial_movements",
|
|
6795
|
-
cashMovements: "v_financial_cash_movements",
|
|
6796
|
-
accountBalances: "v_financial_account_balances",
|
|
6797
|
-
chartOfAccounts: "plg_financial_chart_of_accounts",
|
|
6798
|
-
bankAccounts: "plg_financial_bank_accounts",
|
|
6799
|
-
cardBrands: "plg_financial_card_brands",
|
|
6800
|
-
costCenters: "plg_financial_cost_centers",
|
|
6801
|
-
cashRegisterSessions: "plg_financial_cash_register_sessions",
|
|
6802
|
-
paymentMethods: "plg_financial_payment_methods",
|
|
6803
|
-
paymentMethodTypes: "plg_financial_payment_method_types",
|
|
6804
|
-
notas: "plg_financial_nfe",
|
|
6805
|
-
// A contabilidade da LINHA. public.items é a linha; o que só o financeiro
|
|
6806
|
-
// sabe dela — plano de contas, centro de custo, imposto, CFOP — mora aqui,
|
|
6807
|
-
// com item_id de chave. Ver 038_the_invoice_gets_a_line.sql.
|
|
6808
|
-
invoiceItems: "plg_financial_invoice_items",
|
|
6809
|
-
// A regra de comissão. O registry apontava para `commission_rules`, sem
|
|
6810
|
-
// prefixo — nome que a 028 aposentou e que não existe em pool nenhum.
|
|
6811
|
-
commissionRules: "plg_financial_commission_rules",
|
|
6812
|
-
commissionRuleCriteria: "plg_financial_commission_rule_criteria",
|
|
6813
|
-
commissionFunctions: "plg_financial_commission_functions",
|
|
6814
|
-
// The legacy `movement_kind='bill'` SHAPE, derived from the instalment
|
|
6815
|
-
// balances (financial 040). Reading it instead of the mirror is what let the
|
|
6816
|
-
// mirror stop being written: the columns are the same, the source is one.
|
|
6817
|
-
// The instalment IS the obligation; the `movement_kind='bill'` shape and
|
|
6818
|
-
// its compat view left in financial 044.
|
|
6819
|
-
bills: "v_financial_installment_balances",
|
|
6820
|
-
commissions: "v_financial_commissions",
|
|
6821
|
-
commissionGenerations: "plg_financial_commission_generations",
|
|
6822
|
-
periods: "plg_financial_periods",
|
|
6823
|
-
trialBalance: "v_financial_trial_balance",
|
|
6824
|
-
aging: "v_financial_aging",
|
|
6825
|
-
agingSummary: "v_financial_aging_summary",
|
|
6826
|
-
lineMargin: "v_financial_line_margin",
|
|
6827
|
-
marginByMonth: "v_financial_margin_by_month",
|
|
6828
|
-
recurringBills: "plg_financial_recurring_bills",
|
|
6829
|
-
discountsGiven: "v_financial_discounts_given"
|
|
6830
|
-
};
|
|
6831
|
-
|
|
6832
8238
|
// src/data/supabase.ts
|
|
6833
8239
|
function hasCriteriaValue(v) {
|
|
6834
8240
|
if (!v) return false;
|
|
@@ -6863,8 +8269,8 @@ async function sweepOverdue() {
|
|
|
6863
8269
|
_overdueSwept = true;
|
|
6864
8270
|
try {
|
|
6865
8271
|
const { core, pub } = getClients();
|
|
6866
|
-
const
|
|
6867
|
-
const { data: overdueMovs } = await pub.from(T.movements).update({ status: "overdue" }).in("status", ["pending", "partial"]).lt("due_date",
|
|
8272
|
+
const today4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8273
|
+
const { data: overdueMovs } = await pub.from(T.movements).update({ status: "overdue" }).in("status", ["pending", "partial"]).lt("due_date", today4).select("invoice_id");
|
|
6868
8274
|
if (overdueMovs && overdueMovs.length > 0) {
|
|
6869
8275
|
const invoiceIds = [...new Set(overdueMovs.map((m) => m.invoice_id).filter(Boolean))];
|
|
6870
8276
|
for (const invId of invoiceIds) {
|
|
@@ -6880,7 +8286,7 @@ async function sweepOverdue() {
|
|
|
6880
8286
|
} catch {
|
|
6881
8287
|
}
|
|
6882
8288
|
}
|
|
6883
|
-
function
|
|
8289
|
+
function round23(n) {
|
|
6884
8290
|
return Math.round(n * 100) / 100;
|
|
6885
8291
|
}
|
|
6886
8292
|
async function findInvoiceIdsByLineAccounting(core, pub, filters) {
|
|
@@ -6954,7 +8360,8 @@ function rowToInvoice(r) {
|
|
|
6954
8360
|
metadata: r.metadata ?? void 0,
|
|
6955
8361
|
tenantId: r.tenant_id,
|
|
6956
8362
|
createdAt: r.created_at,
|
|
6957
|
-
updatedAt: r.updated_at
|
|
8363
|
+
updatedAt: r.updated_at,
|
|
8364
|
+
createdBy: r.created_by ?? void 0
|
|
6958
8365
|
};
|
|
6959
8366
|
}
|
|
6960
8367
|
async function getMethodFeePct(pub, paymentMethodId) {
|
|
@@ -7051,15 +8458,15 @@ function createSupabaseFinancialProvider() {
|
|
|
7051
8458
|
async createInvoice(input) {
|
|
7052
8459
|
const { core, pub } = getClients();
|
|
7053
8460
|
const tenantId = getTenantId();
|
|
7054
|
-
const lineTotal = (i) =>
|
|
7055
|
-
const totalAmount =
|
|
8461
|
+
const lineTotal = (i) => round23(i.quantity * i.unitPrice - (i.discount ?? 0) + (i.surcharge ?? 0));
|
|
8462
|
+
const totalAmount = round23(input.items.reduce((sum, i) => sum + lineTotal(i), 0));
|
|
7056
8463
|
const itemsSummary = input.items.map((i) => i.description).filter(Boolean).join(", ");
|
|
7057
8464
|
const installments = (input.installments.length > 0 ? input.installments : [
|
|
7058
8465
|
{ amount: totalAmount, dueDate: input.invoiceDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }
|
|
7059
8466
|
]).map((inst, n) => ({
|
|
7060
8467
|
number: inst.installmentNumber ?? n + 1,
|
|
7061
8468
|
due_date: inst.dueDate,
|
|
7062
|
-
amount:
|
|
8469
|
+
amount: round23(inst.amount),
|
|
7063
8470
|
...inst.metadata ? { metadata: inst.metadata } : {}
|
|
7064
8471
|
}));
|
|
7065
8472
|
const { data: res, error } = await pub.rpc("financial_create_invoice", {
|
|
@@ -7102,12 +8509,14 @@ function createSupabaseFinancialProvider() {
|
|
|
7102
8509
|
if (Object.keys(row).length > 0) await pub.from("plg_financial_invoices").update(row).eq("id", id);
|
|
7103
8510
|
return await provider.getInvoiceById(id);
|
|
7104
8511
|
},
|
|
7105
|
-
async cancelInvoice(
|
|
8512
|
+
async cancelInvoice(input) {
|
|
7106
8513
|
const { pub } = getClients();
|
|
8514
|
+
const reason = input.reason.trim();
|
|
8515
|
+
if (!reason) throw new Error("A cancellation reason is required");
|
|
7107
8516
|
const { error } = await pub.rpc("financial_cancel_invoice", {
|
|
7108
|
-
p_invoice_id:
|
|
7109
|
-
p_reason:
|
|
7110
|
-
p_idempotency_key:
|
|
8517
|
+
p_invoice_id: input.invoiceId,
|
|
8518
|
+
p_reason: reason,
|
|
8519
|
+
p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:invoice-cancel")
|
|
7111
8520
|
});
|
|
7112
8521
|
if (error) throw error;
|
|
7113
8522
|
},
|
|
@@ -7264,7 +8673,7 @@ function createSupabaseFinancialProvider() {
|
|
|
7264
8673
|
method = type?.transaction_type ?? null;
|
|
7265
8674
|
}
|
|
7266
8675
|
const feePct = before.direction === "receivable" ? await getMethodFeePct(pub, input.paymentMethodId) : 0;
|
|
7267
|
-
const fee =
|
|
8676
|
+
const fee = round23(input.amount * (feePct / 100));
|
|
7268
8677
|
const { error } = await pub.rpc("financial_receive_payment", {
|
|
7269
8678
|
p_installment_id: input.movementId,
|
|
7270
8679
|
p_amount: input.amount,
|
|
@@ -7279,11 +8688,16 @@ function createSupabaseFinancialProvider() {
|
|
|
7279
8688
|
p_payment_date: input.paymentDate,
|
|
7280
8689
|
p_fee_amount: fee || null,
|
|
7281
8690
|
p_notes: input.notes ?? null,
|
|
8691
|
+
p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:payment"),
|
|
7282
8692
|
p_metadata: input.metadata ?? {}
|
|
7283
8693
|
});
|
|
7284
8694
|
if (error) throw error;
|
|
7285
|
-
|
|
7286
|
-
|
|
8695
|
+
try {
|
|
8696
|
+
const { data: after } = await pub.from(T.bills).select("*").eq("installment_id", input.movementId).maybeSingle();
|
|
8697
|
+
return billRowToMovement(after ?? before);
|
|
8698
|
+
} catch {
|
|
8699
|
+
return billRowToMovement(before);
|
|
8700
|
+
}
|
|
7287
8701
|
},
|
|
7288
8702
|
async cancelMovement(id) {
|
|
7289
8703
|
const { pub } = getClients();
|
|
@@ -7532,7 +8946,7 @@ function createSupabaseFinancialProvider() {
|
|
|
7532
8946
|
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
7533
8947
|
async getStatement(query) {
|
|
7534
8948
|
const { core, pub } = getClients();
|
|
7535
|
-
const
|
|
8949
|
+
const today4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7536
8950
|
const { data: acctRows } = await pub.from("plg_financial_accounts").select("id, name, is_active");
|
|
7537
8951
|
const allAccounts = acctRows ?? [];
|
|
7538
8952
|
const consolidated = !query.bankAccountId;
|
|
@@ -7547,7 +8961,7 @@ function createSupabaseFinancialProvider() {
|
|
|
7547
8961
|
const netOf = (r) => r.direction === "credit" ? (Number(r.paid_amount) || 0) - (Number(r.fee_amount) || 0) : -(Number(r.paid_amount) || 0);
|
|
7548
8962
|
const { data: priorRows } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").lt("payment_date", query.dateRange.from));
|
|
7549
8963
|
const priorNet = (priorRows ?? []).reduce((s, r) => s + netOf(r), 0);
|
|
7550
|
-
const openingBalance =
|
|
8964
|
+
const openingBalance = round23(initialSum + priorNet);
|
|
7551
8965
|
const { data: movs } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").gte("payment_date", query.dateRange.from).lte("payment_date", query.dateRange.to).order("payment_date").order("created_at"));
|
|
7552
8966
|
const invoiceIds = [...new Set((movs ?? []).map((r) => r.invoice_id).filter(Boolean))];
|
|
7553
8967
|
const invoiceById = /* @__PURE__ */ new Map();
|
|
@@ -7564,17 +8978,17 @@ function createSupabaseFinancialProvider() {
|
|
|
7564
8978
|
const gross = Number(r.paid_amount) || 0;
|
|
7565
8979
|
const isCredit = r.direction === "credit";
|
|
7566
8980
|
const fee = isCredit ? Number(r.fee_amount) || 0 : 0;
|
|
7567
|
-
const net = isCredit ?
|
|
8981
|
+
const net = isCredit ? round23(gross - fee) : gross;
|
|
7568
8982
|
const meta = r.metadata ?? {};
|
|
7569
8983
|
const isTransfer = r.movement_kind === "transfer";
|
|
7570
8984
|
const entryKind = isTransfer ? meta.transferRole === "in" ? "transfer-in" : "transfer-out" : "movement";
|
|
7571
8985
|
const counterAccountId = isTransfer ? meta.counterAccountId : void 0;
|
|
7572
|
-
if (isCredit) balance =
|
|
7573
|
-
else balance =
|
|
8986
|
+
if (isCredit) balance = round23(balance + net);
|
|
8987
|
+
else balance = round23(balance - net);
|
|
7574
8988
|
if (!(consolidated && isTransfer)) {
|
|
7575
|
-
if (isCredit) totalCredits =
|
|
7576
|
-
else totalDebits =
|
|
7577
|
-
totalFees =
|
|
8989
|
+
if (isCredit) totalCredits = round23(totalCredits + net);
|
|
8990
|
+
else totalDebits = round23(totalDebits + net);
|
|
8991
|
+
totalFees = round23(totalFees + fee);
|
|
7578
8992
|
}
|
|
7579
8993
|
return {
|
|
7580
8994
|
movement,
|
|
@@ -7612,8 +9026,8 @@ function createSupabaseFinancialProvider() {
|
|
|
7612
9026
|
};
|
|
7613
9027
|
}
|
|
7614
9028
|
}
|
|
7615
|
-
const { data: todayRows } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").lte("payment_date",
|
|
7616
|
-
const accountCurrentBalance =
|
|
9029
|
+
const { data: todayRows } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").lte("payment_date", today4));
|
|
9030
|
+
const accountCurrentBalance = round23(initialSum + (todayRows ?? []).reduce((s, r) => s + netOf(r), 0));
|
|
7617
9031
|
return {
|
|
7618
9032
|
entries,
|
|
7619
9033
|
openingBalance,
|
|
@@ -7621,7 +9035,7 @@ function createSupabaseFinancialProvider() {
|
|
|
7621
9035
|
totalCredits,
|
|
7622
9036
|
totalDebits,
|
|
7623
9037
|
totalFees,
|
|
7624
|
-
net:
|
|
9038
|
+
net: round23(totalCredits - totalDebits),
|
|
7625
9039
|
accountId: query.bankAccountId,
|
|
7626
9040
|
accountCurrentBalance
|
|
7627
9041
|
};
|
|
@@ -8103,15 +9517,15 @@ function createSupabaseFinancialProvider() {
|
|
|
8103
9517
|
const totalBalance = (accounts ?? []).reduce((s, a) => s + (Number(a.balance) || 0), 0);
|
|
8104
9518
|
const allMovs = movs ?? [];
|
|
8105
9519
|
const unpaid = allMovs.filter((m) => ["pending", "partial", "overdue"].includes(m.status));
|
|
8106
|
-
const
|
|
9520
|
+
const today4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8107
9521
|
const unpaidCredit = unpaid.filter((m) => m.direction === "credit");
|
|
8108
9522
|
const unpaidDebit = unpaid.filter((m) => m.direction === "debit");
|
|
8109
|
-
const overdueCredit = unpaidCredit.filter((m) => m.status === "overdue" || m.due_date <
|
|
8110
|
-
const overdueDebit = unpaidDebit.filter((m) => m.status === "overdue" || m.due_date <
|
|
9523
|
+
const overdueCredit = unpaidCredit.filter((m) => m.status === "overdue" || m.due_date < today4);
|
|
9524
|
+
const overdueDebit = unpaidDebit.filter((m) => m.status === "overdue" || m.due_date < today4);
|
|
8111
9525
|
const monthAgo = /* @__PURE__ */ new Date();
|
|
8112
9526
|
monthAgo.setDate(monthAgo.getDate() - 30);
|
|
8113
9527
|
const monthStart = monthAgo.toISOString().slice(0, 10);
|
|
8114
|
-
const period = dateRange ?? { from: monthStart, to:
|
|
9528
|
+
const period = dateRange ?? { from: monthStart, to: today4 };
|
|
8115
9529
|
const monthPaid = (cashMovs ?? []).filter((m) => m.movement_kind === "payment" && m.payment_date >= period.from && m.payment_date <= period.to);
|
|
8116
9530
|
return {
|
|
8117
9531
|
totalBalance,
|
|
@@ -8170,8 +9584,8 @@ function createSupabaseFinancialProvider() {
|
|
|
8170
9584
|
if (!line) return [];
|
|
8171
9585
|
const amount = Number(line.amount) || 0;
|
|
8172
9586
|
const lineDate = line.payment_date ?? line.due_date;
|
|
8173
|
-
const tol = Math.max(0.01,
|
|
8174
|
-
const { data, error } = await pub.from(T.movements).select("*").is("external_source", null).is("reconciled_at", null).eq("direction", line.direction).gte("amount",
|
|
9587
|
+
const tol = Math.max(0.01, round23(amount * 0.02));
|
|
9588
|
+
const { data, error } = await pub.from(T.movements).select("*").is("external_source", null).is("reconciled_at", null).eq("direction", line.direction).gte("amount", round23(amount - tol)).lte("amount", round23(amount + tol)).neq("id", bankMovementId).limit(25);
|
|
8175
9589
|
if (error) throw error;
|
|
8176
9590
|
const candidates = (data ?? []).map((r) => {
|
|
8177
9591
|
const amtDiff = Math.abs((Number(r.amount) || 0) - amount);
|
|
@@ -8181,7 +9595,7 @@ function createSupabaseFinancialProvider() {
|
|
|
8181
9595
|
const dateScore = Math.max(0, 1 - days / 30);
|
|
8182
9596
|
return {
|
|
8183
9597
|
movement: snakeToCamel(r),
|
|
8184
|
-
score:
|
|
9598
|
+
score: round23(0.7 * amtScore + 0.3 * dateScore)
|
|
8185
9599
|
};
|
|
8186
9600
|
}).sort((a, b) => b.score - a.score);
|
|
8187
9601
|
const invoiceIds = [...new Set(candidates.map((c) => c.movement.invoiceId).filter(Boolean))];
|
|
@@ -8536,13 +9950,17 @@ function createFinancialStore(provider) {
|
|
|
8536
9950
|
async payMovement(input) {
|
|
8537
9951
|
try {
|
|
8538
9952
|
await provider.payMovement(input);
|
|
9953
|
+
} catch (err) {
|
|
9954
|
+
toast.error("Failed to record payment", { description: err?.message });
|
|
9955
|
+
throw err;
|
|
9956
|
+
}
|
|
9957
|
+
try {
|
|
8539
9958
|
const query = get().invoiceQuery;
|
|
8540
9959
|
const [result, summary] = await Promise.all([provider.getInvoices(query), provider.getSummary()]);
|
|
8541
9960
|
set({ invoices: result.data, invoicesTotal: result.total, summary });
|
|
8542
9961
|
toast.success("Payment recorded");
|
|
8543
|
-
} catch
|
|
8544
|
-
toast.
|
|
8545
|
-
throw err;
|
|
9962
|
+
} catch {
|
|
9963
|
+
toast.warning("Payment recorded; refresh the list to see the updated balance");
|
|
8546
9964
|
}
|
|
8547
9965
|
},
|
|
8548
9966
|
async reverseMovement(movementId, reason) {
|
|
@@ -8573,9 +9991,9 @@ function createFinancialStore(provider) {
|
|
|
8573
9991
|
throw err;
|
|
8574
9992
|
}
|
|
8575
9993
|
},
|
|
8576
|
-
async cancelInvoice(
|
|
9994
|
+
async cancelInvoice(input) {
|
|
8577
9995
|
try {
|
|
8578
|
-
await provider.cancelInvoice(
|
|
9996
|
+
await provider.cancelInvoice(input);
|
|
8579
9997
|
const query = get().invoiceQuery;
|
|
8580
9998
|
const [result, summary] = await Promise.all([provider.getInvoices(query), provider.getSummary()]);
|
|
8581
9999
|
set({ invoices: result.data, invoicesTotal: result.total, summary });
|
|
@@ -8794,6 +10212,10 @@ var financialRegistries = [
|
|
|
8794
10212
|
entity: paymentMethodEntity,
|
|
8795
10213
|
icon: "Wallet",
|
|
8796
10214
|
description: "Configured payment methods with fees and installment rules",
|
|
10215
|
+
// O nome convida ao erro: pedido "salva essa conta a pagar do fornecedor X,
|
|
10216
|
+
// pago hoje", o assistente cadastrou o FORNECEDOR aqui como forma de
|
|
10217
|
+
// pagamento. Isto é configuração de como o CLIENTE paga a empresa.
|
|
10218
|
+
agentHint: "CONFIGURATION of how customers pay this business (card brand, fee, installment rules). NEVER for a supplier, a bill or an expense \u2014 those are payables, see the createPayable tool",
|
|
8797
10219
|
display: "table"
|
|
8798
10220
|
},
|
|
8799
10221
|
{
|
|
@@ -8997,15 +10419,25 @@ var payables = {
|
|
|
8997
10419
|
check: "existe pelo menos uma conta cadastrada"
|
|
8998
10420
|
}
|
|
8999
10421
|
],
|
|
10422
|
+
// The anchored steps ARE the tour: startFeatureSetup turns each one into a
|
|
10423
|
+
// spotlight stop, in this order, navigating per stop. The unanchored ones
|
|
10424
|
+
// stay prose — briefing for the assistant to say in its own words.
|
|
10425
|
+
// O tour NAVEGA até a tela de cadastro (3 paradas, cada uma com rota, então
|
|
10426
|
+
// reiniciar renavega). Chegando, a `practice` assume: a pessoa preenche os
|
|
10427
|
+
// campos sozinha com os marcadores numerados, para FIXAR o cadastro.
|
|
9000
10428
|
steps: [
|
|
9001
|
-
{ do: "Financeiro
|
|
9002
|
-
{ do: "
|
|
9003
|
-
{
|
|
9004
|
-
|
|
9005
|
-
why: "O cronograma nasce aqui. Editar depois recria o parcelamento inteiro."
|
|
9006
|
-
},
|
|
10429
|
+
{ do: "O Financeiro mora aqui.", anchor: "nav./financial", route: "/financial" },
|
|
10430
|
+
{ do: "Abra Contas a Pagar neste menu.", anchor: "modnav.payables", route: "/financial/payables/list" },
|
|
10431
|
+
{ do: "Clique em Cadastrar manualmente para abrir uma conta nova.", anchor: "action.manual", route: "/financial/payables/list" },
|
|
10432
|
+
{ do: "Diga a quem se deve, adicione um item com o valor, confira as parcelas e salve." },
|
|
9007
10433
|
{ do: "Ao pagar, d\xEA baixa NA PARCELA, escolhendo a conta de onde o dinheiro saiu." }
|
|
9008
10434
|
],
|
|
10435
|
+
practice: [
|
|
10436
|
+
{ anchor: "financial.contact", label: "Selecione o fornecedor" },
|
|
10437
|
+
{ anchor: "financial.items", label: "Adicione um item com o valor" },
|
|
10438
|
+
{ anchor: "financial.installments", label: "Confira as parcelas" },
|
|
10439
|
+
{ anchor: "savebar.save", label: "Salve a conta", final: true }
|
|
10440
|
+
],
|
|
9009
10441
|
rules: [
|
|
9010
10442
|
"Uma conta a pagar \xE9 um cabe\xE7alho com parcelas: o que vence, o que se paga e o que aparece no extrato \xE9 sempre a PARCELA.",
|
|
9011
10443
|
"Baixa exige dizer de qual conta saiu \u2014 \xE9 o que faz o extrato bater."
|
|
@@ -9038,15 +10470,18 @@ var receivables = {
|
|
|
9038
10470
|
check: "a tela de receber oferece pelo menos uma forma"
|
|
9039
10471
|
}
|
|
9040
10472
|
],
|
|
10473
|
+
// Só as abas de TOPO do módulo viram `modnav.<id>`; os filhos (list/new) são
|
|
10474
|
+
// um SegmentedControl sem âncora, então o tour navega pela aba e pela rota.
|
|
9041
10475
|
steps: [
|
|
9042
|
-
{ do: "Financeiro
|
|
10476
|
+
{ do: "O Financeiro mora aqui.", anchor: "nav./financial", route: "/financial" },
|
|
10477
|
+
{ do: "Abra Contas a Receber neste menu \u2014 a lista mostra o que est\xE1 em aberto.", anchor: "modnav.receivables", route: "/financial/receivables/list" },
|
|
10478
|
+
{ do: "Cadastre uma cobran\xE7a \xE0 m\xE3o s\xF3 para o que n\xE3o nasceu de uma venda.", anchor: "action.manual", route: "/financial/receivables/list" },
|
|
9043
10479
|
{
|
|
9044
|
-
do: "
|
|
10480
|
+
do: "O normal \xE9 receber pela pr\xF3pria linha, escolhendo a forma e \u2014 no cart\xE3o \u2014 o n\xFAmero de parcelas.",
|
|
9045
10481
|
byAssistant: true,
|
|
9046
10482
|
tool: "markPaymentReceived",
|
|
9047
10483
|
why: "Eu consigo dar baixa direto quando voc\xEA me disser qual atendimento ou pedido foi pago."
|
|
9048
|
-
}
|
|
9049
|
-
{ do: "S\xF3 lance uma cobran\xE7a \xE0 m\xE3o (Nova) para o que n\xE3o nasceu de uma venda." }
|
|
10484
|
+
}
|
|
9050
10485
|
],
|
|
9051
10486
|
rules: [
|
|
9052
10487
|
"Venda conclu\xEDda vira fatura por gatilho: NENHUMA tela escreve fatura por fora. Se voc\xEA est\xE1 cadastrando \xE0 m\xE3o o que foi vendido, alguma coisa est\xE1 errada antes daqui.",
|
|
@@ -9080,9 +10515,11 @@ var cashRegisters = {
|
|
|
9080
10515
|
}
|
|
9081
10516
|
],
|
|
9082
10517
|
steps: [
|
|
9083
|
-
{ do: "Financeiro
|
|
10518
|
+
{ do: "O Financeiro mora aqui.", anchor: "nav./financial", route: "/financial" },
|
|
10519
|
+
{ do: "Abra Caixas neste menu.", anchor: "modnav.cash-registers", route: "/financial/cash/registers" },
|
|
10520
|
+
{ do: "Abra o caixa dizendo o valor que est\xE1 na gaveta agora.", anchor: "financial.cash-open", route: "/financial/cash/registers" },
|
|
9084
10521
|
{ do: "Durante o dia, o movimento entra sozinho pelo que for recebido." },
|
|
9085
|
-
{ do: "No fim, feche informando o que foi
|
|
10522
|
+
{ do: "No fim, feche informando o que foi CONTADO de verdade \u2014 inclusive quando n\xE3o bate.", anchor: "financial.cash-close", route: "/financial/cash/registers" },
|
|
9086
10523
|
{ do: "Quem tem al\xE7ada confere e aprova a sess\xE3o fechada.", why: "\xC9 a aprova\xE7\xE3o que congela o dia." }
|
|
9087
10524
|
],
|
|
9088
10525
|
rules: [
|
|
@@ -9406,11 +10843,11 @@ function buildFinancialOnboarding() {
|
|
|
9406
10843
|
function CashSessionHeaderControl({ config, provider, store: store6 }) {
|
|
9407
10844
|
const sessions = useStore(store6, (s) => s.cashSessions);
|
|
9408
10845
|
const fetchCashSessions = useStore(store6, (s) => s.fetchCashSessions);
|
|
9409
|
-
const [open, setOpen] =
|
|
9410
|
-
|
|
10846
|
+
const [open, setOpen] = React15__default.useState(false);
|
|
10847
|
+
React15__default.useEffect(() => {
|
|
9411
10848
|
void fetchCashSessions?.();
|
|
9412
10849
|
}, [fetchCashSessions]);
|
|
9413
|
-
|
|
10850
|
+
React15__default.useEffect(() => {
|
|
9414
10851
|
if (!open) return;
|
|
9415
10852
|
const close = () => setOpen(false);
|
|
9416
10853
|
window.addEventListener("hashchange", close);
|
|
@@ -9444,6 +10881,7 @@ function CashSessionHeaderControl({ config, provider, store: store6 }) {
|
|
|
9444
10881
|
|
|
9445
10882
|
// src/locales/en.ts
|
|
9446
10883
|
var en = {
|
|
10884
|
+
"financial.payment.pendingRetry": "Confirmation is pending. Do not start another payment. Retry the same details before leaving this screen.",
|
|
9447
10885
|
"financial.cards.addExpense": "Add expense",
|
|
9448
10886
|
"financial.cards.expenseOnCard": "Expense on this card",
|
|
9449
10887
|
"financial.cards.allBillsValue": "All bills value",
|
|
@@ -9508,6 +10946,8 @@ var en = {
|
|
|
9508
10946
|
"financial.invoice.bulkCancelTitle": "Cancel {{count}} invoice(s)?",
|
|
9509
10947
|
"financial.invoice.cancelConfirm": "This action cannot be undone.",
|
|
9510
10948
|
"financial.invoice.cancelDesc": "The invoice will be marked as cancelled. All pending installments will also be cancelled. This invoice will no longer count towards totals.",
|
|
10949
|
+
"financial.invoice.cancelReason": "Reason for cancellation",
|
|
10950
|
+
"financial.invoice.cancelReasonPlaceholder": "Explain why this invoice is being cancelled",
|
|
9511
10951
|
"financial.invoice.cancelInvoice": "Cancel Invoice",
|
|
9512
10952
|
"financial.invoice.cancelling": "Cancelling...",
|
|
9513
10953
|
"financial.invoice.contact": "Contact",
|
|
@@ -10011,6 +11451,7 @@ var en = {
|
|
|
10011
11451
|
|
|
10012
11452
|
// src/locales/pt-BR.ts
|
|
10013
11453
|
var ptBR = {
|
|
11454
|
+
"financial.payment.pendingRetry": "A confirma\xE7\xE3o ainda est\xE1 pendente. N\xE3o abra outro pagamento. Tente novamente com os mesmos dados antes de sair desta tela.",
|
|
10014
11455
|
"financial.cards.addExpense": "Adicionar despesa",
|
|
10015
11456
|
"financial.cards.expenseOnCard": "Despesa neste cart\xE3o",
|
|
10016
11457
|
"financial.cards.allBillsValue": "Total das faturas",
|
|
@@ -10075,6 +11516,8 @@ var ptBR = {
|
|
|
10075
11516
|
"financial.invoice.bulkCancelTitle": "Cancelar {{count}} fatura(s)?",
|
|
10076
11517
|
"financial.invoice.cancelConfirm": "Esta a\xE7\xE3o n\xE3o pode ser desfeita.",
|
|
10077
11518
|
"financial.invoice.cancelDesc": "A fatura ser\xE1 marcada como cancelada. Todas as parcelas pendentes tamb\xE9m ser\xE3o canceladas. Esta fatura n\xE3o contar\xE1 mais nos totais.",
|
|
11519
|
+
"financial.invoice.cancelReason": "Motivo do cancelamento",
|
|
11520
|
+
"financial.invoice.cancelReasonPlaceholder": "Explique por que esta fatura est\xE1 sendo cancelada",
|
|
10078
11521
|
"financial.invoice.cancelInvoice": "Cancelar Fatura",
|
|
10079
11522
|
"financial.invoice.cancelling": "Cancelando...",
|
|
10080
11523
|
"financial.invoice.contact": "Contato",
|
|
@@ -10604,14 +12047,14 @@ function CommissionRuleDialog({
|
|
|
10604
12047
|
onClose,
|
|
10605
12048
|
onSaved
|
|
10606
12049
|
}) {
|
|
10607
|
-
const [name, setName] =
|
|
10608
|
-
const [description, setDescription] =
|
|
10609
|
-
const [priority, setPriority] =
|
|
10610
|
-
const [isActive, setIsActive] =
|
|
10611
|
-
const [isDefault, setIsDefault] =
|
|
10612
|
-
const [saving, setSaving] =
|
|
10613
|
-
const [error, setError] =
|
|
10614
|
-
const initial =
|
|
12050
|
+
const [name, setName] = React15__default.useState(rule?.name ?? "");
|
|
12051
|
+
const [description, setDescription] = React15__default.useState(rule?.description ?? "");
|
|
12052
|
+
const [priority, setPriority] = React15__default.useState(String(rule?.priority ?? 1));
|
|
12053
|
+
const [isActive, setIsActive] = React15__default.useState(rule?.isActive ?? true);
|
|
12054
|
+
const [isDefault, setIsDefault] = React15__default.useState(rule?.isDefault ?? !hasDefault);
|
|
12055
|
+
const [saving, setSaving] = React15__default.useState(false);
|
|
12056
|
+
const [error, setError] = React15__default.useState(null);
|
|
12057
|
+
const initial = React15__default.useMemo(() => {
|
|
10615
12058
|
const get = (t2, key = "ids") => rule?.criteria.find((c) => c.criteriaType === t2)?.criteriaValue?.[key] ?? [];
|
|
10616
12059
|
return {
|
|
10617
12060
|
services: get("service"),
|
|
@@ -10622,12 +12065,12 @@ function CommissionRuleDialog({
|
|
|
10622
12065
|
ranges: get("time_range", "ranges")
|
|
10623
12066
|
};
|
|
10624
12067
|
}, [rule]);
|
|
10625
|
-
const [services, setServices] =
|
|
10626
|
-
const [categories, setCategories] =
|
|
10627
|
-
const [professionals, setProfessionals] =
|
|
10628
|
-
const [units, setUnits] =
|
|
10629
|
-
const [weekdays, setWeekdays] =
|
|
10630
|
-
const [ranges, setRanges] =
|
|
12068
|
+
const [services, setServices] = React15__default.useState(initial.services);
|
|
12069
|
+
const [categories, setCategories] = React15__default.useState(initial.categories);
|
|
12070
|
+
const [professionals, setProfessionals] = React15__default.useState(initial.professionals);
|
|
12071
|
+
const [units, setUnits] = React15__default.useState(initial.units);
|
|
12072
|
+
const [weekdays, setWeekdays] = React15__default.useState(initial.weekdays);
|
|
12073
|
+
const [ranges, setRanges] = React15__default.useState(initial.ranges);
|
|
10631
12074
|
const criteriaCount = [services, categories, professionals, units].filter((a) => a.length > 0).length + (weekdays.length ? 1 : 0) + (ranges.length ? 1 : 0);
|
|
10632
12075
|
async function handleSave() {
|
|
10633
12076
|
if (!name.trim()) {
|
|
@@ -10870,11 +12313,11 @@ function blankFunction(order) {
|
|
|
10870
12313
|
};
|
|
10871
12314
|
}
|
|
10872
12315
|
function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
|
|
10873
|
-
const [functions, setFunctions] =
|
|
12316
|
+
const [functions, setFunctions] = React15__default.useState(
|
|
10874
12317
|
rule.functions.length ? rule.functions : [blankFunction(0)]
|
|
10875
12318
|
);
|
|
10876
|
-
const [saving, setSaving] =
|
|
10877
|
-
const [error, setError] =
|
|
12319
|
+
const [saving, setSaving] = React15__default.useState(false);
|
|
12320
|
+
const [error, setError] = React15__default.useState(null);
|
|
10878
12321
|
function patch(i, changes) {
|
|
10879
12322
|
setFunctions((fs) => fs.map((f, j) => j === i ? { ...f, ...changes } : f));
|
|
10880
12323
|
}
|
|
@@ -11158,19 +12601,19 @@ function functionChip(fn) {
|
|
|
11158
12601
|
return fn.valueType === "percentage" ? `${who}: ${fn.valueAmount}%` : `${who}: R$ ${fn.valueAmount.toFixed(2)}`;
|
|
11159
12602
|
}
|
|
11160
12603
|
function CommissionRulesView({ provider }) {
|
|
11161
|
-
const [rules, setRules] =
|
|
11162
|
-
const [catalogs, setCatalogs] =
|
|
12604
|
+
const [rules, setRules] = React15__default.useState(null);
|
|
12605
|
+
const [catalogs, setCatalogs] = React15__default.useState({
|
|
11163
12606
|
services: [],
|
|
11164
12607
|
categories: [],
|
|
11165
12608
|
professionals: [],
|
|
11166
12609
|
units: []
|
|
11167
12610
|
});
|
|
11168
|
-
const [error, setError] =
|
|
11169
|
-
const [funnel, setFunnel] =
|
|
11170
|
-
const [editing, setEditing] =
|
|
11171
|
-
const [functionsFor, setFunctionsFor] =
|
|
11172
|
-
const [deleting, setDeleting] =
|
|
11173
|
-
const load =
|
|
12611
|
+
const [error, setError] = React15__default.useState(null);
|
|
12612
|
+
const [funnel, setFunnel] = React15__default.useState({});
|
|
12613
|
+
const [editing, setEditing] = React15__default.useState(null);
|
|
12614
|
+
const [functionsFor, setFunctionsFor] = React15__default.useState(null);
|
|
12615
|
+
const [deleting, setDeleting] = React15__default.useState(null);
|
|
12616
|
+
const load = React15__default.useCallback(async () => {
|
|
11174
12617
|
try {
|
|
11175
12618
|
setError(null);
|
|
11176
12619
|
const [r, c] = await Promise.all([
|
|
@@ -11184,17 +12627,17 @@ function CommissionRulesView({ provider }) {
|
|
|
11184
12627
|
setRules([]);
|
|
11185
12628
|
}
|
|
11186
12629
|
}, [provider]);
|
|
11187
|
-
|
|
12630
|
+
React15__default.useEffect(() => {
|
|
11188
12631
|
void load();
|
|
11189
12632
|
}, [load]);
|
|
11190
12633
|
const funnelActive = Object.values(funnel).some(Boolean);
|
|
11191
|
-
const visible =
|
|
12634
|
+
const visible = React15__default.useMemo(() => {
|
|
11192
12635
|
if (!rules) return [];
|
|
11193
12636
|
const ranked = rankRules(rules);
|
|
11194
12637
|
if (!funnelActive) return ranked;
|
|
11195
12638
|
return ranked.filter((r) => r.isDefault || ruleMatches(r, funnel, catalogs));
|
|
11196
12639
|
}, [rules, funnel, funnelActive, catalogs]);
|
|
11197
|
-
const winner =
|
|
12640
|
+
const winner = React15__default.useMemo(() => {
|
|
11198
12641
|
if (!funnelActive) return null;
|
|
11199
12642
|
return visible.find((r) => r.isActive && !r.isDefault) ?? visible.find((r) => r.isActive && r.isDefault) ?? null;
|
|
11200
12643
|
}, [visible, funnelActive]);
|
|
@@ -21443,8 +22886,8 @@ function ReceivePaymentModal({
|
|
|
21443
22886
|
onClose,
|
|
21444
22887
|
onPaid
|
|
21445
22888
|
}) {
|
|
21446
|
-
const provider =
|
|
21447
|
-
const store6 =
|
|
22889
|
+
const provider = React15__default.useMemo(() => createSafeFinancialProvider(), []);
|
|
22890
|
+
const store6 = React15__default.useMemo(() => createFinancialStore(provider), [provider]);
|
|
21448
22891
|
return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
|
|
21449
22892
|
PaymentModal,
|
|
21450
22893
|
{
|
|
@@ -21462,7 +22905,7 @@ function createFinancialSettingsPlugin(options) {
|
|
|
21462
22905
|
...financialRegistries.filter((r) => RECEIVING_REGISTRIES.includes(r.id)),
|
|
21463
22906
|
...options?.extraRegistries ?? []
|
|
21464
22907
|
];
|
|
21465
|
-
const SettingsTab = () =>
|
|
22908
|
+
const SettingsTab = () => React15__default.createElement(PluginSettingsPanel, {
|
|
21466
22909
|
title: "Pagamentos",
|
|
21467
22910
|
registries,
|
|
21468
22911
|
hostPluginId: "financial",
|
|
@@ -21505,19 +22948,19 @@ function ClientAccountPanel({
|
|
|
21505
22948
|
currency = BRL
|
|
21506
22949
|
}) {
|
|
21507
22950
|
const provider = useFinancialProvider();
|
|
21508
|
-
const [account, setAccount] =
|
|
21509
|
-
const [busy, setBusy] =
|
|
21510
|
-
const [paying, setPaying] =
|
|
22951
|
+
const [account, setAccount] = React15__default.useState(null);
|
|
22952
|
+
const [busy, setBusy] = React15__default.useState(false);
|
|
22953
|
+
const [paying, setPaying] = React15__default.useState(null);
|
|
21511
22954
|
async function openPayment(invoiceId) {
|
|
21512
22955
|
const res = await provider.getMovements({ invoiceId, pageSize: 200 });
|
|
21513
22956
|
const open = (res.data ?? []).find((m) => m.movementKind === "bill" && m.amount - (m.paidAmount ?? 0) > 5e-3);
|
|
21514
22957
|
if (open) setPaying(open);
|
|
21515
22958
|
}
|
|
21516
|
-
const load =
|
|
22959
|
+
const load = React15__default.useCallback(async () => {
|
|
21517
22960
|
if (!personId) return;
|
|
21518
22961
|
setAccount(await provider.getClientAccount(personId));
|
|
21519
22962
|
}, [personId, provider]);
|
|
21520
|
-
|
|
22963
|
+
React15__default.useEffect(() => {
|
|
21521
22964
|
void load();
|
|
21522
22965
|
}, [load]);
|
|
21523
22966
|
const forThis = account?.invoices.filter((i) => orderId && i.orderId === orderId) ?? [];
|
|
@@ -21619,8 +23062,8 @@ function ClientAccountModal({
|
|
|
21619
23062
|
onGenerate,
|
|
21620
23063
|
onClose
|
|
21621
23064
|
}) {
|
|
21622
|
-
const provider =
|
|
21623
|
-
const store6 =
|
|
23065
|
+
const provider = React15__default.useMemo(() => createSafeFinancialProvider(), []);
|
|
23066
|
+
const store6 = React15__default.useMemo(() => createFinancialStore(provider), [provider]);
|
|
21624
23067
|
return /* @__PURE__ */ jsx(RightRailPage, { title: "Conta do cliente", open: true, onClose, width: "max-w-md", children: /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
|
|
21625
23068
|
ClientAccountPanel,
|
|
21626
23069
|
{
|
|
@@ -21729,9 +23172,10 @@ function createFinancialPlugin(options) {
|
|
|
21729
23172
|
const store6 = createFinancialStore(provider);
|
|
21730
23173
|
const dashboardWidgets = createFinancialDashboardWidgets({ config, provider, store: store6 });
|
|
21731
23174
|
const registries = [...financialRegistries, ...options?.settingsRegistries ?? []];
|
|
21732
|
-
const PersonStatementBound = (props) =>
|
|
23175
|
+
const PersonStatementBound = (props) => React15__default.createElement(PersonStatementWidget, { person: props.item, provider, config });
|
|
21733
23176
|
PersonStatementBound.displayName = "FinancialPersonStatement";
|
|
21734
|
-
const PageComponent = () =>
|
|
23177
|
+
const PageComponent = () => React15__default.createElement(FinancialPage, { config, provider, store: store6, registries });
|
|
23178
|
+
const AgentMount = createFinancialAgentMount({ config, provider });
|
|
21735
23179
|
return {
|
|
21736
23180
|
id: "financial",
|
|
21737
23181
|
defaultAgentRole: "finance",
|
|
@@ -21788,7 +23232,7 @@ function createFinancialPlugin(options) {
|
|
|
21788
23232
|
// tem — lá é um ícone no cabeçalho, de qualquer tela.
|
|
21789
23233
|
headerControls: config.modules.cashRegisters ? [{
|
|
21790
23234
|
id: "financial.cash-session",
|
|
21791
|
-
component: (() =>
|
|
23235
|
+
component: (() => React15__default.createElement(CashSessionHeaderControl, { config, provider, store: store6 })),
|
|
21792
23236
|
order: 90,
|
|
21793
23237
|
permission: { feature: "financial", action: "read" }
|
|
21794
23238
|
}] : [],
|
|
@@ -21890,6 +23334,17 @@ function createFinancialPlugin(options) {
|
|
|
21890
23334
|
component: PersonStatementBound,
|
|
21891
23335
|
order: 0,
|
|
21892
23336
|
permission: { feature: "financial", action: "read" }
|
|
23337
|
+
},
|
|
23338
|
+
{
|
|
23339
|
+
// Headless. Publica ao assistente a porta de lançar uma conta a pagar
|
|
23340
|
+
// em QUALQUER rota — ver payable-agent.tsx. `shell.topbar.end` é a
|
|
23341
|
+
// única zona global do shell que de fato é renderizada e que fica FORA
|
|
23342
|
+
// do switch de rota (mesma leitura do pill do plugin-scribe).
|
|
23343
|
+
id: "financial-agent-mount",
|
|
23344
|
+
zone: "shell.topbar.end",
|
|
23345
|
+
component: AgentMount,
|
|
23346
|
+
order: -20,
|
|
23347
|
+
permission: { feature: "financial", action: "read" }
|
|
21893
23348
|
}
|
|
21894
23349
|
],
|
|
21895
23350
|
dashboardWidgets,
|
|
@@ -21980,9 +23435,9 @@ function createFinancialPlugin(options) {
|
|
|
21980
23435
|
// lado, e a corrida de "Plugins" não é partida ao meio.
|
|
21981
23436
|
group: "Financeiro",
|
|
21982
23437
|
component: (() => {
|
|
21983
|
-
const FinancialSettingsTab = () =>
|
|
23438
|
+
const FinancialSettingsTab = () => React15__default.createElement(PluginSettingsPanel, {
|
|
21984
23439
|
title: "Financial Settings",
|
|
21985
|
-
generalSettings:
|
|
23440
|
+
generalSettings: React15__default.createElement(FinancialGeneralSettings),
|
|
21986
23441
|
registries,
|
|
21987
23442
|
hostPluginId: "financial",
|
|
21988
23443
|
routeBase: "/settings/financial"
|
|
@@ -22007,7 +23462,7 @@ function createFinancialPlugin(options) {
|
|
|
22007
23462
|
icon: "HandCoins",
|
|
22008
23463
|
group: "Financeiro",
|
|
22009
23464
|
component: (() => {
|
|
22010
|
-
const CommissionRulesTab = () =>
|
|
23465
|
+
const CommissionRulesTab = () => React15__default.createElement(CommissionRulesView, { provider });
|
|
22011
23466
|
CommissionRulesTab.displayName = "CommissionRulesTab";
|
|
22012
23467
|
return CommissionRulesTab;
|
|
22013
23468
|
})(),
|