@fayz-ai/plugin-financial 0.19.1 → 0.21.0

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/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import * as React25 from 'react';
2
2
  import React25__default, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
3
- import { recordRailFields, RECORD_GROUP, activeFacet, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, getSupabaseClientOptional, createSafeDataProvider, registerMessageDefaults, reportDataError, useTranslation, createPendingCommand, useUnitMode, useActiveUnitId, useDataChanged, countByTenant, tl, resolveDataProvider, getActiveTenantId, emitDataChanged, exportCSV } from '@fayz-ai/core';
4
- import { INLINE_LINK_TONE, SearchSelect, ModalTask, toast, Button, useInModalTaskHost, useSaveBar, tourAnchor, DatePicker, SubpageHeader, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, Card, CardContent, Skeleton, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MultiSelect, PageTransition, ModulePage, Badge, TooltipProvider, ConfirmDialog, CurrencyInput, Input, PageHeaderActions, Hint, ModalFooter, PageTabs, Tooltip, TooltipTrigger, TooltipContent, Checkbox, Popover, PopoverTrigger, PopoverContent, DashboardCanvas, ComingSoon, StatTiles, ListView, DateRangeControl, FilterPanel, FilterPanelField, SegmentedControl, RequiredFilter, FilterMenuRow, ModalFlow, Switch, WizardSteps, KpiCard, ChartWidget, CardHeader, CardTitle, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
3
+ import { recordRailFields, RECORD_GROUP, activeFacet, createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, getSupabaseClientOptional, createSafeDataProvider, registerMessageDefaults, reportDataError, useTranslation, createPendingCommand, useUnitMode, useDataChanged, countByTenant, tl, resolveDataProvider, useActiveUnitId, getActiveTenantId, emitDataChanged, exportCSV } from '@fayz-ai/core';
4
+ import { INLINE_LINK_TONE, SearchSelect, ModalTask, toast, Button, useInModalTaskHost, useSaveBar, tourAnchor, DatePicker, SubpageHeader, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, Card, CardContent, Skeleton, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MultiSelect, PageTransition, ModulePage, Badge, TooltipProvider, ConfirmDialog, CurrencyInput, Input, PageHeaderActions, ChoiceSelect, Hint, ModalFooter, PageTabs, Tooltip, TooltipTrigger, TooltipContent, Checkbox, Popover, PopoverTrigger, PopoverContent, openPrintWindow, printDocument, DashboardCanvas, ComingSoon, StatTiles, ListView, DateRangeControl, FilterPanel, FilterPanelField, SegmentedControl, RequiredFilter, FilterMenuRow, ChoiceList, ModalFlow, Switch, WizardSteps, KpiCard, ChartWidget, CardHeader, CardTitle, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
- import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, createCrudPage, PluginSettingsPanel, dedup, useUnitFilter, useLimitGuard, usePermissionOptional, useLookupCreate, OrderFormSkeleton, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, OrderFormAddItemStep, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, PersonLink, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, useModuleNavAccess, PluginOnboardingGateSkeleton, ModuleActionBar, SettingsGroup, ToggleRow, createArchetypeLookup, invalidateLimit, useAgentSurface, parseViewId, EntitlementGate, UpgradePrompt, useChatStore, adminNavigateTo, revealPage, useAuth, GetStarted, resolveEntityRoute, PermissionGate, createOneShotAsk, askAI, requestChatUpload, toInlineFile, shrinkForVision } from '@fayz-ai/admin';
7
- import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, Receipt, Gift, Briefcase, Package, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Paperclip, Ticket, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Undo2, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Search, ChevronsUpDown, ChevronsDownUp, ChevronRight, Building2, ChevronDown, Image, UploadCloud, Calendar, User, Unlink, Eye, Inbox, Clock, ClipboardCheck, BookOpen, Info, FolderOpen, Folder, RotateCcw, ArrowLeft, ChevronLeft, Calculator, Square, Play, ChevronUp, Printer, CalendarClock, BadgeCheck, Target, Settings, ArrowRight, Users, RefreshCw, Download, Camera, Sparkles, Link2, FileSearch, CheckCheck, Pause, ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell, Music, ShoppingBag, FileDown, ArrowUpRight, ArrowDownRight, EyeOff } from 'lucide-react';
6
+ import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, createCrudPage, PluginSettingsPanel, dedup, useUnitFilter, useLimitGuard, usePermissionOptional, useLookupCreate, OrderFormSkeleton, WidgetSlot, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, adminNavigateTo, OrderFormAddItemStep, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, PersonLink, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, useModuleNavAccess, PluginOnboardingGateSkeleton, ModuleActionBar, SettingsGroup, ToggleRow, createArchetypeLookup, invalidateLimit, useAgentSurface, parseViewId, EntitlementGate, UpgradePrompt, useChatStore, revealPage, useAuth, GetStarted, resolveEntityRoute, PermissionGate, createOneShotAsk, askAI, requestChatUpload, toInlineFile, shrinkForVision } from '@fayz-ai/admin';
7
+ import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, Receipt, Gift, Briefcase, Package, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Paperclip, Ticket, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Undo2, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Search, ChevronsUpDown, ChevronsDownUp, ChevronRight, Building2, Image, UploadCloud, Calendar, User, Unlink, ChevronDown, Eye, Inbox, Clock, ClipboardCheck, BookOpen, Info, FolderOpen, Folder, RotateCcw, Settings, ArrowLeft, ChevronLeft, Calculator, Square, Play, ChevronUp, Printer, CalendarClock, BadgeCheck, Target, ArrowRight, Users, RefreshCw, Download, Camera, Sparkles, Link2, FileSearch, CheckCheck, Pause, ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell, Music, ShoppingBag, FileDown, ArrowUpRight, ArrowDownRight, EyeOff } from 'lucide-react';
8
8
  import { createPortal } from 'react-dom';
9
9
  import { createStore as createStore$1 } from 'zustand/vanilla';
10
10
  import { useStore } from 'zustand';
@@ -2489,22 +2489,27 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2489
2489
  const printSelected = useCallback((rows) => {
2490
2490
  const safe = (value) => String(value ?? "").replace(/[&<>"']/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character] ?? character);
2491
2491
  const body = rows.map((row) => `<tr><td>${safe(row.dueDate)}</td><td>${safe(row.contactName)}<small>${safe(row.itemsSummary)}</small></td><td>${safe(formatCurrency(row.totalAmount, currency))}</td><td>${row.installmentNumber ?? 1}/${row.totalInstallments ?? 1}</td></tr>`).join("");
2492
- const popup = window.open("", "_blank");
2492
+ const title = direction === "debit" ? "Contas a pagar" : "Contas a receber";
2493
+ const popup = printDocument({
2494
+ title,
2495
+ css: "body{font:12px sans-serif}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #ddd;padding:8px;text-align:left}small{display:block;color:#666;margin-top:2px}",
2496
+ body: `<h1>${title}</h1><table><thead><tr><th>Vencimento</th><th>Descri\xE7\xE3o / itens</th><th>Valor</th><th>Parcela</th></tr></thead><tbody>${body}</tbody></table>`
2497
+ });
2493
2498
  if (!popup) {
2494
2499
  toast.error(t2("financial.invoice.printBlocked"));
2495
2500
  return;
2496
2501
  }
2497
2502
  popup.opener = null;
2498
- popup.document.write(`<html><head><title>${direction === "debit" ? "Contas a pagar" : "Contas a receber"}</title><style>body{font:12px sans-serif;padding:24px}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #ddd;padding:8px;text-align:left}small{display:block;color:#666;margin-top:2px}</style></head><body><h1>${direction === "debit" ? "Contas a pagar" : "Contas a receber"}</h1><table><thead><tr><th>Vencimento</th><th>Descri\xE7\xE3o / itens</th><th>Valor</th><th>Parcela</th></tr></thead><tbody>${body}</tbody></table><script>window.print();window.close()</script></body></html>`);
2499
- popup.document.close();
2500
2503
  }, [currency, direction, t2]);
2501
2504
  const bulkActions = useMemo(() => {
2502
2505
  const actions = [
2503
- ...direction === "debit" ? [{ id: "cnab", label: t2("financial.invoice.generateCnab"), icon: FileText, onSelect: (rows) => setCnabTarget(rows.filter((row) => row.status !== "paid" && row.status !== "cancelled")) }] : [],
2504
2506
  { id: "print", label: t2("financial.invoice.printSelected"), icon: Printer, onSelect: printSelected },
2505
2507
  { id: "export", label: t2("financial.invoice.exportSelected"), icon: Download, onSelect: exportSelected }
2506
2508
  ];
2507
2509
  if (!canEdit) return actions;
2510
+ if (direction === "debit") {
2511
+ actions.unshift({ id: "cnab", label: t2("financial.invoice.generateCnab"), icon: FileText, onSelect: (rows) => setCnabTarget(rows.filter((row) => row.status !== "paid" && row.status !== "cancelled")) });
2512
+ }
2508
2513
  actions.push({
2509
2514
  id: "cancel",
2510
2515
  label: t2("financial.invoice.bulkCancel"),
@@ -2560,17 +2565,20 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2560
2565
  value: unitFilter.options.find((u) => u.id === unitFilter.value)?.name ?? unitFilter.allLabel,
2561
2566
  icon: Building2,
2562
2567
  className: "h-9 w-full justify-start",
2563
- align: "start",
2564
- children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
2565
- /* @__PURE__ */ jsx(FilterMenuRow, { active: !unitFilter.value, onClick: () => {
2566
- unitFilter.setValue(void 0);
2567
- close();
2568
- }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: unitFilter.allLabel }) }),
2569
- unitFilter.options.map((u) => /* @__PURE__ */ jsx(FilterMenuRow, { active: unitFilter.value === u.id, onClick: () => {
2570
- unitFilter.setValue(u.id);
2571
- close();
2572
- }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: u.name }) }, u.id))
2573
- ] })
2568
+ align: "end",
2569
+ children: (close) => /* @__PURE__ */ jsx(
2570
+ ChoiceList,
2571
+ {
2572
+ options: unitFilter.options.map((u) => ({ value: u.id, label: u.name })),
2573
+ value: unitFilter.value,
2574
+ onSelect: unitFilter.setValue,
2575
+ allLabel: unitFilter.allLabel,
2576
+ searchable: true,
2577
+ searchPlaceholder: unitFilter.def?.searchPlaceholder,
2578
+ manage: unitFilter.def?.manage,
2579
+ onDone: close
2580
+ }
2581
+ )
2574
2582
  }
2575
2583
  ) : locations.length > 1 ? /* @__PURE__ */ jsx(
2576
2584
  RequiredFilter,
@@ -2579,17 +2587,18 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2579
2587
  value: locations.find((u) => u.id === unitId)?.name ?? t2("financial.invoice.filterAllUnits"),
2580
2588
  icon: Building2,
2581
2589
  className: "h-9 w-full justify-start",
2582
- align: "start",
2583
- children: (close) => /* @__PURE__ */ jsxs("div", { className: "max-h-72 space-y-0.5 overflow-y-auto", children: [
2584
- /* @__PURE__ */ jsx(FilterMenuRow, { active: !unitId, onClick: () => {
2585
- setUnitId("");
2586
- close();
2587
- }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: t2("financial.invoice.filterAllUnits") }) }),
2588
- locations.map((u) => /* @__PURE__ */ jsx(FilterMenuRow, { active: unitId === u.id, onClick: () => {
2589
- setUnitId(u.id);
2590
- close();
2591
- }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: u.name }) }, u.id))
2592
- ] })
2590
+ align: "end",
2591
+ children: (close) => /* @__PURE__ */ jsx(
2592
+ ChoiceList,
2593
+ {
2594
+ options: locations.map((u) => ({ value: u.id, label: u.name })),
2595
+ value: unitId || void 0,
2596
+ onSelect: (v) => setUnitId(v ?? ""),
2597
+ allLabel: t2("financial.invoice.filterAllUnits"),
2598
+ searchable: true,
2599
+ onDone: close
2600
+ }
2601
+ )
2593
2602
  }
2594
2603
  ) : null;
2595
2604
  const statusField = /* @__PURE__ */ jsx(
@@ -2874,6 +2883,43 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2874
2883
  cnabTarget !== null ? /* @__PURE__ */ jsx(CnabRemittanceModal, { invoices: cnabTarget, bankAccounts, provider, onClose: () => setCnabTarget(null) }) : null
2875
2884
  ] });
2876
2885
  }
2886
+
2887
+ // src/lib/routes.ts
2888
+ var ACCOUNTS_ROUTE = "/registry/accounts";
2889
+ var CHART_OF_ACCOUNTS_ROUTE = "/settings/financial/chart-of-accounts";
2890
+ var COST_CENTERS_ROUTE = "/settings/financial/cost-centers";
2891
+ var FINANCIAL_INVOICE_ZONES = {
2892
+ headerActions: "financial.invoice.header.actions"
2893
+ };
2894
+ function useWritableUnits(fallback = []) {
2895
+ const unitMode = useUnitMode();
2896
+ const activeUnitId = useActiveUnitId() ?? void 0;
2897
+ const reachable = useUnitFilter().options;
2898
+ const choices = !unitMode ? fallback : activeUnitId ? reachable.filter((u) => u.id === activeUnitId) : reachable;
2899
+ return { unitMode, activeUnitId, choices };
2900
+ }
2901
+ function UnitField({ choices, value, onChange, label }) {
2902
+ const t2 = useTranslation();
2903
+ const can = usePermissionOptional();
2904
+ return /* @__PURE__ */ jsxs("div", { children: [
2905
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: label ?? t2("financial.invoiceForm.unit") }),
2906
+ /* @__PURE__ */ jsx(
2907
+ ChoiceSelect,
2908
+ {
2909
+ className: "mt-1",
2910
+ icon: Building2,
2911
+ "aria-label": label ?? t2("financial.invoiceForm.unit"),
2912
+ options: choices.map((c) => ({ value: c.id, label: c.name })),
2913
+ value: value || void 0,
2914
+ onChange: (v) => onChange(v ?? ""),
2915
+ allLabel: "Todas / sem unidade",
2916
+ searchable: true,
2917
+ searchPlaceholder: t2("financial.invoiceForm.searchUnit"),
2918
+ manage: can("units", "manage") ? { label: "Gerenciar unidades", onSelect: () => adminNavigateTo("/settings/locations") } : void 0
2919
+ }
2920
+ )
2921
+ ] });
2922
+ }
2877
2923
  var PAYABLE_INSTRUCTIONS = [
2878
2924
  'Uma "conta a pagar" \xE9 uma DESPESA com um FORNECEDOR \u2014 quem a empresa deve. Para registrar uma,',
2879
2925
  "chame `createPayable`. NUNCA use `createRecord` em `financial:payment-methods` para isso: aquele",
@@ -4664,6 +4710,7 @@ function InstallmentSourceFields({
4664
4710
  const t2 = useTranslation();
4665
4711
  const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes);
4666
4712
  const bankAccounts = useFinancialStore((s) => s.bankAccounts);
4713
+ const can = usePermissionOptional();
4667
4714
  const selectedType = paymentMethodTypes.find((tp) => tp.id === value.payMethodTypeId);
4668
4715
  const selectedAccount = bankAccounts.find((a) => a.id === value.payAccountId);
4669
4716
  const accounts = eligibleAccountsFor(bankAccounts, direction, selectedType);
@@ -4712,13 +4759,15 @@ function InstallmentSourceFields({
4712
4759
  ariaLabel: t2("financial.invoiceForm.paidFrom"),
4713
4760
  clearLabel: t2("financial.invoiceForm.clear"),
4714
4761
  className: `${INLINE_LINK_TONE.dashed} ${tone(!!selectedAccount)}`,
4715
- onChange: (id) => onChange({ ...value, payAccountId: id })
4762
+ onChange: (id) => onChange({ ...value, payAccountId: id }),
4763
+ onManage: can("financial", "manage") ? () => adminNavigateTo(ACCOUNTS_ROUTE) : void 0,
4764
+ manageLabel: t2("financial.invoiceForm.manageAccounts")
4716
4765
  }
4717
4766
  )
4718
4767
  ] });
4719
4768
  }
4720
4769
  var norm2 = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
4721
- function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className }) {
4770
+ function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className, onManage, manageLabel }) {
4722
4771
  const [open, setOpen] = React25__default.useState(false);
4723
4772
  const [q, setQ] = React25__default.useState("");
4724
4773
  const selected = accounts.find((a) => a.id === value);
@@ -4783,7 +4832,22 @@ function AccountPicker({ accounts, value, onChange, disabled, title, placeholder
4783
4832
  },
4784
4833
  a.id
4785
4834
  )) }),
4786
- value && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => pick(void 0), className: "mt-1 w-full rounded-md px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted", children: clearLabel })
4835
+ value && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => pick(void 0), className: "mt-1 w-full rounded-md px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted", children: clearLabel }),
4836
+ onManage && /* @__PURE__ */ jsxs(
4837
+ "button",
4838
+ {
4839
+ type: "button",
4840
+ onClick: () => {
4841
+ setOpen(false);
4842
+ onManage();
4843
+ },
4844
+ className: "mt-1 flex w-full items-center gap-2 rounded-md border-t px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground",
4845
+ children: [
4846
+ /* @__PURE__ */ jsx(Settings, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
4847
+ manageLabel
4848
+ ]
4849
+ }
4850
+ )
4787
4851
  ] })
4788
4852
  ] });
4789
4853
  }
@@ -4841,7 +4905,8 @@ function InstallmentRow({
4841
4905
  ] })
4842
4906
  ] }),
4843
4907
  /* @__PURE__ */ jsx("div", { className: "w-44 shrink-0", onClick: stop, children: source }),
4844
- (actions || onClick) && /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5", onClick: stop, children: [
4908
+ (actions || onClick) && // Min width fits attach + "Pagar": rows where Pay is absent (unsaved, paid) keep the source column aligned.
4909
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-32 shrink-0 items-center justify-end gap-1.5", onClick: stop, children: [
4845
4910
  actions,
4846
4911
  onClick && /* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true })
4847
4912
  ] })
@@ -5013,95 +5078,6 @@ function summarizeInvoiceLines(rows, visible = 2) {
5013
5078
  function invoiceOpensForEditing(status) {
5014
5079
  return status !== "paid" && status !== "cancelled";
5015
5080
  }
5016
- function LocationSelector({ locations, value, onChange }) {
5017
- const t2 = useTranslation();
5018
- const [open, setOpen] = useState(false);
5019
- const [search, setSearch] = useState("");
5020
- const ref = useRef(null);
5021
- useEffect(() => {
5022
- function handler(e) {
5023
- if (ref.current && !ref.current.contains(e.target)) setOpen(false);
5024
- }
5025
- document.addEventListener("mousedown", handler);
5026
- return () => document.removeEventListener("mousedown", handler);
5027
- }, []);
5028
- const selected = locations.find((l) => l.id === value);
5029
- const filtered = locations.filter(
5030
- (l) => !search.trim() || l.name.toLowerCase().includes(search.toLowerCase())
5031
- );
5032
- return /* @__PURE__ */ jsxs("div", { ref, className: "relative", children: [
5033
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.invoiceForm.unit") }),
5034
- /* @__PURE__ */ jsxs(
5035
- "button",
5036
- {
5037
- type: "button",
5038
- onClick: () => setOpen(!open),
5039
- className: "w-full mt-1 flex items-center gap-2 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm text-left hover:bg-muted/30 transition-colors",
5040
- children: [
5041
- /* @__PURE__ */ jsx(Building2, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }),
5042
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: selected?.name ?? (value ? t2("financial.invoiceForm.selectUnit") : "Todas / sem unidade") }),
5043
- /* @__PURE__ */ jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}` })
5044
- ]
5045
- }
5046
- ),
5047
- open && /* @__PURE__ */ jsxs("div", { className: "fayz-glass-surface absolute top-full left-0 right-0 z-50 mt-1 rounded-lg border bg-popover shadow-lg overflow-hidden min-w-[260px]", children: [
5048
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 border-b", children: [
5049
- /* @__PURE__ */ jsx(Building2, { className: "h-4 w-4 text-muted-foreground" }),
5050
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: t2("financial.invoiceForm.selectUnitTitle") }),
5051
- /* @__PURE__ */ jsx("button", { onClick: () => setOpen(false), className: "ml-auto text-muted-foreground hover:text-foreground", children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" }) })
5052
- ] }),
5053
- /* @__PURE__ */ jsx("div", { className: "p-2 border-b", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5", children: [
5054
- /* @__PURE__ */ jsx(Search, { className: "h-3.5 w-3.5 text-muted-foreground" }),
5055
- /* @__PURE__ */ jsx(
5056
- "input",
5057
- {
5058
- type: "text",
5059
- autoFocus: true,
5060
- value: search,
5061
- onChange: (e) => setSearch(e.target.value),
5062
- placeholder: t2("financial.invoiceForm.searchUnit"),
5063
- className: "flex-1 bg-transparent text-sm outline-none"
5064
- }
5065
- )
5066
- ] }) }),
5067
- /* @__PURE__ */ jsxs("div", { className: "max-h-56 overflow-auto py-1", children: [
5068
- /* @__PURE__ */ jsx(
5069
- "button",
5070
- {
5071
- type: "button",
5072
- onClick: () => {
5073
- onChange("");
5074
- setOpen(false);
5075
- setSearch("");
5076
- },
5077
- className: `w-full px-3 py-2.5 text-left text-sm hover:bg-muted/50 ${!value ? "bg-muted/30" : ""}`,
5078
- children: "Todas / sem unidade"
5079
- }
5080
- ),
5081
- filtered.map((loc) => /* @__PURE__ */ jsxs(
5082
- "button",
5083
- {
5084
- onClick: () => {
5085
- onChange(loc.id);
5086
- setOpen(false);
5087
- setSearch("");
5088
- },
5089
- className: `w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-muted/50 transition-colors ${loc.id === value ? "bg-muted/30" : ""}`,
5090
- children: [
5091
- /* @__PURE__ */ jsx("div", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-muted shrink-0", children: /* @__PURE__ */ jsx(Building2, { className: "h-4 w-4 text-muted-foreground" }) }),
5092
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
5093
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: loc.name }),
5094
- loc.isHQ && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: t2("financial.invoiceForm.headquarters") })
5095
- ] })
5096
- ]
5097
- },
5098
- loc.id
5099
- )),
5100
- filtered.length === 0 && /* @__PURE__ */ jsx("p", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: t2("financial.invoiceForm.noUnitsFound") })
5101
- ] })
5102
- ] })
5103
- ] });
5104
- }
5105
5081
  var formId = 1;
5106
5082
  function fid() {
5107
5083
  return `f${formId++}`;
@@ -5136,29 +5112,27 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5136
5112
  const config = useFinancialConfig();
5137
5113
  const provider = useFinancialProvider();
5138
5114
  const { currency, itemTypes: itemTypesRaw = [], locations = [] } = config;
5139
- const unitMode = useUnitMode();
5140
- const activeUnitId = useActiveUnitId();
5115
+ const { unitMode, activeUnitId, choices: unitChoices } = useWritableUnits(locations);
5141
5116
  const reachableUnits = useUnitFilter().options;
5142
- const unitChoices = unitMode ? reachableUnits : locations;
5143
- const showUnitField = unitChoices.length > 1;
5117
+ const showUnitField = unitMode ? reachableUnits.length > 1 : unitChoices.length > 1;
5144
5118
  const [giftCardsEnabled, setGiftCardsEnabled] = useState(false);
5145
5119
  useEffect(() => {
5146
- let cancelled = false;
5120
+ let cancelled2 = false;
5147
5121
  if (direction !== "credit" || !provider.giftCardsEnabled) {
5148
5122
  setGiftCardsEnabled(false);
5149
5123
  return;
5150
5124
  }
5151
5125
  void provider.giftCardsEnabled().then((enabled) => {
5152
- if (!cancelled) setGiftCardsEnabled(enabled);
5126
+ if (!cancelled2) setGiftCardsEnabled(enabled);
5153
5127
  }).catch(() => {
5154
- if (!cancelled) setGiftCardsEnabled(false);
5128
+ if (!cancelled2) setGiftCardsEnabled(false);
5155
5129
  });
5156
5130
  return () => {
5157
- cancelled = true;
5131
+ cancelled2 = true;
5158
5132
  };
5159
5133
  }, [direction, provider]);
5160
5134
  const giftCardLookup = useMemo(() => {
5161
- if (!giftCardsEnabled || !provider.getGiftCards) return null;
5135
+ if (!giftCardsEnabled || !provider.findGiftCardByCode) return null;
5162
5136
  const asOption = (card) => ({
5163
5137
  id: card.id,
5164
5138
  label: `Gift Card ${card.code.replace(/(.{4})(?=.)/g, "$1 ")}`,
@@ -5167,21 +5141,34 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5167
5141
  price: card.faceValue,
5168
5142
  data: { price: card.faceValue, code: card.code, state: card.state }
5169
5143
  });
5144
+ const resolve = async (raw) => {
5145
+ const card = await provider.findGiftCardByCode(raw);
5146
+ if (!card) throw new Error(t2("financial.giftCardLine.notFound"));
5147
+ if (card.state === "revoked") throw new Error(t2("financial.giftCardLine.revoked"));
5148
+ if (card.state === "expired") throw new Error(t2("financial.giftCardLine.expired"));
5149
+ if (card.state !== "issued") throw new Error(t2("financial.giftCardLine.alreadySold"));
5150
+ return asOption(card);
5151
+ };
5170
5152
  return {
5171
5153
  async search(query) {
5172
- const cards = await provider.getGiftCards({ search: query.trim() || void 0, state: "issued" });
5173
- return cards.map(asOption);
5154
+ try {
5155
+ return [await resolve(query)];
5156
+ } catch {
5157
+ return [];
5158
+ }
5174
5159
  },
5175
- async getById(id) {
5176
- const cards = await provider.getGiftCards({ state: "issued" });
5177
- const card = cards.find((entry) => entry.id === id);
5178
- return card ? asOption(card) : null;
5160
+ async getById() {
5161
+ return null;
5179
5162
  },
5180
- async list() {
5181
- return (await provider.getGiftCards({ state: "issued" })).map(asOption);
5163
+ byCode: {
5164
+ resolve,
5165
+ label: t2("financial.giftCardLine.label"),
5166
+ placeholder: t2("financial.giftCardLine.placeholder"),
5167
+ hint: t2("financial.giftCardLine.hint"),
5168
+ display: ({ description }) => description.replace(/^Gift Card\s+/, "")
5182
5169
  }
5183
5170
  };
5184
- }, [giftCardsEnabled, provider]);
5171
+ }, [giftCardsEnabled, provider, t2]);
5185
5172
  const entityLookups = useMemo(() => giftCardLookup ? { ...config.entityLookups, gift_card: giftCardLookup } : config.entityLookups, [config.entityLookups, giftCardLookup]);
5186
5173
  const translatedItemTypes = React25__default.useMemo(
5187
5174
  () => itemTypesRaw.map((tp) => {
@@ -5210,6 +5197,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5210
5197
  const createInvoice = useFinancialStore((s) => s.createInvoice);
5211
5198
  const guardMovements = useLimitGuard("movements_month");
5212
5199
  const can = usePermissionOptional();
5200
+ const canManageFinancial = can("financial", "manage");
5213
5201
  const isEdit = !!editId;
5214
5202
  const [attachmentsOpen, setAttachmentsOpen] = useState(false);
5215
5203
  useEffect(() => {
@@ -5217,15 +5205,15 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5217
5205
  void fetchPaymentMethods();
5218
5206
  }, []);
5219
5207
  useEffect(() => {
5220
- let cancelled = false;
5208
+ let cancelled2 = false;
5221
5209
  void Promise.all([provider.getChartOfAccounts(), provider.getCostCenters()]).then(([accountRows, costCenterRows]) => {
5222
- if (cancelled) return;
5210
+ if (cancelled2) return;
5223
5211
  setAccounts(accountRows);
5224
5212
  setCostCenters(costCenterRows);
5225
5213
  }).catch(() => {
5226
5214
  });
5227
5215
  return () => {
5228
- cancelled = true;
5216
+ cancelled2 = true;
5229
5217
  };
5230
5218
  }, [provider]);
5231
5219
  const contactLookup = useMemo(
@@ -5271,6 +5259,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5271
5259
  const [expandedItemId, setExpandedItemId] = useState(null);
5272
5260
  const [addingItem, setAddingItem] = useState(false);
5273
5261
  const [installments, setInstallments] = useState([]);
5262
+ const [cancelled, setCancelled] = useState(false);
5263
+ const [savedTotal, setSavedTotal] = useState(null);
5274
5264
  const [savedMovements, setSavedMovements] = useState([]);
5275
5265
  const [payingMovement, setPayingMovement] = useState(null);
5276
5266
  const [pendingReceipt, setPendingReceipt] = useState();
@@ -5352,6 +5342,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5352
5342
  setInvoiceDate(invoice.invoiceDate);
5353
5343
  setSavedInvoiceDate(invoice.invoiceDate);
5354
5344
  setSavedContactId(invoice.contactId);
5345
+ setCancelled(invoice.status === "cancelled");
5346
+ setSavedTotal(invoice.totalAmount);
5355
5347
  setFiscalNumber(String(invoice.metadata?.fiscalNumber ?? ""));
5356
5348
  setObservations(invoice.observations ?? "");
5357
5349
  const invoiceItems = await provider.getInvoiceItems(editId);
@@ -5399,7 +5391,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5399
5391
  return sum + it.quantity * it.unitPrice - (it.discount ?? 0) + (it.surcharge ?? 0);
5400
5392
  }, 0);
5401
5393
  const itemsDirty = isEdit && loaded && itemsKey(items) !== savedItemsKey;
5402
- const scheduleTotal = isEdit && savedMovements.length > 0 && !itemsDirty ? Math.round(savedMovements.reduce((sum, m) => sum + m.amount, 0) * 100) / 100 : totalAmount;
5394
+ const scheduleTotal = isEdit && savedMovements.length > 0 && !itemsDirty ? savedTotal ?? Math.round(savedMovements.reduce((sum, m) => sum + m.amount, 0) * 100) / 100 : totalAmount;
5403
5395
  const prevItemCount = useRef(items.length);
5404
5396
  useEffect(() => {
5405
5397
  if (items.length > 0 && prevItemCount.current === 0 && installments.length === 0 && !isEdit) {
@@ -5412,7 +5404,11 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5412
5404
  }, [items.length]);
5413
5405
  const somaParcelas = Math.round(installments.reduce((s, i) => s + i.amount, 0) * 100);
5414
5406
  useEffect(() => {
5415
- if (installments.length === 0 || scheduleTotal <= 0) return;
5407
+ if (installments.length === 0) return;
5408
+ if (scheduleTotal <= 0) {
5409
+ if (!isEdit) setInstallments([]);
5410
+ return;
5411
+ }
5416
5412
  if (somaParcelas === Math.round(scheduleTotal * 100)) return;
5417
5413
  const count = installments.length;
5418
5414
  const perInstallment = Math.floor(scheduleTotal * 100 / count) / 100;
@@ -5421,7 +5417,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5421
5417
  ...inst,
5422
5418
  amount: i === 0 ? perInstallment + remainder : perInstallment
5423
5419
  })));
5424
- }, [scheduleTotal, somaParcelas, installments.length]);
5420
+ }, [scheduleTotal, somaParcelas, installments.length, isEdit]);
5425
5421
  function addItemOfKind(kind) {
5426
5422
  const newItem = {
5427
5423
  _id: fid(),
@@ -5623,6 +5619,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5623
5619
  if (isEdit && editId) {
5624
5620
  if (canEditItems && provider.replaceInvoiceItems && itemsDirty) {
5625
5621
  await provider.replaceInvoiceItems(editId, items.map(({ _id, ...rest }) => rest));
5622
+ setSavedTotal(Math.round(totalAmount * 100) / 100);
5626
5623
  }
5627
5624
  await provider.updateInvoice(editId, {
5628
5625
  unitId: unitId || void 0,
@@ -5762,23 +5759,26 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5762
5759
  const subtitle = isEdit ? t2("financial.invoiceForm.editing") : t2("financial.invoiceForm.creating");
5763
5760
  if (loading) return /* @__PURE__ */ jsx(OrderFormSkeleton, { parentLabel: embedded ? void 0 : direction === "debit" ? t2("financial.nav.payables") : t2("financial.nav.receivables"), onBack: embedded ? void 0 : () => onSaved?.() });
5764
5761
  const money4 = (v) => formatCurrency(v, currency);
5765
- const actions = isEdit ? /* @__PURE__ */ jsxs("div", { className: "relative", ref: menuRef, children: [
5766
- /* @__PURE__ */ jsx(Button, { variant: "outline", size: "icon", onClick: () => setMenuOpen(!menuOpen), "aria-label": "More actions", children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" }) }),
5767
- menuOpen && /* @__PURE__ */ jsx("div", { className: "fayz-glass-surface absolute right-0 top-full z-20 mt-1 w-44 rounded-lg border bg-popover py-1 shadow-md", style: { animation: "field-slide-in 150ms ease-out" }, children: /* @__PURE__ */ jsxs(
5768
- "button",
5769
- {
5770
- onClick: () => {
5771
- setConfirmCancel(true);
5772
- setMenuOpen(false);
5773
- },
5774
- className: "flex w-full items-center gap-2 px-3 py-2 text-xs text-destructive transition-colors hover:bg-destructive/10",
5775
- children: [
5776
- /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
5777
- " ",
5778
- t2("financial.invoiceForm.cancelInvoice")
5779
- ]
5780
- }
5781
- ) })
5762
+ const actions = isEdit ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
5763
+ /* @__PURE__ */ jsx(WidgetSlot, { zone: FINANCIAL_INVOICE_ZONES.headerActions, invoiceId: editId, direction, cancelled }),
5764
+ /* @__PURE__ */ jsxs("div", { className: "relative", ref: menuRef, children: [
5765
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "icon", onClick: () => setMenuOpen(!menuOpen), "aria-label": "More actions", children: /* @__PURE__ */ jsx(MoreVertical, { className: "h-4 w-4" }) }),
5766
+ menuOpen && /* @__PURE__ */ jsx("div", { className: "fayz-glass-surface absolute right-0 top-full z-20 mt-1 w-44 rounded-lg border bg-popover py-1 shadow-md", style: { animation: "field-slide-in 150ms ease-out" }, children: /* @__PURE__ */ jsxs(
5767
+ "button",
5768
+ {
5769
+ onClick: () => {
5770
+ setConfirmCancel(true);
5771
+ setMenuOpen(false);
5772
+ },
5773
+ className: "flex w-full items-center gap-2 px-3 py-2 text-xs text-destructive transition-colors hover:bg-destructive/10",
5774
+ children: [
5775
+ /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
5776
+ " ",
5777
+ t2("financial.invoiceForm.cancelInvoice")
5778
+ ]
5779
+ }
5780
+ ) })
5781
+ ] })
5782
5782
  ] }) : null;
5783
5783
  const paidSoFar = savedMovements.reduce((sum, m) => sum + (m.paidAmount ?? 0), 0);
5784
5784
  const hasPayments = paidSoFar > 0;
@@ -5966,7 +5966,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5966
5966
  ] }) });
5967
5967
  const sideSections = /* @__PURE__ */ jsxs(Fragment, { children: [
5968
5968
  /* @__PURE__ */ jsx(OrderFormBlock, { title: t2("financial.invoice.details"), children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
5969
- showUnitField && /* @__PURE__ */ jsx(LocationSelector, { locations: unitChoices, value: unitId, onChange: setUnitId }),
5969
+ showUnitField && /* @__PURE__ */ jsx(UnitField, { choices: unitChoices, value: unitId, onChange: setUnitId }),
5970
5970
  /* @__PURE__ */ jsxs("div", { children: [
5971
5971
  /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.invoiceForm.date") }),
5972
5972
  isEdit && hasPayments ? /* @__PURE__ */ jsx("p", { className: "mt-1 px-1 py-2 text-sm", children: (/* @__PURE__ */ new Date(`${invoiceDate}T00:00:00`)).toLocaleDateString() }) : /* @__PURE__ */ jsx(DatePicker, { value: invoiceDate, onChange: setInvoiceDate, className: "mt-1" })
@@ -6131,7 +6131,10 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6131
6131
  label: accountLabel(a),
6132
6132
  subtitle: a.accountType ?? void 0
6133
6133
  })),
6134
- onChange: (id) => updateItem(item._id, { accountId: id || void 0 })
6134
+ onChange: (id) => updateItem(item._id, { accountId: id || void 0 }),
6135
+ onManage: canManageFinancial ? () => adminNavigateTo(CHART_OF_ACCOUNTS_ROUTE) : void 0,
6136
+ manageLabel: t2("financial.invoiceForm.manageChart"),
6137
+ clearLabel: t2("financial.invoiceForm.clear")
6135
6138
  }
6136
6139
  )
6137
6140
  )
@@ -6154,7 +6157,10 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6154
6157
  id: c.id,
6155
6158
  label: c.code ? `${c.code} - ${c.name}` : c.name
6156
6159
  })),
6157
- onChange: (id) => updateItem(item._id, { costCenterId: id || void 0 })
6160
+ onChange: (id) => updateItem(item._id, { costCenterId: id || void 0 }),
6161
+ onManage: canManageFinancial ? () => adminNavigateTo(COST_CENTERS_ROUTE) : void 0,
6162
+ manageLabel: t2("financial.invoiceForm.manageCostCenters"),
6163
+ clearLabel: t2("financial.invoiceForm.clear")
6158
6164
  }
6159
6165
  )
6160
6166
  }] : []
@@ -6655,11 +6661,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6655
6661
  ...totalRemaining > 0 ? [{ label: t2("financial.invoice.balance"), value: formatCurrency(totalRemaining, currency) }] : []
6656
6662
  ];
6657
6663
  direction === "debit" ? t2("financial.invoice.accountsPayable") : t2("financial.invoice.accountsReceivable");
6658
- const actions = (
6659
- // Declared: the shell gives it a caret on desktop and the FAB on a phone.
6660
- // This was an outline button beside a hand-rolled popup — its own useRef
6661
- // outside-click listener and an inline `style={{animation}}` — sitting next
6662
- // to a Radix Dropdown that already exists.
6664
+ const actions = /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6665
+ /* @__PURE__ */ jsx(WidgetSlot, { zone: FINANCIAL_INVOICE_ZONES.headerActions, invoiceId: invoice.id, direction, cancelled }),
6663
6666
  /* @__PURE__ */ jsx(
6664
6667
  PageActions,
6665
6668
  {
@@ -6669,7 +6672,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
6669
6672
  ] : []
6670
6673
  }
6671
6674
  )
6672
- );
6675
+ ] });
6673
6676
  const sideBlocks = /* @__PURE__ */ jsxs(Fragment, { children: [
6674
6677
  invoice.bookingStartsAt && /* @__PURE__ */ jsx(OrderDetailBlock, { title: t2("financial.invoice.booking"), children: /* @__PURE__ */ jsxs(
6675
6678
  "button",
@@ -7124,12 +7127,16 @@ function CashSessionReviewModal({ sessionId, open, onOpenChange, mode, onReviewe
7124
7127
  }
7125
7128
  function handlePrint() {
7126
7129
  if (!session) return;
7127
- const popup = window.open("", "_blank", "width=720,height=900");
7130
+ const popup = openPrintWindow("width=720,height=900");
7128
7131
  if (!popup) return;
7129
7132
  const title = session.bankAccountName ?? t2("financial.cash.title");
7130
7133
  const rows = (movements ?? []).map((m) => `<tr><td>${m.paymentDate ? new Date(m.paymentDate).toLocaleString() : "\u2014"}</td><td>${m.method ? t2(`financial.method.${m.method}`) : "\u2014"}</td><td style="text-align:right">${m.direction === "credit" ? "+" : "-"}${formatCurrency(m.paidAmount, currency)}</td></tr>`).join("");
7131
- popup.document.write(`<html><head><title>${title}</title><style>body{font:12px sans-serif;padding:24px}table{width:100%;border-collapse:collapse;margin-top:16px}th,td{border-bottom:1px solid #ddd;padding:8px;text-align:left}h1{font-size:16px;margin-bottom:8px}p{margin:2px 0}</style></head><body><h1>${title}</h1><p>${t2("financial.cash.opened")}: ${new Date(session.openedAt).toLocaleString()}${session.openedByName ? ` ${t2("financial.cash.by")} ${session.openedByName}` : ""}</p>` + (session.closedAt ? `<p>${t2("financial.cash.closeLabel")}: ${new Date(session.closedAt).toLocaleString()}${session.closedByName ? ` ${t2("financial.cash.by")} ${session.closedByName}` : ""}</p>` : "") + `<p>${t2("financial.cash.openingBalance")}: ${formatCurrency(session.openingBalance, currency)}</p><p>${t2("financial.cash.finalValue")}: ${formatCurrency(session.closingBalance ?? 0, currency)}</p><p>${t2("financial.cash.difference")}: ${(session.difference ?? 0) >= 0 ? "+" : ""}${formatCurrency(session.difference ?? 0, currency)}</p><table><thead><tr><th>${t2("financial.cash.opened")}</th><th>${t2("financial.cash.method")}</th><th style="text-align:right">${t2("financial.cash.finalValue")}</th></tr></thead><tbody>${rows}</tbody></table><script>window.print();window.close()</script></body></html>`);
7132
- popup.document.close();
7134
+ printDocument({
7135
+ title,
7136
+ css: "body{font:12px sans-serif}table{width:100%;border-collapse:collapse;margin-top:16px}th,td{border-bottom:1px solid #ddd;padding:8px;text-align:left}h1{font-size:16px;margin-bottom:8px}p{margin:2px 0}",
7137
+ body: `<h1>${title}</h1><p>${t2("financial.cash.opened")}: ${new Date(session.openedAt).toLocaleString()}${session.openedByName ? ` ${t2("financial.cash.by")} ${session.openedByName}` : ""}</p>` + (session.closedAt ? `<p>${t2("financial.cash.closeLabel")}: ${new Date(session.closedAt).toLocaleString()}${session.closedByName ? ` ${t2("financial.cash.by")} ${session.closedByName}` : ""}</p>` : "") + `<p>${t2("financial.cash.openingBalance")}: ${formatCurrency(session.openingBalance, currency)}</p><p>${t2("financial.cash.finalValue")}: ${formatCurrency(session.closingBalance ?? 0, currency)}</p><p>${t2("financial.cash.difference")}: ${(session.difference ?? 0) >= 0 ? "+" : ""}${formatCurrency(session.difference ?? 0, currency)}</p><table><thead><tr><th>${t2("financial.cash.opened")}</th><th>${t2("financial.cash.method")}</th><th style="text-align:right">${t2("financial.cash.finalValue")}</th></tr></thead><tbody>${rows}</tbody></table>`,
7138
+ target: popup
7139
+ });
7133
7140
  }
7134
7141
  return /* @__PURE__ */ jsx(Modal, { open, onOpenChange, children: /* @__PURE__ */ jsxs(ModalContent, { size: "lg", noPadding: true, className: "gap-0", children: [
7135
7142
  /* @__PURE__ */ jsxs(ModalHeader, { className: "border-b px-4 py-3", children: [
@@ -9789,6 +9796,21 @@ var shortDate2 = (value) => {
9789
9796
  const [year, month, day] = value.split("-");
9790
9797
  return year && month && day ? `${day}/${month}/${year}` : value;
9791
9798
  };
9799
+ var shiftDate = (value, days) => {
9800
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00`);
9801
+ if (Number.isNaN(d.getTime())) return value;
9802
+ d.setDate(d.getDate() + days);
9803
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
9804
+ };
9805
+ var toAmountInput = (n) => round23(Math.max(0, n)).toFixed(2).replace(".", ",");
9806
+ var CAMPO_FILTRO = "h-[30px] w-full rounded-input border border-input bg-card px-2 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-1 focus:ring-ring";
9807
+ var LINK_DAYS = 3;
9808
+ var LINK_AMOUNT_TOLERANCE = 0.1;
9809
+ var WIDEN_DAYS = 15;
9810
+ var parseAmount = (value) => {
9811
+ const n = Number.parseFloat(value.replace(",", "."));
9812
+ return Number.isFinite(n) ? n : void 0;
9813
+ };
9792
9814
  var round23 = (n) => Math.round((Number(n) || 0) * 100) / 100;
9793
9815
  var emptyItem = () => ({ description: "", quantity: 1, unitPrice: 0 });
9794
9816
  function LinhaDoExtrato2({ line, fmt }) {
@@ -9813,6 +9835,35 @@ function LinhaDoExtrato2({ line, fmt }) {
9813
9835
  ] })
9814
9836
  ] });
9815
9837
  }
9838
+ function CandidatoBotao({ item, selected, onToggle, fmt, t: t2 }) {
9839
+ const { candidate: c, mode, openAmount: openAmount2 } = item;
9840
+ const parcela = c.movement.installmentNumber;
9841
+ return /* @__PURE__ */ jsxs(
9842
+ "button",
9843
+ {
9844
+ type: "button",
9845
+ onClick: onToggle,
9846
+ "aria-pressed": selected,
9847
+ className: `flex w-full items-center gap-3 rounded-md border p-2.5 text-left transition-colors ${selected ? "border-primary bg-primary/5 ring-2 ring-primary/20" : "hover:bg-muted/40"}`,
9848
+ children: [
9849
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
9850
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm", children: c.invoice?.contactName || c.movement.notes || c.invoice?.fiscalNumber || t2("financial.reconciliation.match") }),
9851
+ /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[10px] text-muted-foreground", children: [
9852
+ /* @__PURE__ */ jsx("span", { children: shortDate2(c.movement.dueDate) }),
9853
+ c.invoice?.fiscalNumber && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
9854
+ "#",
9855
+ c.invoice.fiscalNumber,
9856
+ parcela ? ` \xB7 ${parcela}` : ""
9857
+ ] }),
9858
+ c.score > 0 && /* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded bg-magic/10 px-1 py-px font-medium text-magic-soft-foreground", children: t2("financial.reconciliation.matchScore", { score: Math.round(c.score * 100) }) }),
9859
+ /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded px-1 py-px font-medium ${mode === "settle" ? "bg-warning/10 text-warning" : "bg-success/10 text-success"}`, children: mode === "settle" ? t2("financial.reconciliation.candidateOpen", { amount: fmt(openAmount2) }) : t2("financial.reconciliation.candidatePaid") })
9860
+ ] })
9861
+ ] }),
9862
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold", children: fmt(c.movement.amount) })
9863
+ ]
9864
+ }
9865
+ );
9866
+ }
9816
9867
  function EscolhaCard({ icon: Icon, title, hint, active, onClick }) {
9817
9868
  return /* @__PURE__ */ jsxs(
9818
9869
  "button",
@@ -9831,17 +9882,18 @@ function EscolhaCard({ icon: Icon, title, hint, active, onClick }) {
9831
9882
  }
9832
9883
  );
9833
9884
  }
9834
- function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9885
+ function ReconcileFlow({ line, initialIntent, initialTarget, initialStep = 0, onClose, onDone }) {
9835
9886
  const t2 = useTranslation();
9836
9887
  const { currency } = useFinancialConfig();
9837
9888
  const provider = useFinancialProvider();
9838
9889
  const fmt = useCallback((n) => formatCurrency(n, currency), [currency]);
9839
- const [step, setStep] = useState(0);
9890
+ const [step, setStep] = useState(initialStep);
9840
9891
  const [intent, setIntent] = useState(initialIntent ?? null);
9841
9892
  const [saving, setSaving] = useState(false);
9842
9893
  const [candidates, setCandidates] = useState([]);
9843
9894
  const [candidatesLoading, setCandidatesLoading] = useState(false);
9844
- const [targetId, setTargetId] = useState(null);
9895
+ const [target, setTarget] = useState(initialTarget ?? null);
9896
+ const targetId = target?.candidate.movement.id ?? null;
9845
9897
  const [adjustments, setAdjustments] = useState([]);
9846
9898
  const [contactId, setContactId] = useState("");
9847
9899
  const [contactName, setContactName] = useState(line.counterpartyName ?? "");
@@ -9864,7 +9916,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9864
9916
  void provider.getBankAccounts().then(setContas).catch(() => setContas([]));
9865
9917
  }, [provider, line.bankAccountId]);
9866
9918
  useEffect(() => {
9867
- if (intent !== "link" || !provider.suggestReconciliation) return;
9919
+ if (intent !== "link" || !provider.suggestReconciliation || provider.searchReconciliationTargets) return;
9868
9920
  let cancelled = false;
9869
9921
  setCandidatesLoading(true);
9870
9922
  provider.suggestReconciliation(line.id).then((raw) => {
@@ -9878,10 +9930,72 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9878
9930
  cancelled = true;
9879
9931
  };
9880
9932
  }, [intent, provider, line.id]);
9881
- const target = useMemo(
9882
- () => candidates.find((c) => c.candidate.movement.id === targetId) ?? null,
9883
- [candidates, targetId]
9884
- );
9933
+ const podeBuscar = !!provider.searchReconciliationTargets;
9934
+ const filtrosIniciais = () => ({
9935
+ dueFrom: shiftDate(line.date, -LINK_DAYS),
9936
+ dueTo: shiftDate(line.date, LINK_DAYS),
9937
+ status: "all",
9938
+ texto: "",
9939
+ min: toAmountInput(line.amount * (1 - LINK_AMOUNT_TOLERANCE)),
9940
+ max: toAmountInput(line.amount * (1 + LINK_AMOUNT_TOLERANCE))
9941
+ });
9942
+ const [rascunho, setRascunho] = useState(filtrosIniciais);
9943
+ const [aplicado, setAplicado] = useState(filtrosIniciais);
9944
+ const mudarFiltro = (patch) => setRascunho((prev) => ({ ...prev, ...patch }));
9945
+ const filtrosPendentes = JSON.stringify(rascunho) !== JSON.stringify(aplicado);
9946
+ const [resultados, setResultados] = useState([]);
9947
+ const [paginaBusca, setPaginaBusca] = useState(1);
9948
+ const [temMais, setTemMais] = useState(false);
9949
+ const [buscando, setBuscando] = useState(false);
9950
+ const buscaSeq = useRef(0);
9951
+ const buscar = useCallback(async (pagina) => {
9952
+ if (!provider.searchReconciliationTargets) return;
9953
+ const seq = ++buscaSeq.current;
9954
+ setBuscando(true);
9955
+ try {
9956
+ const res = await provider.searchReconciliationTargets(line.id, {
9957
+ dueFrom: aplicado.dueFrom || void 0,
9958
+ dueTo: aplicado.dueTo || void 0,
9959
+ status: aplicado.status,
9960
+ search: aplicado.texto.trim() || void 0,
9961
+ amountMin: parseAmount(aplicado.min),
9962
+ amountMax: parseAmount(aplicado.max),
9963
+ page: pagina
9964
+ });
9965
+ if (seq !== buscaSeq.current) return;
9966
+ const novos = resolveCandidates(res.data);
9967
+ setResultados((prev) => {
9968
+ if (pagina === 1) return novos;
9969
+ const vistos = new Set(prev.map((r) => r.candidate.movement.id));
9970
+ return [...prev, ...novos.filter((r) => !vistos.has(r.candidate.movement.id))];
9971
+ });
9972
+ setPaginaBusca(pagina);
9973
+ setTemMais(res.hasMore);
9974
+ } catch (e) {
9975
+ if (seq !== buscaSeq.current) return;
9976
+ toast.error(e instanceof Error ? e.message : t2("common.error"));
9977
+ } finally {
9978
+ if (seq === buscaSeq.current) setBuscando(false);
9979
+ }
9980
+ }, [provider, line.id, aplicado, t2]);
9981
+ const ultimaBusca = useRef(null);
9982
+ const naSelecao = intent === "link" && step === 1;
9983
+ useEffect(() => {
9984
+ if (!naSelecao || !podeBuscar) return;
9985
+ const chave = JSON.stringify(aplicado);
9986
+ if (ultimaBusca.current === chave) return;
9987
+ ultimaBusca.current = chave;
9988
+ void buscar(1);
9989
+ }, [naSelecao, podeBuscar, aplicado, buscar]);
9990
+ const contasEncontradas = useMemo(() => {
9991
+ const lista = podeBuscar ? resultados : candidates;
9992
+ return initialTarget && !lista.some((c) => c.candidate.movement.id === initialTarget.candidate.movement.id) ? [initialTarget, ...lista] : lista;
9993
+ }, [podeBuscar, resultados, candidates, initialTarget]);
9994
+ const ampliarBusca = () => {
9995
+ const amplo = { ...rascunho, dueFrom: shiftDate(line.date, -WIDEN_DAYS), dueTo: shiftDate(line.date, WIDEN_DAYS), min: "", max: "" };
9996
+ setRascunho(amplo);
9997
+ setAplicado(amplo);
9998
+ };
9885
9999
  const contaDoExtrato = useMemo(
9886
10000
  () => contas.find((a) => a.id === line.bankAccountId) ?? null,
9887
10001
  [contas, line.bankAccountId]
@@ -9906,6 +10020,15 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9906
10020
  const rows = q ? planoDoTipo.filter((a) => `${a.code ?? ""} ${a.name}`.toLocaleLowerCase("pt-BR").includes(q)) : planoDoTipo;
9907
10021
  return rows.slice(0, 50).map((a) => ({ id: a.id, label: accountLabel(a) }));
9908
10022
  }, [planoDoTipo]);
10023
+ const descricaoPadrao = (type) => type === "interest" ? t2("financial.reconciliation.adjustmentDefaultInterest") : type === "fine" ? t2("financial.reconciliation.adjustmentDefaultFine") : isOut ? t2("financial.reconciliation.adjustmentDefaultDiscountOut") : t2("financial.reconciliation.adjustmentDefaultDiscountIn");
10024
+ const buscaPlanoDoAjuste = useCallback((adj, query) => {
10025
+ const natureza = adjustmentDirection(line.direction, adj.type) === "debit" ? "expense" : "revenue";
10026
+ const folhas = postableAccounts(chart);
10027
+ const doTipo = folhas.filter((a) => a.accountType === natureza);
10028
+ const base = doTipo.length > 0 ? doTipo : folhas;
10029
+ const q = query.trim().toLocaleLowerCase("pt-BR");
10030
+ return (q ? base.filter((a) => `${a.code ?? ""} ${a.name}`.toLocaleLowerCase("pt-BR").includes(q)) : base).slice(0, 50).map((a) => ({ id: a.id, label: accountLabel(a) }));
10031
+ }, [chart, line.direction]);
9909
10032
  const valorAQuitar = intent === "link" && target ? target.openAmount : round23(line.amount);
9910
10033
  const quita = intent === "bill" || intent === "link" && target?.mode === "settle";
9911
10034
  const leitor = useMemo(() => {
@@ -9942,8 +10065,9 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9942
10065
  const somaItens = sumItems(items.filter((i) => i.description.trim()));
9943
10066
  const erroItens = itemsMismatch(items, line.amount, fmt);
9944
10067
  const gap = target ? adjustmentGap(line.amount, target.openAmount) : 0;
9945
- const erroAjuste = intent === "link" && target?.mode === "settle" && Math.abs(gap) >= 0.01 ? adjustmentsMismatch(adjustments, gap, fmt) : null;
9946
- const bloqueioPasso1 = !intent ? t2("financial.reconciliation.pickIntent") : intent === "link" && !target ? t2("financial.reconciliation.pickCandidate") : null;
10068
+ const erroAjuste = intent === "link" && target?.mode === "settle" && Math.abs(gap) >= 0.01 ? adjustmentsMismatch(adjustments, gap, fmt) ?? (adjustments.some((a) => round23(a.amount) > 0 && !a.accountId) ? t2("financial.reconciliation.adjustmentNeedsAccount") : null) : null;
10069
+ const bloqueioPasso1 = !intent ? t2("financial.reconciliation.pickIntent") : null;
10070
+ const bloqueioSelecao = intent === "link" && !target ? t2("financial.reconciliation.pickCandidate") : null;
9947
10071
  const bloqueioPasso2 = intent === "transfer" ? counterAccountId ? null : t2("financial.reconciliation.pickAccount") : intent === "bill" ? description.trim() || items.some((i) => i.description.trim()) ? erroItens : t2("financial.reconciliation.needDescription") : erroAjuste;
9948
10072
  const bloqueioPasso3 = saving ? t2("common.saving") : null;
9949
10073
  async function confirmar() {
@@ -10070,37 +10194,148 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10070
10194
  onClick: () => setIntent("transfer")
10071
10195
  }
10072
10196
  )
10197
+ ] })
10198
+ ] });
10199
+ const statusDaConta = (item) => item.mode === "link_only" ? t2("financial.reconciliation.searchStatusPaid") : item.candidate.movement.status === "overdue" ? t2("financial.reconciliation.statusOverdue") : t2("financial.reconciliation.searchStatusOpen");
10200
+ const valorAConciliar = (item) => item.mode === "link_only" ? item.candidate.movement.amount : item.openAmount;
10201
+ const parcelaDe = (item) => {
10202
+ const n = item.candidate.movement.installmentNumber ?? 1;
10203
+ return item.candidate.installmentCount ? `${n}/${item.candidate.installmentCount}` : String(n);
10204
+ };
10205
+ const filtrar = () => {
10206
+ if (filtrosPendentes) setAplicado(rascunho);
10207
+ else void buscar(1);
10208
+ };
10209
+ const passoSelecionar = /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-3", children: [
10210
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
10211
+ t2("financial.reconciliation.selectBillFor", {
10212
+ date: shortDate2(line.date),
10213
+ description: line.description || line.externalId
10214
+ }),
10215
+ " ",
10216
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: fmt(line.amount) })
10073
10217
  ] }),
10074
- intent === "link" && /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-card border p-2.5", children: [
10075
- /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: t2("financial.reconciliation.candidates") }),
10076
- candidatesLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [
10077
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full rounded-md" }),
10078
- /* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-full rounded-md" })
10079
- ] }) : candidates.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-4 text-center text-xs text-muted-foreground", children: t2("financial.reconciliation.noCandidates") }) : candidates.map(({ candidate: c, mode, openAmount: openAmount2 }) => {
10080
- const escolhido = targetId === c.movement.id;
10081
- return /* @__PURE__ */ jsxs(
10082
- "button",
10218
+ podeBuscar && /* @__PURE__ */ jsxs(
10219
+ FilterPanel,
10220
+ {
10221
+ title: t2("financial.reconciliation.filtersTitle"),
10222
+ dirty: filtrosPendentes,
10223
+ onApply: filtrar,
10224
+ applyLabel: t2("financial.reconciliation.filter"),
10225
+ pendingTitle: t2("financial.reconciliation.filterPending"),
10226
+ pendingHint: t2("financial.reconciliation.searchPendingHint"),
10227
+ busy: buscando,
10228
+ moreLabel: t2("financial.reconciliation.filterMore"),
10229
+ lessLabel: t2("financial.reconciliation.filterLess"),
10230
+ advancedCount: rascunho.status !== "all" ? 1 : 0,
10231
+ advanced: /* @__PURE__ */ jsx(FilterPanelField, { label: t2("financial.reconciliation.searchStatus"), children: /* @__PURE__ */ jsx(
10232
+ SegmentedControl,
10083
10233
  {
10084
- type: "button",
10085
- onClick: () => setTargetId(escolhido ? null : c.movement.id),
10086
- "aria-pressed": escolhido,
10087
- className: `flex w-full items-center gap-3 rounded-md border p-2.5 text-left transition-colors ${escolhido ? "border-primary bg-primary/5 ring-2 ring-primary/20" : "hover:bg-muted/40"}`,
10088
- children: [
10089
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
10090
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm", children: c.invoice?.contactName || c.movement.notes || c.invoice?.fiscalNumber || t2("financial.reconciliation.match") }),
10091
- /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
10092
- shortDate2(c.movement.dueDate),
10093
- /* @__PURE__ */ jsx("span", { className: "ml-1.5 inline-flex items-center rounded bg-magic/10 px-1 py-px font-medium text-magic-soft-foreground", children: t2("financial.reconciliation.matchScore", { score: Math.round(c.score * 100) }) }),
10094
- /* @__PURE__ */ jsx("span", { className: `ml-1.5 inline-flex items-center rounded px-1 py-px font-medium ${mode === "settle" ? "bg-warning/10 text-warning" : "bg-success/10 text-success"}`, children: mode === "settle" ? t2("financial.reconciliation.candidateOpen", { amount: fmt(openAmount2) }) : t2("financial.reconciliation.candidatePaid") })
10095
- ] })
10096
- ] }),
10097
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold", children: fmt(c.movement.amount) })
10234
+ "aria-label": t2("financial.reconciliation.searchStatus"),
10235
+ value: rascunho.status,
10236
+ onChange: (v) => mudarFiltro({ status: v }),
10237
+ options: [
10238
+ { value: "all", label: t2("financial.reconciliation.searchStatusAllShort") },
10239
+ { value: "open", label: t2("financial.reconciliation.searchStatusOpen") },
10240
+ { value: "paid", label: t2("financial.reconciliation.searchStatusPaid") }
10098
10241
  ]
10099
- },
10100
- c.movement.id
10101
- );
10102
- })
10103
- ] })
10242
+ }
10243
+ ) }),
10244
+ footer: /* @__PURE__ */ jsx(
10245
+ Input,
10246
+ {
10247
+ value: rascunho.texto,
10248
+ placeholder: t2("financial.reconciliation.searchText"),
10249
+ "aria-label": t2("financial.reconciliation.searchText"),
10250
+ onChange: (e) => mudarFiltro({ texto: e.target.value }),
10251
+ onKeyDown: (e) => {
10252
+ if (e.key !== "Enter") return;
10253
+ e.preventDefault();
10254
+ filtrar();
10255
+ }
10256
+ }
10257
+ ),
10258
+ children: [
10259
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("financial.reconciliation.searchDueFrom"), children: /* @__PURE__ */ jsx("input", { type: "date", value: rascunho.dueFrom, onChange: (e) => mudarFiltro({ dueFrom: e.target.value }), className: CAMPO_FILTRO }) }),
10260
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("financial.reconciliation.searchDueTo"), children: /* @__PURE__ */ jsx("input", { type: "date", value: rascunho.dueTo, onChange: (e) => mudarFiltro({ dueTo: e.target.value }), className: CAMPO_FILTRO }) }),
10261
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("financial.reconciliation.searchAmountMin"), children: /* @__PURE__ */ jsx("input", { inputMode: "decimal", placeholder: "0,00", value: rascunho.min, onChange: (e) => mudarFiltro({ min: e.target.value }), className: CAMPO_FILTRO }) }),
10262
+ /* @__PURE__ */ jsx(FilterPanelField, { label: t2("financial.reconciliation.searchAmountMax"), children: /* @__PURE__ */ jsx("input", { inputMode: "decimal", placeholder: "0,00", value: rascunho.max, onChange: (e) => mudarFiltro({ max: e.target.value }), className: CAMPO_FILTRO }) })
10263
+ ]
10264
+ }
10265
+ ),
10266
+ (buscando || candidatesLoading) && contasEncontradas.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
10267
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full rounded-md" }),
10268
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full rounded-md" })
10269
+ ] }) : contasEncontradas.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-card border py-6 text-center", children: [
10270
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.reconciliation.searchEmpty") }),
10271
+ podeBuscar && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: ampliarBusca, children: t2("financial.reconciliation.searchWiden") })
10272
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
10273
+ /* @__PURE__ */ jsx("div", { className: "space-y-2 sm:hidden", children: contasEncontradas.map((item) => /* @__PURE__ */ jsx(
10274
+ CandidatoBotao,
10275
+ {
10276
+ item,
10277
+ selected: targetId === item.candidate.movement.id,
10278
+ onToggle: () => setTarget(targetId === item.candidate.movement.id ? null : item),
10279
+ fmt,
10280
+ t: t2
10281
+ },
10282
+ item.candidate.movement.id
10283
+ )) }),
10284
+ /* @__PURE__ */ jsx("div", { className: "hidden overflow-hidden rounded-card border sm:block", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
10285
+ /* @__PURE__ */ jsx("thead", { className: "bg-muted/50 text-left text-[11px] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
10286
+ /* @__PURE__ */ jsx("th", { className: "w-8 p-2" }),
10287
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colDue") }),
10288
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colCounterparty") }),
10289
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colDescription") }),
10290
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colInstallment") }),
10291
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.searchStatus") }),
10292
+ /* @__PURE__ */ jsx("th", { className: "p-2 text-right font-medium", children: t2("financial.reconciliation.colToReconcile") }),
10293
+ /* @__PURE__ */ jsx("th", { className: "p-2 text-right font-medium", children: t2("financial.reconciliation.colDelta") })
10294
+ ] }) }),
10295
+ /* @__PURE__ */ jsx("tbody", { children: contasEncontradas.map((item) => {
10296
+ const c = item.candidate;
10297
+ const escolhido = targetId === c.movement.id;
10298
+ const valor = valorAConciliar(item);
10299
+ const delta = round23(line.amount - valor);
10300
+ return /* @__PURE__ */ jsxs(
10301
+ "tr",
10302
+ {
10303
+ onClick: () => setTarget(escolhido ? null : item),
10304
+ className: `cursor-pointer border-t transition-colors ${escolhido ? "bg-primary/10" : "hover:bg-muted/40"}`,
10305
+ children: [
10306
+ /* @__PURE__ */ jsx("td", { className: "p-2", children: /* @__PURE__ */ jsx(
10307
+ "input",
10308
+ {
10309
+ type: "radio",
10310
+ readOnly: true,
10311
+ checked: escolhido,
10312
+ "aria-label": c.invoice?.contactName ?? c.movement.notes ?? c.movement.id
10313
+ }
10314
+ ) }),
10315
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2", children: shortDate2(c.movement.dueDate) }),
10316
+ /* @__PURE__ */ jsx("td", { className: "max-w-[12rem] truncate p-2 font-medium", title: c.invoice?.contactName, children: c.invoice?.contactName || "\u2014" }),
10317
+ /* @__PURE__ */ jsxs("td", { className: "max-w-[14rem] truncate p-2", title: c.movement.notes, children: [
10318
+ c.invoice?.fiscalNumber && /* @__PURE__ */ jsxs("span", { className: "mr-1 text-muted-foreground", children: [
10319
+ "#",
10320
+ c.invoice.fiscalNumber
10321
+ ] }),
10322
+ c.movement.notes
10323
+ ] }),
10324
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2", children: parcelaDe(item) }),
10325
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2", children: /* @__PURE__ */ jsx("span", { className: `rounded px-1.5 py-0.5 text-[10px] font-medium ${item.mode === "link_only" ? "bg-success/10 text-success" : c.movement.status === "overdue" ? "bg-destructive/10 text-destructive" : "bg-muted text-foreground"}`, children: statusDaConta(item) }) }),
10326
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2 text-right font-semibold", children: fmt(valor) }),
10327
+ /* @__PURE__ */ jsx("td", { className: `whitespace-nowrap p-2 text-right text-[11px] ${Math.abs(delta) < 0.01 ? "text-success" : "text-destructive"}`, children: Math.abs(delta) < 0.01 ? "OK" : `${delta > 0 ? "+" : "\u2212"}${fmt(Math.abs(delta))}` })
10328
+ ]
10329
+ },
10330
+ c.movement.id
10331
+ );
10332
+ }) })
10333
+ ] }) })
10334
+ ] }),
10335
+ temMais && /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "ghost", disabled: buscando, onClick: () => void buscar(paginaBusca + 1), children: [
10336
+ buscando && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin", "aria-hidden": true }),
10337
+ t2("financial.reconciliation.searchMore")
10338
+ ] }) })
10104
10339
  ] });
10105
10340
  const passoDetalhes = /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-3", children: [
10106
10341
  intent === "bill" && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -10152,30 +10387,28 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10152
10387
  onChange: (e) => setItems((prev) => prev.map((x, i) => i === idx ? { ...x, description: e.target.value } : x))
10153
10388
  }
10154
10389
  ) }),
10155
- /* @__PURE__ */ jsx(
10390
+ /* @__PURE__ */ jsx("div", { className: "w-16 shrink-0", children: /* @__PURE__ */ jsx(
10156
10391
  Input,
10157
10392
  {
10158
10393
  "aria-label": t2("financial.reconciliation.itemQuantity"),
10159
- className: "w-16 shrink-0",
10160
10394
  type: "number",
10161
10395
  min: "0",
10162
10396
  step: "0.001",
10163
10397
  value: item.quantity,
10164
10398
  onChange: (e) => setItems((prev) => prev.map((x, i) => i === idx ? { ...x, quantity: Number(e.target.value) } : x))
10165
10399
  }
10166
- ),
10167
- /* @__PURE__ */ jsx(
10400
+ ) }),
10401
+ /* @__PURE__ */ jsx("div", { className: "w-24 shrink-0", children: /* @__PURE__ */ jsx(
10168
10402
  Input,
10169
10403
  {
10170
10404
  "aria-label": t2("financial.reconciliation.itemUnitPrice"),
10171
- className: "w-24 shrink-0",
10172
10405
  type: "number",
10173
10406
  min: "0",
10174
10407
  step: "0.01",
10175
10408
  value: item.unitPrice,
10176
10409
  onChange: (e) => setItems((prev) => prev.map((x, i) => i === idx ? { ...x, unitPrice: Number(e.target.value) } : x))
10177
10410
  }
10178
- ),
10411
+ ) }),
10179
10412
  /* @__PURE__ */ jsx(Hint, { label: t2("common.delete"), children: /* @__PURE__ */ jsx(
10180
10413
  Button,
10181
10414
  {
@@ -10276,58 +10509,86 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10276
10509
  ] })
10277
10510
  ] }),
10278
10511
  target.mode === "link_only" ? /* @__PURE__ */ jsx("p", { className: "rounded-card border border-success/40 bg-success/5 px-3 py-2.5 text-[11px] leading-snug", children: t2("financial.reconciliation.linkOnlyExplain") }) : Math.abs(gap) < 0.01 ? /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.reconciliation.noAdjustmentNeeded") }) : /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
10279
- /* @__PURE__ */ jsx("p", { className: "rounded-card border border-warning/40 bg-warning/5 px-3 py-2 text-[11px] leading-snug", children: t2("financial.reconciliation.adjustmentNeeded", {
10512
+ /* @__PURE__ */ jsx("p", { className: "rounded-card border border-warning/40 bg-warning/5 px-3 py-2 text-[11px] leading-snug", children: t2(isOut ? "financial.reconciliation.adjustmentNeeded" : "financial.reconciliation.adjustmentNeededIn", {
10280
10513
  gap: fmt(Math.abs(gap)),
10281
10514
  side: gap > 0 ? t2("financial.reconciliation.adjustmentMore") : t2("financial.reconciliation.adjustmentLess")
10282
10515
  }) }),
10283
- adjustments.map((adj, idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-1.5", children: [
10284
- /* @__PURE__ */ jsx("div", { className: "w-28 shrink-0", children: /* @__PURE__ */ jsxs(
10285
- Select,
10286
- {
10287
- value: adj.type,
10288
- onValueChange: (v) => setAdjustments((prev) => prev.map((x, i) => i === idx ? { ...x, type: v } : x)),
10289
- children: [
10290
- /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
10291
- /* @__PURE__ */ jsxs(SelectContent, { children: [
10292
- /* @__PURE__ */ jsx(SelectItem, { value: "interest", children: t2("financial.reconciliation.interest") }),
10293
- /* @__PURE__ */ jsx(SelectItem, { value: "fine", children: t2("financial.reconciliation.fine") }),
10294
- /* @__PURE__ */ jsx(SelectItem, { value: "discount", children: t2("financial.reconciliation.discount") })
10295
- ] })
10296
- ]
10297
- }
10298
- ) }),
10299
- /* @__PURE__ */ jsx(
10300
- Input,
10301
- {
10302
- "aria-label": t2("financial.reconciliation.adjustmentAmount"),
10303
- className: "w-24 shrink-0",
10304
- type: "number",
10305
- min: "0",
10306
- step: "0.01",
10307
- value: adj.amount,
10308
- onChange: (e) => setAdjustments((prev) => prev.map((x, i) => i === idx ? { ...x, amount: Number(e.target.value) } : x))
10309
- }
10310
- ),
10311
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
10312
- Input,
10313
- {
10314
- "aria-label": t2("financial.reconciliation.adjustmentDescription"),
10315
- placeholder: t2("financial.reconciliation.adjustmentDescription"),
10316
- value: adj.description ?? "",
10317
- onChange: (e) => setAdjustments((prev) => prev.map((x, i) => i === idx ? { ...x, description: e.target.value } : x))
10318
- }
10319
- ) }),
10320
- /* @__PURE__ */ jsx(Hint, { label: t2("common.delete"), children: /* @__PURE__ */ jsx(
10321
- Button,
10322
- {
10323
- size: "sm",
10324
- variant: "ghost",
10325
- "aria-label": t2("common.delete"),
10326
- onClick: () => setAdjustments((prev) => prev.filter((_, i) => i !== idx)),
10327
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5", "aria-hidden": true })
10328
- }
10329
- ) })
10330
- ] }, idx)),
10516
+ adjustments.map((adj, idx) => {
10517
+ const mudar = (patch) => setAdjustments((prev) => prev.map((x, i) => i === idx ? { ...x, ...patch } : x));
10518
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-card border p-2.5", children: [
10519
+ /* @__PURE__ */ jsxs("div", { className: "grid min-w-0 flex-1 items-end gap-2 sm:grid-cols-2", children: [
10520
+ /* @__PURE__ */ jsxs("div", { children: [
10521
+ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t2("financial.reconciliation.adjustmentType") }),
10522
+ /* @__PURE__ */ jsxs(
10523
+ Select,
10524
+ {
10525
+ value: adj.type,
10526
+ onValueChange: (v) => {
10527
+ const type = v;
10528
+ const escrita = adj.description?.trim() && adj.description !== descricaoPadrao(adj.type);
10529
+ mudar(escrita ? { type } : { type, description: descricaoPadrao(type) });
10530
+ },
10531
+ children: [
10532
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
10533
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
10534
+ /* @__PURE__ */ jsx(SelectItem, { value: "interest", children: t2("financial.reconciliation.interest") }),
10535
+ /* @__PURE__ */ jsx(SelectItem, { value: "fine", children: t2("financial.reconciliation.fine") }),
10536
+ /* @__PURE__ */ jsx(SelectItem, { value: "discount", children: t2("financial.reconciliation.discount") })
10537
+ ] })
10538
+ ]
10539
+ }
10540
+ )
10541
+ ] }),
10542
+ /* @__PURE__ */ jsx(
10543
+ CurrencyInput,
10544
+ {
10545
+ label: t2("financial.reconciliation.adjustmentAmount"),
10546
+ value: adj.amount,
10547
+ onChange: (v) => mudar({ amount: v }),
10548
+ symbol: currency.symbol,
10549
+ locale: currency.locale,
10550
+ currencyCode: currency.code
10551
+ }
10552
+ ),
10553
+ /* @__PURE__ */ jsx(
10554
+ SearchSelect,
10555
+ {
10556
+ label: `${t2("financial.reconciliation.chartAccount")} *`,
10557
+ value: adj.accountId ?? "",
10558
+ displayValue: chart.find((a) => a.id === adj.accountId)?.name,
10559
+ placeholder: t2("financial.reconciliation.chartAccountPlaceholder"),
10560
+ minChars: 0,
10561
+ loadOnOpen: true,
10562
+ noResultsLabel: t2("financial.reconciliation.chartAccountNone"),
10563
+ clearLabel: "Limpar",
10564
+ onSearch: (q) => buscaPlanoDoAjuste(adj, q),
10565
+ onChange: (id) => mudar({ accountId: id || void 0 })
10566
+ }
10567
+ ),
10568
+ /* @__PURE__ */ jsxs("div", { children: [
10569
+ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t2("financial.reconciliation.adjustmentDescription") }),
10570
+ /* @__PURE__ */ jsx(
10571
+ Input,
10572
+ {
10573
+ placeholder: t2("financial.reconciliation.adjustmentDescriptionPlaceholder"),
10574
+ value: adj.description ?? "",
10575
+ onChange: (e) => mudar({ description: e.target.value })
10576
+ }
10577
+ )
10578
+ ] })
10579
+ ] }),
10580
+ /* @__PURE__ */ jsx("div", { className: "pt-6", children: /* @__PURE__ */ jsx(Hint, { label: t2("common.delete"), children: /* @__PURE__ */ jsx(
10581
+ Button,
10582
+ {
10583
+ size: "sm",
10584
+ variant: "ghost",
10585
+ "aria-label": t2("common.delete"),
10586
+ onClick: () => setAdjustments((prev) => prev.filter((_, i) => i !== idx)),
10587
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5", "aria-hidden": true })
10588
+ }
10589
+ ) }) })
10590
+ ] }, idx);
10591
+ }),
10331
10592
  /* @__PURE__ */ jsxs(
10332
10593
  Button,
10333
10594
  {
@@ -10335,6 +10596,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10335
10596
  variant: "outline",
10336
10597
  onClick: () => setAdjustments((prev) => [...prev, {
10337
10598
  type: gap > 0 ? "interest" : "discount",
10599
+ description: descricaoPadrao(gap > 0 ? "interest" : "discount"),
10338
10600
  // A diferença que ainda não foi explicada já vem no campo:
10339
10601
  // digitar de novo um número que a tela acabou de calcular é
10340
10602
  // trabalho que o produto podia poupar.
@@ -10433,6 +10695,13 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10433
10695
  ) : /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t2("financial.reconciliation.noPaymentNeeded") })
10434
10696
  ] });
10435
10697
  const motivo = (texto) => texto ? /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: texto }) : void 0;
10698
+ const paginaDetalhes = {
10699
+ id: "details",
10700
+ title: intent === "bill" ? t2("financial.reconciliation.stepItems") : intent === "link" ? t2("financial.reconciliation.stepAdjust") : t2("financial.reconciliation.stepDetails"),
10701
+ render: () => passoDetalhes,
10702
+ footer: motivo(bloqueioPasso2),
10703
+ blockedReason: bloqueioPasso2 ?? void 0
10704
+ };
10436
10705
  const pages = [
10437
10706
  {
10438
10707
  id: "classify",
@@ -10441,13 +10710,15 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10441
10710
  footer: motivo(bloqueioPasso1),
10442
10711
  blockedReason: bloqueioPasso1 ?? void 0
10443
10712
  },
10444
- {
10445
- id: "details",
10446
- title: intent === "bill" ? t2("financial.reconciliation.stepItems") : t2("financial.reconciliation.stepDetails"),
10447
- render: () => passoDetalhes,
10448
- footer: motivo(bloqueioPasso2),
10449
- blockedReason: bloqueioPasso2 ?? void 0
10450
- },
10713
+ ...intent === "link" ? [{
10714
+ id: "select",
10715
+ title: t2("financial.reconciliation.stepSelectBill"),
10716
+ render: () => passoSelecionar,
10717
+ footer: motivo(bloqueioSelecao),
10718
+ blockedReason: bloqueioSelecao ?? void 0
10719
+ }] : [],
10720
+ // Like V1: linking asks for an adjustment only when there is a gap to explain.
10721
+ ...intent !== "link" || !target || target.mode === "settle" && Math.abs(gap) >= 0.01 ? [paginaDetalhes] : [],
10451
10722
  {
10452
10723
  id: "confirm",
10453
10724
  title: t2("financial.reconciliation.stepConfirm"),
@@ -10464,7 +10735,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10464
10735
  if (!o && !saving) onClose();
10465
10736
  },
10466
10737
  mode: "steps",
10467
- size: "lg",
10738
+ size: intent === "link" && pages[step]?.id === "select" ? "4xl" : "lg",
10468
10739
  title: t2("financial.reconciliation.flowTitle"),
10469
10740
  pages,
10470
10741
  current: step,
@@ -10474,8 +10745,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10474
10745
  setStep(Math.max(0, alvo));
10475
10746
  return;
10476
10747
  }
10477
- if (step === 0 && bloqueioPasso1) return;
10478
- if (step === 1 && bloqueioPasso2) return;
10748
+ if (pages.slice(step, alvo).some((p) => p.blockedReason)) return;
10479
10749
  setStep(Math.min(pages.length - 1, alvo));
10480
10750
  },
10481
10751
  doneLabel: saving ? t2("common.saving") : t2("financial.reconciliation.confirmAction"),
@@ -10810,9 +11080,16 @@ function ReconciliationView() {
10810
11080
  t: t2,
10811
11081
  onSugerir: () => void sugerir(line),
10812
11082
  onTransferir: () => setFlow({ line, intent: "transfer" }),
10813
- onVincular: () => setFlow({ line, intent: "link" }),
11083
+ onVincular: () => setFlow({ line, intent: "link", step: 1 }),
10814
11084
  onLancarConta: () => setFlow({ line, intent: "bill" }),
10815
- onAceitarCandidato: (movId) => void vincular(line.id, movId),
11085
+ onAceitarCandidato: (cand) => {
11086
+ const [alvo] = resolveCandidates([cand]);
11087
+ if (alvo.mode === "link_only" && Math.abs(cand.movement.amount - line.amount) < 0.01) {
11088
+ void vincular(line.id, cand.movement.id);
11089
+ } else {
11090
+ setFlow({ line, intent: "link", target: alvo, step: 1 });
11091
+ }
11092
+ },
10816
11093
  onDesfazer: () => void desfazer(line.id),
10817
11094
  onVerConta: () => {
10818
11095
  const modulo = line.direction === "debit" ? "payables" : "receivables";
@@ -10842,6 +11119,8 @@ function ReconciliationView() {
10842
11119
  {
10843
11120
  line: flow.line,
10844
11121
  initialIntent: flow.intent,
11122
+ initialTarget: flow.target,
11123
+ initialStep: flow.step,
10845
11124
  onClose: () => setFlow(null),
10846
11125
  onDone: () => {
10847
11126
  setFlow(null);
@@ -10971,7 +11250,7 @@ function StatementCard({
10971
11250
  }
10972
11251
  )
10973
11252
  ] }),
10974
- /* @__PURE__ */ jsxs(Button, { size: "sm", className: "shrink-0", disabled: busy, onClick: () => onAceitarCandidato(sugestao.movement.id), children: [
11253
+ /* @__PURE__ */ jsxs(Button, { size: "sm", className: "shrink-0", disabled: busy, onClick: () => onAceitarCandidato(sugestao), children: [
10975
11254
  /* @__PURE__ */ jsx(Link2, { className: "h-3 w-3" }),
10976
11255
  t2("financial.reconciliation.match")
10977
11256
  ] })
@@ -11229,6 +11508,7 @@ function RecurringBillsList({ direction, onNavigate }) {
11229
11508
  function RecurringBillFormView({ direction, valueId, onBack }) {
11230
11509
  const provider = useFinancialProvider();
11231
11510
  const { locations, currency, itemTypes: configuredItemTypes, entityLookups } = useFinancialConfig();
11511
+ const t2 = useTranslation();
11232
11512
  const isPayable = direction === "payable";
11233
11513
  const [value, setValue] = React25__default.useState(null);
11234
11514
  const [loaded, setLoaded] = React25__default.useState(!valueId);
@@ -11260,6 +11540,7 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11260
11540
  const [isActive, setIsActive] = React25__default.useState(true);
11261
11541
  const [accounts, setAccounts] = React25__default.useState([]);
11262
11542
  const [costCenters, setCostCenters] = React25__default.useState([]);
11543
+ const { choices: unitChoices } = useWritableUnits(availableLocations);
11263
11544
  const [saving, setSaving] = React25__default.useState(false);
11264
11545
  const [ending, setEnding] = React25__default.useState(false);
11265
11546
  const [error, setError] = React25__default.useState(null);
@@ -11325,12 +11606,16 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11325
11606
  cancelled = true;
11326
11607
  };
11327
11608
  }, [provider, direction, valueId]);
11609
+ const translatedItemTypes = React25__default.useMemo(() => configuredItemTypes.map((tp) => {
11610
+ const label = t2(`financial.itemType.${tp.value}`);
11611
+ return label === `financial.itemType.${tp.value}` ? tp : { ...tp, label };
11612
+ }), [configuredItemTypes, t2]);
11328
11613
  const offeredItemTypes = React25__default.useMemo(() => availableInvoiceItemTypes(
11329
- configuredItemTypes,
11614
+ translatedItemTypes,
11330
11615
  Object.keys(entityLookups),
11331
- configuredItemTypes.find((item) => item.value === "other")?.label ?? "Outro",
11616
+ t2("financial.itemType.other"),
11332
11617
  direction
11333
- ), [configuredItemTypes, entityLookups, direction]);
11618
+ ), [translatedItemTypes, entityLookups, direction, t2]);
11334
11619
  const itemTypes = useStockedItemTypes(offeredItemTypes, entityLookups);
11335
11620
  const selectableAccounts = React25__default.useMemo(() => {
11336
11621
  const postable = postableAccounts(accounts);
@@ -11456,13 +11741,10 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11456
11741
  ] }) : void 0,
11457
11742
  sideSections: /* @__PURE__ */ jsxs(Fragment, { children: [
11458
11743
  /* @__PURE__ */ jsx(OrderFormBlock, { title: "Recorr\xEAncia", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
11459
- /* @__PURE__ */ jsx(Field2, { label: "Unidade", children: /* @__PURE__ */ jsxs("select", { value: unitId, onChange: (event) => setUnitId(event.target.value), className: "h-9 w-full rounded-input border border-input bg-card px-3 text-sm", children: [
11460
- /* @__PURE__ */ jsx("option", { value: "", children: "Todas / sem unidade" }),
11461
- availableLocations.map((location) => /* @__PURE__ */ jsx("option", { value: location.id, children: location.name }, location.id))
11462
- ] }) }),
11744
+ /* @__PURE__ */ jsx(UnitField, { label: "Unidade", choices: unitChoices, value: unitId, onChange: setUnitId }),
11463
11745
  /* @__PURE__ */ jsx(Field2, { label: "Periodicidade *", children: /* @__PURE__ */ jsx("select", { value: frequency, onChange: (event) => setFrequency(event.target.value), className: "h-9 w-full rounded-input border border-input bg-card px-3 text-sm", children: FREQUENCIES.map((item) => /* @__PURE__ */ jsx("option", { value: item.value, children: item.label }, item.value)) }) }),
11464
11746
  /* @__PURE__ */ jsx(Field2, { label: "Dia do vencimento *", children: /* @__PURE__ */ jsx(Input, { type: "number", min: 1, max: 31, value: dueDay, onChange: (event) => setDueDay(event.target.value) }) }),
11465
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
11747
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
11466
11748
  /* @__PURE__ */ jsx(Field2, { label: "In\xEDcio *", children: /* @__PURE__ */ jsx(DatePicker, { value: startsOn, onChange: setStartsOn, className: "w-full" }) }),
11467
11749
  /* @__PURE__ */ jsx(Field2, { label: "T\xE9rmino", children: /* @__PURE__ */ jsx(DatePicker, { value: endsOn, onChange: setEndsOn, className: "w-full" }) })
11468
11750
  ] }),
@@ -12424,6 +12706,10 @@ var STOPWORDS = /* @__PURE__ */ new Set([
12424
12706
  "pagamento",
12425
12707
  "pagto",
12426
12708
  "pag",
12709
+ "pago",
12710
+ "paga",
12711
+ "pgto",
12712
+ "recebimento",
12427
12713
  "boleto",
12428
12714
  "debito",
12429
12715
  "credito",
@@ -12468,15 +12754,20 @@ function nameScore(descricao, nome) {
12468
12754
  if (texto.size === 0) return 0;
12469
12755
  const achados = alvo.filter((w) => texto.has(w)).length;
12470
12756
  const razao = achados / alvo.length;
12471
- if (achados === 1 && alvo.length > 2) return razao * 0.4;
12472
- return razao;
12757
+ const nomeSet = new Set(alvo);
12758
+ const reverso = texto.size >= 2 && [...texto].every((w) => nomeSet.has(w)) ? 1 : 0;
12759
+ if (achados === 1 && alvo.length > 2) return Math.max(reverso, razao * 0.4);
12760
+ return Math.max(reverso, razao);
12473
12761
  }
12474
- function amountScore(lineAmount, movementAmount) {
12762
+ function amountScore(lineAmount, movementAmount, tolerance = 0.02) {
12475
12763
  if (lineAmount <= 0) return 0;
12476
12764
  const diff = Math.abs(movementAmount - lineAmount);
12477
- if (diff > lineAmount * 0.02 + 0.01) return 0;
12478
- return Math.max(0, 1 - diff / (lineAmount * 0.02 + 0.01));
12765
+ const limite = lineAmount * tolerance + 0.01;
12766
+ if (diff > limite) return 0;
12767
+ return Math.max(0, 1 - diff / limite);
12479
12768
  }
12769
+ var NAME_AMOUNT_TOLERANCE = 0.1;
12770
+ var STRONG_NAME = 0.5;
12480
12771
  function dateScore(lineDate, refDate) {
12481
12772
  if (!lineDate || !refDate) return 0;
12482
12773
  const days = Math.abs(new Date(lineDate).getTime() - new Date(refDate).getTime()) / 864e5;
@@ -12484,13 +12775,16 @@ function dateScore(lineDate, refDate) {
12484
12775
  }
12485
12776
  function scorePair(line, movement, context = {}) {
12486
12777
  if (movement.direction !== line.direction) return 0;
12487
- const valor = amountScore(line.amount, movement.amount);
12488
- if (valor === 0) return 0;
12489
12778
  const texto = `${line.description ?? ""} ${line.counterpartyName ?? ""}`;
12490
12779
  const nome = Math.max(
12491
12780
  nameScore(texto, context.contactName),
12492
12781
  nameScore(texto, movement.notes)
12493
12782
  );
12783
+ let valor = amountScore(line.amount, movement.amount);
12784
+ if (valor === 0 && nome >= STRONG_NAME) {
12785
+ valor = 0.7 * amountScore(line.amount, movement.amount, NAME_AMOUNT_TOLERANCE);
12786
+ }
12787
+ if (valor === 0) return 0;
12494
12788
  const data = dateScore(line.date, movement.paymentDate ?? movement.dueDate);
12495
12789
  return Math.round((0.45 * valor + 0.35 * nome + 0.2 * data) * 100) / 100;
12496
12790
  }
@@ -13378,6 +13672,22 @@ function createMockFinancialProvider(options) {
13378
13672
  };
13379
13673
  }).sort((a, b) => b.score - a.score).slice(0, 25);
13380
13674
  },
13675
+ async searchReconciliationTargets(statementLineId, query) {
13676
+ const line = store6.statementLines.find((l) => l.id === statementLineId);
13677
+ if (!line) return { data: [], hasMore: false };
13678
+ const status = query.status ?? "all";
13679
+ const termo = query.search?.trim().toLocaleLowerCase("pt-BR");
13680
+ const rows = store6.movements.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction).filter((m) => m.status !== "cancelled").filter((m) => {
13681
+ const paga = m.movementKind === "payment" || m.status === "paid";
13682
+ return status === "all" || (status === "paid" ? paga : !paga);
13683
+ }).filter((m) => !query.dueFrom || (m.dueDate ?? "") >= query.dueFrom).filter((m) => !query.dueTo || (m.dueDate ?? "") <= query.dueTo).filter((m) => query.amountMin == null || m.amount >= query.amountMin).filter((m) => query.amountMax == null || m.amount <= query.amountMax).map((m) => ({ movement: m, invoice: m.invoiceId ? store6.invoices.find((i) => i.id === m.invoiceId) : void 0 })).filter(({ movement, invoice }) => !termo || `${invoice?.contactName ?? ""} ${invoice?.fiscalNumber ?? ""} ${movement.notes ?? ""}`.toLocaleLowerCase("pt-BR").includes(termo)).sort((a, b) => (a.movement.dueDate ?? "").localeCompare(b.movement.dueDate ?? ""));
13684
+ const pageSize = query.pageSize ?? 50;
13685
+ const start = ((query.page ?? 1) - 1) * pageSize;
13686
+ return {
13687
+ data: rows.slice(start, start + pageSize).map((c) => ({ ...c, score: 0 })),
13688
+ hasMore: rows.length > start + pageSize
13689
+ };
13690
+ },
13381
13691
  // A mesma cabeça da conciliação de verdade (match-statement), sobre a
13382
13692
  // memória: valor, nome e data, e um lançamento por linha.
13383
13693
  async suggestReconciliationBatch(lines) {
@@ -13685,6 +13995,19 @@ async function writeLineAccounting(pub, tenantId, insertedItems, inputItems) {
13685
13995
  const { error } = await pub.from(T.invoiceItems).upsert(rows, { onConflict: "item_id" });
13686
13996
  if (error) console.warn("[financial] line accounting mirror skipped:", error.message);
13687
13997
  }
13998
+ function shiftIsoDate(value, days) {
13999
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
14000
+ d.setUTCDate(d.getUTCDate() + days);
14001
+ return d.toISOString().slice(0, 10);
14002
+ }
14003
+ function matchScore(line, m) {
14004
+ const amount = Number(line.amount) || 0;
14005
+ const amtDiff = Math.abs((Number(m.amount) || 0) - amount);
14006
+ const amtScore = amount > 0 ? Math.max(0, 1 - amtDiff / amount) : 0;
14007
+ const refDate = m.dueDate ?? m.paymentDate;
14008
+ const days = line.date && refDate ? Math.abs((new Date(line.date).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
14009
+ return round26(0.7 * amtScore + 0.3 * Math.max(0, 1 - days / 30));
14010
+ }
13688
14011
  function billRowToMovement(r) {
13689
14012
  return {
13690
14013
  id: r.installment_id,
@@ -13781,6 +14104,85 @@ function createSupabaseFinancialProvider() {
13781
14104
  externalSource: data.external_source
13782
14105
  };
13783
14106
  }
14107
+ async function searchBillTargets(line, query) {
14108
+ const { pub } = getClients();
14109
+ const pageSize = query.pageSize ?? 50;
14110
+ const page = query.page ?? 1;
14111
+ let qb = pub.from("v_financial_invoice_lines_due").select("installment_id,invoice_id,direction,number,due_date,description,counterparty_name,installment_number,installment_count,amount,paid,status").eq("direction", line.direction === "debit" ? "payable" : "receivable");
14112
+ const status = query.status ?? "all";
14113
+ qb = status === "open" ? qb.in("status", ["pending", "partial", "overdue"]) : status === "paid" ? qb.eq("status", "paid") : qb.neq("status", "cancelled");
14114
+ if (query.dueFrom) qb = qb.gte("due_date", query.dueFrom);
14115
+ if (query.dueTo) qb = qb.lte("due_date", query.dueTo);
14116
+ if (query.amountMin != null) qb = qb.gte("amount", query.amountMin);
14117
+ if (query.amountMax != null) qb = qb.lte("amount", query.amountMax);
14118
+ const termo = query.search?.trim().replace(/[,()*%]/g, " ").replace(/\s+/g, " ");
14119
+ if (termo) {
14120
+ qb = qb.or(["description", "number", "counterparty_name"].map((c) => `${c}.ilike.%${termo}%`).join(","));
14121
+ }
14122
+ const { data, error } = await qb.order("due_date", { ascending: true }).range((page - 1) * pageSize, page * pageSize);
14123
+ if (error) throw error;
14124
+ const rows = data ?? [];
14125
+ const hasMore = rows.length > pageSize;
14126
+ const visiveis = rows.slice(0, pageSize);
14127
+ const pagas = visiveis.filter((r) => r.status === "paid").map((r) => r.installment_id);
14128
+ const pagamentoDe = /* @__PURE__ */ new Map();
14129
+ if (pagas.length) {
14130
+ const { data: allocs } = await pub.from(T.allocations).select("installment_id,payment_movement_id").in("installment_id", pagas);
14131
+ const ids = [...new Set((allocs ?? []).map((a) => a.payment_movement_id).filter(Boolean))];
14132
+ const { data: pays } = ids.length ? await pub.from(T.movements).select("*").in("id", ids).neq("status", "cancelled").is("reversed_by_id", null) : { data: [] };
14133
+ const byId = new Map((pays ?? []).map((p) => [p.id, p]));
14134
+ for (const a of allocs ?? []) {
14135
+ const p = byId.get(a.payment_movement_id);
14136
+ if (p) pagamentoDe.set(a.installment_id, p);
14137
+ }
14138
+ }
14139
+ const candidates = [];
14140
+ for (const r of visiveis) {
14141
+ const invoice = {
14142
+ id: r.invoice_id,
14143
+ contactName: r.counterparty_name ?? void 0,
14144
+ fiscalNumber: r.number ?? void 0
14145
+ };
14146
+ let movement;
14147
+ if (r.status === "paid") {
14148
+ const p = pagamentoDe.get(r.installment_id);
14149
+ if (!p || p.reconciled_at) continue;
14150
+ movement = {
14151
+ ...snakeToCamel(p),
14152
+ invoiceId: r.invoice_id,
14153
+ settlesMovementId: r.installment_id,
14154
+ dueDate: r.due_date,
14155
+ notes: p.notes ?? r.description ?? void 0
14156
+ };
14157
+ } else {
14158
+ movement = {
14159
+ id: r.installment_id,
14160
+ invoiceId: r.invoice_id,
14161
+ direction: line.direction,
14162
+ movementKind: "bill",
14163
+ amount: Number(r.amount) || 0,
14164
+ paidAmount: Number(r.paid) || 0,
14165
+ status: r.status,
14166
+ dueDate: r.due_date,
14167
+ installmentNumber: r.installment_number ?? void 0,
14168
+ notes: r.description ?? void 0
14169
+ };
14170
+ }
14171
+ const pareado = scorePair(
14172
+ { amount: line.amount, date: line.date, direction: line.direction, description: line.description ?? "" },
14173
+ movement,
14174
+ { contactName: r.counterparty_name ?? void 0 }
14175
+ );
14176
+ candidates.push({
14177
+ movement,
14178
+ invoice,
14179
+ score: pareado > 0 ? pareado : round26(0.6 * matchScore(line, movement)),
14180
+ installmentCount: r.installment_count ?? void 0
14181
+ });
14182
+ }
14183
+ candidates.sort((a, b) => b.score - a.score);
14184
+ return { data: candidates, hasMore };
14185
+ }
13784
14186
  async function markReconciled(statementLineId, matchedMovementId) {
13785
14187
  const { pub } = getClients();
13786
14188
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
@@ -15421,6 +15823,31 @@ function createSupabaseFinancialProvider() {
15421
15823
  customerEmail: r.customer_email
15422
15824
  }));
15423
15825
  },
15826
+ async findGiftCardByCode(raw) {
15827
+ const code = raw.replace(/[\s.-]/g, "").toUpperCase();
15828
+ if (!code) return null;
15829
+ const { pub } = getClients();
15830
+ let { data, error } = await pub.rpc("financial_gift_card_by_code", { p_code: code });
15831
+ if (error?.code === "PGRST202") {
15832
+ ({ data, error } = await pub.rpc("financial_gift_cards", { p_search: code, p_limit: 5 }));
15833
+ }
15834
+ if (error) throw error;
15835
+ const row = (data ?? []).find((r) => String(r.code).toUpperCase() === code);
15836
+ if (!row) return null;
15837
+ return {
15838
+ id: row.id,
15839
+ code: row.code,
15840
+ faceValue: Number(row.face_value) || 0,
15841
+ balance: row.balance == null ? null : Number(row.balance),
15842
+ state: row.state,
15843
+ currency: row.currency,
15844
+ issuedAt: row.issued_at,
15845
+ expiresAt: row.expires_at,
15846
+ activatedAt: row.activated_at,
15847
+ note: row.note,
15848
+ customerEmail: row.customer_email
15849
+ };
15850
+ },
15424
15851
  async issueGiftCards(input) {
15425
15852
  const { pub } = getClients();
15426
15853
  const { data, error } = await pub.rpc("financial_issue_gift_cards", {
@@ -15580,7 +16007,17 @@ function createSupabaseFinancialProvider() {
15580
16007
  if (o) c.invoice = { id: o.id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number };
15581
16008
  }
15582
16009
  }
15583
- return candidates;
16010
+ const linha = { id: line.id, amount, date: lineDate, direction: line.direction, externalSource: line.external_source };
16011
+ const abertas = await searchBillTargets(linha, {
16012
+ status: "open",
16013
+ amountMin: round26(amount - tol),
16014
+ amountMax: round26(amount + tol),
16015
+ pageSize: 25
16016
+ }).catch(() => ({ data: [], hasMore: false }));
16017
+ return [...candidates, ...abertas.data].sort((a, b) => b.score - a.score);
16018
+ },
16019
+ async searchReconciliationTargets(statementLineId, query) {
16020
+ return searchBillTargets(await readStatementLine(statementLineId), query);
15584
16021
  },
15585
16022
  // ------------------------------------------------------------------
15586
16023
  // A SUGESTÃO QUE CHEGA SOZINHA, para a página inteira.
@@ -15600,16 +16037,38 @@ function createSupabaseFinancialProvider() {
15600
16037
  const abertas = lines.filter((line) => !line.reconciledAt && (Number(line.amount) || 0) > 0);
15601
16038
  if (abertas.length === 0) return {};
15602
16039
  const faixas = [...new Set(abertas.map((line) => round26(line.amount)))].map((valor) => {
15603
- const tol = Math.max(0.01, round26(valor * 0.02));
16040
+ const tol = Math.max(0.01, round26(valor * NAME_AMOUNT_TOLERANCE));
15604
16041
  return `and(amount.gte.${round26(valor - tol)},amount.lte.${round26(valor + tol)})`;
15605
16042
  });
15606
16043
  const direcoes = [...new Set(abertas.map((line) => line.direction))];
15607
16044
  const { data, error } = await pub.from(T.movements).select("*").is("external_source", null).is("reconciled_at", null).in("direction", direcoes).or(faixas.join(",")).limit(500);
15608
16045
  if (error) throw error;
15609
16046
  const movimentos = (data ?? []).map((r) => snakeToCamel(r));
15610
- if (movimentos.length === 0) return {};
16047
+ const datas = abertas.map((line) => line.date).filter(Boolean).sort();
15611
16048
  const contexto = /* @__PURE__ */ new Map();
15612
- const invoiceIds = [...new Set(movimentos.map((m) => m.invoiceId).filter(Boolean))];
16049
+ if (datas.length) {
16050
+ const { data: billRows } = await pub.from("v_financial_invoice_lines_due").select("installment_id,invoice_id,direction,number,due_date,description,counterparty_name,installment_number,amount,paid,status").in("direction", direcoes.map((d) => d === "debit" ? "payable" : "receivable")).in("status", ["pending", "partial", "overdue"]).gte("due_date", shiftIsoDate(datas[0], -30)).lte("due_date", shiftIsoDate(datas[datas.length - 1], 30)).or(faixas.join(",")).limit(500);
16051
+ for (const r of billRows ?? []) {
16052
+ movimentos.push({
16053
+ id: r.installment_id,
16054
+ invoiceId: r.invoice_id,
16055
+ direction: r.direction === "payable" ? "debit" : "credit",
16056
+ movementKind: "bill",
16057
+ amount: Number(r.amount) || 0,
16058
+ paidAmount: Number(r.paid) || 0,
16059
+ status: r.status,
16060
+ dueDate: r.due_date,
16061
+ installmentNumber: r.installment_number ?? void 0,
16062
+ notes: r.description ?? void 0
16063
+ });
16064
+ contexto.set(r.installment_id, {
16065
+ contactName: r.counterparty_name ?? void 0,
16066
+ invoice: { id: r.invoice_id, contactName: r.counterparty_name ?? void 0, fiscalNumber: r.number ?? void 0 }
16067
+ });
16068
+ }
16069
+ }
16070
+ if (movimentos.length === 0) return {};
16071
+ const invoiceIds = [...new Set(movimentos.filter((m) => !contexto.has(m.id)).map((m) => m.invoiceId).filter(Boolean))];
15613
16072
  if (invoiceIds.length > 0) {
15614
16073
  const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", invoiceIds);
15615
16074
  const byId = new Map((orders ?? []).map((o) => [o.id, o]));
@@ -15628,7 +16087,7 @@ function createSupabaseFinancialProvider() {
15628
16087
  async reconcileMovement(input) {
15629
16088
  if (input.mode === "settle" && input.matchedMovementId) {
15630
16089
  const line = await readStatementLine(input.bankMovementId);
15631
- const { data: alvo } = await getClients().pub.from(T.movements).select("amount, paid_amount").eq("id", input.matchedMovementId).maybeSingle();
16090
+ const { data: alvo } = await getClients().pub.from(T.bills).select("amount, paid").eq("installment_id", input.matchedMovementId).maybeSingle();
15632
16091
  if (!alvo) throw new Error("Lan\xE7amento n\xE3o encontrado");
15633
16092
  await runReconcileLink(
15634
16093
  reconcileFacade(),
@@ -15636,7 +16095,7 @@ function createSupabaseFinancialProvider() {
15636
16095
  {
15637
16096
  movementId: input.matchedMovementId,
15638
16097
  mode: "settle",
15639
- openAmount: round26((Number(alvo.amount) || 0) - (Number(alvo.paid_amount) || 0))
16098
+ openAmount: round26((Number(alvo.amount) || 0) - (Number(alvo.paid) || 0))
15640
16099
  },
15641
16100
  { payment: input.payment, adjustments: input.adjustments }
15642
16101
  );
@@ -15911,7 +16370,8 @@ function createSupabaseFinancialProvider() {
15911
16370
  };
15912
16371
  for (const payee of payees) {
15913
16372
  if (text2(payee.document)) {
15914
- await pub.from("people").update({ document_number: text2(payee.document) }).eq("id", payee.contactId);
16373
+ const { error: error2 } = await pub.from("people").update({ document_number: text2(payee.document) }).eq("id", payee.contactId);
16374
+ if (error2) throw error2;
15915
16375
  }
15916
16376
  const row = {
15917
16377
  bank_code: text2(payee.bankCode),
@@ -15930,18 +16390,16 @@ function createSupabaseFinancialProvider() {
15930
16390
  const { data: existing } = await pub.from(T.payeeAccounts).select("id").eq("owner_type", "person").eq("owner_id", payee.contactId).order("is_default", { ascending: false }).order("created_at").limit(1).maybeSingle();
15931
16391
  targetId = existing?.id;
15932
16392
  }
15933
- if (targetId) {
15934
- await pub.from(T.payeeAccounts).update(row).eq("id", targetId);
15935
- } else {
15936
- await pub.from(T.payeeAccounts).insert({
15937
- ...row,
15938
- tenant_id: tenantId,
15939
- owner_type: "person",
15940
- owner_id: payee.contactId,
15941
- label: "Principal",
15942
- is_default: true
15943
- });
15944
- }
16393
+ const { data: written, error } = targetId ? await pub.from(T.payeeAccounts).update(row).eq("id", targetId).select("id") : await pub.from(T.payeeAccounts).insert({
16394
+ ...row,
16395
+ tenant_id: tenantId,
16396
+ owner_type: "person",
16397
+ owner_id: payee.contactId,
16398
+ label: "Principal",
16399
+ is_default: true
16400
+ }).select("id");
16401
+ if (error) throw error;
16402
+ if (!written?.length) throw new Error("payee account was not saved (no financial edit permission?)");
15945
16403
  }
15946
16404
  },
15947
16405
  /**
@@ -18029,6 +18487,9 @@ var en = {
18029
18487
  "financial.invoiceForm.paidNeedsMethod": "Tell us the payment method and the account",
18030
18488
  "financial.invoiceForm.willBePaid": "This installment will be settled",
18031
18489
  "financial.invoiceForm.clear": "Clear",
18490
+ "financial.invoiceForm.manageAccounts": "Manage accounts",
18491
+ "financial.invoiceForm.manageChart": "Manage chart of accounts",
18492
+ "financial.invoiceForm.manageCostCenters": "Manage cost centers",
18032
18493
  "financial.invoice.newManualHint": "Open the full invoice form",
18033
18494
  "financial.invoice.noDetails": "No additional details recorded",
18034
18495
  "financial.invoice.unlinkedPayments": "Payments not linked to an installment",
@@ -18077,6 +18538,13 @@ var en = {
18077
18538
  "financial.itemType.product": "Product",
18078
18539
  "financial.itemType.other": "Other",
18079
18540
  "financial.itemType.gift_card": "Gift Card",
18541
+ "financial.giftCardLine.label": "Gift card code",
18542
+ "financial.giftCardLine.placeholder": "0000 0000 0000 0000",
18543
+ "financial.giftCardLine.hint": "Scan or type the full card code.",
18544
+ "financial.giftCardLine.notFound": "Gift card not found",
18545
+ "financial.giftCardLine.revoked": "Gift card cancelled \u2014 cannot be sold",
18546
+ "financial.giftCardLine.expired": "Gift card expired",
18547
+ "financial.giftCardLine.alreadySold": "This gift card has already been sold",
18080
18548
  "financial.invoiceForm.cancel": "Cancel",
18081
18549
  "financial.invoiceForm.close": "Close",
18082
18550
  "financial.invoiceForm.creating": "Creating new invoice",
@@ -18280,13 +18748,41 @@ var en = {
18280
18748
  "financial.reconciliation.reconciled": "Reconciled",
18281
18749
  "financial.reconciliation.undo": "Undo",
18282
18750
  "financial.reconciliation.flowTitle": "Reconcile transaction",
18283
- "financial.reconciliation.stepClassify": "How to classify",
18751
+ "financial.reconciliation.stepClassify": "What to do",
18284
18752
  "financial.reconciliation.stepItems": "The items",
18285
18753
  "financial.reconciliation.stepDetails": "The details",
18286
18754
  "financial.reconciliation.stepConfirm": "Confirm",
18287
18755
  "financial.reconciliation.confirmAction": "Confirm",
18288
18756
  "financial.reconciliation.howToClassify": "How do you want to classify this transaction?",
18289
18757
  "financial.reconciliation.pickIntent": "Pick what this transaction is",
18758
+ "financial.reconciliation.foundTitle": "Bills found",
18759
+ "financial.reconciliation.searchWiden": "Widen search (\xB115 days, any amount)",
18760
+ "financial.reconciliation.stepSelectBill": "Select the bill",
18761
+ "financial.reconciliation.stepAdjust": "Adjustment",
18762
+ "financial.reconciliation.selectBillFor": "Statement of {{date}} \xB7 {{description}} \xB7",
18763
+ "financial.reconciliation.statusOverdue": "Overdue",
18764
+ "financial.reconciliation.colDue": "Due",
18765
+ "financial.reconciliation.colCounterparty": "Counterparty",
18766
+ "financial.reconciliation.colDescription": "Number / Description",
18767
+ "financial.reconciliation.colInstallment": "Installment",
18768
+ "financial.reconciliation.colToReconcile": "To reconcile",
18769
+ "financial.reconciliation.colDelta": "\u0394 vs statement",
18770
+ "financial.reconciliation.searchStatusAllShort": "All",
18771
+ "financial.reconciliation.searchPendingHint": "The list still shows the previous filters. Click Filter to apply.",
18772
+ "financial.reconciliation.searchTitle": "Find a bill",
18773
+ "financial.reconciliation.searchDueFrom": "Due from",
18774
+ "financial.reconciliation.searchDueTo": "to",
18775
+ "financial.reconciliation.searchStatus": "Status",
18776
+ "financial.reconciliation.searchStatusAll": "All (not reconciled)",
18777
+ "financial.reconciliation.searchStatusOpen": "Open",
18778
+ "financial.reconciliation.searchStatusPaid": "Paid",
18779
+ "financial.reconciliation.searchText": "Search (counterparty, number, description)",
18780
+ "financial.reconciliation.searchTextPlaceholder": "Type and press Enter",
18781
+ "financial.reconciliation.searchAmountMin": "Min amount",
18782
+ "financial.reconciliation.searchAmountMax": "Max amount",
18783
+ "financial.reconciliation.searchAction": "Search",
18784
+ "financial.reconciliation.searchEmpty": "No bills match these filters.",
18785
+ "financial.reconciliation.searchMore": "Load more",
18290
18786
  "financial.reconciliation.pickCandidate": "Pick the entry to link",
18291
18787
  "financial.reconciliation.needDescription": "Describe the entry",
18292
18788
  "financial.reconciliation.intentLink": "Link to an existing entry",
@@ -18322,6 +18818,14 @@ var en = {
18322
18818
  "financial.reconciliation.linkOnlyExplain": "This bill was already paid in the system. Confirming only ties the statement line to it \u2014 no second payment, no change to the amount.",
18323
18819
  "financial.reconciliation.noAdjustmentNeeded": "The amount matches the bill. Nothing to adjust.",
18324
18820
  "financial.reconciliation.adjustmentNeeded": "The bank took {{gap}} {{side}} than the bill asked for. Say what that difference is.",
18821
+ "financial.reconciliation.adjustmentNeededIn": "{{gap}} {{side}} came in than the bill charged. Say what that difference is.",
18822
+ "financial.reconciliation.adjustmentNeedsAccount": "Pick the chart account for each adjustment.",
18823
+ "financial.reconciliation.adjustmentType": "Type",
18824
+ "financial.reconciliation.adjustmentDescriptionPlaceholder": "E.g. late interest",
18825
+ "financial.reconciliation.adjustmentDefaultInterest": "Late interest",
18826
+ "financial.reconciliation.adjustmentDefaultFine": "Late fine",
18827
+ "financial.reconciliation.adjustmentDefaultDiscountOut": "Discount obtained",
18828
+ "financial.reconciliation.adjustmentDefaultDiscountIn": "Discount granted",
18325
18829
  "financial.reconciliation.adjustmentMore": "more",
18326
18830
  "financial.reconciliation.adjustmentLess": "less",
18327
18831
  "financial.reconciliation.interest": "Interest",
@@ -18923,6 +19427,9 @@ var ptBR = {
18923
19427
  "financial.invoiceForm.paidNeedsMethod": "Diga a forma de pagamento e a conta",
18924
19428
  "financial.invoiceForm.willBePaid": "Esta parcela entra como paga",
18925
19429
  "financial.invoiceForm.clear": "Limpar",
19430
+ "financial.invoiceForm.manageAccounts": "Gerenciar contas",
19431
+ "financial.invoiceForm.manageChart": "Gerenciar plano de contas",
19432
+ "financial.invoiceForm.manageCostCenters": "Gerenciar centros de custo",
18926
19433
  "financial.invoice.newManualHint": "Preencher os campos da conta",
18927
19434
  "financial.invoice.noDetails": "Nenhum detalhe adicional registrado",
18928
19435
  "financial.invoice.unlinkedPayments": "Pagamentos sem parcela vinculada",
@@ -18971,6 +19478,13 @@ var ptBR = {
18971
19478
  "financial.itemType.product": "Produto",
18972
19479
  "financial.itemType.other": "Outro",
18973
19480
  "financial.itemType.gift_card": "Gift Card",
19481
+ "financial.giftCardLine.label": "C\xF3digo do gift card",
19482
+ "financial.giftCardLine.placeholder": "0000 0000 0000 0000",
19483
+ "financial.giftCardLine.hint": "Escaneie ou digite o c\xF3digo completo do cart\xE3o.",
19484
+ "financial.giftCardLine.notFound": "Gift card n\xE3o encontrado",
19485
+ "financial.giftCardLine.revoked": "Gift card cancelado \u2014 n\xE3o pode ser vendido",
19486
+ "financial.giftCardLine.expired": "Gift card expirado",
19487
+ "financial.giftCardLine.alreadySold": "Este gift card j\xE1 foi vendido",
18974
19488
  "financial.invoiceForm.cancel": "Cancelar",
18975
19489
  "financial.invoiceForm.close": "Fechar",
18976
19490
  "financial.invoiceForm.creating": "Criando nova fatura",
@@ -19177,13 +19691,41 @@ var ptBR = {
19177
19691
  "financial.reconciliation.reconciled": "Conciliado",
19178
19692
  "financial.reconciliation.undo": "Desfazer",
19179
19693
  "financial.reconciliation.flowTitle": "Conciliar movimenta\xE7\xE3o",
19180
- "financial.reconciliation.stepClassify": "Como classificar",
19694
+ "financial.reconciliation.stepClassify": "O que deseja fazer",
19181
19695
  "financial.reconciliation.stepItems": "Os itens",
19182
19696
  "financial.reconciliation.stepDetails": "Os detalhes",
19183
19697
  "financial.reconciliation.stepConfirm": "Confirmar",
19184
19698
  "financial.reconciliation.confirmAction": "Confirmar",
19185
19699
  "financial.reconciliation.howToClassify": "Como voc\xEA quer classificar esta transa\xE7\xE3o?",
19186
19700
  "financial.reconciliation.pickIntent": "Escolha o que esta transa\xE7\xE3o \xE9",
19701
+ "financial.reconciliation.foundTitle": "Contas encontradas",
19702
+ "financial.reconciliation.searchWiden": "Ampliar busca (\xB115 dias, qualquer valor)",
19703
+ "financial.reconciliation.stepSelectBill": "Selecione a conta",
19704
+ "financial.reconciliation.stepAdjust": "Ajuste",
19705
+ "financial.reconciliation.selectBillFor": "Extrato de {{date}} \xB7 {{description}} \xB7",
19706
+ "financial.reconciliation.statusOverdue": "Vencido",
19707
+ "financial.reconciliation.colDue": "Venc.",
19708
+ "financial.reconciliation.colCounterparty": "Contraparte",
19709
+ "financial.reconciliation.colDescription": "N\xFAmero / Descri\xE7\xE3o",
19710
+ "financial.reconciliation.colInstallment": "Parcela",
19711
+ "financial.reconciliation.colToReconcile": "Valor a conciliar",
19712
+ "financial.reconciliation.colDelta": "\u0394 vs extrato",
19713
+ "financial.reconciliation.searchStatusAllShort": "Todos",
19714
+ "financial.reconciliation.searchPendingHint": "A lista ainda mostra o recorte anterior. Clique em Filtrar para aplicar.",
19715
+ "financial.reconciliation.searchTitle": "Buscar conta",
19716
+ "financial.reconciliation.searchDueFrom": "Vencimento de",
19717
+ "financial.reconciliation.searchDueTo": "at\xE9",
19718
+ "financial.reconciliation.searchStatus": "Situa\xE7\xE3o",
19719
+ "financial.reconciliation.searchStatusAll": "Todos (n\xE3o conciliados)",
19720
+ "financial.reconciliation.searchStatusOpen": "Em aberto",
19721
+ "financial.reconciliation.searchStatusPaid": "Pagos",
19722
+ "financial.reconciliation.searchText": "Buscar (contraparte, n\xFAmero, descri\xE7\xE3o)",
19723
+ "financial.reconciliation.searchTextPlaceholder": "Digite e pressione Enter",
19724
+ "financial.reconciliation.searchAmountMin": "Valor m\xEDn.",
19725
+ "financial.reconciliation.searchAmountMax": "Valor m\xE1x.",
19726
+ "financial.reconciliation.searchAction": "Buscar",
19727
+ "financial.reconciliation.searchEmpty": "Nenhuma conta com esses filtros.",
19728
+ "financial.reconciliation.searchMore": "Carregar mais",
19187
19729
  "financial.reconciliation.pickCandidate": "Escolha o lan\xE7amento a vincular",
19188
19730
  "financial.reconciliation.needDescription": "Descreva o lan\xE7amento",
19189
19731
  "financial.reconciliation.intentLink": "Vincular a um lan\xE7amento",
@@ -19219,6 +19761,14 @@ var ptBR = {
19219
19761
  "financial.reconciliation.linkOnlyExplain": "Esta conta j\xE1 foi paga no sistema. Confirmar apenas amarra a linha do extrato a ela \u2014 sem criar um segundo pagamento e sem mexer no valor.",
19220
19762
  "financial.reconciliation.noAdjustmentNeeded": "O valor bate com a conta. Nada a ajustar.",
19221
19763
  "financial.reconciliation.adjustmentNeeded": "O banco tirou {{gap}} {{side}} do que a conta pedia. Diga o que \xE9 essa diferen\xE7a.",
19764
+ "financial.reconciliation.adjustmentNeededIn": "Entrou {{gap}} {{side}} do que a conta cobrava. Diga o que \xE9 essa diferen\xE7a.",
19765
+ "financial.reconciliation.adjustmentNeedsAccount": "Escolha o plano de contas de cada ajuste.",
19766
+ "financial.reconciliation.adjustmentType": "Tipo",
19767
+ "financial.reconciliation.adjustmentDescriptionPlaceholder": "Ex.: juros por atraso",
19768
+ "financial.reconciliation.adjustmentDefaultInterest": "Juros por atraso",
19769
+ "financial.reconciliation.adjustmentDefaultFine": "Multa por atraso",
19770
+ "financial.reconciliation.adjustmentDefaultDiscountOut": "Desconto obtido",
19771
+ "financial.reconciliation.adjustmentDefaultDiscountIn": "Desconto concedido",
19222
19772
  "financial.reconciliation.adjustmentMore": "a mais",
19223
19773
  "financial.reconciliation.adjustmentLess": "a menos",
19224
19774
  "financial.reconciliation.interest": "Juros",
@@ -44490,6 +45040,239 @@ BEGIN
44490
45040
  RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', false);
44491
45041
  END $$;
44492
45042
 
45043
+ `;
45044
+ var MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP = `-- ---------------------------------------------------------------------------
45045
+ -- 117 \u2014 copying lines onto an invoice is an internal step, not a door.
45046
+ --
45047
+ -- Both copiers are SECURITY DEFINER with no caller check and were executable by
45048
+ -- PUBLIC (so anon too): anyone holding an invoice id could write public.items
45049
+ -- into it. Every real caller is another SECURITY DEFINER function owned by
45050
+ -- postgres (fn_invoice_from_order_internal, order_raise_receivable,
45051
+ -- financial_recurring_run, financial_consolidate_occurrence), which keeps its
45052
+ -- right to execute as owner. So the fix is the grant, not the body.
45053
+ --
45054
+ -- Idempotent: grants only.
45055
+ -- ---------------------------------------------------------------------------
45056
+
45057
+ REVOKE ALL ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;
45058
+ GRANT EXECUTE ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) TO service_role;
45059
+
45060
+ REVOKE ALL ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) FROM PUBLIC, anon, authenticated;
45061
+ GRANT EXECUTE ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) TO service_role;
45062
+ `;
45063
+ var MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE = `-- 118_gift_card_found_by_exact_code.sql
45064
+ --
45065
+ -- Selling a gift card on a receivable takes the code typed at the counter and
45066
+ -- must match exactly one card. financial_gift_cards (027) matches by substring,
45067
+ -- so a partial code answered with real codes of other cards. This lookup
45068
+ -- returns the one card whose code equals the input (spaces, dots, dashes and
45069
+ -- case ignored), or nothing.
45070
+ --
45071
+ -- The shop table is optional and applied after this chain, so it is resolved
45072
+ -- at run time, as in 027.
45073
+
45074
+ CREATE OR REPLACE FUNCTION public.financial_gift_card_by_code(p_code text)
45075
+ RETURNS TABLE (
45076
+ id uuid,
45077
+ code text,
45078
+ face_value numeric,
45079
+ balance numeric,
45080
+ balance_known boolean,
45081
+ state text,
45082
+ currency text,
45083
+ issued_at timestamptz,
45084
+ expires_at timestamptz,
45085
+ activated_at timestamptz,
45086
+ note text,
45087
+ customer_email text
45088
+ )
45089
+ LANGUAGE plpgsql
45090
+ STABLE
45091
+ SECURITY DEFINER
45092
+ SET search_path TO ''
45093
+ AS $$
45094
+ DECLARE
45095
+ v_tenant uuid := app.current_tenant_id();
45096
+ v_code text := upper(regexp_replace(coalesce(p_code, ''), '[\\s.\\-]', '', 'g'));
45097
+ v_activated text;
45098
+ BEGIN
45099
+ IF v_tenant IS NULL THEN
45100
+ RAISE EXCEPTION 'financial_gift_card_by_code: sem tenant na sess\xE3o' USING ERRCODE = '42501';
45101
+ END IF;
45102
+ -- Whoever can raise a receivable can sell a card on it.
45103
+ IF NOT app.has_permission('financial.create', NULL) THEN
45104
+ RAISE EXCEPTION 'financial_gift_card_by_code: exige financial.create' USING ERRCODE = '42501';
45105
+ END IF;
45106
+ IF v_code = '' OR to_regclass('public.plg_shop_gift_cards') IS NULL THEN
45107
+ RETURN;
45108
+ END IF;
45109
+
45110
+ IF EXISTS (SELECT 1 FROM information_schema.columns c
45111
+ WHERE c.table_schema = 'public' AND c.table_name = 'plg_shop_gift_cards'
45112
+ AND c.column_name = 'activated_at') THEN
45113
+ v_activated := 'g.activated_at';
45114
+ ELSE
45115
+ v_activated :=
45116
+ 'CASE WHEN g.status = ''disabled'' AND g.balance = 0 THEN NULL'
45117
+ || ' WHEN g.updated_at > g.created_at THEN g.updated_at'
45118
+ || ' ELSE g.created_at END';
45119
+ END IF;
45120
+
45121
+ -- Same state vocabulary as financial_gift_cards (027).
45122
+ RETURN QUERY EXECUTE
45123
+ 'SELECT r.id, r.code, r.initial_balance,'
45124
+ || 'CASE WHEN r.never THEN NULL ELSE r.balance END,'
45125
+ || '(NOT r.never),'
45126
+ || 'CASE WHEN r.never THEN ''issued'''
45127
+ || ' WHEN r.balance = 0 OR r.status = ''redeemed'' THEN ''redeemed'''
45128
+ || ' WHEN r.expires_at IS NOT NULL AND r.expires_at < now() THEN ''expired'''
45129
+ || ' WHEN r.status = ''disabled'' THEN ''revoked'''
45130
+ || ' ELSE ''active'' END,'
45131
+ || 'r.currency, r.created_at, r.expires_at, r.activated_at, r.note, r.customer_email '
45132
+ || 'FROM (SELECT g.id, g.code, g.initial_balance, g.balance, g.currency, g.status,'
45133
+ || 'g.created_at, g.expires_at, g.note, g.customer_email,'
45134
+ || '(g.status = ''disabled'' AND g.balance = 0) AS never,'
45135
+ || v_activated || ' AS activated_at '
45136
+ || 'FROM public.plg_shop_gift_cards g '
45137
+ || 'WHERE g.tenant_id = $1 AND upper(g.code) = $2) r '
45138
+ || 'LIMIT 1'
45139
+ USING v_tenant, v_code;
45140
+ END $$;
45141
+
45142
+ COMMENT ON FUNCTION public.financial_gift_card_by_code(text) IS
45143
+ 'The one gift card whose code equals the input (spaces/case ignored), for selling it on a receivable. Never matches part of a code (118).';
45144
+
45145
+ REVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM PUBLIC;
45146
+ REVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM anon;
45147
+ GRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO authenticated;
45148
+ GRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO service_role;
45149
+ `;
45150
+ var MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED = `-- An unpaid bill migrated from V1 could not be rescheduled.
45151
+ --
45152
+ -- Editing the lines of such a bill saved the new lines and total, then
45153
+ -- \`financial_reschedule_invoice\` refused with "carries legacy bill movements":
45154
+ -- the installment kept the old amount and every later save failed the same way.
45155
+ --
45156
+ -- The \`bill\` mirror in plg_financial_movements has had no reader since 044 \u2014
45157
+ -- the installment is the obligation. Its amount is append-only, its status is
45158
+ -- not, so an unpaid mirror is cancelled (stamped in metadata) and the schedule
45159
+ -- goes through. A mirror that records a payment still blocks.
45160
+ --
45161
+ -- Idempotent: replaces the function.
45162
+
45163
+ CREATE OR REPLACE FUNCTION public.financial_reschedule_invoice(
45164
+ p_invoice_id uuid,
45165
+ p_installments jsonb,
45166
+ p_idempotency_key text DEFAULT NULL::text
45167
+ )
45168
+ RETURNS jsonb
45169
+ LANGUAGE plpgsql
45170
+ SECURITY DEFINER
45171
+ SET search_path TO ''
45172
+ AS $function$
45173
+ DECLARE
45174
+ v_tenant uuid := public.plg_financial_require_tenant();
45175
+ v_invoice public.plg_financial_invoices%ROWTYPE;
45176
+ v_paid numeric;
45177
+ v_mirror int;
45178
+ v_sum numeric := 0;
45179
+ v_i jsonb;
45180
+ v_n int := 0;
45181
+ v_out jsonb := '[]'::jsonb;
45182
+ v_inst_id uuid;
45183
+ v_due date;
45184
+ v_amount numeric;
45185
+ v_meta jsonb;
45186
+ v_total int;
45187
+ BEGIN
45188
+ SELECT * INTO v_invoice FROM public.plg_financial_invoices WHERE id = p_invoice_id AND tenant_id = v_tenant;
45189
+ IF NOT FOUND THEN
45190
+ RAISE EXCEPTION 'financial_reschedule_invoice: invoice % not found', p_invoice_id USING ERRCODE = 'P0002';
45191
+ END IF;
45192
+ PERFORM public.plg_financial_require_permission('financial.edit', v_invoice.unit_id);
45193
+ PERFORM public.plg_financial_claim_idempotency(v_tenant, p_idempotency_key);
45194
+
45195
+ IF v_invoice.cancelled_at IS NOT NULL THEN
45196
+ RAISE EXCEPTION 'financial_reschedule_invoice: invoice % is cancelled', v_invoice.number USING ERRCODE = '55000';
45197
+ END IF;
45198
+
45199
+ SELECT coalesce(sum(b.paid), 0) INTO v_paid
45200
+ FROM public.v_financial_installment_balances b WHERE b.invoice_id = p_invoice_id;
45201
+ IF v_paid > 0 THEN
45202
+ RAISE EXCEPTION 'financial_reschedule_invoice: invoice % has % already received; its schedule is history now', v_invoice.number, v_paid
45203
+ USING ERRCODE = '55000',
45204
+ HINT = 'Move the due date of an unpaid installment instead, or cancel and reissue.';
45205
+ END IF;
45206
+
45207
+ -- The legacy \`bill\` mirror can't change amount, but nothing reads it any more
45208
+ -- (the installment is the obligation since 044) and its status may move.
45209
+ -- Retire it unless it records a payment the installments don't know about.
45210
+ SELECT count(*) INTO v_mirror FROM public.plg_financial_movements m
45211
+ WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill'
45212
+ AND m.status <> 'cancelled' AND coalesce(m.paid_amount, 0) > 0;
45213
+ IF v_mirror > 0 THEN
45214
+ RAISE EXCEPTION 'financial_reschedule_invoice: invoice % carries a paid legacy bill movement and cannot be rescheduled', v_invoice.number
45215
+ USING ERRCODE = '55000',
45216
+ HINT = 'Cancel and reissue this invoice.';
45217
+ END IF;
45218
+ UPDATE public.plg_financial_movements m
45219
+ SET status = 'cancelled', updated_at = now(),
45220
+ metadata = coalesce(m.metadata, '{}'::jsonb)
45221
+ || jsonb_build_object('retired_by', 'financial_reschedule_invoice', 'retired_at', now())
45222
+ WHERE m.financial_invoice_id = p_invoice_id AND m.movement_kind = 'bill' AND m.status <> 'cancelled';
45223
+
45224
+ IF jsonb_typeof(p_installments) <> 'array' OR jsonb_array_length(p_installments) = 0 THEN
45225
+ RAISE EXCEPTION 'financial_reschedule_invoice: installments must be a non-empty array' USING ERRCODE = '22023';
45226
+ END IF;
45227
+ FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP
45228
+ v_sum := v_sum + round(coalesce((v_i ->> 'amount')::numeric, 0), 2);
45229
+ END LOOP;
45230
+ IF v_sum <> round(v_invoice.total, 2) THEN
45231
+ RAISE EXCEPTION 'financial_reschedule_invoice: installments sum to % but the invoice total is %', v_sum, v_invoice.total
45232
+ USING ERRCODE = '23514';
45233
+ END IF;
45234
+
45235
+ v_total := jsonb_array_length(p_installments);
45236
+
45237
+ -- 1. A sobra sai PRIMEIRO. Se ela tiver movimento, o banco recusa aqui \u2014 e \xE9
45238
+ -- a recusa certa: encolher o cronograma apagaria a linha que um
45239
+ -- lan\xE7amento aponta.
45240
+ DELETE FROM public.plg_financial_invoice_installments
45241
+ WHERE invoice_id = p_invoice_id AND number > v_total;
45242
+
45243
+ -- 2. E a parcela de n\xFAmero N continua sendo a de n\xFAmero N.
45244
+ FOR v_i IN SELECT * FROM jsonb_array_elements(p_installments) LOOP
45245
+ v_n := v_n + 1;
45246
+ v_due := coalesce((v_i ->> 'due_date')::date, v_invoice.competence_date, current_date);
45247
+ v_amount := round(coalesce((v_i ->> 'amount')::numeric, 0), 2);
45248
+ v_meta := CASE WHEN jsonb_typeof(v_i -> 'metadata') = 'object' THEN v_i -> 'metadata' ELSE NULL END;
45249
+
45250
+ UPDATE public.plg_financial_invoice_installments
45251
+ SET due_date = v_due,
45252
+ amount = v_amount,
45253
+ metadata = coalesce(v_meta, metadata),
45254
+ updated_at = now()
45255
+ WHERE invoice_id = p_invoice_id AND number = v_n
45256
+ RETURNING id INTO v_inst_id;
45257
+
45258
+ IF v_inst_id IS NULL THEN
45259
+ INSERT INTO public.plg_financial_invoice_installments
45260
+ (tenant_id, unit_id, invoice_id, number, due_date, amount, metadata)
45261
+ VALUES (v_tenant, v_invoice.unit_id, p_invoice_id, v_n, v_due, v_amount, coalesce(v_meta, '{}'::jsonb))
45262
+ RETURNING id INTO v_inst_id;
45263
+ END IF;
45264
+
45265
+ v_out := v_out || jsonb_build_object('id', v_inst_id, 'number', v_n, 'due_date', v_due, 'amount', v_amount);
45266
+ END LOOP;
45267
+
45268
+ PERFORM public.plg_financial_audit(v_tenant, 'financial.invoice.reschedule', 'plg_financial_invoices', p_invoice_id::text,
45269
+ jsonb_build_object('number', v_invoice.number, 'installments', v_n, 'idempotency_key', p_idempotency_key));
45270
+
45271
+ RETURN jsonb_build_object('invoice_id', p_invoice_id, 'installments', v_out);
45272
+ END $function$;
45273
+
45274
+ COMMENT ON FUNCTION public.financial_reschedule_invoice(uuid, jsonb, text) IS
45275
+ 'Rewrites the schedule by UPDATING each numbered installment in place, and retires an unpaid legacy bill mirror instead of refusing (119).';
44493
45276
  `;
44494
45277
  var MIGRATIONS = [
44495
45278
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
@@ -44608,7 +45391,10 @@ var MIGRATIONS = [
44608
45391
  { id: "113_so_a_conta_integrada_aparece_na_conciliacao", sql: MIGRATION_113_SO_A_CONTA_INTEGRADA_APARECE_NA_CONCILIACAO },
44609
45392
  { id: "114_a_parcela_diz_por_qual_conta_ela_foi_paga", sql: MIGRATION_114_A_PARCELA_DIZ_POR_QUAL_CONTA_ELA_FOI_PAGA },
44610
45393
  { id: "115_a_remessa_aponta_para_a_conta_que_existe", sql: MIGRATION_115_A_REMESSA_APONTA_PARA_A_CONTA_QUE_EXISTE },
44611
- { id: "116_um_estorno_nao_se_estorna", sql: MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA }
45394
+ { id: "116_um_estorno_nao_se_estorna", sql: MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA },
45395
+ { id: "117_copying_lines_is_an_internal_step", sql: MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP },
45396
+ { id: "118_gift_card_found_by_exact_code", sql: MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE },
45397
+ { id: "119_an_unpaid_legacy_bill_can_be_rescheduled", sql: MIGRATION_119_AN_UNPAID_LEGACY_BILL_CAN_BE_RESCHEDULED }
44612
45398
  ];
44613
45399
  var STANDALONE_CONFIG = resolveConfig();
44614
45400
  registerTranslations(financialLocales);
@@ -45018,7 +45804,6 @@ function createSafeFinancialProvider() {
45018
45804
  () => createMockFinancialProvider()
45019
45805
  );
45020
45806
  }
45021
- var ACCOUNTS_ROUTE = "/registry/accounts";
45022
45807
  var FINANCIAL_NAV_FEATURES = {
45023
45808
  reconciliation: "fin_reconciliation",
45024
45809
  cards: "fin_cards",
@@ -45396,6 +46181,6 @@ function createFinancialPlugin(options) {
45396
46181
  };
45397
46182
  }
45398
46183
 
45399
- export { ClientAccountInline, ClientAccountModal, ClientAccountPanel, FINANCIAL_NAV_FEATURES, ReceivePaymentModal, buildFinancialDomainEvents, buildFinancialMessageDefaults, createFinancialPlugin, createFinancialSettingsPlugin, createMockFinancialProvider, createSafeFinancialProvider };
46184
+ export { ClientAccountInline, ClientAccountModal, ClientAccountPanel, FINANCIAL_INVOICE_ZONES, FINANCIAL_NAV_FEATURES, ReceivePaymentModal, buildFinancialDomainEvents, buildFinancialMessageDefaults, createFinancialPlugin, createFinancialSettingsPlugin, createMockFinancialProvider, createSafeFinancialProvider };
45400
46185
  //# sourceMappingURL=index.js.map
45401
46186
  //# sourceMappingURL=index.js.map