@fayz-ai/plugin-financial 0.19.1 → 0.20.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, 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, 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';
@@ -2500,11 +2500,13 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2500
2500
  }, [currency, direction, t2]);
2501
2501
  const bulkActions = useMemo(() => {
2502
2502
  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
2503
  { id: "print", label: t2("financial.invoice.printSelected"), icon: Printer, onSelect: printSelected },
2505
2504
  { id: "export", label: t2("financial.invoice.exportSelected"), icon: Download, onSelect: exportSelected }
2506
2505
  ];
2507
2506
  if (!canEdit) return actions;
2507
+ if (direction === "debit") {
2508
+ actions.unshift({ id: "cnab", label: t2("financial.invoice.generateCnab"), icon: FileText, onSelect: (rows) => setCnabTarget(rows.filter((row) => row.status !== "paid" && row.status !== "cancelled")) });
2509
+ }
2508
2510
  actions.push({
2509
2511
  id: "cancel",
2510
2512
  label: t2("financial.invoice.bulkCancel"),
@@ -2560,17 +2562,20 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2560
2562
  value: unitFilter.options.find((u) => u.id === unitFilter.value)?.name ?? unitFilter.allLabel,
2561
2563
  icon: Building2,
2562
2564
  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
- ] })
2565
+ align: "end",
2566
+ children: (close) => /* @__PURE__ */ jsx(
2567
+ ChoiceList,
2568
+ {
2569
+ options: unitFilter.options.map((u) => ({ value: u.id, label: u.name })),
2570
+ value: unitFilter.value,
2571
+ onSelect: unitFilter.setValue,
2572
+ allLabel: unitFilter.allLabel,
2573
+ searchable: true,
2574
+ searchPlaceholder: unitFilter.def?.searchPlaceholder,
2575
+ manage: unitFilter.def?.manage,
2576
+ onDone: close
2577
+ }
2578
+ )
2574
2579
  }
2575
2580
  ) : locations.length > 1 ? /* @__PURE__ */ jsx(
2576
2581
  RequiredFilter,
@@ -2579,17 +2584,18 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2579
2584
  value: locations.find((u) => u.id === unitId)?.name ?? t2("financial.invoice.filterAllUnits"),
2580
2585
  icon: Building2,
2581
2586
  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
- ] })
2587
+ align: "end",
2588
+ children: (close) => /* @__PURE__ */ jsx(
2589
+ ChoiceList,
2590
+ {
2591
+ options: locations.map((u) => ({ value: u.id, label: u.name })),
2592
+ value: unitId || void 0,
2593
+ onSelect: (v) => setUnitId(v ?? ""),
2594
+ allLabel: t2("financial.invoice.filterAllUnits"),
2595
+ searchable: true,
2596
+ onDone: close
2597
+ }
2598
+ )
2593
2599
  }
2594
2600
  ) : null;
2595
2601
  const statusField = /* @__PURE__ */ jsx(
@@ -2874,6 +2880,40 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
2874
2880
  cnabTarget !== null ? /* @__PURE__ */ jsx(CnabRemittanceModal, { invoices: cnabTarget, bankAccounts, provider, onClose: () => setCnabTarget(null) }) : null
2875
2881
  ] });
2876
2882
  }
2883
+
2884
+ // src/lib/routes.ts
2885
+ var ACCOUNTS_ROUTE = "/registry/accounts";
2886
+ var CHART_OF_ACCOUNTS_ROUTE = "/settings/financial/chart-of-accounts";
2887
+ var COST_CENTERS_ROUTE = "/settings/financial/cost-centers";
2888
+ function useWritableUnits(fallback = []) {
2889
+ const unitMode = useUnitMode();
2890
+ const activeUnitId = useActiveUnitId() ?? void 0;
2891
+ const reachable = useUnitFilter().options;
2892
+ const choices = !unitMode ? fallback : activeUnitId ? reachable.filter((u) => u.id === activeUnitId) : reachable;
2893
+ return { unitMode, activeUnitId, choices };
2894
+ }
2895
+ function UnitField({ choices, value, onChange, label }) {
2896
+ const t2 = useTranslation();
2897
+ const can = usePermissionOptional();
2898
+ return /* @__PURE__ */ jsxs("div", { children: [
2899
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: label ?? t2("financial.invoiceForm.unit") }),
2900
+ /* @__PURE__ */ jsx(
2901
+ ChoiceSelect,
2902
+ {
2903
+ className: "mt-1",
2904
+ icon: Building2,
2905
+ "aria-label": label ?? t2("financial.invoiceForm.unit"),
2906
+ options: choices.map((c) => ({ value: c.id, label: c.name })),
2907
+ value: value || void 0,
2908
+ onChange: (v) => onChange(v ?? ""),
2909
+ allLabel: "Todas / sem unidade",
2910
+ searchable: true,
2911
+ searchPlaceholder: t2("financial.invoiceForm.searchUnit"),
2912
+ manage: can("units", "manage") ? { label: "Gerenciar unidades", onSelect: () => adminNavigateTo("/settings/locations") } : void 0
2913
+ }
2914
+ )
2915
+ ] });
2916
+ }
2877
2917
  var PAYABLE_INSTRUCTIONS = [
2878
2918
  'Uma "conta a pagar" \xE9 uma DESPESA com um FORNECEDOR \u2014 quem a empresa deve. Para registrar uma,',
2879
2919
  "chame `createPayable`. NUNCA use `createRecord` em `financial:payment-methods` para isso: aquele",
@@ -4664,6 +4704,7 @@ function InstallmentSourceFields({
4664
4704
  const t2 = useTranslation();
4665
4705
  const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes);
4666
4706
  const bankAccounts = useFinancialStore((s) => s.bankAccounts);
4707
+ const can = usePermissionOptional();
4667
4708
  const selectedType = paymentMethodTypes.find((tp) => tp.id === value.payMethodTypeId);
4668
4709
  const selectedAccount = bankAccounts.find((a) => a.id === value.payAccountId);
4669
4710
  const accounts = eligibleAccountsFor(bankAccounts, direction, selectedType);
@@ -4712,13 +4753,15 @@ function InstallmentSourceFields({
4712
4753
  ariaLabel: t2("financial.invoiceForm.paidFrom"),
4713
4754
  clearLabel: t2("financial.invoiceForm.clear"),
4714
4755
  className: `${INLINE_LINK_TONE.dashed} ${tone(!!selectedAccount)}`,
4715
- onChange: (id) => onChange({ ...value, payAccountId: id })
4756
+ onChange: (id) => onChange({ ...value, payAccountId: id }),
4757
+ onManage: can("financial", "manage") ? () => adminNavigateTo(ACCOUNTS_ROUTE) : void 0,
4758
+ manageLabel: t2("financial.invoiceForm.manageAccounts")
4716
4759
  }
4717
4760
  )
4718
4761
  ] });
4719
4762
  }
4720
4763
  var norm2 = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
4721
- function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className }) {
4764
+ function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className, onManage, manageLabel }) {
4722
4765
  const [open, setOpen] = React25__default.useState(false);
4723
4766
  const [q, setQ] = React25__default.useState("");
4724
4767
  const selected = accounts.find((a) => a.id === value);
@@ -4783,7 +4826,22 @@ function AccountPicker({ accounts, value, onChange, disabled, title, placeholder
4783
4826
  },
4784
4827
  a.id
4785
4828
  )) }),
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 })
4829
+ 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 }),
4830
+ onManage && /* @__PURE__ */ jsxs(
4831
+ "button",
4832
+ {
4833
+ type: "button",
4834
+ onClick: () => {
4835
+ setOpen(false);
4836
+ onManage();
4837
+ },
4838
+ 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",
4839
+ children: [
4840
+ /* @__PURE__ */ jsx(Settings, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
4841
+ manageLabel
4842
+ ]
4843
+ }
4844
+ )
4787
4845
  ] })
4788
4846
  ] });
4789
4847
  }
@@ -5013,95 +5071,6 @@ function summarizeInvoiceLines(rows, visible = 2) {
5013
5071
  function invoiceOpensForEditing(status) {
5014
5072
  return status !== "paid" && status !== "cancelled";
5015
5073
  }
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
5074
  var formId = 1;
5106
5075
  function fid() {
5107
5076
  return `f${formId++}`;
@@ -5136,11 +5105,9 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5136
5105
  const config = useFinancialConfig();
5137
5106
  const provider = useFinancialProvider();
5138
5107
  const { currency, itemTypes: itemTypesRaw = [], locations = [] } = config;
5139
- const unitMode = useUnitMode();
5140
- const activeUnitId = useActiveUnitId();
5108
+ const { unitMode, activeUnitId, choices: unitChoices } = useWritableUnits(locations);
5141
5109
  const reachableUnits = useUnitFilter().options;
5142
- const unitChoices = unitMode ? reachableUnits : locations;
5143
- const showUnitField = unitChoices.length > 1;
5110
+ const showUnitField = unitMode ? reachableUnits.length > 1 : unitChoices.length > 1;
5144
5111
  const [giftCardsEnabled, setGiftCardsEnabled] = useState(false);
5145
5112
  useEffect(() => {
5146
5113
  let cancelled = false;
@@ -5158,7 +5125,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5158
5125
  };
5159
5126
  }, [direction, provider]);
5160
5127
  const giftCardLookup = useMemo(() => {
5161
- if (!giftCardsEnabled || !provider.getGiftCards) return null;
5128
+ if (!giftCardsEnabled || !provider.findGiftCardByCode) return null;
5162
5129
  const asOption = (card) => ({
5163
5130
  id: card.id,
5164
5131
  label: `Gift Card ${card.code.replace(/(.{4})(?=.)/g, "$1 ")}`,
@@ -5167,21 +5134,34 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5167
5134
  price: card.faceValue,
5168
5135
  data: { price: card.faceValue, code: card.code, state: card.state }
5169
5136
  });
5137
+ const resolve = async (raw) => {
5138
+ const card = await provider.findGiftCardByCode(raw);
5139
+ if (!card) throw new Error(t2("financial.giftCardLine.notFound"));
5140
+ if (card.state === "revoked") throw new Error(t2("financial.giftCardLine.revoked"));
5141
+ if (card.state === "expired") throw new Error(t2("financial.giftCardLine.expired"));
5142
+ if (card.state !== "issued") throw new Error(t2("financial.giftCardLine.alreadySold"));
5143
+ return asOption(card);
5144
+ };
5170
5145
  return {
5171
5146
  async search(query) {
5172
- const cards = await provider.getGiftCards({ search: query.trim() || void 0, state: "issued" });
5173
- return cards.map(asOption);
5147
+ try {
5148
+ return [await resolve(query)];
5149
+ } catch {
5150
+ return [];
5151
+ }
5174
5152
  },
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;
5153
+ async getById() {
5154
+ return null;
5179
5155
  },
5180
- async list() {
5181
- return (await provider.getGiftCards({ state: "issued" })).map(asOption);
5156
+ byCode: {
5157
+ resolve,
5158
+ label: t2("financial.giftCardLine.label"),
5159
+ placeholder: t2("financial.giftCardLine.placeholder"),
5160
+ hint: t2("financial.giftCardLine.hint"),
5161
+ display: ({ description }) => description.replace(/^Gift Card\s+/, "")
5182
5162
  }
5183
5163
  };
5184
- }, [giftCardsEnabled, provider]);
5164
+ }, [giftCardsEnabled, provider, t2]);
5185
5165
  const entityLookups = useMemo(() => giftCardLookup ? { ...config.entityLookups, gift_card: giftCardLookup } : config.entityLookups, [config.entityLookups, giftCardLookup]);
5186
5166
  const translatedItemTypes = React25__default.useMemo(
5187
5167
  () => itemTypesRaw.map((tp) => {
@@ -5210,6 +5190,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5210
5190
  const createInvoice = useFinancialStore((s) => s.createInvoice);
5211
5191
  const guardMovements = useLimitGuard("movements_month");
5212
5192
  const can = usePermissionOptional();
5193
+ const canManageFinancial = can("financial", "manage");
5213
5194
  const isEdit = !!editId;
5214
5195
  const [attachmentsOpen, setAttachmentsOpen] = useState(false);
5215
5196
  useEffect(() => {
@@ -5412,7 +5393,11 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5412
5393
  }, [items.length]);
5413
5394
  const somaParcelas = Math.round(installments.reduce((s, i) => s + i.amount, 0) * 100);
5414
5395
  useEffect(() => {
5415
- if (installments.length === 0 || scheduleTotal <= 0) return;
5396
+ if (installments.length === 0) return;
5397
+ if (scheduleTotal <= 0) {
5398
+ if (!isEdit) setInstallments([]);
5399
+ return;
5400
+ }
5416
5401
  if (somaParcelas === Math.round(scheduleTotal * 100)) return;
5417
5402
  const count = installments.length;
5418
5403
  const perInstallment = Math.floor(scheduleTotal * 100 / count) / 100;
@@ -5421,7 +5406,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5421
5406
  ...inst,
5422
5407
  amount: i === 0 ? perInstallment + remainder : perInstallment
5423
5408
  })));
5424
- }, [scheduleTotal, somaParcelas, installments.length]);
5409
+ }, [scheduleTotal, somaParcelas, installments.length, isEdit]);
5425
5410
  function addItemOfKind(kind) {
5426
5411
  const newItem = {
5427
5412
  _id: fid(),
@@ -5966,7 +5951,7 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
5966
5951
  ] }) });
5967
5952
  const sideSections = /* @__PURE__ */ jsxs(Fragment, { children: [
5968
5953
  /* @__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 }),
5954
+ showUnitField && /* @__PURE__ */ jsx(UnitField, { choices: unitChoices, value: unitId, onChange: setUnitId }),
5970
5955
  /* @__PURE__ */ jsxs("div", { children: [
5971
5956
  /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.invoiceForm.date") }),
5972
5957
  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 +6116,10 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6131
6116
  label: accountLabel(a),
6132
6117
  subtitle: a.accountType ?? void 0
6133
6118
  })),
6134
- onChange: (id) => updateItem(item._id, { accountId: id || void 0 })
6119
+ onChange: (id) => updateItem(item._id, { accountId: id || void 0 }),
6120
+ onManage: canManageFinancial ? () => adminNavigateTo(CHART_OF_ACCOUNTS_ROUTE) : void 0,
6121
+ manageLabel: t2("financial.invoiceForm.manageChart"),
6122
+ clearLabel: t2("financial.invoiceForm.clear")
6135
6123
  }
6136
6124
  )
6137
6125
  )
@@ -6154,7 +6142,10 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6154
6142
  id: c.id,
6155
6143
  label: c.code ? `${c.code} - ${c.name}` : c.name
6156
6144
  })),
6157
- onChange: (id) => updateItem(item._id, { costCenterId: id || void 0 })
6145
+ onChange: (id) => updateItem(item._id, { costCenterId: id || void 0 }),
6146
+ onManage: canManageFinancial ? () => adminNavigateTo(COST_CENTERS_ROUTE) : void 0,
6147
+ manageLabel: t2("financial.invoiceForm.manageCostCenters"),
6148
+ clearLabel: t2("financial.invoiceForm.clear")
6158
6149
  }
6159
6150
  )
6160
6151
  }] : []
@@ -9789,6 +9780,21 @@ var shortDate2 = (value) => {
9789
9780
  const [year, month, day] = value.split("-");
9790
9781
  return year && month && day ? `${day}/${month}/${year}` : value;
9791
9782
  };
9783
+ var shiftDate = (value, days) => {
9784
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00`);
9785
+ if (Number.isNaN(d.getTime())) return value;
9786
+ d.setDate(d.getDate() + days);
9787
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
9788
+ };
9789
+ var toAmountInput = (n) => round23(Math.max(0, n)).toFixed(2).replace(".", ",");
9790
+ 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";
9791
+ var LINK_DAYS = 3;
9792
+ var LINK_AMOUNT_TOLERANCE = 0.1;
9793
+ var WIDEN_DAYS = 15;
9794
+ var parseAmount = (value) => {
9795
+ const n = Number.parseFloat(value.replace(",", "."));
9796
+ return Number.isFinite(n) ? n : void 0;
9797
+ };
9792
9798
  var round23 = (n) => Math.round((Number(n) || 0) * 100) / 100;
9793
9799
  var emptyItem = () => ({ description: "", quantity: 1, unitPrice: 0 });
9794
9800
  function LinhaDoExtrato2({ line, fmt }) {
@@ -9813,6 +9819,35 @@ function LinhaDoExtrato2({ line, fmt }) {
9813
9819
  ] })
9814
9820
  ] });
9815
9821
  }
9822
+ function CandidatoBotao({ item, selected, onToggle, fmt, t: t2 }) {
9823
+ const { candidate: c, mode, openAmount: openAmount2 } = item;
9824
+ const parcela = c.movement.installmentNumber;
9825
+ return /* @__PURE__ */ jsxs(
9826
+ "button",
9827
+ {
9828
+ type: "button",
9829
+ onClick: onToggle,
9830
+ "aria-pressed": selected,
9831
+ 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"}`,
9832
+ children: [
9833
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
9834
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm", children: c.invoice?.contactName || c.movement.notes || c.invoice?.fiscalNumber || t2("financial.reconciliation.match") }),
9835
+ /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[10px] text-muted-foreground", children: [
9836
+ /* @__PURE__ */ jsx("span", { children: shortDate2(c.movement.dueDate) }),
9837
+ c.invoice?.fiscalNumber && /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
9838
+ "#",
9839
+ c.invoice.fiscalNumber,
9840
+ parcela ? ` \xB7 ${parcela}` : ""
9841
+ ] }),
9842
+ 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) }) }),
9843
+ /* @__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") })
9844
+ ] })
9845
+ ] }),
9846
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-semibold", children: fmt(c.movement.amount) })
9847
+ ]
9848
+ }
9849
+ );
9850
+ }
9816
9851
  function EscolhaCard({ icon: Icon, title, hint, active, onClick }) {
9817
9852
  return /* @__PURE__ */ jsxs(
9818
9853
  "button",
@@ -9831,17 +9866,18 @@ function EscolhaCard({ icon: Icon, title, hint, active, onClick }) {
9831
9866
  }
9832
9867
  );
9833
9868
  }
9834
- function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9869
+ function ReconcileFlow({ line, initialIntent, initialTarget, initialStep = 0, onClose, onDone }) {
9835
9870
  const t2 = useTranslation();
9836
9871
  const { currency } = useFinancialConfig();
9837
9872
  const provider = useFinancialProvider();
9838
9873
  const fmt = useCallback((n) => formatCurrency(n, currency), [currency]);
9839
- const [step, setStep] = useState(0);
9874
+ const [step, setStep] = useState(initialStep);
9840
9875
  const [intent, setIntent] = useState(initialIntent ?? null);
9841
9876
  const [saving, setSaving] = useState(false);
9842
9877
  const [candidates, setCandidates] = useState([]);
9843
9878
  const [candidatesLoading, setCandidatesLoading] = useState(false);
9844
- const [targetId, setTargetId] = useState(null);
9879
+ const [target, setTarget] = useState(initialTarget ?? null);
9880
+ const targetId = target?.candidate.movement.id ?? null;
9845
9881
  const [adjustments, setAdjustments] = useState([]);
9846
9882
  const [contactId, setContactId] = useState("");
9847
9883
  const [contactName, setContactName] = useState(line.counterpartyName ?? "");
@@ -9864,7 +9900,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9864
9900
  void provider.getBankAccounts().then(setContas).catch(() => setContas([]));
9865
9901
  }, [provider, line.bankAccountId]);
9866
9902
  useEffect(() => {
9867
- if (intent !== "link" || !provider.suggestReconciliation) return;
9903
+ if (intent !== "link" || !provider.suggestReconciliation || provider.searchReconciliationTargets) return;
9868
9904
  let cancelled = false;
9869
9905
  setCandidatesLoading(true);
9870
9906
  provider.suggestReconciliation(line.id).then((raw) => {
@@ -9878,10 +9914,72 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9878
9914
  cancelled = true;
9879
9915
  };
9880
9916
  }, [intent, provider, line.id]);
9881
- const target = useMemo(
9882
- () => candidates.find((c) => c.candidate.movement.id === targetId) ?? null,
9883
- [candidates, targetId]
9884
- );
9917
+ const podeBuscar = !!provider.searchReconciliationTargets;
9918
+ const filtrosIniciais = () => ({
9919
+ dueFrom: shiftDate(line.date, -LINK_DAYS),
9920
+ dueTo: shiftDate(line.date, LINK_DAYS),
9921
+ status: "all",
9922
+ texto: "",
9923
+ min: toAmountInput(line.amount * (1 - LINK_AMOUNT_TOLERANCE)),
9924
+ max: toAmountInput(line.amount * (1 + LINK_AMOUNT_TOLERANCE))
9925
+ });
9926
+ const [rascunho, setRascunho] = useState(filtrosIniciais);
9927
+ const [aplicado, setAplicado] = useState(filtrosIniciais);
9928
+ const mudarFiltro = (patch) => setRascunho((prev) => ({ ...prev, ...patch }));
9929
+ const filtrosPendentes = JSON.stringify(rascunho) !== JSON.stringify(aplicado);
9930
+ const [resultados, setResultados] = useState([]);
9931
+ const [paginaBusca, setPaginaBusca] = useState(1);
9932
+ const [temMais, setTemMais] = useState(false);
9933
+ const [buscando, setBuscando] = useState(false);
9934
+ const buscaSeq = useRef(0);
9935
+ const buscar = useCallback(async (pagina) => {
9936
+ if (!provider.searchReconciliationTargets) return;
9937
+ const seq = ++buscaSeq.current;
9938
+ setBuscando(true);
9939
+ try {
9940
+ const res = await provider.searchReconciliationTargets(line.id, {
9941
+ dueFrom: aplicado.dueFrom || void 0,
9942
+ dueTo: aplicado.dueTo || void 0,
9943
+ status: aplicado.status,
9944
+ search: aplicado.texto.trim() || void 0,
9945
+ amountMin: parseAmount(aplicado.min),
9946
+ amountMax: parseAmount(aplicado.max),
9947
+ page: pagina
9948
+ });
9949
+ if (seq !== buscaSeq.current) return;
9950
+ const novos = resolveCandidates(res.data);
9951
+ setResultados((prev) => {
9952
+ if (pagina === 1) return novos;
9953
+ const vistos = new Set(prev.map((r) => r.candidate.movement.id));
9954
+ return [...prev, ...novos.filter((r) => !vistos.has(r.candidate.movement.id))];
9955
+ });
9956
+ setPaginaBusca(pagina);
9957
+ setTemMais(res.hasMore);
9958
+ } catch (e) {
9959
+ if (seq !== buscaSeq.current) return;
9960
+ toast.error(e instanceof Error ? e.message : t2("common.error"));
9961
+ } finally {
9962
+ if (seq === buscaSeq.current) setBuscando(false);
9963
+ }
9964
+ }, [provider, line.id, aplicado, t2]);
9965
+ const ultimaBusca = useRef(null);
9966
+ const naSelecao = intent === "link" && step === 1;
9967
+ useEffect(() => {
9968
+ if (!naSelecao || !podeBuscar) return;
9969
+ const chave = JSON.stringify(aplicado);
9970
+ if (ultimaBusca.current === chave) return;
9971
+ ultimaBusca.current = chave;
9972
+ void buscar(1);
9973
+ }, [naSelecao, podeBuscar, aplicado, buscar]);
9974
+ const contasEncontradas = useMemo(() => {
9975
+ const lista = podeBuscar ? resultados : candidates;
9976
+ return initialTarget && !lista.some((c) => c.candidate.movement.id === initialTarget.candidate.movement.id) ? [initialTarget, ...lista] : lista;
9977
+ }, [podeBuscar, resultados, candidates, initialTarget]);
9978
+ const ampliarBusca = () => {
9979
+ const amplo = { ...rascunho, dueFrom: shiftDate(line.date, -WIDEN_DAYS), dueTo: shiftDate(line.date, WIDEN_DAYS), min: "", max: "" };
9980
+ setRascunho(amplo);
9981
+ setAplicado(amplo);
9982
+ };
9885
9983
  const contaDoExtrato = useMemo(
9886
9984
  () => contas.find((a) => a.id === line.bankAccountId) ?? null,
9887
9985
  [contas, line.bankAccountId]
@@ -9906,6 +10004,15 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9906
10004
  const rows = q ? planoDoTipo.filter((a) => `${a.code ?? ""} ${a.name}`.toLocaleLowerCase("pt-BR").includes(q)) : planoDoTipo;
9907
10005
  return rows.slice(0, 50).map((a) => ({ id: a.id, label: accountLabel(a) }));
9908
10006
  }, [planoDoTipo]);
10007
+ const descricaoPadrao = (type) => type === "interest" ? t2("financial.reconciliation.adjustmentDefaultInterest") : type === "fine" ? t2("financial.reconciliation.adjustmentDefaultFine") : isOut ? t2("financial.reconciliation.adjustmentDefaultDiscountOut") : t2("financial.reconciliation.adjustmentDefaultDiscountIn");
10008
+ const buscaPlanoDoAjuste = useCallback((adj, query) => {
10009
+ const natureza = adjustmentDirection(line.direction, adj.type) === "debit" ? "expense" : "revenue";
10010
+ const folhas = postableAccounts(chart);
10011
+ const doTipo = folhas.filter((a) => a.accountType === natureza);
10012
+ const base = doTipo.length > 0 ? doTipo : folhas;
10013
+ const q = query.trim().toLocaleLowerCase("pt-BR");
10014
+ 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) }));
10015
+ }, [chart, line.direction]);
9909
10016
  const valorAQuitar = intent === "link" && target ? target.openAmount : round23(line.amount);
9910
10017
  const quita = intent === "bill" || intent === "link" && target?.mode === "settle";
9911
10018
  const leitor = useMemo(() => {
@@ -9942,8 +10049,9 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
9942
10049
  const somaItens = sumItems(items.filter((i) => i.description.trim()));
9943
10050
  const erroItens = itemsMismatch(items, line.amount, fmt);
9944
10051
  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;
10052
+ 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;
10053
+ const bloqueioPasso1 = !intent ? t2("financial.reconciliation.pickIntent") : null;
10054
+ const bloqueioSelecao = intent === "link" && !target ? t2("financial.reconciliation.pickCandidate") : null;
9947
10055
  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
10056
  const bloqueioPasso3 = saving ? t2("common.saving") : null;
9949
10057
  async function confirmar() {
@@ -10070,37 +10178,148 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10070
10178
  onClick: () => setIntent("transfer")
10071
10179
  }
10072
10180
  )
10181
+ ] })
10182
+ ] });
10183
+ const statusDaConta = (item) => item.mode === "link_only" ? t2("financial.reconciliation.searchStatusPaid") : item.candidate.movement.status === "overdue" ? t2("financial.reconciliation.statusOverdue") : t2("financial.reconciliation.searchStatusOpen");
10184
+ const valorAConciliar = (item) => item.mode === "link_only" ? item.candidate.movement.amount : item.openAmount;
10185
+ const parcelaDe = (item) => {
10186
+ const n = item.candidate.movement.installmentNumber ?? 1;
10187
+ return item.candidate.installmentCount ? `${n}/${item.candidate.installmentCount}` : String(n);
10188
+ };
10189
+ const filtrar = () => {
10190
+ if (filtrosPendentes) setAplicado(rascunho);
10191
+ else void buscar(1);
10192
+ };
10193
+ const passoSelecionar = /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-3", children: [
10194
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
10195
+ t2("financial.reconciliation.selectBillFor", {
10196
+ date: shortDate2(line.date),
10197
+ description: line.description || line.externalId
10198
+ }),
10199
+ " ",
10200
+ /* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: fmt(line.amount) })
10073
10201
  ] }),
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",
10202
+ podeBuscar && /* @__PURE__ */ jsxs(
10203
+ FilterPanel,
10204
+ {
10205
+ title: t2("financial.reconciliation.filtersTitle"),
10206
+ dirty: filtrosPendentes,
10207
+ onApply: filtrar,
10208
+ applyLabel: t2("financial.reconciliation.filter"),
10209
+ pendingTitle: t2("financial.reconciliation.filterPending"),
10210
+ pendingHint: t2("financial.reconciliation.searchPendingHint"),
10211
+ busy: buscando,
10212
+ moreLabel: t2("financial.reconciliation.filterMore"),
10213
+ lessLabel: t2("financial.reconciliation.filterLess"),
10214
+ advancedCount: rascunho.status !== "all" ? 1 : 0,
10215
+ advanced: /* @__PURE__ */ jsx(FilterPanelField, { label: t2("financial.reconciliation.searchStatus"), children: /* @__PURE__ */ jsx(
10216
+ SegmentedControl,
10083
10217
  {
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) })
10218
+ "aria-label": t2("financial.reconciliation.searchStatus"),
10219
+ value: rascunho.status,
10220
+ onChange: (v) => mudarFiltro({ status: v }),
10221
+ options: [
10222
+ { value: "all", label: t2("financial.reconciliation.searchStatusAllShort") },
10223
+ { value: "open", label: t2("financial.reconciliation.searchStatusOpen") },
10224
+ { value: "paid", label: t2("financial.reconciliation.searchStatusPaid") }
10098
10225
  ]
10099
- },
10100
- c.movement.id
10101
- );
10102
- })
10103
- ] })
10226
+ }
10227
+ ) }),
10228
+ footer: /* @__PURE__ */ jsx(
10229
+ Input,
10230
+ {
10231
+ value: rascunho.texto,
10232
+ placeholder: t2("financial.reconciliation.searchText"),
10233
+ "aria-label": t2("financial.reconciliation.searchText"),
10234
+ onChange: (e) => mudarFiltro({ texto: e.target.value }),
10235
+ onKeyDown: (e) => {
10236
+ if (e.key !== "Enter") return;
10237
+ e.preventDefault();
10238
+ filtrar();
10239
+ }
10240
+ }
10241
+ ),
10242
+ children: [
10243
+ /* @__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 }) }),
10244
+ /* @__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 }) }),
10245
+ /* @__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 }) }),
10246
+ /* @__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 }) })
10247
+ ]
10248
+ }
10249
+ ),
10250
+ (buscando || candidatesLoading) && contasEncontradas.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
10251
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full rounded-md" }),
10252
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full rounded-md" })
10253
+ ] }) : contasEncontradas.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-card border py-6 text-center", children: [
10254
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.reconciliation.searchEmpty") }),
10255
+ podeBuscar && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: ampliarBusca, children: t2("financial.reconciliation.searchWiden") })
10256
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
10257
+ /* @__PURE__ */ jsx("div", { className: "space-y-2 sm:hidden", children: contasEncontradas.map((item) => /* @__PURE__ */ jsx(
10258
+ CandidatoBotao,
10259
+ {
10260
+ item,
10261
+ selected: targetId === item.candidate.movement.id,
10262
+ onToggle: () => setTarget(targetId === item.candidate.movement.id ? null : item),
10263
+ fmt,
10264
+ t: t2
10265
+ },
10266
+ item.candidate.movement.id
10267
+ )) }),
10268
+ /* @__PURE__ */ jsx("div", { className: "hidden overflow-hidden rounded-card border sm:block", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
10269
+ /* @__PURE__ */ jsx("thead", { className: "bg-muted/50 text-left text-[11px] text-muted-foreground", children: /* @__PURE__ */ jsxs("tr", { children: [
10270
+ /* @__PURE__ */ jsx("th", { className: "w-8 p-2" }),
10271
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colDue") }),
10272
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colCounterparty") }),
10273
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colDescription") }),
10274
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.colInstallment") }),
10275
+ /* @__PURE__ */ jsx("th", { className: "p-2 font-medium", children: t2("financial.reconciliation.searchStatus") }),
10276
+ /* @__PURE__ */ jsx("th", { className: "p-2 text-right font-medium", children: t2("financial.reconciliation.colToReconcile") }),
10277
+ /* @__PURE__ */ jsx("th", { className: "p-2 text-right font-medium", children: t2("financial.reconciliation.colDelta") })
10278
+ ] }) }),
10279
+ /* @__PURE__ */ jsx("tbody", { children: contasEncontradas.map((item) => {
10280
+ const c = item.candidate;
10281
+ const escolhido = targetId === c.movement.id;
10282
+ const valor = valorAConciliar(item);
10283
+ const delta = round23(line.amount - valor);
10284
+ return /* @__PURE__ */ jsxs(
10285
+ "tr",
10286
+ {
10287
+ onClick: () => setTarget(escolhido ? null : item),
10288
+ className: `cursor-pointer border-t transition-colors ${escolhido ? "bg-primary/10" : "hover:bg-muted/40"}`,
10289
+ children: [
10290
+ /* @__PURE__ */ jsx("td", { className: "p-2", children: /* @__PURE__ */ jsx(
10291
+ "input",
10292
+ {
10293
+ type: "radio",
10294
+ readOnly: true,
10295
+ checked: escolhido,
10296
+ "aria-label": c.invoice?.contactName ?? c.movement.notes ?? c.movement.id
10297
+ }
10298
+ ) }),
10299
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2", children: shortDate2(c.movement.dueDate) }),
10300
+ /* @__PURE__ */ jsx("td", { className: "max-w-[12rem] truncate p-2 font-medium", title: c.invoice?.contactName, children: c.invoice?.contactName || "\u2014" }),
10301
+ /* @__PURE__ */ jsxs("td", { className: "max-w-[14rem] truncate p-2", title: c.movement.notes, children: [
10302
+ c.invoice?.fiscalNumber && /* @__PURE__ */ jsxs("span", { className: "mr-1 text-muted-foreground", children: [
10303
+ "#",
10304
+ c.invoice.fiscalNumber
10305
+ ] }),
10306
+ c.movement.notes
10307
+ ] }),
10308
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2", children: parcelaDe(item) }),
10309
+ /* @__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) }) }),
10310
+ /* @__PURE__ */ jsx("td", { className: "whitespace-nowrap p-2 text-right font-semibold", children: fmt(valor) }),
10311
+ /* @__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))}` })
10312
+ ]
10313
+ },
10314
+ c.movement.id
10315
+ );
10316
+ }) })
10317
+ ] }) })
10318
+ ] }),
10319
+ temMais && /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "ghost", disabled: buscando, onClick: () => void buscar(paginaBusca + 1), children: [
10320
+ buscando && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin", "aria-hidden": true }),
10321
+ t2("financial.reconciliation.searchMore")
10322
+ ] }) })
10104
10323
  ] });
10105
10324
  const passoDetalhes = /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-3", children: [
10106
10325
  intent === "bill" && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -10152,30 +10371,28 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10152
10371
  onChange: (e) => setItems((prev) => prev.map((x, i) => i === idx ? { ...x, description: e.target.value } : x))
10153
10372
  }
10154
10373
  ) }),
10155
- /* @__PURE__ */ jsx(
10374
+ /* @__PURE__ */ jsx("div", { className: "w-16 shrink-0", children: /* @__PURE__ */ jsx(
10156
10375
  Input,
10157
10376
  {
10158
10377
  "aria-label": t2("financial.reconciliation.itemQuantity"),
10159
- className: "w-16 shrink-0",
10160
10378
  type: "number",
10161
10379
  min: "0",
10162
10380
  step: "0.001",
10163
10381
  value: item.quantity,
10164
10382
  onChange: (e) => setItems((prev) => prev.map((x, i) => i === idx ? { ...x, quantity: Number(e.target.value) } : x))
10165
10383
  }
10166
- ),
10167
- /* @__PURE__ */ jsx(
10384
+ ) }),
10385
+ /* @__PURE__ */ jsx("div", { className: "w-24 shrink-0", children: /* @__PURE__ */ jsx(
10168
10386
  Input,
10169
10387
  {
10170
10388
  "aria-label": t2("financial.reconciliation.itemUnitPrice"),
10171
- className: "w-24 shrink-0",
10172
10389
  type: "number",
10173
10390
  min: "0",
10174
10391
  step: "0.01",
10175
10392
  value: item.unitPrice,
10176
10393
  onChange: (e) => setItems((prev) => prev.map((x, i) => i === idx ? { ...x, unitPrice: Number(e.target.value) } : x))
10177
10394
  }
10178
- ),
10395
+ ) }),
10179
10396
  /* @__PURE__ */ jsx(Hint, { label: t2("common.delete"), children: /* @__PURE__ */ jsx(
10180
10397
  Button,
10181
10398
  {
@@ -10276,58 +10493,86 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10276
10493
  ] })
10277
10494
  ] }),
10278
10495
  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", {
10496
+ /* @__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
10497
  gap: fmt(Math.abs(gap)),
10281
10498
  side: gap > 0 ? t2("financial.reconciliation.adjustmentMore") : t2("financial.reconciliation.adjustmentLess")
10282
10499
  }) }),
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)),
10500
+ adjustments.map((adj, idx) => {
10501
+ const mudar = (patch) => setAdjustments((prev) => prev.map((x, i) => i === idx ? { ...x, ...patch } : x));
10502
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-card border p-2.5", children: [
10503
+ /* @__PURE__ */ jsxs("div", { className: "grid min-w-0 flex-1 items-end gap-2 sm:grid-cols-2", children: [
10504
+ /* @__PURE__ */ jsxs("div", { children: [
10505
+ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t2("financial.reconciliation.adjustmentType") }),
10506
+ /* @__PURE__ */ jsxs(
10507
+ Select,
10508
+ {
10509
+ value: adj.type,
10510
+ onValueChange: (v) => {
10511
+ const type = v;
10512
+ const escrita = adj.description?.trim() && adj.description !== descricaoPadrao(adj.type);
10513
+ mudar(escrita ? { type } : { type, description: descricaoPadrao(type) });
10514
+ },
10515
+ children: [
10516
+ /* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }),
10517
+ /* @__PURE__ */ jsxs(SelectContent, { children: [
10518
+ /* @__PURE__ */ jsx(SelectItem, { value: "interest", children: t2("financial.reconciliation.interest") }),
10519
+ /* @__PURE__ */ jsx(SelectItem, { value: "fine", children: t2("financial.reconciliation.fine") }),
10520
+ /* @__PURE__ */ jsx(SelectItem, { value: "discount", children: t2("financial.reconciliation.discount") })
10521
+ ] })
10522
+ ]
10523
+ }
10524
+ )
10525
+ ] }),
10526
+ /* @__PURE__ */ jsx(
10527
+ CurrencyInput,
10528
+ {
10529
+ label: t2("financial.reconciliation.adjustmentAmount"),
10530
+ value: adj.amount,
10531
+ onChange: (v) => mudar({ amount: v }),
10532
+ symbol: currency.symbol,
10533
+ locale: currency.locale,
10534
+ currencyCode: currency.code
10535
+ }
10536
+ ),
10537
+ /* @__PURE__ */ jsx(
10538
+ SearchSelect,
10539
+ {
10540
+ label: `${t2("financial.reconciliation.chartAccount")} *`,
10541
+ value: adj.accountId ?? "",
10542
+ displayValue: chart.find((a) => a.id === adj.accountId)?.name,
10543
+ placeholder: t2("financial.reconciliation.chartAccountPlaceholder"),
10544
+ minChars: 0,
10545
+ loadOnOpen: true,
10546
+ noResultsLabel: t2("financial.reconciliation.chartAccountNone"),
10547
+ clearLabel: "Limpar",
10548
+ onSearch: (q) => buscaPlanoDoAjuste(adj, q),
10549
+ onChange: (id) => mudar({ accountId: id || void 0 })
10550
+ }
10551
+ ),
10552
+ /* @__PURE__ */ jsxs("div", { children: [
10553
+ /* @__PURE__ */ jsx("label", { className: "mb-1 block text-xs font-medium text-muted-foreground", children: t2("financial.reconciliation.adjustmentDescription") }),
10554
+ /* @__PURE__ */ jsx(
10555
+ Input,
10556
+ {
10557
+ placeholder: t2("financial.reconciliation.adjustmentDescriptionPlaceholder"),
10558
+ value: adj.description ?? "",
10559
+ onChange: (e) => mudar({ description: e.target.value })
10560
+ }
10561
+ )
10562
+ ] })
10563
+ ] }),
10564
+ /* @__PURE__ */ jsx("div", { className: "pt-6", children: /* @__PURE__ */ jsx(Hint, { label: t2("common.delete"), children: /* @__PURE__ */ jsx(
10565
+ Button,
10566
+ {
10567
+ size: "sm",
10568
+ variant: "ghost",
10569
+ "aria-label": t2("common.delete"),
10570
+ onClick: () => setAdjustments((prev) => prev.filter((_, i) => i !== idx)),
10571
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5", "aria-hidden": true })
10572
+ }
10573
+ ) }) })
10574
+ ] }, idx);
10575
+ }),
10331
10576
  /* @__PURE__ */ jsxs(
10332
10577
  Button,
10333
10578
  {
@@ -10335,6 +10580,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10335
10580
  variant: "outline",
10336
10581
  onClick: () => setAdjustments((prev) => [...prev, {
10337
10582
  type: gap > 0 ? "interest" : "discount",
10583
+ description: descricaoPadrao(gap > 0 ? "interest" : "discount"),
10338
10584
  // A diferença que ainda não foi explicada já vem no campo:
10339
10585
  // digitar de novo um número que a tela acabou de calcular é
10340
10586
  // trabalho que o produto podia poupar.
@@ -10433,6 +10679,13 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10433
10679
  ) : /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t2("financial.reconciliation.noPaymentNeeded") })
10434
10680
  ] });
10435
10681
  const motivo = (texto) => texto ? /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: texto }) : void 0;
10682
+ const paginaDetalhes = {
10683
+ id: "details",
10684
+ title: intent === "bill" ? t2("financial.reconciliation.stepItems") : intent === "link" ? t2("financial.reconciliation.stepAdjust") : t2("financial.reconciliation.stepDetails"),
10685
+ render: () => passoDetalhes,
10686
+ footer: motivo(bloqueioPasso2),
10687
+ blockedReason: bloqueioPasso2 ?? void 0
10688
+ };
10436
10689
  const pages = [
10437
10690
  {
10438
10691
  id: "classify",
@@ -10441,13 +10694,15 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10441
10694
  footer: motivo(bloqueioPasso1),
10442
10695
  blockedReason: bloqueioPasso1 ?? void 0
10443
10696
  },
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
- },
10697
+ ...intent === "link" ? [{
10698
+ id: "select",
10699
+ title: t2("financial.reconciliation.stepSelectBill"),
10700
+ render: () => passoSelecionar,
10701
+ footer: motivo(bloqueioSelecao),
10702
+ blockedReason: bloqueioSelecao ?? void 0
10703
+ }] : [],
10704
+ // Like V1: linking asks for an adjustment only when there is a gap to explain.
10705
+ ...intent !== "link" || !target || target.mode === "settle" && Math.abs(gap) >= 0.01 ? [paginaDetalhes] : [],
10451
10706
  {
10452
10707
  id: "confirm",
10453
10708
  title: t2("financial.reconciliation.stepConfirm"),
@@ -10464,7 +10719,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10464
10719
  if (!o && !saving) onClose();
10465
10720
  },
10466
10721
  mode: "steps",
10467
- size: "lg",
10722
+ size: intent === "link" && pages[step]?.id === "select" ? "4xl" : "lg",
10468
10723
  title: t2("financial.reconciliation.flowTitle"),
10469
10724
  pages,
10470
10725
  current: step,
@@ -10474,8 +10729,7 @@ function ReconcileFlow({ line, initialIntent, onClose, onDone }) {
10474
10729
  setStep(Math.max(0, alvo));
10475
10730
  return;
10476
10731
  }
10477
- if (step === 0 && bloqueioPasso1) return;
10478
- if (step === 1 && bloqueioPasso2) return;
10732
+ if (pages.slice(step, alvo).some((p) => p.blockedReason)) return;
10479
10733
  setStep(Math.min(pages.length - 1, alvo));
10480
10734
  },
10481
10735
  doneLabel: saving ? t2("common.saving") : t2("financial.reconciliation.confirmAction"),
@@ -10810,9 +11064,16 @@ function ReconciliationView() {
10810
11064
  t: t2,
10811
11065
  onSugerir: () => void sugerir(line),
10812
11066
  onTransferir: () => setFlow({ line, intent: "transfer" }),
10813
- onVincular: () => setFlow({ line, intent: "link" }),
11067
+ onVincular: () => setFlow({ line, intent: "link", step: 1 }),
10814
11068
  onLancarConta: () => setFlow({ line, intent: "bill" }),
10815
- onAceitarCandidato: (movId) => void vincular(line.id, movId),
11069
+ onAceitarCandidato: (cand) => {
11070
+ const [alvo] = resolveCandidates([cand]);
11071
+ if (alvo.mode === "link_only" && Math.abs(cand.movement.amount - line.amount) < 0.01) {
11072
+ void vincular(line.id, cand.movement.id);
11073
+ } else {
11074
+ setFlow({ line, intent: "link", target: alvo, step: 1 });
11075
+ }
11076
+ },
10816
11077
  onDesfazer: () => void desfazer(line.id),
10817
11078
  onVerConta: () => {
10818
11079
  const modulo = line.direction === "debit" ? "payables" : "receivables";
@@ -10842,6 +11103,8 @@ function ReconciliationView() {
10842
11103
  {
10843
11104
  line: flow.line,
10844
11105
  initialIntent: flow.intent,
11106
+ initialTarget: flow.target,
11107
+ initialStep: flow.step,
10845
11108
  onClose: () => setFlow(null),
10846
11109
  onDone: () => {
10847
11110
  setFlow(null);
@@ -10971,7 +11234,7 @@ function StatementCard({
10971
11234
  }
10972
11235
  )
10973
11236
  ] }),
10974
- /* @__PURE__ */ jsxs(Button, { size: "sm", className: "shrink-0", disabled: busy, onClick: () => onAceitarCandidato(sugestao.movement.id), children: [
11237
+ /* @__PURE__ */ jsxs(Button, { size: "sm", className: "shrink-0", disabled: busy, onClick: () => onAceitarCandidato(sugestao), children: [
10975
11238
  /* @__PURE__ */ jsx(Link2, { className: "h-3 w-3" }),
10976
11239
  t2("financial.reconciliation.match")
10977
11240
  ] })
@@ -11229,6 +11492,7 @@ function RecurringBillsList({ direction, onNavigate }) {
11229
11492
  function RecurringBillFormView({ direction, valueId, onBack }) {
11230
11493
  const provider = useFinancialProvider();
11231
11494
  const { locations, currency, itemTypes: configuredItemTypes, entityLookups } = useFinancialConfig();
11495
+ const t2 = useTranslation();
11232
11496
  const isPayable = direction === "payable";
11233
11497
  const [value, setValue] = React25__default.useState(null);
11234
11498
  const [loaded, setLoaded] = React25__default.useState(!valueId);
@@ -11260,6 +11524,7 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11260
11524
  const [isActive, setIsActive] = React25__default.useState(true);
11261
11525
  const [accounts, setAccounts] = React25__default.useState([]);
11262
11526
  const [costCenters, setCostCenters] = React25__default.useState([]);
11527
+ const { choices: unitChoices } = useWritableUnits(availableLocations);
11263
11528
  const [saving, setSaving] = React25__default.useState(false);
11264
11529
  const [ending, setEnding] = React25__default.useState(false);
11265
11530
  const [error, setError] = React25__default.useState(null);
@@ -11325,12 +11590,16 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11325
11590
  cancelled = true;
11326
11591
  };
11327
11592
  }, [provider, direction, valueId]);
11593
+ const translatedItemTypes = React25__default.useMemo(() => configuredItemTypes.map((tp) => {
11594
+ const label = t2(`financial.itemType.${tp.value}`);
11595
+ return label === `financial.itemType.${tp.value}` ? tp : { ...tp, label };
11596
+ }), [configuredItemTypes, t2]);
11328
11597
  const offeredItemTypes = React25__default.useMemo(() => availableInvoiceItemTypes(
11329
- configuredItemTypes,
11598
+ translatedItemTypes,
11330
11599
  Object.keys(entityLookups),
11331
- configuredItemTypes.find((item) => item.value === "other")?.label ?? "Outro",
11600
+ t2("financial.itemType.other"),
11332
11601
  direction
11333
- ), [configuredItemTypes, entityLookups, direction]);
11602
+ ), [translatedItemTypes, entityLookups, direction, t2]);
11334
11603
  const itemTypes = useStockedItemTypes(offeredItemTypes, entityLookups);
11335
11604
  const selectableAccounts = React25__default.useMemo(() => {
11336
11605
  const postable = postableAccounts(accounts);
@@ -11456,13 +11725,10 @@ function RecurringBillFormView({ direction, valueId, onBack }) {
11456
11725
  ] }) : void 0,
11457
11726
  sideSections: /* @__PURE__ */ jsxs(Fragment, { children: [
11458
11727
  /* @__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
- ] }) }),
11728
+ /* @__PURE__ */ jsx(UnitField, { label: "Unidade", choices: unitChoices, value: unitId, onChange: setUnitId }),
11463
11729
  /* @__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
11730
  /* @__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: [
11731
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
11466
11732
  /* @__PURE__ */ jsx(Field2, { label: "In\xEDcio *", children: /* @__PURE__ */ jsx(DatePicker, { value: startsOn, onChange: setStartsOn, className: "w-full" }) }),
11467
11733
  /* @__PURE__ */ jsx(Field2, { label: "T\xE9rmino", children: /* @__PURE__ */ jsx(DatePicker, { value: endsOn, onChange: setEndsOn, className: "w-full" }) })
11468
11734
  ] }),
@@ -12424,6 +12690,10 @@ var STOPWORDS = /* @__PURE__ */ new Set([
12424
12690
  "pagamento",
12425
12691
  "pagto",
12426
12692
  "pag",
12693
+ "pago",
12694
+ "paga",
12695
+ "pgto",
12696
+ "recebimento",
12427
12697
  "boleto",
12428
12698
  "debito",
12429
12699
  "credito",
@@ -12468,15 +12738,20 @@ function nameScore(descricao, nome) {
12468
12738
  if (texto.size === 0) return 0;
12469
12739
  const achados = alvo.filter((w) => texto.has(w)).length;
12470
12740
  const razao = achados / alvo.length;
12471
- if (achados === 1 && alvo.length > 2) return razao * 0.4;
12472
- return razao;
12741
+ const nomeSet = new Set(alvo);
12742
+ const reverso = texto.size >= 2 && [...texto].every((w) => nomeSet.has(w)) ? 1 : 0;
12743
+ if (achados === 1 && alvo.length > 2) return Math.max(reverso, razao * 0.4);
12744
+ return Math.max(reverso, razao);
12473
12745
  }
12474
- function amountScore(lineAmount, movementAmount) {
12746
+ function amountScore(lineAmount, movementAmount, tolerance = 0.02) {
12475
12747
  if (lineAmount <= 0) return 0;
12476
12748
  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));
12749
+ const limite = lineAmount * tolerance + 0.01;
12750
+ if (diff > limite) return 0;
12751
+ return Math.max(0, 1 - diff / limite);
12479
12752
  }
12753
+ var NAME_AMOUNT_TOLERANCE = 0.1;
12754
+ var STRONG_NAME = 0.5;
12480
12755
  function dateScore(lineDate, refDate) {
12481
12756
  if (!lineDate || !refDate) return 0;
12482
12757
  const days = Math.abs(new Date(lineDate).getTime() - new Date(refDate).getTime()) / 864e5;
@@ -12484,13 +12759,16 @@ function dateScore(lineDate, refDate) {
12484
12759
  }
12485
12760
  function scorePair(line, movement, context = {}) {
12486
12761
  if (movement.direction !== line.direction) return 0;
12487
- const valor = amountScore(line.amount, movement.amount);
12488
- if (valor === 0) return 0;
12489
12762
  const texto = `${line.description ?? ""} ${line.counterpartyName ?? ""}`;
12490
12763
  const nome = Math.max(
12491
12764
  nameScore(texto, context.contactName),
12492
12765
  nameScore(texto, movement.notes)
12493
12766
  );
12767
+ let valor = amountScore(line.amount, movement.amount);
12768
+ if (valor === 0 && nome >= STRONG_NAME) {
12769
+ valor = 0.7 * amountScore(line.amount, movement.amount, NAME_AMOUNT_TOLERANCE);
12770
+ }
12771
+ if (valor === 0) return 0;
12494
12772
  const data = dateScore(line.date, movement.paymentDate ?? movement.dueDate);
12495
12773
  return Math.round((0.45 * valor + 0.35 * nome + 0.2 * data) * 100) / 100;
12496
12774
  }
@@ -13378,6 +13656,22 @@ function createMockFinancialProvider(options) {
13378
13656
  };
13379
13657
  }).sort((a, b) => b.score - a.score).slice(0, 25);
13380
13658
  },
13659
+ async searchReconciliationTargets(statementLineId, query) {
13660
+ const line = store6.statementLines.find((l) => l.id === statementLineId);
13661
+ if (!line) return { data: [], hasMore: false };
13662
+ const status = query.status ?? "all";
13663
+ const termo = query.search?.trim().toLocaleLowerCase("pt-BR");
13664
+ const rows = store6.movements.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction).filter((m) => m.status !== "cancelled").filter((m) => {
13665
+ const paga = m.movementKind === "payment" || m.status === "paid";
13666
+ return status === "all" || (status === "paid" ? paga : !paga);
13667
+ }).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 ?? ""));
13668
+ const pageSize = query.pageSize ?? 50;
13669
+ const start = ((query.page ?? 1) - 1) * pageSize;
13670
+ return {
13671
+ data: rows.slice(start, start + pageSize).map((c) => ({ ...c, score: 0 })),
13672
+ hasMore: rows.length > start + pageSize
13673
+ };
13674
+ },
13381
13675
  // A mesma cabeça da conciliação de verdade (match-statement), sobre a
13382
13676
  // memória: valor, nome e data, e um lançamento por linha.
13383
13677
  async suggestReconciliationBatch(lines) {
@@ -13685,6 +13979,19 @@ async function writeLineAccounting(pub, tenantId, insertedItems, inputItems) {
13685
13979
  const { error } = await pub.from(T.invoiceItems).upsert(rows, { onConflict: "item_id" });
13686
13980
  if (error) console.warn("[financial] line accounting mirror skipped:", error.message);
13687
13981
  }
13982
+ function shiftIsoDate(value, days) {
13983
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
13984
+ d.setUTCDate(d.getUTCDate() + days);
13985
+ return d.toISOString().slice(0, 10);
13986
+ }
13987
+ function matchScore(line, m) {
13988
+ const amount = Number(line.amount) || 0;
13989
+ const amtDiff = Math.abs((Number(m.amount) || 0) - amount);
13990
+ const amtScore = amount > 0 ? Math.max(0, 1 - amtDiff / amount) : 0;
13991
+ const refDate = m.dueDate ?? m.paymentDate;
13992
+ const days = line.date && refDate ? Math.abs((new Date(line.date).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
13993
+ return round26(0.7 * amtScore + 0.3 * Math.max(0, 1 - days / 30));
13994
+ }
13688
13995
  function billRowToMovement(r) {
13689
13996
  return {
13690
13997
  id: r.installment_id,
@@ -13781,6 +14088,85 @@ function createSupabaseFinancialProvider() {
13781
14088
  externalSource: data.external_source
13782
14089
  };
13783
14090
  }
14091
+ async function searchBillTargets(line, query) {
14092
+ const { pub } = getClients();
14093
+ const pageSize = query.pageSize ?? 50;
14094
+ const page = query.page ?? 1;
14095
+ 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");
14096
+ const status = query.status ?? "all";
14097
+ qb = status === "open" ? qb.in("status", ["pending", "partial", "overdue"]) : status === "paid" ? qb.eq("status", "paid") : qb.neq("status", "cancelled");
14098
+ if (query.dueFrom) qb = qb.gte("due_date", query.dueFrom);
14099
+ if (query.dueTo) qb = qb.lte("due_date", query.dueTo);
14100
+ if (query.amountMin != null) qb = qb.gte("amount", query.amountMin);
14101
+ if (query.amountMax != null) qb = qb.lte("amount", query.amountMax);
14102
+ const termo = query.search?.trim().replace(/[,()*%]/g, " ").replace(/\s+/g, " ");
14103
+ if (termo) {
14104
+ qb = qb.or(["description", "number", "counterparty_name"].map((c) => `${c}.ilike.%${termo}%`).join(","));
14105
+ }
14106
+ const { data, error } = await qb.order("due_date", { ascending: true }).range((page - 1) * pageSize, page * pageSize);
14107
+ if (error) throw error;
14108
+ const rows = data ?? [];
14109
+ const hasMore = rows.length > pageSize;
14110
+ const visiveis = rows.slice(0, pageSize);
14111
+ const pagas = visiveis.filter((r) => r.status === "paid").map((r) => r.installment_id);
14112
+ const pagamentoDe = /* @__PURE__ */ new Map();
14113
+ if (pagas.length) {
14114
+ const { data: allocs } = await pub.from(T.allocations).select("installment_id,payment_movement_id").in("installment_id", pagas);
14115
+ const ids = [...new Set((allocs ?? []).map((a) => a.payment_movement_id).filter(Boolean))];
14116
+ const { data: pays } = ids.length ? await pub.from(T.movements).select("*").in("id", ids).neq("status", "cancelled").is("reversed_by_id", null) : { data: [] };
14117
+ const byId = new Map((pays ?? []).map((p) => [p.id, p]));
14118
+ for (const a of allocs ?? []) {
14119
+ const p = byId.get(a.payment_movement_id);
14120
+ if (p) pagamentoDe.set(a.installment_id, p);
14121
+ }
14122
+ }
14123
+ const candidates = [];
14124
+ for (const r of visiveis) {
14125
+ const invoice = {
14126
+ id: r.invoice_id,
14127
+ contactName: r.counterparty_name ?? void 0,
14128
+ fiscalNumber: r.number ?? void 0
14129
+ };
14130
+ let movement;
14131
+ if (r.status === "paid") {
14132
+ const p = pagamentoDe.get(r.installment_id);
14133
+ if (!p || p.reconciled_at) continue;
14134
+ movement = {
14135
+ ...snakeToCamel(p),
14136
+ invoiceId: r.invoice_id,
14137
+ settlesMovementId: r.installment_id,
14138
+ dueDate: r.due_date,
14139
+ notes: p.notes ?? r.description ?? void 0
14140
+ };
14141
+ } else {
14142
+ movement = {
14143
+ id: r.installment_id,
14144
+ invoiceId: r.invoice_id,
14145
+ direction: line.direction,
14146
+ movementKind: "bill",
14147
+ amount: Number(r.amount) || 0,
14148
+ paidAmount: Number(r.paid) || 0,
14149
+ status: r.status,
14150
+ dueDate: r.due_date,
14151
+ installmentNumber: r.installment_number ?? void 0,
14152
+ notes: r.description ?? void 0
14153
+ };
14154
+ }
14155
+ const pareado = scorePair(
14156
+ { amount: line.amount, date: line.date, direction: line.direction, description: line.description ?? "" },
14157
+ movement,
14158
+ { contactName: r.counterparty_name ?? void 0 }
14159
+ );
14160
+ candidates.push({
14161
+ movement,
14162
+ invoice,
14163
+ score: pareado > 0 ? pareado : round26(0.6 * matchScore(line, movement)),
14164
+ installmentCount: r.installment_count ?? void 0
14165
+ });
14166
+ }
14167
+ candidates.sort((a, b) => b.score - a.score);
14168
+ return { data: candidates, hasMore };
14169
+ }
13784
14170
  async function markReconciled(statementLineId, matchedMovementId) {
13785
14171
  const { pub } = getClients();
13786
14172
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
@@ -15421,6 +15807,31 @@ function createSupabaseFinancialProvider() {
15421
15807
  customerEmail: r.customer_email
15422
15808
  }));
15423
15809
  },
15810
+ async findGiftCardByCode(raw) {
15811
+ const code = raw.replace(/[\s.-]/g, "").toUpperCase();
15812
+ if (!code) return null;
15813
+ const { pub } = getClients();
15814
+ let { data, error } = await pub.rpc("financial_gift_card_by_code", { p_code: code });
15815
+ if (error?.code === "PGRST202") {
15816
+ ({ data, error } = await pub.rpc("financial_gift_cards", { p_search: code, p_limit: 5 }));
15817
+ }
15818
+ if (error) throw error;
15819
+ const row = (data ?? []).find((r) => String(r.code).toUpperCase() === code);
15820
+ if (!row) return null;
15821
+ return {
15822
+ id: row.id,
15823
+ code: row.code,
15824
+ faceValue: Number(row.face_value) || 0,
15825
+ balance: row.balance == null ? null : Number(row.balance),
15826
+ state: row.state,
15827
+ currency: row.currency,
15828
+ issuedAt: row.issued_at,
15829
+ expiresAt: row.expires_at,
15830
+ activatedAt: row.activated_at,
15831
+ note: row.note,
15832
+ customerEmail: row.customer_email
15833
+ };
15834
+ },
15424
15835
  async issueGiftCards(input) {
15425
15836
  const { pub } = getClients();
15426
15837
  const { data, error } = await pub.rpc("financial_issue_gift_cards", {
@@ -15580,7 +15991,17 @@ function createSupabaseFinancialProvider() {
15580
15991
  if (o) c.invoice = { id: o.id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number };
15581
15992
  }
15582
15993
  }
15583
- return candidates;
15994
+ const linha = { id: line.id, amount, date: lineDate, direction: line.direction, externalSource: line.external_source };
15995
+ const abertas = await searchBillTargets(linha, {
15996
+ status: "open",
15997
+ amountMin: round26(amount - tol),
15998
+ amountMax: round26(amount + tol),
15999
+ pageSize: 25
16000
+ }).catch(() => ({ data: [], hasMore: false }));
16001
+ return [...candidates, ...abertas.data].sort((a, b) => b.score - a.score);
16002
+ },
16003
+ async searchReconciliationTargets(statementLineId, query) {
16004
+ return searchBillTargets(await readStatementLine(statementLineId), query);
15584
16005
  },
15585
16006
  // ------------------------------------------------------------------
15586
16007
  // A SUGESTÃO QUE CHEGA SOZINHA, para a página inteira.
@@ -15600,16 +16021,38 @@ function createSupabaseFinancialProvider() {
15600
16021
  const abertas = lines.filter((line) => !line.reconciledAt && (Number(line.amount) || 0) > 0);
15601
16022
  if (abertas.length === 0) return {};
15602
16023
  const faixas = [...new Set(abertas.map((line) => round26(line.amount)))].map((valor) => {
15603
- const tol = Math.max(0.01, round26(valor * 0.02));
16024
+ const tol = Math.max(0.01, round26(valor * NAME_AMOUNT_TOLERANCE));
15604
16025
  return `and(amount.gte.${round26(valor - tol)},amount.lte.${round26(valor + tol)})`;
15605
16026
  });
15606
16027
  const direcoes = [...new Set(abertas.map((line) => line.direction))];
15607
16028
  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
16029
  if (error) throw error;
15609
16030
  const movimentos = (data ?? []).map((r) => snakeToCamel(r));
15610
- if (movimentos.length === 0) return {};
16031
+ const datas = abertas.map((line) => line.date).filter(Boolean).sort();
15611
16032
  const contexto = /* @__PURE__ */ new Map();
15612
- const invoiceIds = [...new Set(movimentos.map((m) => m.invoiceId).filter(Boolean))];
16033
+ if (datas.length) {
16034
+ 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);
16035
+ for (const r of billRows ?? []) {
16036
+ movimentos.push({
16037
+ id: r.installment_id,
16038
+ invoiceId: r.invoice_id,
16039
+ direction: r.direction === "payable" ? "debit" : "credit",
16040
+ movementKind: "bill",
16041
+ amount: Number(r.amount) || 0,
16042
+ paidAmount: Number(r.paid) || 0,
16043
+ status: r.status,
16044
+ dueDate: r.due_date,
16045
+ installmentNumber: r.installment_number ?? void 0,
16046
+ notes: r.description ?? void 0
16047
+ });
16048
+ contexto.set(r.installment_id, {
16049
+ contactName: r.counterparty_name ?? void 0,
16050
+ invoice: { id: r.invoice_id, contactName: r.counterparty_name ?? void 0, fiscalNumber: r.number ?? void 0 }
16051
+ });
16052
+ }
16053
+ }
16054
+ if (movimentos.length === 0) return {};
16055
+ const invoiceIds = [...new Set(movimentos.filter((m) => !contexto.has(m.id)).map((m) => m.invoiceId).filter(Boolean))];
15613
16056
  if (invoiceIds.length > 0) {
15614
16057
  const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", invoiceIds);
15615
16058
  const byId = new Map((orders ?? []).map((o) => [o.id, o]));
@@ -15628,7 +16071,7 @@ function createSupabaseFinancialProvider() {
15628
16071
  async reconcileMovement(input) {
15629
16072
  if (input.mode === "settle" && input.matchedMovementId) {
15630
16073
  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();
16074
+ const { data: alvo } = await getClients().pub.from(T.bills).select("amount, paid").eq("installment_id", input.matchedMovementId).maybeSingle();
15632
16075
  if (!alvo) throw new Error("Lan\xE7amento n\xE3o encontrado");
15633
16076
  await runReconcileLink(
15634
16077
  reconcileFacade(),
@@ -15636,7 +16079,7 @@ function createSupabaseFinancialProvider() {
15636
16079
  {
15637
16080
  movementId: input.matchedMovementId,
15638
16081
  mode: "settle",
15639
- openAmount: round26((Number(alvo.amount) || 0) - (Number(alvo.paid_amount) || 0))
16082
+ openAmount: round26((Number(alvo.amount) || 0) - (Number(alvo.paid) || 0))
15640
16083
  },
15641
16084
  { payment: input.payment, adjustments: input.adjustments }
15642
16085
  );
@@ -15911,7 +16354,8 @@ function createSupabaseFinancialProvider() {
15911
16354
  };
15912
16355
  for (const payee of payees) {
15913
16356
  if (text2(payee.document)) {
15914
- await pub.from("people").update({ document_number: text2(payee.document) }).eq("id", payee.contactId);
16357
+ const { error: error2 } = await pub.from("people").update({ document_number: text2(payee.document) }).eq("id", payee.contactId);
16358
+ if (error2) throw error2;
15915
16359
  }
15916
16360
  const row = {
15917
16361
  bank_code: text2(payee.bankCode),
@@ -15930,18 +16374,16 @@ function createSupabaseFinancialProvider() {
15930
16374
  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
16375
  targetId = existing?.id;
15932
16376
  }
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
- }
16377
+ const { data: written, error } = targetId ? await pub.from(T.payeeAccounts).update(row).eq("id", targetId).select("id") : await pub.from(T.payeeAccounts).insert({
16378
+ ...row,
16379
+ tenant_id: tenantId,
16380
+ owner_type: "person",
16381
+ owner_id: payee.contactId,
16382
+ label: "Principal",
16383
+ is_default: true
16384
+ }).select("id");
16385
+ if (error) throw error;
16386
+ if (!written?.length) throw new Error("payee account was not saved (no financial edit permission?)");
15945
16387
  }
15946
16388
  },
15947
16389
  /**
@@ -18029,6 +18471,9 @@ var en = {
18029
18471
  "financial.invoiceForm.paidNeedsMethod": "Tell us the payment method and the account",
18030
18472
  "financial.invoiceForm.willBePaid": "This installment will be settled",
18031
18473
  "financial.invoiceForm.clear": "Clear",
18474
+ "financial.invoiceForm.manageAccounts": "Manage accounts",
18475
+ "financial.invoiceForm.manageChart": "Manage chart of accounts",
18476
+ "financial.invoiceForm.manageCostCenters": "Manage cost centers",
18032
18477
  "financial.invoice.newManualHint": "Open the full invoice form",
18033
18478
  "financial.invoice.noDetails": "No additional details recorded",
18034
18479
  "financial.invoice.unlinkedPayments": "Payments not linked to an installment",
@@ -18077,6 +18522,13 @@ var en = {
18077
18522
  "financial.itemType.product": "Product",
18078
18523
  "financial.itemType.other": "Other",
18079
18524
  "financial.itemType.gift_card": "Gift Card",
18525
+ "financial.giftCardLine.label": "Gift card code",
18526
+ "financial.giftCardLine.placeholder": "0000 0000 0000 0000",
18527
+ "financial.giftCardLine.hint": "Scan or type the full card code.",
18528
+ "financial.giftCardLine.notFound": "Gift card not found",
18529
+ "financial.giftCardLine.revoked": "Gift card cancelled \u2014 cannot be sold",
18530
+ "financial.giftCardLine.expired": "Gift card expired",
18531
+ "financial.giftCardLine.alreadySold": "This gift card has already been sold",
18080
18532
  "financial.invoiceForm.cancel": "Cancel",
18081
18533
  "financial.invoiceForm.close": "Close",
18082
18534
  "financial.invoiceForm.creating": "Creating new invoice",
@@ -18280,13 +18732,41 @@ var en = {
18280
18732
  "financial.reconciliation.reconciled": "Reconciled",
18281
18733
  "financial.reconciliation.undo": "Undo",
18282
18734
  "financial.reconciliation.flowTitle": "Reconcile transaction",
18283
- "financial.reconciliation.stepClassify": "How to classify",
18735
+ "financial.reconciliation.stepClassify": "What to do",
18284
18736
  "financial.reconciliation.stepItems": "The items",
18285
18737
  "financial.reconciliation.stepDetails": "The details",
18286
18738
  "financial.reconciliation.stepConfirm": "Confirm",
18287
18739
  "financial.reconciliation.confirmAction": "Confirm",
18288
18740
  "financial.reconciliation.howToClassify": "How do you want to classify this transaction?",
18289
18741
  "financial.reconciliation.pickIntent": "Pick what this transaction is",
18742
+ "financial.reconciliation.foundTitle": "Bills found",
18743
+ "financial.reconciliation.searchWiden": "Widen search (\xB115 days, any amount)",
18744
+ "financial.reconciliation.stepSelectBill": "Select the bill",
18745
+ "financial.reconciliation.stepAdjust": "Adjustment",
18746
+ "financial.reconciliation.selectBillFor": "Statement of {{date}} \xB7 {{description}} \xB7",
18747
+ "financial.reconciliation.statusOverdue": "Overdue",
18748
+ "financial.reconciliation.colDue": "Due",
18749
+ "financial.reconciliation.colCounterparty": "Counterparty",
18750
+ "financial.reconciliation.colDescription": "Number / Description",
18751
+ "financial.reconciliation.colInstallment": "Installment",
18752
+ "financial.reconciliation.colToReconcile": "To reconcile",
18753
+ "financial.reconciliation.colDelta": "\u0394 vs statement",
18754
+ "financial.reconciliation.searchStatusAllShort": "All",
18755
+ "financial.reconciliation.searchPendingHint": "The list still shows the previous filters. Click Filter to apply.",
18756
+ "financial.reconciliation.searchTitle": "Find a bill",
18757
+ "financial.reconciliation.searchDueFrom": "Due from",
18758
+ "financial.reconciliation.searchDueTo": "to",
18759
+ "financial.reconciliation.searchStatus": "Status",
18760
+ "financial.reconciliation.searchStatusAll": "All (not reconciled)",
18761
+ "financial.reconciliation.searchStatusOpen": "Open",
18762
+ "financial.reconciliation.searchStatusPaid": "Paid",
18763
+ "financial.reconciliation.searchText": "Search (counterparty, number, description)",
18764
+ "financial.reconciliation.searchTextPlaceholder": "Type and press Enter",
18765
+ "financial.reconciliation.searchAmountMin": "Min amount",
18766
+ "financial.reconciliation.searchAmountMax": "Max amount",
18767
+ "financial.reconciliation.searchAction": "Search",
18768
+ "financial.reconciliation.searchEmpty": "No bills match these filters.",
18769
+ "financial.reconciliation.searchMore": "Load more",
18290
18770
  "financial.reconciliation.pickCandidate": "Pick the entry to link",
18291
18771
  "financial.reconciliation.needDescription": "Describe the entry",
18292
18772
  "financial.reconciliation.intentLink": "Link to an existing entry",
@@ -18322,6 +18802,14 @@ var en = {
18322
18802
  "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
18803
  "financial.reconciliation.noAdjustmentNeeded": "The amount matches the bill. Nothing to adjust.",
18324
18804
  "financial.reconciliation.adjustmentNeeded": "The bank took {{gap}} {{side}} than the bill asked for. Say what that difference is.",
18805
+ "financial.reconciliation.adjustmentNeededIn": "{{gap}} {{side}} came in than the bill charged. Say what that difference is.",
18806
+ "financial.reconciliation.adjustmentNeedsAccount": "Pick the chart account for each adjustment.",
18807
+ "financial.reconciliation.adjustmentType": "Type",
18808
+ "financial.reconciliation.adjustmentDescriptionPlaceholder": "E.g. late interest",
18809
+ "financial.reconciliation.adjustmentDefaultInterest": "Late interest",
18810
+ "financial.reconciliation.adjustmentDefaultFine": "Late fine",
18811
+ "financial.reconciliation.adjustmentDefaultDiscountOut": "Discount obtained",
18812
+ "financial.reconciliation.adjustmentDefaultDiscountIn": "Discount granted",
18325
18813
  "financial.reconciliation.adjustmentMore": "more",
18326
18814
  "financial.reconciliation.adjustmentLess": "less",
18327
18815
  "financial.reconciliation.interest": "Interest",
@@ -18923,6 +19411,9 @@ var ptBR = {
18923
19411
  "financial.invoiceForm.paidNeedsMethod": "Diga a forma de pagamento e a conta",
18924
19412
  "financial.invoiceForm.willBePaid": "Esta parcela entra como paga",
18925
19413
  "financial.invoiceForm.clear": "Limpar",
19414
+ "financial.invoiceForm.manageAccounts": "Gerenciar contas",
19415
+ "financial.invoiceForm.manageChart": "Gerenciar plano de contas",
19416
+ "financial.invoiceForm.manageCostCenters": "Gerenciar centros de custo",
18926
19417
  "financial.invoice.newManualHint": "Preencher os campos da conta",
18927
19418
  "financial.invoice.noDetails": "Nenhum detalhe adicional registrado",
18928
19419
  "financial.invoice.unlinkedPayments": "Pagamentos sem parcela vinculada",
@@ -18971,6 +19462,13 @@ var ptBR = {
18971
19462
  "financial.itemType.product": "Produto",
18972
19463
  "financial.itemType.other": "Outro",
18973
19464
  "financial.itemType.gift_card": "Gift Card",
19465
+ "financial.giftCardLine.label": "C\xF3digo do gift card",
19466
+ "financial.giftCardLine.placeholder": "0000 0000 0000 0000",
19467
+ "financial.giftCardLine.hint": "Escaneie ou digite o c\xF3digo completo do cart\xE3o.",
19468
+ "financial.giftCardLine.notFound": "Gift card n\xE3o encontrado",
19469
+ "financial.giftCardLine.revoked": "Gift card cancelado \u2014 n\xE3o pode ser vendido",
19470
+ "financial.giftCardLine.expired": "Gift card expirado",
19471
+ "financial.giftCardLine.alreadySold": "Este gift card j\xE1 foi vendido",
18974
19472
  "financial.invoiceForm.cancel": "Cancelar",
18975
19473
  "financial.invoiceForm.close": "Fechar",
18976
19474
  "financial.invoiceForm.creating": "Criando nova fatura",
@@ -19177,13 +19675,41 @@ var ptBR = {
19177
19675
  "financial.reconciliation.reconciled": "Conciliado",
19178
19676
  "financial.reconciliation.undo": "Desfazer",
19179
19677
  "financial.reconciliation.flowTitle": "Conciliar movimenta\xE7\xE3o",
19180
- "financial.reconciliation.stepClassify": "Como classificar",
19678
+ "financial.reconciliation.stepClassify": "O que deseja fazer",
19181
19679
  "financial.reconciliation.stepItems": "Os itens",
19182
19680
  "financial.reconciliation.stepDetails": "Os detalhes",
19183
19681
  "financial.reconciliation.stepConfirm": "Confirmar",
19184
19682
  "financial.reconciliation.confirmAction": "Confirmar",
19185
19683
  "financial.reconciliation.howToClassify": "Como voc\xEA quer classificar esta transa\xE7\xE3o?",
19186
19684
  "financial.reconciliation.pickIntent": "Escolha o que esta transa\xE7\xE3o \xE9",
19685
+ "financial.reconciliation.foundTitle": "Contas encontradas",
19686
+ "financial.reconciliation.searchWiden": "Ampliar busca (\xB115 dias, qualquer valor)",
19687
+ "financial.reconciliation.stepSelectBill": "Selecione a conta",
19688
+ "financial.reconciliation.stepAdjust": "Ajuste",
19689
+ "financial.reconciliation.selectBillFor": "Extrato de {{date}} \xB7 {{description}} \xB7",
19690
+ "financial.reconciliation.statusOverdue": "Vencido",
19691
+ "financial.reconciliation.colDue": "Venc.",
19692
+ "financial.reconciliation.colCounterparty": "Contraparte",
19693
+ "financial.reconciliation.colDescription": "N\xFAmero / Descri\xE7\xE3o",
19694
+ "financial.reconciliation.colInstallment": "Parcela",
19695
+ "financial.reconciliation.colToReconcile": "Valor a conciliar",
19696
+ "financial.reconciliation.colDelta": "\u0394 vs extrato",
19697
+ "financial.reconciliation.searchStatusAllShort": "Todos",
19698
+ "financial.reconciliation.searchPendingHint": "A lista ainda mostra o recorte anterior. Clique em Filtrar para aplicar.",
19699
+ "financial.reconciliation.searchTitle": "Buscar conta",
19700
+ "financial.reconciliation.searchDueFrom": "Vencimento de",
19701
+ "financial.reconciliation.searchDueTo": "at\xE9",
19702
+ "financial.reconciliation.searchStatus": "Situa\xE7\xE3o",
19703
+ "financial.reconciliation.searchStatusAll": "Todos (n\xE3o conciliados)",
19704
+ "financial.reconciliation.searchStatusOpen": "Em aberto",
19705
+ "financial.reconciliation.searchStatusPaid": "Pagos",
19706
+ "financial.reconciliation.searchText": "Buscar (contraparte, n\xFAmero, descri\xE7\xE3o)",
19707
+ "financial.reconciliation.searchTextPlaceholder": "Digite e pressione Enter",
19708
+ "financial.reconciliation.searchAmountMin": "Valor m\xEDn.",
19709
+ "financial.reconciliation.searchAmountMax": "Valor m\xE1x.",
19710
+ "financial.reconciliation.searchAction": "Buscar",
19711
+ "financial.reconciliation.searchEmpty": "Nenhuma conta com esses filtros.",
19712
+ "financial.reconciliation.searchMore": "Carregar mais",
19187
19713
  "financial.reconciliation.pickCandidate": "Escolha o lan\xE7amento a vincular",
19188
19714
  "financial.reconciliation.needDescription": "Descreva o lan\xE7amento",
19189
19715
  "financial.reconciliation.intentLink": "Vincular a um lan\xE7amento",
@@ -19219,6 +19745,14 @@ var ptBR = {
19219
19745
  "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
19746
  "financial.reconciliation.noAdjustmentNeeded": "O valor bate com a conta. Nada a ajustar.",
19221
19747
  "financial.reconciliation.adjustmentNeeded": "O banco tirou {{gap}} {{side}} do que a conta pedia. Diga o que \xE9 essa diferen\xE7a.",
19748
+ "financial.reconciliation.adjustmentNeededIn": "Entrou {{gap}} {{side}} do que a conta cobrava. Diga o que \xE9 essa diferen\xE7a.",
19749
+ "financial.reconciliation.adjustmentNeedsAccount": "Escolha o plano de contas de cada ajuste.",
19750
+ "financial.reconciliation.adjustmentType": "Tipo",
19751
+ "financial.reconciliation.adjustmentDescriptionPlaceholder": "Ex.: juros por atraso",
19752
+ "financial.reconciliation.adjustmentDefaultInterest": "Juros por atraso",
19753
+ "financial.reconciliation.adjustmentDefaultFine": "Multa por atraso",
19754
+ "financial.reconciliation.adjustmentDefaultDiscountOut": "Desconto obtido",
19755
+ "financial.reconciliation.adjustmentDefaultDiscountIn": "Desconto concedido",
19222
19756
  "financial.reconciliation.adjustmentMore": "a mais",
19223
19757
  "financial.reconciliation.adjustmentLess": "a menos",
19224
19758
  "financial.reconciliation.interest": "Juros",
@@ -44490,6 +45024,112 @@ BEGIN
44490
45024
  RETURN jsonb_build_object('reversal_id', v_rev, 'movement_id', v_m.id, 'replayed', false);
44491
45025
  END $$;
44492
45026
 
45027
+ `;
45028
+ var MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP = `-- ---------------------------------------------------------------------------
45029
+ -- 117 \u2014 copying lines onto an invoice is an internal step, not a door.
45030
+ --
45031
+ -- Both copiers are SECURITY DEFINER with no caller check and were executable by
45032
+ -- PUBLIC (so anon too): anyone holding an invoice id could write public.items
45033
+ -- into it. Every real caller is another SECURITY DEFINER function owned by
45034
+ -- postgres (fn_invoice_from_order_internal, order_raise_receivable,
45035
+ -- financial_recurring_run, financial_consolidate_occurrence), which keeps its
45036
+ -- right to execute as owner. So the fix is the grant, not the body.
45037
+ --
45038
+ -- Idempotent: grants only.
45039
+ -- ---------------------------------------------------------------------------
45040
+
45041
+ REVOKE ALL ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) FROM PUBLIC, anon, authenticated;
45042
+ GRANT EXECUTE ON FUNCTION public.plg_financial_copy_bill_items(uuid, uuid, jsonb) TO service_role;
45043
+
45044
+ REVOKE ALL ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) FROM PUBLIC, anon, authenticated;
45045
+ GRANT EXECUTE ON FUNCTION public.plg_financial_copy_order_lines_to_invoice(uuid, uuid) TO service_role;
45046
+ `;
45047
+ var MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE = `-- 118_gift_card_found_by_exact_code.sql
45048
+ --
45049
+ -- Selling a gift card on a receivable takes the code typed at the counter and
45050
+ -- must match exactly one card. financial_gift_cards (027) matches by substring,
45051
+ -- so a partial code answered with real codes of other cards. This lookup
45052
+ -- returns the one card whose code equals the input (spaces, dots, dashes and
45053
+ -- case ignored), or nothing.
45054
+ --
45055
+ -- The shop table is optional and applied after this chain, so it is resolved
45056
+ -- at run time, as in 027.
45057
+
45058
+ CREATE OR REPLACE FUNCTION public.financial_gift_card_by_code(p_code text)
45059
+ RETURNS TABLE (
45060
+ id uuid,
45061
+ code text,
45062
+ face_value numeric,
45063
+ balance numeric,
45064
+ balance_known boolean,
45065
+ state text,
45066
+ currency text,
45067
+ issued_at timestamptz,
45068
+ expires_at timestamptz,
45069
+ activated_at timestamptz,
45070
+ note text,
45071
+ customer_email text
45072
+ )
45073
+ LANGUAGE plpgsql
45074
+ STABLE
45075
+ SECURITY DEFINER
45076
+ SET search_path TO ''
45077
+ AS $$
45078
+ DECLARE
45079
+ v_tenant uuid := app.current_tenant_id();
45080
+ v_code text := upper(regexp_replace(coalesce(p_code, ''), '[\\s.\\-]', '', 'g'));
45081
+ v_activated text;
45082
+ BEGIN
45083
+ IF v_tenant IS NULL THEN
45084
+ RAISE EXCEPTION 'financial_gift_card_by_code: sem tenant na sess\xE3o' USING ERRCODE = '42501';
45085
+ END IF;
45086
+ -- Whoever can raise a receivable can sell a card on it.
45087
+ IF NOT app.has_permission('financial.create', NULL) THEN
45088
+ RAISE EXCEPTION 'financial_gift_card_by_code: exige financial.create' USING ERRCODE = '42501';
45089
+ END IF;
45090
+ IF v_code = '' OR to_regclass('public.plg_shop_gift_cards') IS NULL THEN
45091
+ RETURN;
45092
+ END IF;
45093
+
45094
+ IF EXISTS (SELECT 1 FROM information_schema.columns c
45095
+ WHERE c.table_schema = 'public' AND c.table_name = 'plg_shop_gift_cards'
45096
+ AND c.column_name = 'activated_at') THEN
45097
+ v_activated := 'g.activated_at';
45098
+ ELSE
45099
+ v_activated :=
45100
+ 'CASE WHEN g.status = ''disabled'' AND g.balance = 0 THEN NULL'
45101
+ || ' WHEN g.updated_at > g.created_at THEN g.updated_at'
45102
+ || ' ELSE g.created_at END';
45103
+ END IF;
45104
+
45105
+ -- Same state vocabulary as financial_gift_cards (027).
45106
+ RETURN QUERY EXECUTE
45107
+ 'SELECT r.id, r.code, r.initial_balance,'
45108
+ || 'CASE WHEN r.never THEN NULL ELSE r.balance END,'
45109
+ || '(NOT r.never),'
45110
+ || 'CASE WHEN r.never THEN ''issued'''
45111
+ || ' WHEN r.balance = 0 OR r.status = ''redeemed'' THEN ''redeemed'''
45112
+ || ' WHEN r.expires_at IS NOT NULL AND r.expires_at < now() THEN ''expired'''
45113
+ || ' WHEN r.status = ''disabled'' THEN ''revoked'''
45114
+ || ' ELSE ''active'' END,'
45115
+ || 'r.currency, r.created_at, r.expires_at, r.activated_at, r.note, r.customer_email '
45116
+ || 'FROM (SELECT g.id, g.code, g.initial_balance, g.balance, g.currency, g.status,'
45117
+ || 'g.created_at, g.expires_at, g.note, g.customer_email,'
45118
+ || '(g.status = ''disabled'' AND g.balance = 0) AS never,'
45119
+ || v_activated || ' AS activated_at '
45120
+ || 'FROM public.plg_shop_gift_cards g '
45121
+ || 'WHERE g.tenant_id = $1 AND upper(g.code) = $2) r '
45122
+ || 'LIMIT 1'
45123
+ USING v_tenant, v_code;
45124
+ END $$;
45125
+
45126
+ COMMENT ON FUNCTION public.financial_gift_card_by_code(text) IS
45127
+ '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).';
45128
+
45129
+ REVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM PUBLIC;
45130
+ REVOKE ALL ON FUNCTION public.financial_gift_card_by_code(text) FROM anon;
45131
+ GRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO authenticated;
45132
+ GRANT EXECUTE ON FUNCTION public.financial_gift_card_by_code(text) TO service_role;
44493
45133
  `;
44494
45134
  var MIGRATIONS = [
44495
45135
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
@@ -44608,7 +45248,9 @@ var MIGRATIONS = [
44608
45248
  { id: "113_so_a_conta_integrada_aparece_na_conciliacao", sql: MIGRATION_113_SO_A_CONTA_INTEGRADA_APARECE_NA_CONCILIACAO },
44609
45249
  { id: "114_a_parcela_diz_por_qual_conta_ela_foi_paga", sql: MIGRATION_114_A_PARCELA_DIZ_POR_QUAL_CONTA_ELA_FOI_PAGA },
44610
45250
  { 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 }
45251
+ { id: "116_um_estorno_nao_se_estorna", sql: MIGRATION_116_UM_ESTORNO_NAO_SE_ESTORNA },
45252
+ { id: "117_copying_lines_is_an_internal_step", sql: MIGRATION_117_COPYING_LINES_IS_AN_INTERNAL_STEP },
45253
+ { id: "118_gift_card_found_by_exact_code", sql: MIGRATION_118_GIFT_CARD_FOUND_BY_EXACT_CODE }
44612
45254
  ];
44613
45255
  var STANDALONE_CONFIG = resolveConfig();
44614
45256
  registerTranslations(financialLocales);
@@ -45018,7 +45660,6 @@ function createSafeFinancialProvider() {
45018
45660
  () => createMockFinancialProvider()
45019
45661
  );
45020
45662
  }
45021
- var ACCOUNTS_ROUTE = "/registry/accounts";
45022
45663
  var FINANCIAL_NAV_FEATURES = {
45023
45664
  reconciliation: "fin_reconciliation",
45024
45665
  cards: "fin_cards",