@fayz-ai/plugin-financial 0.23.0 → 0.23.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { recordRailFields, RECORD_GROUP, activeFacet, createConnectionStore, con
4
4
  import { INLINE_LINK_TONE, SearchSelect, ModalTask, toast, Button, useInModalTaskHost, useSaveBar, tourAnchor, DatePicker, SegmentedControl, SubpageHeader, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, DataTable, Card, CardContent, Skeleton, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, MultiSelect, PageTransition, ModulePage, Badge, TooltipProvider, ConfirmDialog, CurrencyInput, Input, PageHeaderActions, ChoiceSelect, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, ModalFooter, Checkbox, Hint, ModalDescription, Tooltip, TooltipTrigger, TooltipContent, Popover, PopoverTrigger, PopoverContent, PageTabs, DashboardCanvas, ComingSoon, StatTiles, ListView, DateRangeControl, FilterPanel, FilterPanelField, RequiredFilter, FilterMenuRow, openPrintWindow, printDocument, 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
6
  import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, createCrudPage, PluginSettingsPanel, dedup, useCurrency, useUnitFilter, useLimitGuard, usePermissionOptional, useLookupCreate, OrderFormSkeleton, WidgetSlot, OrderFormBlock, TagsField, OrderFormView, OrderFormItemsCard, OrderFormItemRow, adminNavigateTo, OrderFormAddItemStep, CurrencyScope, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, TagChip, OrderDetailView, PersonLink, useTenantPluginSettings, useModuleNavigation, usePluginOnboardingGate, useModuleNavAccess, PluginOnboardingGateSkeleton, ModuleActionBar, SettingsGroup, ToggleRow, createArchetypeLookup, invalidateLimit, useAgentSurface, useHouseCurrency, parseViewId, EntitlementGate, UpgradePrompt, useChatStore, revealPage, resolveEntityRoute, useAuth, GetStarted, PermissionGate, createOneShotAsk, askAI, requestChatUpload, toInlineFile, shrinkForVision } from '@fayz-ai/admin';
7
- import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban, Receipt, Gift, Briefcase, Package, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, PackagePlus, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Ticket, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Undo2, Paperclip, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Search, ChevronsUpDown, ChevronsDownUp, ChevronRight, Building2, Image, UploadCloud, Calendar, User, Unlink, BookOpen, Info, FolderOpen, Folder, RotateCcw, ChevronDown, Settings, ArrowLeft, ChevronLeft, Eye, Inbox, Clock, ClipboardCheck, 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';
7
+ import { Landmark, CircleDollarSign, FileText, TriangleAlert, Loader2, Check, X, MoreVertical, Ban, Receipt, Gift, Briefcase, Package, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, PackagePlus, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Ticket, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Undo2, Paperclip, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Search, ChevronsUpDown, ChevronsDownUp, ChevronRight, Building2, Image, UploadCloud, Calendar, User, Unlink, BookOpen, Info, ChevronDown, Settings, FolderOpen, Folder, RotateCcw, ArrowLeft, ChevronLeft, Eye, Inbox, Clock, ClipboardCheck, 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';
@@ -4520,6 +4520,118 @@ function renderAccountAvatar(item) {
4520
4520
  if (!r || r.kind !== "card_payable") return null;
4521
4521
  return /* @__PURE__ */ jsx(CardVisual, { size: "lg", lastFour: r.lastFour, color: r.color, brandId: r.cardBrandId });
4522
4522
  }
4523
+ var METHOD_ICONS = {
4524
+ cash: Banknote,
4525
+ pix: QrCode,
4526
+ credit_card: CreditCard,
4527
+ debit_card: CreditCard,
4528
+ bank_transfer: ArrowRightLeft,
4529
+ check: FileCheck,
4530
+ boleto: Receipt,
4531
+ voucher: Ticket
4532
+ };
4533
+ function MethodIcon({ type }) {
4534
+ const Icon = METHOD_ICONS[type?.transactionType ?? ""] ?? Wallet;
4535
+ return /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
4536
+ }
4537
+ function AccountIcon({ account }) {
4538
+ if (!account) return null;
4539
+ if (account.accountType === "card_payable" && (account.lastFour || account.color || account.cardBrandId)) {
4540
+ return /* @__PURE__ */ jsx(CardVisual, { size: "xs", lastFour: account.lastFour ?? void 0, color: account.color ?? void 0, brandId: account.cardBrandId ?? void 0 });
4541
+ }
4542
+ if (account.accountType.startsWith("card_")) return /* @__PURE__ */ jsx(CreditCard, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
4543
+ if (account.accountType === "cash_physical" || account.accountType === "cash_session") {
4544
+ return /* @__PURE__ */ jsx(Banknote, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
4545
+ }
4546
+ const bank = findBrBank(account.bankCode, account.bankName ?? account.name);
4547
+ return bank?.logo ? /* @__PURE__ */ jsx(BankLogo, { bank, size: 16 }) : /* @__PURE__ */ jsx(Landmark, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
4548
+ }
4549
+ var FIELD_CLASS = "mt-1 flex w-full items-center gap-2 rounded-input border border-input bg-card px-3 py-2 text-left text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring";
4550
+ var norm2 = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
4551
+ function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className, onManage, manageLabel, variant = "inline" }) {
4552
+ const [open, setOpen] = React29__default.useState(false);
4553
+ const [q, setQ] = React29__default.useState("");
4554
+ const selected = accounts.find((a) => a.id === value);
4555
+ const list = q ? accounts.filter((a) => norm2(a.name).includes(norm2(q))) : accounts;
4556
+ const pick = (id) => {
4557
+ onChange(id);
4558
+ setOpen(false);
4559
+ setQ("");
4560
+ };
4561
+ return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: (o) => {
4562
+ setOpen(o);
4563
+ if (!o) setQ("");
4564
+ }, children: [
4565
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
4566
+ "button",
4567
+ {
4568
+ type: "button",
4569
+ disabled,
4570
+ title,
4571
+ "aria-label": ariaLabel,
4572
+ className: `${variant === "field" ? FIELD_CLASS : "inline-flex max-w-full items-center gap-1 text-xs"} disabled:cursor-not-allowed disabled:opacity-50 ${className ?? ""}`,
4573
+ children: [
4574
+ selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
4575
+ /* @__PURE__ */ jsx(AccountIcon, { account: selected }),
4576
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: selected.name })
4577
+ ] }) : /* @__PURE__ */ jsx("span", { className: variant === "field" ? "text-muted-foreground" : void 0, children: placeholder }),
4578
+ /* @__PURE__ */ jsx(ChevronDown, { className: `${variant === "field" ? "ml-auto h-4 w-4" : "h-3 w-3"} shrink-0 opacity-50`, "aria-hidden": true })
4579
+ ]
4580
+ }
4581
+ ) }),
4582
+ /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: variant === "field" ? "w-[var(--radix-popover-trigger-width)] p-1" : "w-64 p-1", children: [
4583
+ /* @__PURE__ */ jsxs("div", { className: "relative mb-1", children: [
4584
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }),
4585
+ /* @__PURE__ */ jsx(
4586
+ "input",
4587
+ {
4588
+ autoFocus: true,
4589
+ value: q,
4590
+ onChange: (e) => setQ(e.target.value),
4591
+ onKeyDown: (e) => {
4592
+ if (e.key === "Enter" && list[0]) {
4593
+ e.preventDefault();
4594
+ pick(list[0].id);
4595
+ }
4596
+ },
4597
+ placeholder: "Buscar conta\u2026",
4598
+ className: "h-8 w-full rounded-md border-0 bg-muted/50 pl-7 pr-2 text-xs outline-none focus:ring-0"
4599
+ }
4600
+ )
4601
+ ] }),
4602
+ /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto", children: list.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: "Nenhuma conta encontrada" }) : list.map((a) => /* @__PURE__ */ jsxs(
4603
+ "button",
4604
+ {
4605
+ type: "button",
4606
+ onClick: () => pick(a.id),
4607
+ className: `flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted ${a.id === value ? "font-medium" : ""}`,
4608
+ children: [
4609
+ /* @__PURE__ */ jsx(AccountIcon, { account: a }),
4610
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: a.name }),
4611
+ a.id === value && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true })
4612
+ ]
4613
+ },
4614
+ a.id
4615
+ )) }),
4616
+ 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 }),
4617
+ onManage && /* @__PURE__ */ jsxs(
4618
+ "button",
4619
+ {
4620
+ type: "button",
4621
+ onClick: () => {
4622
+ setOpen(false);
4623
+ onManage();
4624
+ },
4625
+ 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",
4626
+ children: [
4627
+ /* @__PURE__ */ jsx(Settings, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
4628
+ manageLabel
4629
+ ]
4630
+ }
4631
+ )
4632
+ ] })
4633
+ ] });
4634
+ }
4523
4635
  var ACCOUNT_TYPES_FOR_TX2 = {
4524
4636
  cash: ["cash_physical", "cash_session"],
4525
4637
  credit_card: ["card_receivable_credit"],
@@ -4734,7 +4846,7 @@ function usePaymentFields(options) {
4734
4846
  )
4735
4847
  ] }),
4736
4848
  selectedTypeId && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
4737
- /* @__PURE__ */ jsxs("div", { className: "fz-fields-2 stagger-field", style: { animationDelay: "0ms" }, children: [
4849
+ /* @__PURE__ */ jsxs("div", { className: "stagger-field grid grid-cols-2 items-start gap-3", style: { animationDelay: "0ms" }, children: [
4738
4850
  lockAmount ? (
4739
4851
  // O extrato mandou o valor. Ele aparece como FATO, e a linha
4740
4852
  // abaixo diz por quê — um campo cinza sem explicação faz a
@@ -4837,10 +4949,18 @@ function usePaymentFields(options) {
4837
4949
  t2("financial.payment.configureAccounts"),
4838
4950
  " \u2192"
4839
4951
  ] })
4840
- ] }) : /* @__PURE__ */ jsxs("select", { value: bankAccountId, onChange: (e) => setBankAccountId(e.target.value), className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm", children: [
4841
- /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectAccount") }),
4842
- eligibleAccounts2.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
4843
- ] })
4952
+ ] }) : /* @__PURE__ */ jsx(
4953
+ AccountPicker,
4954
+ {
4955
+ variant: "field",
4956
+ accounts: eligibleAccounts2,
4957
+ value: bankAccountId || void 0,
4958
+ onChange: (id) => setBankAccountId(id ?? ""),
4959
+ placeholder: t2("financial.payment.selectAccount"),
4960
+ ariaLabel: t2("financial.payment.bankAccount"),
4961
+ clearLabel: t2("financial.invoiceForm.clear")
4962
+ }
4963
+ )
4844
4964
  ] }),
4845
4965
  /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "150ms" }, children: [
4846
4966
  /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.notes") }),
@@ -4927,6 +5047,8 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
4927
5047
  setPending(command.current.pending);
4928
5048
  }
4929
5049
  }
5050
+ const receiving = movement.direction === "credit";
5051
+ const title = t2(receiving ? "financial.payment.titleReceive" : "financial.payment.title");
4930
5052
  const body = /* @__PURE__ */ jsx("div", { className: "px-5 py-4", children: fields.node });
4931
5053
  const footer = /* @__PURE__ */ jsxs("div", { className: "shrink-0 border-t sticky bottom-0 bg-card px-5 py-4 space-y-2", children: [
4932
5054
  pending2 && !saving && /* @__PURE__ */ jsxs("p", { role: "status", className: "flex items-start gap-1.5 text-[11px] leading-snug text-muted-foreground", children: [
@@ -4945,7 +5067,7 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
4945
5067
  title: fields.blockedReason ?? void 0,
4946
5068
  children: [
4947
5069
  saving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5", "aria-hidden": true }),
4948
- saving ? t2("financial.payment.processing") : fields.blockedReason && !pending2 ? fields.blockedReason : t2("financial.payment.payAmount", { amount: formatCurrency(fields.value.amount, currency) })
5070
+ saving ? t2("financial.payment.processing") : fields.blockedReason && !pending2 ? fields.blockedReason : t2(receiving ? "financial.payment.receiveAmount" : "financial.payment.payAmount", { amount: formatCurrency(fields.value.amount, currency) })
4949
5071
  ]
4950
5072
  }
4951
5073
  )
@@ -4957,7 +5079,7 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
4957
5079
  ModalTask,
4958
5080
  {
4959
5081
  open: true,
4960
- title: t2("financial.payment.title"),
5082
+ title,
4961
5083
  hint: `${t2("financial.payment.installment", { number: String(movement.installmentNumber ?? 1) })} \xB7 ${t2("financial.payment.remaining", { amount: formatCurrency(remaining, currency) })}`,
4962
5084
  onClose: handleClose,
4963
5085
  children: [
@@ -4986,7 +5108,7 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
4986
5108
  children: [
4987
5109
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10", children: [
4988
5110
  /* @__PURE__ */ jsxs("div", { children: [
4989
- /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: t2("financial.payment.title") }),
5111
+ /* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: title }),
4990
5112
  /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
4991
5113
  t2("financial.payment.installment", { number: String(movement.installmentNumber ?? 1) }),
4992
5114
  " \xB7 ",
@@ -5005,32 +5127,6 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
5005
5127
  document.body
5006
5128
  );
5007
5129
  }
5008
- var METHOD_ICONS = {
5009
- cash: Banknote,
5010
- pix: QrCode,
5011
- credit_card: CreditCard,
5012
- debit_card: CreditCard,
5013
- bank_transfer: ArrowRightLeft,
5014
- check: FileCheck,
5015
- boleto: Receipt,
5016
- voucher: Ticket
5017
- };
5018
- function MethodIcon({ type }) {
5019
- const Icon = METHOD_ICONS[type?.transactionType ?? ""] ?? Wallet;
5020
- return /* @__PURE__ */ jsx(Icon, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
5021
- }
5022
- function AccountIcon({ account }) {
5023
- if (!account) return null;
5024
- if (account.accountType === "card_payable" && (account.lastFour || account.color || account.cardBrandId)) {
5025
- return /* @__PURE__ */ jsx(CardVisual, { size: "xs", lastFour: account.lastFour ?? void 0, color: account.color ?? void 0, brandId: account.cardBrandId ?? void 0 });
5026
- }
5027
- if (account.accountType.startsWith("card_")) return /* @__PURE__ */ jsx(CreditCard, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
5028
- if (account.accountType === "cash_physical" || account.accountType === "cash_session") {
5029
- return /* @__PURE__ */ jsx(Banknote, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
5030
- }
5031
- const bank = findBrBank(account.bankCode, account.bankName ?? account.name);
5032
- return bank?.logo ? /* @__PURE__ */ jsx(BankLogo, { bank, size: 16 }) : /* @__PURE__ */ jsx(Landmark, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": true });
5033
- }
5034
5130
  var NONE = "__none__";
5035
5131
  var TRIGGER_CLASS = `h-auto w-auto justify-start gap-1 rounded-none border-0 bg-transparent p-0 text-xs font-normal shadow-none ring-offset-0 focus:ring-0 focus:ring-offset-0 [&>svg]:h-3 [&>svg]:w-3 ${INLINE_LINK_TONE.dashed}`;
5036
5132
  function InstallmentSourceFields({
@@ -5053,7 +5149,7 @@ function InstallmentSourceFields({
5053
5149
  const conta = elegiveis.length === 1 ? elegiveis[0].id : elegiveis.some((a) => a.id === value.payAccountId) ? value.payAccountId : void 0;
5054
5150
  onChange({ payMethodTypeId: typeId || void 0, payAccountId: typeId ? conta : void 0 });
5055
5151
  }
5056
- const tone = (filled) => filled ? "text-foreground" : "text-destructive border-b border-dashed border-destructive";
5152
+ const tone = (filled) => filled ? "text-foreground" : "text-foreground border-b border-dashed border-foreground/60";
5057
5153
  return /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-col items-start gap-0.5", children: [
5058
5154
  /* @__PURE__ */ jsxs(
5059
5155
  Select,
@@ -5098,91 +5194,6 @@ function InstallmentSourceFields({
5098
5194
  )
5099
5195
  ] });
5100
5196
  }
5101
- var norm2 = (v) => v.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
5102
- function AccountPicker({ accounts, value, onChange, disabled, title, placeholder, ariaLabel, clearLabel, className, onManage, manageLabel }) {
5103
- const [open, setOpen] = React29__default.useState(false);
5104
- const [q, setQ] = React29__default.useState("");
5105
- const selected = accounts.find((a) => a.id === value);
5106
- const list = q ? accounts.filter((a) => norm2(a.name).includes(norm2(q))) : accounts;
5107
- const pick = (id) => {
5108
- onChange(id);
5109
- setOpen(false);
5110
- setQ("");
5111
- };
5112
- return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: (o) => {
5113
- setOpen(o);
5114
- if (!o) setQ("");
5115
- }, children: [
5116
- /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
5117
- "button",
5118
- {
5119
- type: "button",
5120
- disabled,
5121
- title,
5122
- "aria-label": ariaLabel,
5123
- className: `inline-flex max-w-full items-center gap-1 text-xs disabled:cursor-not-allowed disabled:opacity-50 ${className ?? ""}`,
5124
- children: [
5125
- selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
5126
- /* @__PURE__ */ jsx(AccountIcon, { account: selected }),
5127
- /* @__PURE__ */ jsx("span", { className: "truncate", children: selected.name })
5128
- ] }) : placeholder,
5129
- /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0 opacity-50", "aria-hidden": true })
5130
- ]
5131
- }
5132
- ) }),
5133
- /* @__PURE__ */ jsxs(PopoverContent, { align: "start", className: "w-64 p-1", children: [
5134
- /* @__PURE__ */ jsxs("div", { className: "relative mb-1", children: [
5135
- /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground", "aria-hidden": true }),
5136
- /* @__PURE__ */ jsx(
5137
- "input",
5138
- {
5139
- autoFocus: true,
5140
- value: q,
5141
- onChange: (e) => setQ(e.target.value),
5142
- onKeyDown: (e) => {
5143
- if (e.key === "Enter" && list[0]) {
5144
- e.preventDefault();
5145
- pick(list[0].id);
5146
- }
5147
- },
5148
- placeholder: "Buscar conta\u2026",
5149
- className: "h-8 w-full rounded-md border-0 bg-muted/50 pl-7 pr-2 text-xs outline-none focus:ring-0"
5150
- }
5151
- )
5152
- ] }),
5153
- /* @__PURE__ */ jsx("div", { className: "max-h-64 overflow-y-auto", children: list.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: "Nenhuma conta encontrada" }) : list.map((a) => /* @__PURE__ */ jsxs(
5154
- "button",
5155
- {
5156
- type: "button",
5157
- onClick: () => pick(a.id),
5158
- className: `flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted ${a.id === value ? "font-medium" : ""}`,
5159
- children: [
5160
- /* @__PURE__ */ jsx(AccountIcon, { account: a }),
5161
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: a.name }),
5162
- a.id === value && /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true })
5163
- ]
5164
- },
5165
- a.id
5166
- )) }),
5167
- 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 }),
5168
- onManage && /* @__PURE__ */ jsxs(
5169
- "button",
5170
- {
5171
- type: "button",
5172
- onClick: () => {
5173
- setOpen(false);
5174
- onManage();
5175
- },
5176
- 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",
5177
- children: [
5178
- /* @__PURE__ */ jsx(Settings, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
5179
- manageLabel
5180
- ]
5181
- }
5182
- )
5183
- ] })
5184
- ] });
5185
- }
5186
5197
  var STATUS = {
5187
5198
  open: { bg: "bg-info-soft text-info-soft-foreground", icon: CircleDashed, labelKey: "financial.invoice.statusOpen" },
5188
5199
  pending: { bg: "bg-warning-soft text-warning-soft-foreground", icon: CircleDashed, labelKey: "financial.invoice.statusPending" },
@@ -6412,7 +6423,6 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6412
6423
  const fonteTravada = isPaid2 || isPartial;
6413
6424
  const formaEscolhida = paymentMethodTypes.find((tp) => tp.id === inst.payMethodTypeId);
6414
6425
  const contaEscolhida = bankAccounts.find((a) => a.id === inst.payAccountId);
6415
- const faltaParaPagar = !inst.payMethodTypeId ? t2("financial.payment.methodRequired") : !inst.payAccountId ? t2("financial.payment.accountRequired") : null;
6416
6426
  return /* @__PURE__ */ jsxs("div", { children: [
6417
6427
  /* @__PURE__ */ jsx(
6418
6428
  InstallmentRow,
@@ -6461,12 +6471,11 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
6461
6471
  !isPaid2 && (saved || !isEdit && !!contactId && items.length > 0 && inst.amount > 0) && /* @__PURE__ */ jsx(
6462
6472
  InstallmentPayButton,
6463
6473
  {
6464
- disabled: saving || !!faltaParaPagar,
6465
- title: faltaParaPagar ?? void 0,
6474
+ disabled: saving,
6466
6475
  onClick: () => {
6467
6476
  void pagarParcela(i);
6468
6477
  },
6469
- children: t2("financial.invoice.pay")
6478
+ children: t2(direction === "credit" ? "financial.invoice.receive" : "financial.invoice.pay")
6470
6479
  }
6471
6480
  )
6472
6481
  ] })
@@ -7711,7 +7720,6 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
7711
7720
  const srcAccountId = srcStr(srcMeta.payAccountId);
7712
7721
  const editavel = !hasPaidDetails && invoice.status !== "cancelled";
7713
7722
  const canChooseSource = editavel && Math.abs(mov.amount) >= 5e-3;
7714
- const faltaParaPagar = !srcTypeId ? t2("financial.payment.methodRequired") : !srcAccountId ? t2("financial.payment.accountRequired") : null;
7715
7723
  const ativa = invoice.status !== "cancelled";
7716
7724
  return /* @__PURE__ */ jsx(
7717
7725
  InstallmentRow,
@@ -7772,10 +7780,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
7772
7780
  InstallmentPayButton,
7773
7781
  {
7774
7782
  onClick: () => setPayingMovement(mov),
7775
- disabled: !!faltaParaPagar,
7776
- title: faltaParaPagar ?? void 0,
7777
7783
  children: [
7778
- t2("financial.invoice.pay"),
7784
+ t2(direction === "credit" ? "financial.invoice.receive" : "financial.invoice.pay"),
7779
7785
  " ",
7780
7786
  remaining < mov.amount ? formatCurrency(remaining, currency) : ""
7781
7787
  ]
@@ -7783,7 +7789,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, em
7783
7789
  ),
7784
7790
  isPaid2 && reversiblePayments(instPayments).length > 0 && ativa && /* @__PURE__ */ jsx(InstallmentActionButton, { icon: Undo2, onClick: () => setReversing(reversiblePayments(instPayments).at(-1)), children: t2("financial.invoice.reverse") }),
7785
7791
  isPartial && ativa && /* @__PURE__ */ jsxs(InstallmentPayButton, { variant: "outline", onClick: () => setPayingMovement(mov), children: [
7786
- t2("financial.invoice.pay"),
7792
+ t2(direction === "credit" ? "financial.invoice.receive" : "financial.invoice.pay"),
7787
7793
  " ",
7788
7794
  formatCurrency(remaining, currency)
7789
7795
  ] })
@@ -8471,6 +8477,50 @@ function CashClosingPanel({
8471
8477
  )
8472
8478
  ] });
8473
8479
  }
8480
+ function dayKeyOf(iso) {
8481
+ if (!iso) return "";
8482
+ const d = new Date(iso);
8483
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
8484
+ }
8485
+ function groupByClosingDay(rows) {
8486
+ const days = [];
8487
+ const sorted = [...rows].sort((a, b) => (b.closedAt ?? "").localeCompare(a.closedAt ?? ""));
8488
+ for (const r of sorted) {
8489
+ const key = dayKeyOf(r.closedAt);
8490
+ const last = days[days.length - 1];
8491
+ if (last && last.key === key) last.rows.push(r);
8492
+ else days.push({ key, rows: [r] });
8493
+ }
8494
+ return days;
8495
+ }
8496
+ function DayHeader({ dayKey, count }) {
8497
+ const t2 = useTranslation();
8498
+ let label = "\u2014";
8499
+ if (dayKey === dayKeyOf((/* @__PURE__ */ new Date()).toISOString())) label = t2("financial.cash.reviewToday");
8500
+ else if (dayKey === dayKeyOf(new Date(Date.now() - 864e5).toISOString())) label = t2("financial.cash.reviewYesterday");
8501
+ else if (dayKey) {
8502
+ const [y, m, d] = dayKey.split("-").map(Number);
8503
+ label = new Date(y, m - 1, d).toLocaleDateString(void 0, { weekday: "long", day: "2-digit", month: "2-digit" });
8504
+ }
8505
+ return (
8506
+ // Stays on screen while its day scrolls, stopping under the page's tab band
8507
+ // like a list's column header. Opaque: rows pass underneath it.
8508
+ /* @__PURE__ */ jsxs(
8509
+ "div",
8510
+ {
8511
+ className: "sticky z-10 flex items-center gap-2 border-b bg-muted px-4 py-1.5 text-xs font-medium text-muted-foreground",
8512
+ style: { top: "var(--fz-page-band-h, 0px)" },
8513
+ children: [
8514
+ /* @__PURE__ */ jsx("span", { className: "first-letter:uppercase", children: label }),
8515
+ /* @__PURE__ */ jsx("span", { className: "rounded-full bg-background px-1.5 text-[10px] tabular-nums", children: count })
8516
+ ]
8517
+ }
8518
+ )
8519
+ );
8520
+ }
8521
+ var HOUR_MINUTE = { hour: "2-digit", minute: "2-digit" };
8522
+ var PANEL_REVIEW_LIMIT = 3;
8523
+ var REVIEW_TAB_QUERY = "tab=review";
8474
8524
  function ReviewQueue({
8475
8525
  variant,
8476
8526
  rows,
@@ -8484,6 +8534,11 @@ function ReviewQueue({
8484
8534
  const me = user?.id ?? null;
8485
8535
  const [reviewing, setReviewing] = useState(null);
8486
8536
  const pendentes = (rows ?? []).filter((r) => r.reviewStatus === "pending");
8537
+ const setCashSessionModalOpen = useFinancialStore((s) => s.setCashSessionModalOpen);
8538
+ const compact = variant === "panel";
8539
+ const shown = compact ? [...pendentes].sort((a, b) => Math.abs(b.difference) - Math.abs(a.difference) || (a.closedAt ?? "").localeCompare(b.closedAt ?? "")).slice(0, PANEL_REVIEW_LIMIT) : pendentes;
8540
+ const hidden = pendentes.length - shown.length;
8541
+ const days = compact ? [] : groupByClosingDay(pendentes);
8487
8542
  const sectionClass = variant === "panel" ? "" : "rounded-card border bg-card shadow-sm";
8488
8543
  return /* @__PURE__ */ jsxs("div", { className: sectionClass, children: [
8489
8544
  showHeader && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-4 py-3", children: [
@@ -8491,39 +8546,68 @@ function ReviewQueue({
8491
8546
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.cash.reviewQueue") }),
8492
8547
  pendentes.length > 0 && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-info/10 px-2 py-0.5 text-[10px] font-semibold text-info", children: pendentes.length })
8493
8548
  ] }),
8494
- showHeader && /* @__PURE__ */ jsx("p", { className: "px-4 pt-3 text-[11px] text-muted-foreground", children: "Quem abre e fecha o caixa n\xE3o \xE9 quem confere. Um fechamento fica aqui at\xE9 algu\xE9m da ger\xEAncia ou do financeiro aprovar." }),
8549
+ showHeader && !compact && /* @__PURE__ */ jsx("p", { className: "px-4 pt-3 text-[11px] text-muted-foreground", children: "Quem abre e fecha o caixa n\xE3o \xE9 quem confere. Um fechamento fica aqui at\xE9 algu\xE9m da ger\xEAncia ou do financeiro aprovar." }),
8495
8550
  error && /* @__PURE__ */ jsx("p", { className: "px-4 pt-2 text-xs text-destructive", children: error }),
8496
8551
  rows === null ? /* @__PURE__ */ jsx("div", { className: "space-y-2 p-4", children: [0, 1].map((i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
8497
8552
  /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" }),
8498
8553
  /* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-32" })
8499
- ] }, i)) }) : pendentes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-6 text-center text-sm text-muted-foreground", children: t2("financial.cash.reviewQueueEmpty") }) : /* @__PURE__ */ jsx("div", { className: "divide-y", children: pendentes.map((r) => {
8500
- const fechei = !!me && r.closedByUserId === me;
8501
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 px-4 py-3", children: [
8502
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
8503
- /* @__PURE__ */ jsxs("p", { className: "text-sm font-medium text-foreground", children: [
8554
+ ] }, i)) }) : pendentes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-4 py-6 text-center text-sm text-muted-foreground", children: t2("financial.cash.reviewQueueEmpty") }) : /* @__PURE__ */ jsxs("div", { className: "divide-y", children: [
8555
+ (compact ? [{ key: "", rows: shown }] : days).map((day3) => /* @__PURE__ */ jsxs(React29__default.Fragment, { children: [
8556
+ !compact && /* @__PURE__ */ jsx(DayHeader, { dayKey: day3.key, count: day3.rows.length }),
8557
+ day3.rows.map((r) => {
8558
+ const fechei = !!me && r.closedByUserId === me;
8559
+ const amount = /* @__PURE__ */ jsxs(Fragment, { children: [
8504
8560
  formatCurrency(r.closingBalance, currency),
8505
8561
  r.difference !== 0 && /* @__PURE__ */ jsxs("span", { className: `ml-2 text-xs tabular-nums ${r.difference > 0 ? "text-success" : "text-destructive"}`, children: [
8506
8562
  r.difference > 0 ? "+" : "",
8507
8563
  formatCurrency(r.difference, currency)
8508
8564
  ] })
8509
- ] }),
8510
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
8511
- t2("financial.cash.closedBy"),
8565
+ ] });
8566
+ const action = fechei ? (
8567
+ // Estado que impede não oferece a ação — e diz por quê.
8568
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.cash.cannotReviewOwn") })
8569
+ ) : /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "shrink-0", onClick: () => setReviewing(r.sessionId), children: [
8570
+ /* @__PURE__ */ jsx(ClipboardCheck, { className: "h-3.5 w-3.5" }),
8512
8571
  " ",
8513
- r.closedByName ?? "\u2014",
8514
- r.closedAt ? ` \xB7 ${new Date(r.closedAt).toLocaleString()}` : ""
8515
- ] })
8516
- ] }),
8517
- fechei ? (
8518
- // Estado que impede não oferece a ação — e diz por quê.
8519
- /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.cash.cannotReviewOwn") })
8520
- ) : /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setReviewing(r.sessionId), children: [
8521
- /* @__PURE__ */ jsx(ClipboardCheck, { className: "h-3.5 w-3.5" }),
8522
- " ",
8523
- t2("financial.cash.review")
8524
- ] })
8525
- ] }, r.sessionId);
8526
- }) }),
8572
+ t2("financial.cash.review")
8573
+ ] });
8574
+ if (compact) {
8575
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-2", children: [
8576
+ /* @__PURE__ */ jsx("p", { className: "shrink-0 text-sm font-medium tabular-nums text-foreground", children: amount }),
8577
+ /* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 truncate text-xs text-muted-foreground", children: [
8578
+ r.closedByName ?? "\u2014",
8579
+ r.closedAt ? ` \xB7 ${new Date(r.closedAt).toLocaleDateString(void 0, { day: "2-digit", month: "2-digit" })}` : ""
8580
+ ] }),
8581
+ action
8582
+ ] }, r.sessionId);
8583
+ }
8584
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 px-4 py-3", children: [
8585
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
8586
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-foreground", children: amount }),
8587
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
8588
+ t2("financial.cash.closedBy"),
8589
+ " ",
8590
+ r.closedByName ?? "\u2014",
8591
+ r.closedAt ? ` \xB7 ${new Date(r.closedAt).toLocaleTimeString(void 0, HOUR_MINUTE)}` : ""
8592
+ ] })
8593
+ ] }),
8594
+ action
8595
+ ] }, r.sessionId);
8596
+ })
8597
+ ] }, day3.key || "all")),
8598
+ hidden > 0 && /* @__PURE__ */ jsxs(
8599
+ "a",
8600
+ {
8601
+ href: `#/financial/cash/registers?${REVIEW_TAB_QUERY}`,
8602
+ onClick: () => setCashSessionModalOpen(false),
8603
+ className: "flex items-center justify-between px-4 py-2.5 text-xs font-medium text-primary transition-colors hover:bg-muted/50",
8604
+ children: [
8605
+ t2("financial.cash.reviewSeeAll", { count: String(pendentes.length) }),
8606
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5" })
8607
+ ]
8608
+ }
8609
+ )
8610
+ ] }),
8527
8611
  reviewing && /* @__PURE__ */ jsx(
8528
8612
  CashSessionReviewModal,
8529
8613
  {
@@ -8554,7 +8638,9 @@ function CashRegistersView({ variant = "page" } = {}) {
8554
8638
  const setCashSessionModalOpen = useFinancialStore((s) => s.setCashSessionModalOpen);
8555
8639
  const [expandedClosing, setExpandedClosing] = useState(null);
8556
8640
  const [viewingDetails, setViewingDetails] = useState(null);
8557
- const [tab, setTab] = useState("open");
8641
+ const [tab, setTab] = useState(
8642
+ () => typeof window !== "undefined" && window.location.hash.includes(REVIEW_TAB_QUERY) ? "review" : "open"
8643
+ );
8558
8644
  const [reviewRows, setReviewRows] = useState(null);
8559
8645
  const [reviewError, setReviewError] = useState(null);
8560
8646
  const loadReviews = React29__default.useCallback(async () => {
@@ -8614,49 +8700,52 @@ function CashRegistersView({ variant = "page" } = {}) {
8614
8700
  }
8615
8701
  )
8616
8702
  ] }, session.id)) });
8617
- const closedSessionsList = /* @__PURE__ */ jsx("div", { className: "divide-y", children: closedSessions.map((session) => /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
8618
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
8619
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: session.bankAccountName ?? t2("financial.cash.title") }),
8620
- /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
8621
- new Date(session.openedAt).toLocaleDateString(),
8622
- " \u2014 ",
8623
- session.closedAt ? new Date(session.closedAt).toLocaleDateString() : ""
8703
+ const closedSessionsList = /* @__PURE__ */ jsx("div", { className: "divide-y", children: groupByClosingDay(closedSessions).map((day3) => /* @__PURE__ */ jsxs(React29__default.Fragment, { children: [
8704
+ /* @__PURE__ */ jsx(DayHeader, { dayKey: day3.key, count: day3.rows.length }),
8705
+ day3.rows.map((session) => /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
8706
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
8707
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: session.bankAccountName ?? t2("financial.cash.title") }),
8708
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
8709
+ dayKeyOf(session.openedAt) === dayKeyOf(session.closedAt) ? new Date(session.openedAt).toLocaleTimeString(void 0, HOUR_MINUTE) : new Date(session.openedAt).toLocaleString(void 0, { day: "2-digit", month: "2-digit", ...HOUR_MINUTE }),
8710
+ " \u2014 ",
8711
+ session.closedAt ? new Date(session.closedAt).toLocaleTimeString(void 0, HOUR_MINUTE) : ""
8712
+ ] }),
8713
+ session.openedByName && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
8714
+ t2("financial.cash.by"),
8715
+ " ",
8716
+ session.openedByName
8717
+ ] })
8624
8718
  ] }),
8625
- session.openedByName && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
8626
- t2("financial.cash.by"),
8627
- " ",
8628
- session.openedByName
8629
- ] })
8630
- ] }),
8631
- /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-start gap-3", children: [
8632
- /* @__PURE__ */ jsxs("div", { className: "text-right", children: [
8633
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
8634
- /* @__PURE__ */ jsxs("span", { children: [
8635
- t2("financial.cash.openLabel"),
8636
- ": ",
8637
- formatCurrency(session.openingBalance, currency)
8719
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-start gap-3", children: [
8720
+ /* @__PURE__ */ jsxs("div", { className: "text-right", children: [
8721
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
8722
+ /* @__PURE__ */ jsxs("span", { children: [
8723
+ t2("financial.cash.openLabel"),
8724
+ ": ",
8725
+ formatCurrency(session.openingBalance, currency)
8726
+ ] }),
8727
+ /* @__PURE__ */ jsxs("span", { children: [
8728
+ t2("financial.cash.closeLabel"),
8729
+ ": ",
8730
+ formatCurrency(session.closingBalance ?? 0, currency)
8731
+ ] })
8638
8732
  ] }),
8639
- /* @__PURE__ */ jsxs("span", { children: [
8640
- t2("financial.cash.closeLabel"),
8733
+ session.difference !== void 0 && session.difference !== 0 && /* @__PURE__ */ jsxs("p", { className: `text-xs font-semibold tabular-nums mt-0.5 ${session.difference > 0 ? "text-success" : "text-destructive"}`, children: [
8734
+ t2("financial.cash.diff"),
8641
8735
  ": ",
8642
- formatCurrency(session.closingBalance ?? 0, currency)
8643
- ] })
8644
- ] }),
8645
- session.difference !== void 0 && session.difference !== 0 && /* @__PURE__ */ jsxs("p", { className: `text-xs font-semibold tabular-nums mt-0.5 ${session.difference > 0 ? "text-success" : "text-destructive"}`, children: [
8646
- t2("financial.cash.diff"),
8647
- ": ",
8648
- session.difference >= 0 ? "+" : "",
8649
- formatCurrency(session.difference, currency)
8736
+ session.difference >= 0 ? "+" : "",
8737
+ formatCurrency(session.difference, currency)
8738
+ ] }),
8739
+ session.notes && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5 max-w-[200px] truncate", title: session.notes, children: session.notes })
8650
8740
  ] }),
8651
- session.notes && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground mt-0.5 max-w-[200px] truncate", title: session.notes, children: session.notes })
8652
- ] }),
8653
- /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setViewingDetails(session.id), children: [
8654
- /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" }),
8655
- " ",
8656
- t2("financial.cash.details")
8741
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setViewingDetails(session.id), children: [
8742
+ /* @__PURE__ */ jsx(Eye, { className: "h-3.5 w-3.5" }),
8743
+ " ",
8744
+ t2("financial.cash.details")
8745
+ ] })
8657
8746
  ] })
8658
- ] })
8659
- ] }) }, session.id)) });
8747
+ ] }) }, session.id))
8748
+ ] }, day3.key || "all")) });
8660
8749
  return /* @__PURE__ */ jsxs("div", { className: inPanel ? "" : "space-y-4", children: [
8661
8750
  !inPanel && /* @__PURE__ */ jsx(
8662
8751
  SubpageHeader,
@@ -11999,6 +12088,7 @@ function ReconciliationView() {
11999
12088
  value: accountId,
12000
12089
  displayValue: selectedAccount?.name ?? "",
12001
12090
  icon: Landmark,
12091
+ leading: selectedAccount ? /* @__PURE__ */ jsx(AccountIcon, { account: selectedAccount }) : void 0,
12002
12092
  placeholder: "Digite...",
12003
12093
  noResultsLabel: t2("financial.reconciliation.noIntegratedAccount"),
12004
12094
  searchingLabel: "Buscando contas...",
@@ -12016,6 +12106,7 @@ function ReconciliationView() {
12016
12106
  id: account.id,
12017
12107
  label: account.name,
12018
12108
  subtitle: account.bankName || account.accountNumber,
12109
+ icon: /* @__PURE__ */ jsx(AccountIcon, { account }),
12019
12110
  group: ACCOUNT_GROUPS2[account.accountType] ?? "Outras contas",
12020
12111
  data: account
12021
12112
  }));
@@ -19812,6 +19903,7 @@ var en = {
19812
19903
  "financial.invoice.paidLabel": "Paid:",
19813
19904
  "financial.invoice.paidOn": "Paid on:",
19814
19905
  "financial.invoice.pay": "Pay",
19906
+ "financial.invoice.receive": "Receive",
19815
19907
  "financial.invoice.reverse": "Reverse",
19816
19908
  "financial.invoice.reverseTitle": "Reverse payment",
19817
19909
  "financial.invoice.reverseReason": "Reason for reversal",
@@ -20215,12 +20307,14 @@ var en = {
20215
20307
  "financial.payment.notes": "Notes",
20216
20308
  "financial.payment.optionalNotes": "Optional notes...",
20217
20309
  "financial.payment.payAmount": "Pay {{amount}}",
20310
+ "financial.payment.receiveAmount": "Receive {{amount}}",
20218
20311
  "financial.payment.processing": "Processing...",
20219
20312
  "financial.payment.remaining": "{{amount}} remaining",
20220
20313
  "financial.payment.select": "Select...",
20221
20314
  "financial.payment.selectAccount": "Select account...",
20222
20315
  "financial.payment.selectBrand": "Select brand...",
20223
20316
  "financial.payment.title": "Record Payment",
20317
+ "financial.payment.titleReceive": "Record Receipt",
20224
20318
  "financial.payment.changeSource": "Change method and account",
20225
20319
  "financial.settings.allowPartial": "Allow partial payments",
20226
20320
  "financial.settings.allowPartialDesc": "Accept payments less than the full installment amount",
@@ -20380,6 +20474,9 @@ var en = {
20380
20474
  "financial.cash.by": "by",
20381
20475
  "financial.cash.closeThisSession": "Close this session",
20382
20476
  "financial.cash.reviewQueue": "Close review",
20477
+ "financial.cash.reviewSeeAll": "See all {count} closes",
20478
+ "financial.cash.reviewToday": "Today",
20479
+ "financial.cash.reviewYesterday": "Yesterday",
20383
20480
  "financial.cash.reviewQueueEmpty": "No closes waiting for review.",
20384
20481
  "financial.cash.closedBy": "Closed by",
20385
20482
  "financial.cash.approve": "Approve",
@@ -20765,6 +20862,7 @@ var ptBR = {
20765
20862
  "financial.invoice.paidLabel": "Pago:",
20766
20863
  "financial.invoice.paidOn": "Pago em:",
20767
20864
  "financial.invoice.pay": "Pagar",
20865
+ "financial.invoice.receive": "Receber",
20768
20866
  "financial.invoice.reverse": "Estornar",
20769
20867
  "financial.invoice.reverseTitle": "Estornar pagamento",
20770
20868
  "financial.invoice.reverseReason": "Motivo do estorno",
@@ -21171,12 +21269,14 @@ var ptBR = {
21171
21269
  "financial.payment.notes": "Observa\xE7\xF5es",
21172
21270
  "financial.payment.optionalNotes": "Observa\xE7\xF5es opcionais...",
21173
21271
  "financial.payment.payAmount": "Pagar {{amount}}",
21272
+ "financial.payment.receiveAmount": "Receber {{amount}}",
21174
21273
  "financial.payment.processing": "Processando...",
21175
21274
  "financial.payment.remaining": "{{amount}} restante",
21176
21275
  "financial.payment.select": "Selecionar...",
21177
21276
  "financial.payment.selectAccount": "Selecionar conta...",
21178
21277
  "financial.payment.selectBrand": "Selecionar bandeira...",
21179
21278
  "financial.payment.title": "Registrar Pagamento",
21279
+ "financial.payment.titleReceive": "Registrar Recebimento",
21180
21280
  "financial.payment.changeSource": "Alterar forma e conta",
21181
21281
  "financial.settings.allowPartial": "Permitir pagamentos parciais",
21182
21282
  "financial.settings.allowPartialDesc": "Aceitar pagamentos menores que o valor total da parcela",
@@ -21336,6 +21436,9 @@ var ptBR = {
21336
21436
  "financial.cash.by": "por",
21337
21437
  "financial.cash.closeThisSession": "Fechar esta sess\xE3o",
21338
21438
  "financial.cash.reviewQueue": "Confer\xEAncia de fechamentos",
21439
+ "financial.cash.reviewSeeAll": "Ver todos os {count} fechamentos",
21440
+ "financial.cash.reviewToday": "Hoje",
21441
+ "financial.cash.reviewYesterday": "Ontem",
21339
21442
  "financial.cash.reviewQueueEmpty": "Nenhum fechamento esperando confer\xEAncia.",
21340
21443
  "financial.cash.closedBy": "Fechado por",
21341
21444
  "financial.cash.approve": "Aprovar",
@@ -48480,6 +48583,374 @@ COMMENT ON COLUMN public.plg_financial_invoices.tags IS 'Free labels the house p
48480
48583
  CREATE INDEX IF NOT EXISTS plg_financial_invoices_tags_idx
48481
48584
  ON public.plg_financial_invoices USING gin (tags);
48482
48585
  `;
48586
+ var MIGRATION_131_THE_BILL_FOLLOWS_THE_PRICE_OF_THE_SALE = `-- 131 \u2014 The bill follows the price of the sale.
48587
+ --
48588
+ -- A receivable is raised when the sale closes, at the total of that instant.
48589
+ -- Both bridges (\`order_raise_receivable\`, \`fn_invoice_from_order_internal\`) are
48590
+ -- idempotent: once the invoice exists they return without looking at the
48591
+ -- amount. So a price changed AFTER closing never reached the bill \u2014 a clinic
48592
+ -- had an appointment at R$ 700 whose receivable still asked for R$ 140, and
48593
+ -- the only symptom was two different numbers on two screens.
48594
+ --
48595
+ -- The rule, in one place:
48596
+ -- * nothing received yet \u2192 the invoice, its lines and its open instalments
48597
+ -- take the sale's total;
48598
+ -- * something received \u2192 nothing moves. A paid invoice is immutable
48599
+ -- (PRD 04, 120); the screens show both numbers and a person decides;
48600
+ -- * the sale drops to zero \u2192 the unpaid invoice is cancelled, not left at 0.
48601
+ --
48602
+ -- The lines follow the model the bill already has. A bill raised here owns a
48603
+ -- copy of the sale's lines (010) and that copy is refreshed. A bill ported
48604
+ -- from V1 has no lines of its own \u2014 \`v_financial_invoice_lines\` reads them
48605
+ -- from the order \u2014 and it stays that way: giving it a copy would freeze lines
48606
+ -- the sync still writes on the order.
48607
+ --
48608
+ -- Receivables of SELLING orders only. A purchase's bill diverges from its
48609
+ -- order on purpose (interest, supplier credit \u2014 120).
48610
+
48611
+ -- \u2500\u2500 1. the reconciliation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
48612
+ CREATE OR REPLACE FUNCTION public.order_reconcile_receivable_internal(p_order_id uuid)
48613
+ RETURNS text
48614
+ LANGUAGE plpgsql
48615
+ SECURITY DEFINER
48616
+ SET search_path TO ''
48617
+ AS $$
48618
+ DECLARE
48619
+ v_order public.orders%ROWTYPE;
48620
+ v_invoice public.plg_financial_invoices%ROWTYPE;
48621
+ v_total numeric;
48622
+ v_paid numeric;
48623
+ v_open_sum numeric;
48624
+ v_last uuid;
48625
+ v_summary text;
48626
+ v_own boolean;
48627
+ BEGIN
48628
+ SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
48629
+ IF NOT FOUND OR NOT (v_order.kind = ANY (public.order_selling_kinds())) THEN
48630
+ RETURN 'none';
48631
+ END IF;
48632
+
48633
+ -- (tenant_id, order_id) is the index; naming the tenant is what uses it.
48634
+ SELECT * INTO v_invoice FROM public.plg_financial_invoices
48635
+ WHERE tenant_id = v_order.tenant_id AND order_id = p_order_id
48636
+ AND cancelled_at IS NULL AND direction = 'receivable'
48637
+ ORDER BY created_at LIMIT 1;
48638
+ IF NOT FOUND THEN RETURN 'none'; END IF;
48639
+
48640
+ -- A sale billed across several live invoices has no single bill that should
48641
+ -- carry its whole total. Nobody does this today; deciding it here would be a guess.
48642
+ IF (SELECT count(*) FROM public.plg_financial_invoices
48643
+ WHERE tenant_id = v_order.tenant_id AND order_id = p_order_id
48644
+ AND cancelled_at IS NULL AND direction = 'receivable') > 1 THEN
48645
+ RETURN 'none';
48646
+ END IF;
48647
+
48648
+ v_total := round(coalesce(v_order.total, 0), 2);
48649
+
48650
+ SELECT coalesce(sum(b.paid), 0) INTO v_paid
48651
+ FROM public.v_financial_installment_balances b WHERE b.invoice_id = v_invoice.id;
48652
+
48653
+ v_own := EXISTS (SELECT 1 FROM public.items WHERE invoice_id = v_invoice.id);
48654
+
48655
+ IF round(coalesce(v_invoice.total, 0), 2) = v_total THEN
48656
+ -- Same amount, but the lines may still be the old ones: a screen that
48657
+ -- writes the total and THEN the lines reaches here with the bill already
48658
+ -- at the new amount. A line with no live sale line behind it is stale.
48659
+ IF v_paid = 0 AND v_own AND (
48660
+ (SELECT count(*) FROM public.items WHERE invoice_id = v_invoice.id)
48661
+ IS DISTINCT FROM (SELECT count(*) FROM public.items WHERE order_id = p_order_id)
48662
+ OR EXISTS (
48663
+ SELECT 1 FROM public.items l
48664
+ WHERE l.invoice_id = v_invoice.id
48665
+ AND NOT EXISTS (
48666
+ SELECT 1 FROM public.item_associations a
48667
+ JOIN public.items s ON s.id = a.from_item_id
48668
+ WHERE a.to_item_id = l.id AND s.order_id = p_order_id AND s.total = l.total))
48669
+ ) THEN
48670
+ DELETE FROM public.items WHERE invoice_id = v_invoice.id;
48671
+ PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice.id);
48672
+ END IF;
48673
+ RETURN 'in_sync';
48674
+ END IF;
48675
+
48676
+ IF v_paid > 0 THEN RETURN 'kept_paid'; END IF;
48677
+
48678
+ IF v_total <= 0 THEN
48679
+ UPDATE public.plg_financial_invoices
48680
+ SET cancelled_at = now(),
48681
+ cancel_reason = coalesce(cancel_reason, 'The sale it was raised from no longer has an amount')
48682
+ WHERE id = v_invoice.id;
48683
+ RETURN 'amended';
48684
+ END IF;
48685
+
48686
+ SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
48687
+ FROM public.items WHERE order_id = p_order_id;
48688
+
48689
+ UPDATE public.plg_financial_invoices
48690
+ SET total = v_total,
48691
+ description = coalesce(nullif(v_summary, ''), description)
48692
+ WHERE id = v_invoice.id;
48693
+
48694
+ -- A stale copy is replaced whole. The copier refuses to run over existing
48695
+ -- lines, hence the delete first.
48696
+ IF v_own THEN
48697
+ DELETE FROM public.items WHERE invoice_id = v_invoice.id;
48698
+ PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice.id);
48699
+ END IF;
48700
+
48701
+ -- The open instalments keep their proportions; the last one takes the cents.
48702
+ SELECT coalesce(sum(amount), 0) INTO v_open_sum
48703
+ FROM public.plg_financial_invoice_installments
48704
+ WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL;
48705
+ SELECT id INTO v_last FROM public.plg_financial_invoice_installments
48706
+ WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL
48707
+ ORDER BY number DESC LIMIT 1;
48708
+
48709
+ IF v_last IS NULL THEN
48710
+ INSERT INTO public.plg_financial_invoice_installments
48711
+ (tenant_id, unit_id, invoice_id, number, due_date, amount)
48712
+ VALUES (v_invoice.tenant_id, v_invoice.unit_id, v_invoice.id,
48713
+ coalesce((SELECT max(number) FROM public.plg_financial_invoice_installments
48714
+ WHERE invoice_id = v_invoice.id), 0) + 1,
48715
+ current_date, v_total);
48716
+ ELSE
48717
+ IF v_open_sum > 0 THEN
48718
+ UPDATE public.plg_financial_invoice_installments
48719
+ SET amount = round(v_total * amount / v_open_sum, 2)
48720
+ WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL AND id <> v_last;
48721
+ END IF;
48722
+ UPDATE public.plg_financial_invoice_installments
48723
+ SET amount = v_total - coalesce((
48724
+ SELECT sum(amount) FROM public.plg_financial_invoice_installments
48725
+ WHERE invoice_id = v_invoice.id AND cancelled_at IS NULL AND id <> v_last), 0)
48726
+ WHERE id = v_last;
48727
+ END IF;
48728
+
48729
+ RETURN 'amended';
48730
+ EXCEPTION WHEN OTHERS THEN
48731
+ -- A closed period, or any other refusal from the books, never fails the
48732
+ -- sale's own write (same rule as raising the receivable, 001). The block
48733
+ -- rolls back whatever it had started, so the bill is left whole.
48734
+ RAISE WARNING 'order_reconcile_receivable: sale % kept its bill as it was: %', p_order_id, SQLERRM;
48735
+ RETURN 'kept_closed';
48736
+ END $$;
48737
+
48738
+ COMMENT ON FUNCTION public.order_reconcile_receivable_internal(uuid) IS
48739
+ 'Makes an UNPAID receivable take its sale''s current total, lines and instalments. Returns none | in_sync | amended | kept_paid | kept_closed. A paid invoice is never touched (131).';
48740
+
48741
+ REVOKE ALL ON FUNCTION public.order_reconcile_receivable_internal(uuid) FROM PUBLIC, anon, authenticated;
48742
+ GRANT EXECUTE ON FUNCTION public.order_reconcile_receivable_internal(uuid) TO service_role;
48743
+
48744
+ -- \u2500\u2500 2. the door a screen uses after rewriting the sale's lines \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
48745
+ -- The trigger below fires on the total; a screen that writes the total and
48746
+ -- THEN the lines (the agenda does) calls this once it is done, so the bill's
48747
+ -- lines are the final ones.
48748
+ CREATE OR REPLACE FUNCTION public.order_reconcile_receivable(p_order_id uuid)
48749
+ RETURNS text
48750
+ LANGUAGE plpgsql
48751
+ SECURITY DEFINER
48752
+ SET search_path TO ''
48753
+ AS $$
48754
+ DECLARE v_tenant uuid;
48755
+ BEGIN
48756
+ SELECT tenant_id INTO v_tenant FROM public.orders WHERE id = p_order_id;
48757
+ IF v_tenant IS NULL OR v_tenant IS DISTINCT FROM app.current_tenant_id() THEN
48758
+ RAISE EXCEPTION 'order_reconcile_receivable: forbidden' USING ERRCODE = '42501';
48759
+ END IF;
48760
+ RETURN public.order_reconcile_receivable_internal(p_order_id);
48761
+ END $$;
48762
+
48763
+ COMMENT ON FUNCTION public.order_reconcile_receivable(uuid) IS
48764
+ 'Tenant-checked door to order_reconcile_receivable_internal: it only ever aligns a bill with a sale the caller''s tenant already owns (131).';
48765
+
48766
+ REVOKE ALL ON FUNCTION public.order_reconcile_receivable(uuid) FROM PUBLIC, anon;
48767
+ GRANT EXECUTE ON FUNCTION public.order_reconcile_receivable(uuid) TO authenticated, service_role;
48768
+
48769
+ -- \u2500\u2500 3. the guarantee: any writer of the total, known or future \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
48770
+ CREATE OR REPLACE FUNCTION public.trg_order_total_reaches_its_invoice()
48771
+ RETURNS trigger
48772
+ LANGUAGE plpgsql
48773
+ SECURITY DEFINER
48774
+ SET search_path TO ''
48775
+ AS $$
48776
+ BEGIN
48777
+ PERFORM public.order_reconcile_receivable_internal(NEW.id);
48778
+ RETURN NULL;
48779
+ END $$;
48780
+
48781
+ DROP TRIGGER IF EXISTS orders_total_reaches_the_invoice ON public.orders;
48782
+ CREATE TRIGGER orders_total_reaches_the_invoice
48783
+ AFTER UPDATE OF total ON public.orders
48784
+ FOR EACH ROW
48785
+ WHEN (NEW.total IS DISTINCT FROM OLD.total)
48786
+ EXECUTE FUNCTION public.trg_order_total_reaches_its_invoice();
48787
+
48788
+ -- \u2500\u2500 4. raising again is the natural repair, and now it repairs the amount \u2500\u2500\u2500
48789
+ CREATE OR REPLACE FUNCTION public.order_raise_receivable(p_order_id uuid, p_due_date date DEFAULT NULL::date)
48790
+ RETURNS uuid
48791
+ LANGUAGE plpgsql
48792
+ SECURITY DEFINER
48793
+ SET search_path TO ''
48794
+ AS $$
48795
+ DECLARE
48796
+ v_order public.orders%ROWTYPE;
48797
+ v_invoice_id uuid;
48798
+ v_number text;
48799
+ v_summary text;
48800
+ v_total numeric;
48801
+ BEGIN
48802
+ SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
48803
+ IF NOT FOUND THEN RETURN NULL; END IF;
48804
+
48805
+ SELECT id INTO v_invoice_id FROM public.plg_financial_invoices
48806
+ WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;
48807
+ IF v_invoice_id IS NOT NULL THEN
48808
+ -- Re-raising stamps the lines on a bill that has none, as before (010),
48809
+ -- and now also repairs the amount.
48810
+ PERFORM public.order_reconcile_receivable_internal(p_order_id);
48811
+ PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
48812
+ RETURN v_invoice_id;
48813
+ END IF;
48814
+
48815
+ v_total := round(coalesce(v_order.total, 0), 2);
48816
+ IF v_total <= 0 THEN RETURN NULL; END IF;
48817
+
48818
+ SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
48819
+ FROM public.items WHERE order_id = p_order_id;
48820
+
48821
+ v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, 'receivable');
48822
+
48823
+ INSERT INTO public.plg_financial_invoices
48824
+ (tenant_id, unit_id, direction, counterparty_person_id, order_id, number,
48825
+ competence_date, issue_date, total, description, metadata, created_by)
48826
+ VALUES
48827
+ (v_order.tenant_id, v_order.unit_id, 'receivable', v_order.party_id, p_order_id, v_number,
48828
+ coalesce(v_order.created_at::date, current_date), current_date,
48829
+ v_total, nullif(v_summary, ''),
48830
+ jsonb_build_object('source', 'order_raise_receivable', 'orderKind', v_order.kind),
48831
+ auth.uid())
48832
+ RETURNING id INTO v_invoice_id;
48833
+
48834
+ PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
48835
+
48836
+ INSERT INTO public.plg_financial_invoice_installments
48837
+ (tenant_id, unit_id, invoice_id, number, due_date, amount)
48838
+ VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, 1,
48839
+ coalesce(p_due_date, current_date), v_total);
48840
+
48841
+ UPDATE public.orders SET
48842
+ stage = 'invoiced',
48843
+ direction = 'credit',
48844
+ metadata = coalesce(metadata, '{}'::jsonb)
48845
+ || jsonb_build_object('invoiceNumber', v_number, 'installmentCount', 1),
48846
+ updated_at = now()
48847
+ WHERE id = p_order_id;
48848
+
48849
+ RETURN v_invoice_id;
48850
+ END $$;
48851
+
48852
+ CREATE OR REPLACE FUNCTION public.fn_invoice_from_order_internal(p_order_id uuid, p_due_date date DEFAULT NULL::date, p_status text DEFAULT NULL::text, p_installments integer DEFAULT 1, p_direction text DEFAULT 'credit'::text)
48853
+ RETURNS uuid
48854
+ LANGUAGE plpgsql
48855
+ SECURITY DEFINER
48856
+ SET search_path TO ''
48857
+ AS $$
48858
+ DECLARE
48859
+ v_order public.orders%ROWTYPE;
48860
+ v_invoice_id uuid;
48861
+ v_dir text;
48862
+ v_number text;
48863
+ v_summary text;
48864
+ v_contact text;
48865
+ v_n int := GREATEST(COALESCE(p_installments, 1), 1);
48866
+ v_total numeric;
48867
+ v_base numeric;
48868
+ v_amt numeric;
48869
+ v_alloc numeric := 0;
48870
+ v_due date;
48871
+ v_status text;
48872
+ i int;
48873
+ BEGIN
48874
+ SELECT * INTO v_order FROM public.orders WHERE id = p_order_id;
48875
+ IF NOT FOUND THEN RAISE EXCEPTION 'fn_invoice_from_order: order % not found', p_order_id; END IF;
48876
+
48877
+ SELECT id INTO v_invoice_id FROM public.plg_financial_invoices
48878
+ WHERE order_id = p_order_id AND cancelled_at IS NULL LIMIT 1;
48879
+ IF v_invoice_id IS NOT NULL THEN
48880
+ -- Idempotent, and the natural repair: an order invoiced before 010 gets its
48881
+ -- lines, and one whose price moved since gets its amount (131). The
48882
+ -- reconciliation is a no-op on anything but an unpaid receivable.
48883
+ PERFORM public.order_reconcile_receivable_internal(p_order_id);
48884
+ PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
48885
+ RETURN p_order_id;
48886
+ END IF;
48887
+ IF EXISTS (SELECT 1 FROM public.plg_financial_movements
48888
+ WHERE invoice_id = p_order_id AND movement_kind = 'bill'
48889
+ AND status <> 'cancelled') THEN
48890
+ RETURN p_order_id;
48891
+ END IF;
48892
+
48893
+ v_dir := CASE WHEN p_direction = 'debit' THEN 'payable' ELSE 'receivable' END;
48894
+ v_number := public.plg_financial_next_invoice_number(v_order.tenant_id, v_dir);
48895
+
48896
+ SELECT string_agg(name, ', ' ORDER BY sort_order) INTO v_summary
48897
+ FROM public.items WHERE order_id = p_order_id;
48898
+ SELECT name INTO v_contact FROM public.people WHERE id = v_order.party_id;
48899
+
48900
+ v_status := v_order.status;
48901
+ IF p_status IS NOT NULL AND EXISTS (
48902
+ SELECT 1 FROM public.order_kinds k
48903
+ WHERE k.kind = v_order.kind
48904
+ AND (k.statuses IS NULL OR p_status = ANY (k.statuses))
48905
+ ) THEN
48906
+ v_status := p_status;
48907
+ END IF;
48908
+
48909
+ UPDATE public.orders SET
48910
+ status = v_status,
48911
+ stage = 'invoiced',
48912
+ direction = p_direction,
48913
+ metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object(
48914
+ 'itemsSummary', COALESCE(v_summary, ''),
48915
+ 'contactName', v_contact,
48916
+ 'installmentCount', v_n,
48917
+ 'quoteNumber', v_order.reference_number,
48918
+ 'direction', p_direction,
48919
+ 'invoiceNumber', v_number
48920
+ ),
48921
+ updated_at = now()
48922
+ WHERE id = p_order_id;
48923
+
48924
+ v_total := round(COALESCE(v_order.total, 0), 2);
48925
+ v_due := COALESCE(p_due_date, current_date);
48926
+
48927
+ INSERT INTO public.plg_financial_invoices
48928
+ (tenant_id, unit_id, direction, counterparty_person_id, order_id, number, competence_date, issue_date,
48929
+ total, description, metadata, created_by)
48930
+ VALUES
48931
+ (v_order.tenant_id, v_order.unit_id, v_dir, v_order.party_id, p_order_id, v_number,
48932
+ COALESCE(v_order.created_at::date, current_date), current_date,
48933
+ v_total, NULLIF(v_summary, ''),
48934
+ jsonb_build_object('source', 'fn_invoice_from_order', 'orderKind', v_order.kind),
48935
+ auth.uid())
48936
+ RETURNING id INTO v_invoice_id;
48937
+
48938
+ PERFORM public.plg_financial_copy_order_lines_to_invoice(p_order_id, v_invoice_id);
48939
+
48940
+ v_base := round(v_total / v_n, 2);
48941
+ FOR i IN 1..v_n LOOP
48942
+ IF i = v_n THEN v_amt := round(v_total - v_alloc, 2);
48943
+ ELSE v_amt := v_base; v_alloc := v_alloc + v_base; END IF;
48944
+
48945
+ INSERT INTO public.plg_financial_invoice_installments
48946
+ (tenant_id, unit_id, invoice_id, number, due_date, amount)
48947
+ VALUES (v_order.tenant_id, v_order.unit_id, v_invoice_id, i,
48948
+ (v_due + ((i - 1) || ' months')::interval)::date, v_amt);
48949
+ END LOOP;
48950
+
48951
+ RETURN p_order_id;
48952
+ END $$;
48953
+ `;
48483
48954
  var MIGRATIONS = [
48484
48955
  { id: "000_baseline", sql: MIGRATION_000_BASELINE },
48485
48956
  { id: "001_the_comanda_becomes_a_receivable_when_it_closes", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },
@@ -48611,7 +49082,8 @@ var MIGRATIONS = [
48611
49082
  { id: "127_a_bill_line_enters_stock_as_it_really_arrived", sql: MIGRATION_127_A_BILL_LINE_ENTERS_STOCK_AS_IT_REALLY_ARRIVED },
48612
49083
  { id: "128_a_stock_receipt_shows_what_was_paid", sql: MIGRATION_128_A_STOCK_RECEIPT_SHOWS_WHAT_WAS_PAID },
48613
49084
  { id: "129_a_bill_can_be_written_in_another_currency", sql: MIGRATION_129_A_BILL_CAN_BE_WRITTEN_IN_ANOTHER_CURRENCY },
48614
- { id: "130_a_bill_can_be_tagged", sql: MIGRATION_130_A_BILL_CAN_BE_TAGGED }
49085
+ { id: "130_a_bill_can_be_tagged", sql: MIGRATION_130_A_BILL_CAN_BE_TAGGED },
49086
+ { id: "131_the_bill_follows_the_price_of_the_sale", sql: MIGRATION_131_THE_BILL_FOLLOWS_THE_PRICE_OF_THE_SALE }
48615
49087
  ];
48616
49088
  var STANDALONE_CONFIG = resolveConfig();
48617
49089
  registerTranslations(financialLocales);