@fayz-ai/plugin-financial 0.14.10 → 0.15.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.
Files changed (34) hide show
  1. package/dist/components/ClientAccountModal.d.ts +3 -0
  2. package/dist/components/ClientAccountModal.d.ts.map +1 -1
  3. package/dist/components/FinancialGeneralSettings.d.ts +18 -1
  4. package/dist/components/FinancialGeneralSettings.d.ts.map +1 -1
  5. package/dist/components/PaymentModal.d.ts +1 -1
  6. package/dist/components/PaymentModal.d.ts.map +1 -1
  7. package/dist/data/supabase.d.ts.map +1 -1
  8. package/dist/data/types.d.ts +25 -0
  9. package/dist/data/types.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2438 -717
  13. package/dist/index.js.map +1 -1
  14. package/dist/locales/en.d.ts.map +1 -1
  15. package/dist/locales/pt-BR.d.ts.map +1 -1
  16. package/dist/migrations/index.d.ts +9 -0
  17. package/dist/migrations/index.d.ts.map +1 -1
  18. package/dist/types.d.ts +7 -0
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/views/ClientAccountPanel.d.ts.map +1 -1
  21. package/dist/views/CommissionRulesView.d.ts.map +1 -1
  22. package/dist/views/CommissionsView.d.ts.map +1 -1
  23. package/dist/views/FinancialPage.d.ts +1 -1
  24. package/dist/views/FinancialPage.d.ts.map +1 -1
  25. package/dist/views/GiftCardsView.d.ts.map +1 -1
  26. package/dist/views/InvoiceDetailView.d.ts +6 -2
  27. package/dist/views/InvoiceDetailView.d.ts.map +1 -1
  28. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  29. package/dist/views/InvoiceListView.d.ts.map +1 -1
  30. package/dist/views/NotasFiscaisView.d.ts.map +1 -1
  31. package/dist/views/ReconciliationView.d.ts.map +1 -1
  32. package/dist/views/StatementsView.d.ts.map +1 -1
  33. package/dist/views/payable-agent.d.ts.map +1 -1
  34. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as React15 from 'react';
2
- import React15__default, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
3
- import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, createSafeDataProvider, registerMessageDefaults, reportDataError, useTranslation, createPendingCommand, countByTenant, getSupabaseClientOptional, getActiveTenantId, exportCSV, useDataChanged } from '@fayz-ai/core';
4
- import { toast, CurrencyInput, DatePicker, Button, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, PageTransition, ModulePage, SubpageHeader, Skeleton, Badge, TooltipProvider, ConfirmDialog, MultiSelect, Tooltip, TooltipTrigger, TooltipContent, Input, Checkbox, ModalFooter, DashboardCanvas, PageHeaderActions, Card, FilterChip, useSaveBar, tourAnchor, SearchSelect, DateRangeChip, ListView, KpiCard, ChartWidget, CardHeader, CardTitle, CardContent, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
5
- import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, PluginSettingsPanel, dedup, useModuleNavigation, usePermissionOptional, useModuleNavAccess, ModuleActionBar, useTenantPluginSettings, SettingsGroup, ToggleRow, useAgentSurface, parseViewId, EntitlementGate, UpgradePrompt, createArchetypeLookup, useChatStore, adminNavigateTo, revealPage, GetStarted, useAuth, resolveEntityRoute, PermissionGate, PersonLink, useLimitGuard, useLookupCreate, OrderFormSkeleton, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, OrderFormAddItemStep, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, invalidateLimit, askAI, requestChatUpload, toInlineFile } from '@fayz-ai/admin';
6
- import { CircleDollarSign, FileText, X, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Pencil, Check, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, DollarSign, Trash2, Landmark, ChevronDown, Clock, BookOpen, ChevronRight, Info, Calculator, StickyNote, Play, Square, ChevronUp, ClipboardCheck, FileCheck2, ExternalLink, FileX2, RefreshCw, Send, Ban, CalendarClock, Gift, Loader2, BadgeCheck, Target, Settings, ArrowRight, Receipt, Users, ArrowDownCircle, ArrowUpCircle, Sparkles, Link2, Search, Download, Paperclip, CheckCheck, MoreVertical, CircleCheckBig, CircleDashed, Camera, CircleAlert, CircleEllipsis, CalendarDays, LayoutList, Layers, Undo2, Building2, ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell, Music, ShoppingBag, ArrowUpRight, ArrowDownRight, EyeOff, Eye } from 'lucide-react';
2
+ import React15__default, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
3
+ import { createConnectionStore, connectionSettings, connectionStatus, settingsStatus, connectionRuns, registerTranslations, createSafeDataProvider, registerMessageDefaults, reportDataError, useTranslation, createPendingCommand, useDataChanged, countByTenant, getSupabaseClientOptional, getActiveTenantId, exportCSV } from '@fayz-ai/core';
4
+ import { ModalTask, toast, useInModalTaskHost, CurrencyInput, DatePicker, Button, SubpageHeader, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, Modal, ModalContent, ModalHeader, ModalTitle, ModalDescription, ModalBody, DataTable, PageTransition, ModulePage, Skeleton, Badge, TooltipProvider, ConfirmDialog, MultiSelect, Tooltip, TooltipTrigger, TooltipContent, Input, Checkbox, ModalFooter, DashboardCanvas, ListFilterBar, DateRangeControl, PageHeaderActions, Card, FilterMenu, FilterMenuRow, Hint, useSaveBar, tourAnchor, SearchSelect, ListView, KpiCard, ChartWidget, CardHeader, CardTitle, CardContent, TableWidget, useDashboardNavigate, GoldCard } from '@fayz-ai/ui';
5
+ import { createPluginContext, fayzConnectorOAuth, formatCurrency, RightRailPage, PluginSettingsPanel, dedup, useUnitStore, OrderDetailSkeleton, PageActions, OrderDetailBlock, OrderDetailView, PersonLink, useTenantPluginSettings, useModuleNavigation, usePermissionOptional, useModuleNavAccess, ModuleActionBar, SettingsGroup, ToggleRow, useAgentSurface, parseViewId, EntitlementGate, UpgradePrompt, createArchetypeLookup, useChatStore, adminNavigateTo, revealPage, GetStarted, useAuth, resolveEntityRoute, PermissionGate, useLimitGuard, useLookupCreate, OrderFormSkeleton, OrderFormBlock, OrderFormView, OrderFormItemsCard, OrderFormItemRow, OrderFormAddItemStep, invalidateLimit, askAI, requestChatUpload, toInlineFile } from '@fayz-ai/admin';
6
+ import { CircleDollarSign, FileText, FileCheck, ArrowRightLeft, CreditCard, QrCode, Banknote, Pencil, Check, X, Ban, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, CalendarDays, ExternalLink, DollarSign, LayoutList, Layers, Paperclip, Camera, Undo2, ChevronDown, Building2, Wallet, Plus, AlertTriangle, Filter, TrendingUp, Copy, Trash2, Landmark, Clock, BookOpen, ChevronRight, Info, Calculator, StickyNote, Play, Square, ChevronUp, ClipboardCheck, FileCheck2, FileX2, RefreshCw, Send, CalendarClock, Gift, Loader2, BadgeCheck, Target, Settings, ArrowRight, Receipt, Users, ArrowDownCircle, ArrowUpCircle, Sparkles, Link2, Search, Download, CheckCheck, MoreVertical, ShoppingCart, Car, Home, Utensils, Zap, Film, HeartPulse, Dumbbell, Music, ShoppingBag, ArrowUpRight, ArrowDownRight, EyeOff, Eye } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
8
  import { createPortal } from 'react-dom';
9
9
  import { createStore as createStore$1 } from 'zustand/vanilla';
@@ -685,6 +685,8 @@ var STATUS_OPTIONS = [
685
685
  { value: "overdue", labelKey: "financial.invoice.statusOverdue", color: "bg-destructive-soft text-destructive-soft-foreground", icon: CircleAlert },
686
686
  { value: "cancelled", labelKey: "financial.invoice.statusCancelled", color: "bg-muted text-muted-foreground", icon: Ban }
687
687
  ];
688
+ var DATE_PRESETS = ["thisMonth", "lastMonth", "last30", "last90"];
689
+ var MAX_RANGE_DAYS = 90;
688
690
  var isoDate = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
689
691
  function presetRange(preset) {
690
692
  const now2 = /* @__PURE__ */ new Date();
@@ -698,10 +700,16 @@ function presetRange(preset) {
698
700
  from.setDate(from.getDate() - 29);
699
701
  return { from: isoDate(from), to: isoDate(now2) };
700
702
  }
701
- case "thisYear":
702
- return { from: isoDate(new Date(now2.getFullYear(), 0, 1)), to: isoDate(new Date(now2.getFullYear(), 11, 31)) };
703
+ case "last90": {
704
+ const from = new Date(now2);
705
+ from.setDate(from.getDate() - (MAX_RANGE_DAYS - 1));
706
+ return { from: isoDate(from), to: isoDate(now2) };
707
+ }
703
708
  }
704
709
  }
710
+ function spanDays(from, to) {
711
+ return Math.round(((/* @__PURE__ */ new Date(`${to}T00:00:00`)).getTime() - (/* @__PURE__ */ new Date(`${from}T00:00:00`)).getTime()) / 864e5) + 1;
712
+ }
705
713
  function StatusBadge({ status }) {
706
714
  const t2 = useTranslation();
707
715
  const opt = STATUS_OPTIONS.find((o) => o.value === status);
@@ -820,22 +828,33 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
820
828
  const [datePreset, setDatePreset] = useState("custom");
821
829
  const [dateFrom, setDateFrom] = useState(mesCorrente.from);
822
830
  const [dateTo, setDateTo] = useState(mesCorrente.to);
823
- const dateRange = useMemo(() => {
824
- if (!dateFrom && !dateTo) return void 0;
825
- return { from: dateFrom || "0001-01-01", to: dateTo || "9999-12-31" };
826
- }, [dateFrom, dateTo]);
831
+ const dateRange = useMemo(() => ({ from: dateFrom, to: dateTo }), [dateFrom, dateTo]);
832
+ const dateIsDefault = dateFrom === mesCorrente.from && dateTo === mesCorrente.to;
827
833
  const applyPreset = (value) => {
828
834
  setDatePreset(value);
829
- if (value === "all") {
830
- setDateFrom("");
831
- setDateTo("");
832
- return;
833
- }
834
835
  if (value === "custom") return;
835
836
  const range = presetRange(value);
836
837
  setDateFrom(range.from);
837
838
  setDateTo(range.to);
838
839
  };
840
+ const setFromClamped = (from) => {
841
+ if (!from) return;
842
+ setDateFrom(from);
843
+ if (dateTo && spanDays(from, dateTo) > MAX_RANGE_DAYS) {
844
+ const to = /* @__PURE__ */ new Date(`${from}T00:00:00`);
845
+ to.setDate(to.getDate() + (MAX_RANGE_DAYS - 1));
846
+ setDateTo(isoDate(to));
847
+ }
848
+ };
849
+ const setToClamped = (to) => {
850
+ if (!to) return;
851
+ setDateTo(to);
852
+ if (dateFrom && spanDays(dateFrom, to) > MAX_RANGE_DAYS) {
853
+ const from = /* @__PURE__ */ new Date(`${to}T00:00:00`);
854
+ from.setDate(from.getDate() - (MAX_RANGE_DAYS - 1));
855
+ setDateFrom(isoDate(from));
856
+ }
857
+ };
839
858
  useEffect(() => {
840
859
  let active = true;
841
860
  Promise.all([provider.getChartOfAccounts(), provider.getCostCenters()]).then(([accountRows, costCenterRows]) => {
@@ -955,68 +974,107 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
955
974
  direction === "debit" ? "contas-a-pagar" : "contas-a-receber"
956
975
  );
957
976
  };
958
- const listFilters = /* @__PURE__ */ jsx(
959
- DateRangeChip,
977
+ const filtersMenu = /* @__PURE__ */ jsx(
978
+ FilterMenu,
960
979
  {
961
- label: t2("financial.invoice.filterPeriod"),
962
- allLabel: t2("financial.invoice.filterAllPeriods"),
963
- from: dateFrom,
964
- to: dateTo,
965
- preset: datePreset,
966
- onPresetChange: (next) => applyPreset(next),
967
- onChange: ({ from, to }) => {
968
- setDateFrom(from);
969
- setDateTo(to);
980
+ label: t2("crud.list.filters"),
981
+ activeCount: (accountId ? 1 : 0) + (costCenterId ? 1 : 0) + (statusFilter.length ? 1 : 0) + (dateIsDefault ? 0 : 1),
982
+ onClear: () => {
983
+ setAccountId("");
984
+ setCostCenterId("");
985
+ setStatusFilter([]);
986
+ setDatePreset("custom");
987
+ setDateFrom(mesCorrente.from);
988
+ setDateTo(mesCorrente.to);
970
989
  },
971
- presets: [
972
- { value: "thisMonth", label: t2("financial.invoice.filterThisMonth") },
973
- { value: "lastMonth", label: t2("financial.invoice.filterLastMonth") },
974
- { value: "last30", label: t2("financial.invoice.filterLast30") },
975
- { value: "thisYear", label: t2("financial.invoice.filterThisYear") }
976
- ],
977
- labels: {
978
- from: t2("financial.invoice.filterFrom"),
979
- to: t2("financial.invoice.filterTo")
980
- }
990
+ clearLabel: t2("financial.invoice.filterClear"),
991
+ fields: [
992
+ {
993
+ id: "status",
994
+ label: t2("financial.invoice.statusCol"),
995
+ icon: CircleDashed,
996
+ activeCount: statusFilter.length,
997
+ render: (_close, q) => /* @__PURE__ */ jsxs(Fragment, { children: [
998
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: statusFilter.length === 0, onClick: () => setStatusFilter([]), children: t2("crud.list.allFacet") }),
999
+ tags.filter((o) => o.label.toLowerCase().includes(q.toLowerCase())).map((o) => {
1000
+ const on = statusFilter.includes(o.value);
1001
+ return /* @__PURE__ */ jsx(
1002
+ FilterMenuRow,
1003
+ {
1004
+ active: on,
1005
+ onClick: () => setStatusFilter(on ? statusFilter.filter((s) => s !== o.value) : [...statusFilter, o.value]),
1006
+ children: /* @__PURE__ */ jsx("span", { className: "truncate", children: o.label })
1007
+ },
1008
+ o.value
1009
+ );
1010
+ })
1011
+ ] })
1012
+ },
1013
+ {
1014
+ id: "period",
1015
+ label: t2("financial.invoice.filterPeriod"),
1016
+ icon: CalendarDays,
1017
+ activeCount: dateIsDefault ? 0 : 1,
1018
+ render: () => /* @__PURE__ */ jsxs(Fragment, { children: [
1019
+ DATE_PRESETS.map((p) => /* @__PURE__ */ jsx(FilterMenuRow, { active: datePreset === p, onClick: () => applyPreset(p), children: t2(`financial.invoice.filter${p.charAt(0).toUpperCase()}${p.slice(1)}`) }, p)),
1020
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: datePreset === "custom", onClick: () => setDatePreset("custom"), children: t2("financial.invoice.filterCustom") }),
1021
+ datePreset === "custom" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 p-1.5", children: [
1022
+ /* @__PURE__ */ jsx(DatePicker, { variant: "chip", format: "numeric", className: "w-full", value: dateFrom, onChange: setFromClamped }),
1023
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2013" }),
1024
+ /* @__PURE__ */ jsx(DatePicker, { variant: "chip", format: "numeric", className: "w-full", value: dateTo, onChange: setToClamped })
1025
+ ] }),
1026
+ /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[10px] text-muted-foreground", children: t2("financial.invoice.filterMaxRange") })
1027
+ ] })
1028
+ },
1029
+ ...postable.length > 0 ? [{
1030
+ id: "account",
1031
+ label: t2("financial.invoice.filterAccount"),
1032
+ icon: Landmark,
1033
+ activeCount: accountId ? 1 : 0,
1034
+ searchable: true,
1035
+ render: (close, q) => /* @__PURE__ */ jsxs(Fragment, { children: [
1036
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !accountId, onClick: () => {
1037
+ setAccountId("");
1038
+ close();
1039
+ }, children: t2("financial.invoice.filterAllAccounts") }),
1040
+ postable.filter((a) => accountLabel(a).toLowerCase().includes(q.toLowerCase())).map((a) => /* @__PURE__ */ jsx(FilterMenuRow, { active: accountId === a.id, onClick: () => {
1041
+ setAccountId(a.id);
1042
+ close();
1043
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: accountLabel(a) }) }, a.id))
1044
+ ] })
1045
+ }] : [],
1046
+ ...costCenters.length > 0 ? [{
1047
+ id: "costCenter",
1048
+ label: t2("financial.invoice.filterCostCenter"),
1049
+ icon: Building2,
1050
+ activeCount: costCenterId ? 1 : 0,
1051
+ searchable: true,
1052
+ render: (close, q) => /* @__PURE__ */ jsxs(Fragment, { children: [
1053
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !costCenterId, onClick: () => {
1054
+ setCostCenterId("");
1055
+ close();
1056
+ }, children: t2("financial.invoice.filterAllCostCenters") }),
1057
+ costCenters.filter((c) => `${c.code} ${c.name}`.toLowerCase().includes(q.toLowerCase())).map((c) => /* @__PURE__ */ jsx(FilterMenuRow, { active: costCenterId === c.id, onClick: () => {
1058
+ setCostCenterId(c.id);
1059
+ close();
1060
+ }, children: /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
1061
+ c.code,
1062
+ " - ",
1063
+ c.name
1064
+ ] }) }, c.id))
1065
+ ] })
1066
+ }] : []
1067
+ ]
981
1068
  }
982
1069
  );
983
- const advancedFilters = /* @__PURE__ */ jsxs(Fragment, { children: [
984
- postable.length > 0 && /* @__PURE__ */ jsx(
985
- FilterChip,
986
- {
987
- icon: Landmark,
988
- label: t2("financial.invoice.filterAccount"),
989
- allLabel: t2("financial.invoice.filterAllAccounts"),
990
- value: accountId || void 0,
991
- onChange: (next) => setAccountId(next ?? ""),
992
- options: postable.map((account) => ({
993
- value: account.id,
994
- label: accountLabel(account)
995
- }))
996
- }
997
- ),
998
- costCenters.length > 0 && /* @__PURE__ */ jsx(
999
- FilterChip,
1000
- {
1001
- icon: Building2,
1002
- label: t2("financial.invoice.filterCostCenter"),
1003
- allLabel: t2("financial.invoice.filterAllCostCenters"),
1004
- value: costCenterId || void 0,
1005
- onChange: (next) => setCostCenterId(next ?? ""),
1006
- options: costCenters.map((costCenter) => ({
1007
- value: costCenter.id,
1008
- label: `${costCenter.code} - ${costCenter.name}`
1009
- }))
1010
- }
1011
- )
1012
- ] });
1013
- const advancedFilterCount = (accountId ? 1 : 0) + (costCenterId ? 1 : 0);
1014
- const activeFilterCount = advancedFilterCount + (statusFilter.length > 0 ? 1 : 0) + (dateFrom || dateTo ? 1 : 0);
1070
+ const activeFilterCount = (accountId ? 1 : 0) + (costCenterId ? 1 : 0) + (statusFilter.length > 0 ? 1 : 0) + (dateIsDefault ? 0 : 1);
1015
1071
  const clearFilters = () => {
1016
1072
  setAccountId("");
1017
1073
  setCostCenterId("");
1018
1074
  setStatusFilter([]);
1019
- applyPreset("all");
1075
+ setDatePreset("custom");
1076
+ setDateFrom(mesCorrente.from);
1077
+ setDateTo(mesCorrente.to);
1020
1078
  };
1021
1079
  const newActions = onNewGated ? [
1022
1080
  {
@@ -1060,14 +1118,8 @@ function InvoiceListView({ direction, onNew, onNewReceipt, onEdit }) {
1060
1118
  search,
1061
1119
  onSearchChange: setSearch,
1062
1120
  searchDebounce: 0,
1063
- tags,
1064
- allTagLabel: t2("crud.list.allFacet"),
1065
- activeTag: statusFilter.length === 1 ? statusFilter[0] : void 0,
1066
- onTagChange: (v) => setStatusFilter(v ? [v] : []),
1067
- filters: listFilters,
1068
- advancedFilters,
1121
+ filtersTrigger: filtersMenu,
1069
1122
  activeFilterCount,
1070
- advancedFilterCount,
1071
1123
  onClearFilters: clearFilters,
1072
1124
  clearFiltersLabel: t2("financial.invoice.filterClear"),
1073
1125
  filtersLabel: t2("crud.list.filters"),
@@ -1544,7 +1596,12 @@ function askHowItWasPaid(invoiceId, options) {
1544
1596
  };
1545
1597
  }
1546
1598
  function createPayableTools({ config, provider }) {
1547
- const lookup = config.contactLookup ?? createArchetypeLookup({ archetype: "person", kind: "supplier" });
1599
+ const lookup = config.contactLookup ?? createArchetypeLookup({
1600
+ archetype: "person",
1601
+ kind: "supplier",
1602
+ createKinds: ["supplier", "staff", "customer"],
1603
+ kindLabels: { supplier: "Fornecedor", customer: "Cliente", staff: "Funcion\xE1rio" }
1604
+ });
1548
1605
  return [
1549
1606
  {
1550
1607
  tool: RESOLVE_CONTEXT,
@@ -2157,7 +2214,12 @@ function PayableDraftSurface({ config, draft }) {
2157
2214
  const live = React15.useRef(draft);
2158
2215
  live.current = draft;
2159
2216
  const lookup = React15.useMemo(
2160
- () => config.contactLookup ?? createArchetypeLookup({ archetype: "person", kind: "supplier" }),
2217
+ () => config.contactLookup ?? createArchetypeLookup({
2218
+ archetype: "person",
2219
+ kind: "supplier",
2220
+ createKinds: ["supplier", "staff", "customer"],
2221
+ kindLabels: { supplier: "Fornecedor", customer: "Cliente", staff: "Funcion\xE1rio" }
2222
+ }),
2161
2223
  [config.contactLookup]
2162
2224
  );
2163
2225
  React15.useEffect(() => {
@@ -2432,6 +2494,173 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
2432
2494
  }
2433
2495
  }
2434
2496
  const SelectedIcon = selectedType ? TYPE_ICONS[txType] ?? Banknote : null;
2497
+ const inTaskHost = useInModalTaskHost();
2498
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2499
+ pending2 && /* @__PURE__ */ jsx("p", { role: "status", className: "px-5 pt-4 text-sm text-warning", children: t2("financial.payment.pendingRetry") }),
2500
+ /* @__PURE__ */ jsxs("fieldset", { disabled: pending2, className: "px-5 py-4 space-y-4", children: [
2501
+ /* @__PURE__ */ jsxs("div", { children: [
2502
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.method") }),
2503
+ /* @__PURE__ */ jsx(
2504
+ "div",
2505
+ {
2506
+ className: "grid transition-all duration-200 ease-out",
2507
+ style: { gridTemplateRows: selectedTypeId && !expandMethodPicker ? "1fr" : "0fr" },
2508
+ children: /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: selectedType && /* @__PURE__ */ jsxs(
2509
+ "button",
2510
+ {
2511
+ onClick: () => setExpandMethodPicker(true),
2512
+ className: `flex items-center gap-2 mt-1.5 w-full rounded-button border px-3 py-2.5 text-left transition-colors hover:bg-muted/30 ${TYPE_COLORS[txType] ?? ""}`,
2513
+ "data-active": "true",
2514
+ children: [
2515
+ SelectedIcon && /* @__PURE__ */ jsx(SelectedIcon, { className: `h-4 w-4 ${ICON_COLORS[txType] ?? ""}` }),
2516
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium flex-1", children: selectedType?.name }),
2517
+ /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-muted-foreground" })
2518
+ ]
2519
+ }
2520
+ ) })
2521
+ }
2522
+ ),
2523
+ /* @__PURE__ */ jsx(
2524
+ "div",
2525
+ {
2526
+ className: "grid transition-all duration-200 ease-out",
2527
+ style: { gridTemplateRows: expandMethodPicker ? "1fr" : "0fr" },
2528
+ children: /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: paymentMethodTypes.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-input bg-muted/30 px-3 py-3 text-center mt-1.5", children: [
2529
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.payment.noMethods") }),
2530
+ /* @__PURE__ */ jsxs("a", { href: "#/financial/settings", className: "mt-1.5 inline-block text-xs font-medium text-primary hover:underline", children: [
2531
+ t2("financial.payment.configureMethods"),
2532
+ " \u2192"
2533
+ ] })
2534
+ ] }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 mt-1.5", children: paymentMethodTypes.map((type) => {
2535
+ const tt = type.transactionType ?? "";
2536
+ const Icon = TYPE_ICONS[tt] ?? Banknote;
2537
+ const isActive = selectedTypeId === type.id;
2538
+ return /* @__PURE__ */ jsxs(
2539
+ "button",
2540
+ {
2541
+ "data-active": isActive,
2542
+ onClick: () => handleSelectType(type.id),
2543
+ className: `flex flex-col items-center gap-1.5 rounded-button border px-2 py-3 text-center transition-all ${TYPE_COLORS[tt] ?? TYPE_COLORS.cash}`,
2544
+ children: [
2545
+ /* @__PURE__ */ jsx(Icon, { className: `h-5 w-5 ${isActive ? ICON_COLORS[tt] ?? "" : "text-muted-foreground"}` }),
2546
+ /* @__PURE__ */ jsx("span", { className: `text-[11px] font-medium leading-tight ${isActive ? "text-foreground" : "text-muted-foreground"}`, children: type.name })
2547
+ ]
2548
+ },
2549
+ type.id
2550
+ );
2551
+ }) }) })
2552
+ }
2553
+ )
2554
+ ] }),
2555
+ selectedTypeId && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2556
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 stagger-field", style: { animationDelay: "0ms" }, children: [
2557
+ /* @__PURE__ */ jsx(
2558
+ CurrencyInput,
2559
+ {
2560
+ label: t2("financial.payment.amount"),
2561
+ value: amount,
2562
+ onChange: setAmount,
2563
+ symbol: currency.symbol,
2564
+ locale: currency.locale,
2565
+ currencyCode: currency.code
2566
+ }
2567
+ ),
2568
+ /* @__PURE__ */ jsxs("div", { children: [
2569
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.date") }),
2570
+ /* @__PURE__ */ jsx(DatePicker, { value: paymentDate, onChange: setPaymentDate, className: "mt-1" })
2571
+ ] })
2572
+ ] }),
2573
+ filteredMethods.length > 0 && /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "50ms" }, children: [
2574
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: needsCard ? t2("financial.payment.card") : t2("financial.payment.account") }),
2575
+ /* @__PURE__ */ jsxs("select", { value: paymentMethodId, onChange: (e) => setPaymentMethodId(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: [
2576
+ /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.select") }),
2577
+ filteredMethods.map((m) => /* @__PURE__ */ jsx("option", { value: m.id, children: m.name }, m.id))
2578
+ ] })
2579
+ ] }),
2580
+ needsCard && /* @__PURE__ */ jsxs("div", { className: `grid gap-3 stagger-field ${needsCardInstallments ? "grid-cols-2" : "grid-cols-1"}`, style: { animationDelay: "100ms" }, children: [
2581
+ /* @__PURE__ */ jsxs("div", { children: [
2582
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.cardBrand") }),
2583
+ /* @__PURE__ */ jsxs("select", { value: cardBrand, onChange: (e) => setCardBrand(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: [
2584
+ /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectBrand") }),
2585
+ ["Visa", "Mastercard", "American Express", "Elo", "Hipercard", "Diners Club"].map((b) => /* @__PURE__ */ jsx("option", { value: b, children: b }, b))
2586
+ ] })
2587
+ ] }),
2588
+ needsCardInstallments && /* @__PURE__ */ jsxs("div", { children: [
2589
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.cardInstallments") }),
2590
+ /* @__PURE__ */ jsx("select", { value: cardInstallments, onChange: (e) => setCardInstallments(Number(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: Array.from({ length: 12 }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsxs("option", { value: n, children: [
2591
+ n,
2592
+ "x ",
2593
+ n > 1 ? formatCurrency(amount / n, currency) : ""
2594
+ ] }, n)) })
2595
+ ] })
2596
+ ] }),
2597
+ needsOpenCashSession && /* @__PURE__ */ jsxs("div", { className: "stagger-field rounded-lg border border-dashed border-warning/40 bg-warning/5 px-3 py-2.5 text-center", children: [
2598
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.payment.cashNeedsOpenSession") }),
2599
+ /* @__PURE__ */ jsxs("a", { href: "#/financial/cash-registers", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
2600
+ t2("financial.payment.openCashSession"),
2601
+ " \u2192"
2602
+ ] })
2603
+ ] }),
2604
+ /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "100ms" }, children: [
2605
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.bankAccount") }),
2606
+ eligibleAccounts2.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-1 rounded-lg border border-dashed border-input bg-muted/30 px-3 py-2.5 text-center", children: [
2607
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: bankAccounts.length > 0 && selectedType ? t2("financial.payment.noAccountForMethod", { method: selectedType.name }) : t2("financial.payment.noAccounts") }),
2608
+ /* @__PURE__ */ jsxs("a", { href: "#/settings/financial/bank-accounts", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
2609
+ t2("financial.payment.configureAccounts"),
2610
+ " \u2192"
2611
+ ] })
2612
+ ] }) : /* @__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: [
2613
+ /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectAccount") }),
2614
+ eligibleAccounts2.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
2615
+ ] })
2616
+ ] }),
2617
+ /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "150ms" }, children: [
2618
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.notes") }),
2619
+ /* @__PURE__ */ jsx("input", { type: "text", value: notes, onChange: (e) => setNotes(e.target.value), placeholder: t2("financial.payment.optionalNotes"), 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 focus:outline-none focus:ring-2 focus:ring-ring" })
2620
+ ] })
2621
+ ] }, selectedTypeId)
2622
+ ] }),
2623
+ /* @__PURE__ */ jsx("style", { children: `
2624
+ .stagger-field {
2625
+ animation: field-slide-in 250ms ease-out both;
2626
+ }
2627
+ @keyframes field-slide-in {
2628
+ from { opacity: 0; transform: translateY(8px); }
2629
+ to { opacity: 1; transform: translateY(0); }
2630
+ }
2631
+ ` })
2632
+ ] });
2633
+ const footer = /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 justify-end gap-2 px-5 py-4 border-t sticky bottom-0 bg-card", children: [
2634
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, disabled: pending2 || saving, children: t2("financial.payment.cancel") }),
2635
+ /* @__PURE__ */ jsxs(
2636
+ Button,
2637
+ {
2638
+ variant: "default",
2639
+ size: "sm",
2640
+ onClick: handlePay,
2641
+ disabled: saving || !pending2 && (amount <= 0 || !selectedTypeId || !bankAccountId || needsOpenCashSession),
2642
+ children: [
2643
+ /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
2644
+ saving ? t2("financial.payment.processing") : t2("financial.payment.payAmount", { amount: formatCurrency(amount, currency) })
2645
+ ]
2646
+ }
2647
+ )
2648
+ ] });
2649
+ if (inTaskHost) {
2650
+ return /* @__PURE__ */ jsxs(
2651
+ ModalTask,
2652
+ {
2653
+ open: true,
2654
+ title: t2("financial.payment.title"),
2655
+ hint: `${t2("financial.payment.installment", { number: String(movement.installmentNumber ?? 1) })} \xB7 ${t2("financial.payment.remaining", { amount: formatCurrency(remaining, currency) })}`,
2656
+ onClose: handleClose,
2657
+ children: [
2658
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", onKeyDown: handleKeyDown, children: body }),
2659
+ footer
2660
+ ]
2661
+ }
2662
+ );
2663
+ }
2435
2664
  return createPortal(
2436
2665
  /* @__PURE__ */ jsx(
2437
2666
  "div",
@@ -2461,155 +2690,8 @@ function PaymentModal({ movement, receiptUrl, onClose, onPaid }) {
2461
2690
  ] }),
2462
2691
  /* @__PURE__ */ jsx("button", { onClick: handleClose, className: "p-1 text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
2463
2692
  ] }),
2464
- pending2 && /* @__PURE__ */ jsx("p", { role: "status", className: "px-5 pt-4 text-sm text-warning", children: t2("financial.payment.pendingRetry") }),
2465
- /* @__PURE__ */ jsxs("fieldset", { disabled: pending2, className: "px-5 py-4 space-y-4", children: [
2466
- /* @__PURE__ */ jsxs("div", { children: [
2467
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.method") }),
2468
- /* @__PURE__ */ jsx(
2469
- "div",
2470
- {
2471
- className: "grid transition-all duration-200 ease-out",
2472
- style: { gridTemplateRows: selectedTypeId && !expandMethodPicker ? "1fr" : "0fr" },
2473
- children: /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: selectedType && /* @__PURE__ */ jsxs(
2474
- "button",
2475
- {
2476
- onClick: () => setExpandMethodPicker(true),
2477
- className: `flex items-center gap-2 mt-1.5 w-full rounded-button border px-3 py-2.5 text-left transition-colors hover:bg-muted/30 ${TYPE_COLORS[txType] ?? ""}`,
2478
- "data-active": "true",
2479
- children: [
2480
- SelectedIcon && /* @__PURE__ */ jsx(SelectedIcon, { className: `h-4 w-4 ${ICON_COLORS[txType] ?? ""}` }),
2481
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium flex-1", children: selectedType?.name }),
2482
- /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-muted-foreground" })
2483
- ]
2484
- }
2485
- ) })
2486
- }
2487
- ),
2488
- /* @__PURE__ */ jsx(
2489
- "div",
2490
- {
2491
- className: "grid transition-all duration-200 ease-out",
2492
- style: { gridTemplateRows: expandMethodPicker ? "1fr" : "0fr" },
2493
- children: /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: paymentMethodTypes.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed border-input bg-muted/30 px-3 py-3 text-center mt-1.5", children: [
2494
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.payment.noMethods") }),
2495
- /* @__PURE__ */ jsxs("a", { href: "#/financial/settings", className: "mt-1.5 inline-block text-xs font-medium text-primary hover:underline", children: [
2496
- t2("financial.payment.configureMethods"),
2497
- " \u2192"
2498
- ] })
2499
- ] }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 mt-1.5", children: paymentMethodTypes.map((type) => {
2500
- const tt = type.transactionType ?? "";
2501
- const Icon = TYPE_ICONS[tt] ?? Banknote;
2502
- const isActive = selectedTypeId === type.id;
2503
- return /* @__PURE__ */ jsxs(
2504
- "button",
2505
- {
2506
- "data-active": isActive,
2507
- onClick: () => handleSelectType(type.id),
2508
- className: `flex flex-col items-center gap-1.5 rounded-button border px-2 py-3 text-center transition-all ${TYPE_COLORS[tt] ?? TYPE_COLORS.cash}`,
2509
- children: [
2510
- /* @__PURE__ */ jsx(Icon, { className: `h-5 w-5 ${isActive ? ICON_COLORS[tt] ?? "" : "text-muted-foreground"}` }),
2511
- /* @__PURE__ */ jsx("span", { className: `text-[11px] font-medium leading-tight ${isActive ? "text-foreground" : "text-muted-foreground"}`, children: type.name })
2512
- ]
2513
- },
2514
- type.id
2515
- );
2516
- }) }) })
2517
- }
2518
- )
2519
- ] }),
2520
- selectedTypeId && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
2521
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3 stagger-field", style: { animationDelay: "0ms" }, children: [
2522
- /* @__PURE__ */ jsx(
2523
- CurrencyInput,
2524
- {
2525
- label: t2("financial.payment.amount"),
2526
- value: amount,
2527
- onChange: setAmount,
2528
- symbol: currency.symbol,
2529
- locale: currency.locale,
2530
- currencyCode: currency.code
2531
- }
2532
- ),
2533
- /* @__PURE__ */ jsxs("div", { children: [
2534
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.date") }),
2535
- /* @__PURE__ */ jsx(DatePicker, { value: paymentDate, onChange: setPaymentDate, className: "mt-1" })
2536
- ] })
2537
- ] }),
2538
- filteredMethods.length > 0 && /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "50ms" }, children: [
2539
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: needsCard ? t2("financial.payment.card") : t2("financial.payment.account") }),
2540
- /* @__PURE__ */ jsxs("select", { value: paymentMethodId, onChange: (e) => setPaymentMethodId(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: [
2541
- /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.select") }),
2542
- filteredMethods.map((m) => /* @__PURE__ */ jsx("option", { value: m.id, children: m.name }, m.id))
2543
- ] })
2544
- ] }),
2545
- needsCard && /* @__PURE__ */ jsxs("div", { className: `grid gap-3 stagger-field ${needsCardInstallments ? "grid-cols-2" : "grid-cols-1"}`, style: { animationDelay: "100ms" }, children: [
2546
- /* @__PURE__ */ jsxs("div", { children: [
2547
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.cardBrand") }),
2548
- /* @__PURE__ */ jsxs("select", { value: cardBrand, onChange: (e) => setCardBrand(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: [
2549
- /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectBrand") }),
2550
- ["Visa", "Mastercard", "American Express", "Elo", "Hipercard", "Diners Club"].map((b) => /* @__PURE__ */ jsx("option", { value: b, children: b }, b))
2551
- ] })
2552
- ] }),
2553
- needsCardInstallments && /* @__PURE__ */ jsxs("div", { children: [
2554
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.cardInstallments") }),
2555
- /* @__PURE__ */ jsx("select", { value: cardInstallments, onChange: (e) => setCardInstallments(Number(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: Array.from({ length: 12 }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsxs("option", { value: n, children: [
2556
- n,
2557
- "x ",
2558
- n > 1 ? formatCurrency(amount / n, currency) : ""
2559
- ] }, n)) })
2560
- ] })
2561
- ] }),
2562
- needsOpenCashSession && /* @__PURE__ */ jsxs("div", { className: "stagger-field rounded-lg border border-dashed border-warning/40 bg-warning/5 px-3 py-2.5 text-center", children: [
2563
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.payment.cashNeedsOpenSession") }),
2564
- /* @__PURE__ */ jsxs("a", { href: "#/financial/cash-registers", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
2565
- t2("financial.payment.openCashSession"),
2566
- " \u2192"
2567
- ] })
2568
- ] }),
2569
- /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "100ms" }, children: [
2570
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.bankAccount") }),
2571
- eligibleAccounts2.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-1 rounded-lg border border-dashed border-input bg-muted/30 px-3 py-2.5 text-center", children: [
2572
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: bankAccounts.length > 0 && selectedType ? t2("financial.payment.noAccountForMethod", { method: selectedType.name }) : t2("financial.payment.noAccounts") }),
2573
- /* @__PURE__ */ jsxs("a", { href: "#/settings/financial/bank-accounts", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
2574
- t2("financial.payment.configureAccounts"),
2575
- " \u2192"
2576
- ] })
2577
- ] }) : /* @__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: [
2578
- /* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectAccount") }),
2579
- eligibleAccounts2.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
2580
- ] })
2581
- ] }),
2582
- /* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "150ms" }, children: [
2583
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.notes") }),
2584
- /* @__PURE__ */ jsx("input", { type: "text", value: notes, onChange: (e) => setNotes(e.target.value), placeholder: t2("financial.payment.optionalNotes"), 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 focus:outline-none focus:ring-2 focus:ring-ring" })
2585
- ] })
2586
- ] }, selectedTypeId)
2587
- ] }),
2588
- /* @__PURE__ */ jsx("style", { children: `
2589
- .stagger-field {
2590
- animation: field-slide-in 250ms ease-out both;
2591
- }
2592
- @keyframes field-slide-in {
2593
- from { opacity: 0; transform: translateY(8px); }
2594
- to { opacity: 1; transform: translateY(0); }
2595
- }
2596
- ` }),
2597
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-4 border-t sticky bottom-0 bg-card", children: [
2598
- /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, disabled: pending2 || saving, children: t2("financial.payment.cancel") }),
2599
- /* @__PURE__ */ jsxs(
2600
- Button,
2601
- {
2602
- variant: "default",
2603
- size: "sm",
2604
- onClick: handlePay,
2605
- disabled: saving || !pending2 && (amount <= 0 || !selectedTypeId || !bankAccountId || needsOpenCashSession),
2606
- children: [
2607
- /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
2608
- saving ? t2("financial.payment.processing") : t2("financial.payment.payAmount", { amount: formatCurrency(amount, currency) })
2609
- ]
2610
- }
2611
- )
2612
- ] })
2693
+ body,
2694
+ footer
2613
2695
  ]
2614
2696
  }
2615
2697
  )
@@ -2748,7 +2830,8 @@ function InvoiceFormView({ direction, editId, onSaved, focusReceipt = false, fro
2748
2830
  () => createArchetypeLookup({
2749
2831
  archetype: "person",
2750
2832
  kind: direction === "debit" ? "supplier" : "customer",
2751
- kindLabels: { supplier: "Fornecedor", customer: "Cliente" }
2833
+ createKinds: direction === "debit" ? ["supplier", "staff", "customer"] : ["customer", "supplier", "staff"],
2834
+ kindLabels: { supplier: "Fornecedor", customer: "Cliente", staff: "Funcion\xE1rio" }
2752
2835
  }),
2753
2836
  [direction]
2754
2837
  );
@@ -3621,7 +3704,7 @@ function itemKindLabel(kind, t2) {
3621
3704
  const label = t2(`financial.itemKind.${kind}`);
3622
3705
  return label.startsWith("financial.itemKind.") ? void 0 : label;
3623
3706
  }
3624
- function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel }) {
3707
+ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel, embedded }) {
3625
3708
  const t2 = useTranslation();
3626
3709
  const config = useFinancialConfig();
3627
3710
  const { currency } = config;
@@ -3643,6 +3726,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
3643
3726
  const [movements, setMovements] = useState([]);
3644
3727
  const [payments, setPayments] = useState([]);
3645
3728
  const [loading, setLoading] = useState(true);
3729
+ const [error, setError] = useState(null);
3646
3730
  const [reversing, setReversing] = useState(null);
3647
3731
  const [reverseReason, setReverseReason] = useState("");
3648
3732
  const [payingMovement, setPayingMovement] = useState(null);
@@ -3666,31 +3750,41 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
3666
3750
  setCancelReason("");
3667
3751
  cancelRequestKey.current = createIdempotencyKey("financial:invoice-cancel");
3668
3752
  }
3753
+ const reqRef = useRef(0);
3669
3754
  async function loadData() {
3755
+ const req = ++reqRef.current;
3670
3756
  setLoading(true);
3671
- const [inv, invItems, movResult, anexos] = await Promise.all([
3672
- provider.getInvoiceById(invoiceId),
3673
- provider.getInvoiceItems(invoiceId),
3674
- // Scope to THIS invoice (not the tenant-wide first-50 page) so every
3675
- // installment/payment loads regardless of how many movements exist.
3676
- provider.getMovements({ direction, invoiceId, pageSize: 500 }),
3677
- // Opcional no provider: um provider que não sabe de anexo devolve vazio
3678
- // em vez de derrubar a tela inteira do detalhe.
3679
- provider.getInvoiceAttachments?.(invoiceId).catch(() => []) ?? Promise.resolve([])
3680
- ]);
3681
- setInvoice(inv);
3682
- setItems(invItems);
3683
- setAttachments(anexos);
3684
- setMovements(
3685
- movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "bill").sort((a, b) => (a.installmentNumber ?? 0) - (b.installmentNumber ?? 0))
3686
- );
3687
- setPayments(
3688
- movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "payment").sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? ""))
3689
- );
3690
- setLoading(false);
3757
+ setError(null);
3758
+ try {
3759
+ const [inv, invItems, movResult, anexos] = await Promise.all([
3760
+ provider.getInvoiceById(invoiceId),
3761
+ provider.getInvoiceItems(invoiceId),
3762
+ // Scope to THIS invoice (not the tenant-wide first-50 page) so every
3763
+ // installment/payment loads regardless of how many movements exist.
3764
+ provider.getMovements({ direction, invoiceId, pageSize: 500 }),
3765
+ // Opcional no provider: um provider que não sabe de anexo devolve vazio
3766
+ // em vez de derrubar a tela inteira do detalhe.
3767
+ provider.getInvoiceAttachments?.(invoiceId).catch(() => []) ?? Promise.resolve([])
3768
+ ]);
3769
+ if (req !== reqRef.current) return;
3770
+ setInvoice(inv);
3771
+ setItems(invItems);
3772
+ setAttachments(anexos);
3773
+ setMovements(
3774
+ movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "bill").sort((a, b) => (a.installmentNumber ?? 0) - (b.installmentNumber ?? 0))
3775
+ );
3776
+ setPayments(
3777
+ movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "payment").sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? ""))
3778
+ );
3779
+ } catch (e) {
3780
+ if (req !== reqRef.current) return;
3781
+ setError(e?.message ?? String(e));
3782
+ } finally {
3783
+ if (req === reqRef.current) setLoading(false);
3784
+ }
3691
3785
  }
3692
3786
  useEffect(() => {
3693
- loadData();
3787
+ void loadData();
3694
3788
  }, [invoiceId]);
3695
3789
  useDataChanged({ table: T.movements }, () => {
3696
3790
  void loadData();
@@ -3732,7 +3826,17 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
3732
3826
  }
3733
3827
  }
3734
3828
  if (loading) {
3735
- return /* @__PURE__ */ jsx(OrderDetailSkeleton, { parentLabel, onBack });
3829
+ return /* @__PURE__ */ jsx(OrderDetailSkeleton, { parentLabel: embedded ? void 0 : parentLabel, onBack: embedded ? void 0 : onBack });
3830
+ }
3831
+ if (error) {
3832
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
3833
+ /* @__PURE__ */ jsx(SubpageHeader, { title: t2("financial.invoice.loadFailed"), onBack, parentLabel }),
3834
+ /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-destructive/30 bg-destructive/5 px-4 py-3", children: [
3835
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: t2("financial.invoice.loadFailedDesc") }),
3836
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-destructive/80 break-words", children: error })
3837
+ ] }),
3838
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => void loadData(), children: t2("financial.invoice.retry") })
3839
+ ] });
3736
3840
  }
3737
3841
  if (!invoice) {
3738
3842
  return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
@@ -3780,7 +3884,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
3780
3884
  PageActions,
3781
3885
  {
3782
3886
  actions: !cancelled && invoice.status !== "paid" ? [
3783
- { id: "edit", label: t2("financial.invoice.edit"), icon: Pencil, intent: "primary", onSelect: () => onEdit?.() },
3887
+ ...onEdit ? [{ id: "edit", label: t2("financial.invoice.edit"), icon: Pencil, intent: "primary", onSelect: () => onEdit() }] : [],
3784
3888
  { id: "cancel", label: t2("financial.invoice.cancelInvoice"), icon: Ban, intent: "danger", onSelect: () => setConfirmCancel(true) }
3785
3889
  ] : []
3786
3890
  }
@@ -3808,10 +3912,11 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
3808
3912
  /* @__PURE__ */ jsx(
3809
3913
  OrderDetailView,
3810
3914
  {
3915
+ embedded,
3811
3916
  title: invoice.fiscalNumber ? `#${invoice.fiscalNumber}` : invoice.contactName ?? invoice.invoiceDate,
3812
3917
  badges: [{ label: t2(statusConfig.labelKey), tone: statusConfig.tone }],
3813
- onBack,
3814
- backLabel: parentLabel,
3918
+ onBack: embedded ? void 0 : onBack,
3919
+ backLabel: embedded ? void 0 : parentLabel,
3815
3920
  meta: invoice,
3816
3921
  actions,
3817
3922
  stats: [
@@ -4036,7 +4141,8 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
4036
4141
  return /* @__PURE__ */ jsx(
4037
4142
  "div",
4038
4143
  {
4039
- className: "fixed inset-0 z-[200] flex flex-col bg-black/80 p-4",
4144
+ "data-modal-passthrough": true,
4145
+ className: "pointer-events-auto fixed inset-0 z-[200] flex flex-col bg-black/80 p-4",
4040
4146
  onClick: () => setViewing(null),
4041
4147
  role: "dialog",
4042
4148
  "aria-modal": "true",
@@ -4117,53 +4223,54 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
4117
4223
  })(),
4118
4224
  document.body
4119
4225
  ),
4120
- reversing && createPortal(
4121
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[200] flex items-center justify-center bg-black/40 p-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-2xl border bg-card p-4 shadow-lg", children: [
4122
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.invoice.reverseTitle") }),
4123
- /* @__PURE__ */ jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [
4124
- formatCurrency(reversing.amount ?? 0, currency),
4125
- " \xB7 ",
4126
- reversing.paymentDate ?? ""
4127
- ] }),
4128
- /* @__PURE__ */ jsx("label", { className: "mt-3 block text-[10px] font-medium uppercase text-muted-foreground", children: t2("financial.invoice.reverseReason") }),
4129
- /* @__PURE__ */ jsx(
4130
- "textarea",
4131
- {
4132
- rows: 2,
4133
- value: reverseReason,
4134
- onChange: (e) => setReverseReason(e.target.value),
4135
- className: "mt-1 w-full rounded-input border border-input bg-card px-2 py-1.5 text-xs"
4136
- }
4137
- ),
4138
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex justify-end gap-2", children: [
4139
- /* @__PURE__ */ jsx(
4140
- "button",
4141
- {
4142
- onClick: () => {
4143
- setReversing(null);
4144
- setReverseReason("");
4145
- },
4146
- className: "rounded-lg border px-3 py-1.5 text-xs font-medium hover:bg-muted",
4147
- children: t2("common.cancel")
4148
- }
4149
- ),
4150
- /* @__PURE__ */ jsx(
4151
- "button",
4152
- {
4153
- disabled: !reverseReason.trim(),
4154
- onClick: async () => {
4155
- await reverseMovement(reversing.id, reverseReason.trim());
4156
- setReversing(null);
4157
- setReverseReason("");
4158
- loadData();
4159
- },
4160
- className: "rounded-button bg-destructive px-3 py-1.5 text-xs font-medium text-destructive-foreground disabled:opacity-50",
4161
- children: t2("financial.invoice.reverse")
4162
- }
4163
- )
4164
- ] })
4165
- ] }) }),
4166
- document.body
4226
+ reversing && /* @__PURE__ */ jsxs(
4227
+ ModalTask,
4228
+ {
4229
+ open: true,
4230
+ title: t2("financial.invoice.reverseTitle"),
4231
+ hint: `${formatCurrency(reversing.amount ?? 0, currency)} \xB7 ${reversing.paymentDate ?? ""}`,
4232
+ width: "max-w-sm",
4233
+ onClose: () => {
4234
+ setReversing(null);
4235
+ setReverseReason("");
4236
+ },
4237
+ children: [
4238
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-5 py-4", children: [
4239
+ /* @__PURE__ */ jsx("label", { className: "block text-[10px] font-medium uppercase text-muted-foreground", children: t2("financial.invoice.reverseReason") }),
4240
+ /* @__PURE__ */ jsx(
4241
+ "textarea",
4242
+ {
4243
+ rows: 3,
4244
+ autoFocus: true,
4245
+ value: reverseReason,
4246
+ onChange: (e) => setReverseReason(e.target.value),
4247
+ className: "mt-1 w-full rounded-input border border-input bg-card px-3 py-2 text-sm"
4248
+ }
4249
+ )
4250
+ ] }),
4251
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 justify-end gap-2 border-t px-5 py-3", children: [
4252
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => {
4253
+ setReversing(null);
4254
+ setReverseReason("");
4255
+ }, children: t2("common.cancel") }),
4256
+ /* @__PURE__ */ jsx(
4257
+ Button,
4258
+ {
4259
+ variant: "destructive",
4260
+ size: "sm",
4261
+ disabled: !reverseReason.trim(),
4262
+ onClick: async () => {
4263
+ await reverseMovement(reversing.id, reverseReason.trim());
4264
+ setReversing(null);
4265
+ setReverseReason("");
4266
+ loadData();
4267
+ },
4268
+ children: t2("financial.invoice.reverse")
4269
+ }
4270
+ )
4271
+ ] })
4272
+ ]
4273
+ }
4167
4274
  ),
4168
4275
  payingMovement && /* @__PURE__ */ jsx(
4169
4276
  PaymentModal,
@@ -4181,37 +4288,39 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
4181
4288
  }
4182
4289
  }
4183
4290
  ),
4184
- confirmCancel && createPortal(
4185
- /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", onClick: closeCancelDialog, children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-modal border bg-card shadow-lg mx-4 p-5", onClick: (e) => e.stopPropagation(), style: { animation: "field-slide-in 200ms ease-out" }, children: [
4186
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
4187
- /* @__PURE__ */ jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10 shrink-0", children: /* @__PURE__ */ jsx(Ban, { className: "h-5 w-5 text-destructive" }) }),
4188
- /* @__PURE__ */ jsxs("div", { children: [
4189
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.invoice.cancelInvoice") }),
4190
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.invoice.cancelConfirm") })
4191
- ] })
4192
- ] }),
4193
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-4", children: t2("financial.invoice.cancelDesc") }),
4194
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-cancel-reason", children: t2("financial.invoice.cancelReason") }),
4195
- /* @__PURE__ */ jsx(
4196
- "textarea",
4197
- {
4198
- id: "invoice-cancel-reason",
4199
- value: cancelReason,
4200
- onChange: (event) => setCancelReason(event.target.value),
4201
- placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
4202
- className: "mb-4 min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
4203
- autoFocus: true
4204
- }
4205
- ),
4206
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
4207
- /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeCancelDialog, children: t2("financial.invoice.keepInvoice") }),
4208
- /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancel, disabled: cancelling || !cancelReason.trim(), children: [
4209
- /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
4210
- cancelling ? t2("financial.invoice.cancelling") : t2("financial.invoice.yesCancel")
4291
+ confirmCancel && /* @__PURE__ */ jsxs(
4292
+ ModalTask,
4293
+ {
4294
+ open: true,
4295
+ title: t2("financial.invoice.cancelInvoice"),
4296
+ hint: t2("financial.invoice.cancelConfirm"),
4297
+ width: "max-w-sm",
4298
+ onClose: closeCancelDialog,
4299
+ children: [
4300
+ /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-5 py-4", children: [
4301
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-4", children: t2("financial.invoice.cancelDesc") }),
4302
+ /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium mb-1", htmlFor: "invoice-cancel-reason", children: t2("financial.invoice.cancelReason") }),
4303
+ /* @__PURE__ */ jsx(
4304
+ "textarea",
4305
+ {
4306
+ id: "invoice-cancel-reason",
4307
+ value: cancelReason,
4308
+ onChange: (event) => setCancelReason(event.target.value),
4309
+ placeholder: t2("financial.invoice.cancelReasonPlaceholder"),
4310
+ className: "min-h-20 w-full resize-y rounded-md border bg-background px-3 py-2 text-sm",
4311
+ autoFocus: true
4312
+ }
4313
+ )
4314
+ ] }),
4315
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 justify-end gap-2 border-t px-5 py-3", children: [
4316
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: closeCancelDialog, children: t2("financial.invoice.keepInvoice") }),
4317
+ /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: handleCancel, disabled: cancelling || !cancelReason.trim(), children: [
4318
+ /* @__PURE__ */ jsx(Ban, { className: "h-3.5 w-3.5" }),
4319
+ cancelling ? t2("financial.invoice.cancelling") : t2("financial.invoice.yesCancel")
4320
+ ] })
4211
4321
  ] })
4212
- ] })
4213
- ] }) }),
4214
- document.body
4322
+ ]
4323
+ }
4215
4324
  ),
4216
4325
  /* @__PURE__ */ jsx("style", { children: `
4217
4326
  @keyframes field-slide-in {
@@ -5136,12 +5245,37 @@ function StatementsView({ onNavigate }) {
5136
5245
  const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
5137
5246
  const fetchStatement = useFinancialStore((s) => s.fetchStatement);
5138
5247
  const [accountId, setAccountId] = useState("");
5139
- const [dateFrom, setDateFrom] = useState(() => {
5248
+ const MAX_RANGE_DAYS2 = 90;
5249
+ const hojeIso = () => {
5140
5250
  const d = /* @__PURE__ */ new Date();
5141
- d.setMonth(d.getMonth() - 3);
5142
- return d.toISOString().slice(0, 10);
5143
- });
5144
- const [dateTo, setDateTo] = useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
5251
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5252
+ };
5253
+ const defaultFrom = useMemo(() => {
5254
+ const d = /* @__PURE__ */ new Date();
5255
+ d.setDate(d.getDate() - 29);
5256
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
5257
+ }, []);
5258
+ const [dateFrom, setDateFrom] = useState(defaultFrom);
5259
+ const [dateTo, setDateTo] = useState(hojeIso);
5260
+ const spanDays2 = (from, to) => Math.round(((/* @__PURE__ */ new Date(`${to}T00:00:00`)).getTime() - (/* @__PURE__ */ new Date(`${from}T00:00:00`)).getTime()) / 864e5) + 1;
5261
+ const setFromClamped = (from) => {
5262
+ if (!from) return;
5263
+ setDateFrom(from);
5264
+ if (dateTo && spanDays2(from, dateTo) > MAX_RANGE_DAYS2) {
5265
+ const d = /* @__PURE__ */ new Date(`${from}T00:00:00`);
5266
+ d.setDate(d.getDate() + (MAX_RANGE_DAYS2 - 1));
5267
+ setDateTo(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`);
5268
+ }
5269
+ };
5270
+ const setToClamped = (to) => {
5271
+ if (!to) return;
5272
+ setDateTo(to);
5273
+ if (dateFrom && spanDays2(dateFrom, to) > MAX_RANGE_DAYS2) {
5274
+ const d = /* @__PURE__ */ new Date(`${to}T00:00:00`);
5275
+ d.setDate(d.getDate() - (MAX_RANGE_DAYS2 - 1));
5276
+ setDateFrom(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`);
5277
+ }
5278
+ };
5145
5279
  const [search, setSearch] = useState("");
5146
5280
  const [showTransfer, setShowTransfer] = useState(false);
5147
5281
  useEffect(() => {
@@ -5307,11 +5441,18 @@ function StatementsView({ onNavigate }) {
5307
5441
  "extrato"
5308
5442
  );
5309
5443
  };
5444
+ const headerActions = React15__default.useMemo(() => [{
5445
+ id: "new-transfer",
5446
+ label: t2("financial.statements.newTransfer"),
5447
+ icon: ArrowRightLeft,
5448
+ intent: "primary",
5449
+ // Uma transferência é um lançamento novo — o FAB é um "+".
5450
+ creates: true,
5451
+ disabled: bankAccounts.length < 2,
5452
+ onSelect: () => setShowTransfer(true)
5453
+ }], [t2, bankAccounts.length]);
5310
5454
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5311
- /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => setShowTransfer(true), disabled: bankAccounts.length < 2, children: [
5312
- /* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5" }),
5313
- t2("financial.statements.newTransfer")
5314
- ] }) }),
5455
+ /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: headerActions }) }),
5315
5456
  accountId && statement && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-2", children: [
5316
5457
  /* @__PURE__ */ jsx(SummaryStat, { label: t2("financial.statements.opening"), value: fmt(statement.openingBalance) }),
5317
5458
  /* @__PURE__ */ jsx(SummaryStat, { label: t2("financial.statements.totalCredits"), value: fmt(statement.totalCredits), positive: true }),
@@ -5335,29 +5476,67 @@ function StatementsView({ onNavigate }) {
5335
5476
  ] }),
5336
5477
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex flex-wrap items-center justify-end gap-2", children: [
5337
5478
  /* @__PURE__ */ jsx(
5338
- FilterChip,
5479
+ FilterMenu,
5339
5480
  {
5340
- icon: Landmark,
5341
- label: t2("financial.statements.account"),
5342
- allLabel: t2("financial.summary.allAccounts"),
5343
- value: accountId && accountId !== ALL ? accountId : void 0,
5344
- onChange: (next) => setAccountId(next ?? ALL),
5345
- options: bankAccounts.map((a) => ({ value: a.id, label: a.name }))
5481
+ label: t2("crud.list.filters"),
5482
+ activeCount: (accountId && accountId !== ALL ? 1 : 0) + (dateFrom === defaultFrom && dateTo === hojeIso() ? 0 : 1),
5483
+ onClear: () => {
5484
+ setAccountId(ALL);
5485
+ setDateFrom(defaultFrom);
5486
+ setDateTo(hojeIso());
5487
+ },
5488
+ clearLabel: t2("financial.invoice.filterClear"),
5489
+ fields: [
5490
+ {
5491
+ id: "account",
5492
+ label: t2("financial.statements.account"),
5493
+ icon: Landmark,
5494
+ activeCount: accountId && accountId !== ALL ? 1 : 0,
5495
+ searchable: true,
5496
+ render: (close, q) => /* @__PURE__ */ jsxs(Fragment, { children: [
5497
+ /* @__PURE__ */ jsx(FilterMenuRow, { active: !accountId || accountId === ALL, onClick: () => {
5498
+ setAccountId(ALL);
5499
+ close();
5500
+ }, children: t2("financial.summary.allAccounts") }),
5501
+ bankAccounts.filter((a) => a.name.toLowerCase().includes(q.toLowerCase())).map((a) => /* @__PURE__ */ jsx(FilterMenuRow, { active: accountId === a.id, onClick: () => {
5502
+ setAccountId(a.id);
5503
+ close();
5504
+ }, children: /* @__PURE__ */ jsx("span", { className: "truncate", children: a.name }) }, a.id))
5505
+ ] })
5506
+ },
5507
+ {
5508
+ id: "period",
5509
+ label: t2("financial.invoice.filterPeriod"),
5510
+ icon: CalendarDays,
5511
+ activeCount: dateFrom === defaultFrom && dateTo === hojeIso() ? 0 : 1,
5512
+ render: () => /* @__PURE__ */ jsxs(Fragment, { children: [
5513
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 p-1.5", children: [
5514
+ /* @__PURE__ */ jsx(DatePicker, { variant: "chip", format: "numeric", className: "w-full", value: dateFrom, onChange: setFromClamped }),
5515
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2013" }),
5516
+ /* @__PURE__ */ jsx(DatePicker, { variant: "chip", format: "numeric", className: "w-full", value: dateTo, onChange: setToClamped })
5517
+ ] }),
5518
+ /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[10px] text-muted-foreground", children: t2("financial.invoice.filterMaxRange") })
5519
+ ] })
5520
+ }
5521
+ ]
5346
5522
  }
5347
5523
  ),
5348
- /* @__PURE__ */ jsx(DatePicker, { value: dateFrom, onChange: setDateFrom, variant: "chip" }),
5349
- /* @__PURE__ */ jsx(DatePicker, { value: dateTo, onChange: setDateTo, variant: "chip" }),
5350
- entries.length > 0 && /* @__PURE__ */ jsx(
5351
- "button",
5524
+ entries.length > 0 && // O mesmo utilitário da barra de toda lista (CrudListView): ghost
5525
+ // + `size="icon"` + `fz-tap`. À mão era um quadrado de 32px, um
5526
+ // quarto desenho de botão de ícone e abaixo do piso do toque; e o
5527
+ // `title=` nativo não é anunciado por leitor de tela — Hint é.
5528
+ /* @__PURE__ */ jsx(Hint, { label: t2("crud.export.action"), children: /* @__PURE__ */ jsx(
5529
+ Button,
5352
5530
  {
5353
5531
  type: "button",
5354
- onClick: exportEntries,
5532
+ variant: "ghost",
5533
+ size: "icon",
5534
+ className: "fz-tap shrink-0 text-muted-foreground",
5355
5535
  "aria-label": t2("crud.export.action"),
5356
- title: t2("crud.export.action"),
5357
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
5536
+ onClick: exportEntries,
5358
5537
  children: /* @__PURE__ */ jsx(Download, { className: "h-4 w-4" })
5359
5538
  }
5360
- )
5539
+ ) })
5361
5540
  ] })
5362
5541
  ] }),
5363
5542
  /* @__PURE__ */ jsx(
@@ -5420,6 +5599,7 @@ function CommissionsView() {
5420
5599
  const provider = useFinancialProvider();
5421
5600
  const [from, setFrom] = React15__default.useState(firstOfMonth());
5422
5601
  const [to, setTo] = React15__default.useState(today2());
5602
+ const [preset, setPreset] = React15__default.useState("this_month");
5423
5603
  const [running, setRunning] = React15__default.useState(false);
5424
5604
  const [lastRun, setLastRun] = React15__default.useState(null);
5425
5605
  const [generation, setGeneration] = React15__default.useState(null);
@@ -5490,70 +5670,41 @@ function CommissionsView() {
5490
5670
  setBusy(null);
5491
5671
  }
5492
5672
  }
5673
+ const headerActions = React15__default.useMemo(() => [{
5674
+ id: "commission-rules",
5675
+ label: "Configurar regras de comiss\xE3o",
5676
+ icon: Settings,
5677
+ intent: "utility",
5678
+ onSelect: () => {
5679
+ window.location.hash = "/settings/financial-commissions";
5680
+ }
5681
+ }], []);
5493
5682
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
5494
5683
  /* @__PURE__ */ jsx(
5495
5684
  SubpageHeader,
5496
5685
  {
5497
5686
  title: labels.commissions,
5498
5687
  subtitle: t2("financial.commissions.subtitle"),
5499
- actions: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
5500
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
5501
- Button,
5502
- {
5503
- variant: "outline",
5504
- size: "sm",
5505
- "aria-label": "Configurar regras de comiss\xE3o",
5506
- onClick: () => {
5507
- window.location.hash = "/settings/financial-commissions";
5508
- },
5509
- children: [
5510
- /* @__PURE__ */ jsx(Settings, { className: "h-3.5 w-3.5" }),
5511
- "Regras"
5512
- ]
5513
- }
5514
- ) }),
5515
- /* @__PURE__ */ jsx(TooltipContent, { className: "text-xs", children: "Configurar as regras que geram estes valores" })
5516
- ] }) }) })
5688
+ actions: /* @__PURE__ */ jsx(PageActions, { actions: headerActions })
5517
5689
  }
5518
5690
  ),
5519
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-3 rounded-2xl border border-border bg-card px-4 py-3", children: [
5520
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium text-muted-foreground", children: [
5521
- "De",
5522
- /* @__PURE__ */ jsx(
5523
- "input",
5524
- {
5525
- type: "date",
5526
- value: from,
5527
- onChange: (e) => setFrom(e.target.value),
5528
- className: "mt-1 block rounded-input border border-input bg-card px-2 py-1.5 text-sm"
5529
- }
5530
- )
5531
- ] }),
5532
- /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium text-muted-foreground", children: [
5533
- "At\xE9",
5534
- /* @__PURE__ */ jsx(
5535
- "input",
5536
- {
5537
- type: "date",
5538
- value: to,
5539
- onChange: (e) => setTo(e.target.value),
5540
- className: "mt-1 block rounded-input border border-input bg-card px-2 py-1.5 text-sm"
5541
- }
5542
- )
5543
- ] }),
5544
- /* @__PURE__ */ jsxs(
5545
- "button",
5691
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-2xl border border-border bg-card px-4 py-3", children: [
5692
+ /* @__PURE__ */ jsx(
5693
+ DateRangeControl,
5546
5694
  {
5547
- type: "button",
5548
- onClick: () => void run(),
5549
- disabled: running,
5550
- className: "inline-flex items-center gap-1.5 rounded-button bg-foreground px-4 py-2 text-sm font-semibold text-background disabled:opacity-50",
5551
- children: [
5552
- running ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Play, { className: "h-4 w-4", "aria-hidden": true }),
5553
- "Apurar per\xEDodo"
5554
- ]
5695
+ value: { from, to },
5696
+ onChange: (r) => {
5697
+ setFrom(r.from);
5698
+ setTo(r.to);
5699
+ },
5700
+ preset,
5701
+ onPresetChange: (p) => setPreset(p)
5555
5702
  }
5556
5703
  ),
5704
+ /* @__PURE__ */ jsxs(Button, { type: "button", onClick: () => void run(), disabled: running, children: [
5705
+ running ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Play, { className: "h-4 w-4", "aria-hidden": true }),
5706
+ "Apurar per\xEDodo"
5707
+ ] }),
5557
5708
  lastRun && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: lastRun }),
5558
5709
  /* @__PURE__ */ jsxs("div", { className: "ml-auto text-right", children: [
5559
5710
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Total do per\xEDodo" }),
@@ -5584,43 +5735,18 @@ function CommissionsView() {
5584
5735
  }
5585
5736
  ),
5586
5737
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
5587
- generation.status === "draft" && /* @__PURE__ */ jsxs(
5588
- "button",
5589
- {
5590
- type: "button",
5591
- onClick: () => void approve(),
5592
- disabled: busy !== null,
5593
- className: "inline-flex items-center gap-1.5 rounded-button border bg-card px-3 py-1.5 text-xs font-medium hover:bg-muted disabled:opacity-50",
5594
- children: [
5595
- busy === "approve" ? /* @__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 }),
5596
- "Aprovar lan\xE7amentos"
5597
- ]
5598
- }
5599
- ),
5600
- generation.status === "approved" && /* @__PURE__ */ jsxs(
5601
- "button",
5602
- {
5603
- type: "button",
5604
- onClick: () => void payRun(),
5605
- disabled: busy !== null,
5606
- className: "inline-flex items-center gap-1.5 rounded-button bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
5607
- children: [
5608
- busy === "payrun" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Receipt, { className: "h-3.5 w-3.5", "aria-hidden": true }),
5609
- "Gerar contas a pagar"
5610
- ]
5611
- }
5612
- ),
5613
- generation.status === "paid" && /* @__PURE__ */ jsxs(
5614
- "a",
5615
- {
5616
- href: "#/financial/payables/list",
5617
- className: "inline-flex items-center gap-1.5 rounded-button border bg-card px-3 py-1.5 text-xs font-medium hover:bg-muted",
5618
- children: [
5619
- /* @__PURE__ */ jsx(Receipt, { className: "h-3.5 w-3.5", "aria-hidden": true }),
5620
- " Ver em Contas a Pagar"
5621
- ]
5622
- }
5623
- )
5738
+ generation.status === "draft" && /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => void approve(), disabled: busy !== null, children: [
5739
+ busy === "approve" ? /* @__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 }),
5740
+ "Aprovar lan\xE7amentos"
5741
+ ] }),
5742
+ generation.status === "approved" && /* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: () => void payRun(), disabled: busy !== null, children: [
5743
+ busy === "payrun" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Receipt, { className: "h-3.5 w-3.5", "aria-hidden": true }),
5744
+ "Gerar contas a pagar"
5745
+ ] }),
5746
+ generation.status === "paid" && /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsxs("a", { href: "#/financial/payables/list", children: [
5747
+ /* @__PURE__ */ jsx(Receipt, { className: "h-3.5 w-3.5", "aria-hidden": true }),
5748
+ " Ver em Contas a Pagar"
5749
+ ] }) })
5624
5750
  ] })
5625
5751
  ] }),
5626
5752
  /* @__PURE__ */ jsx("p", { className: "mt-2 text-[11px] text-muted-foreground", children: generation.status === "draft" ? "Apurar n\xE3o \xE9 autorizar: o border\xF4 s\xF3 recolhe o que estiver aprovado." : generation.status === "approved" ? "O border\xF4 junta o aprovado por pessoa e levanta UMA conta a pagar para cada." : "As comiss\xF5es viraram conta a pagar \u2014 vivem onde vive o aluguel: extrato e DRE." })
@@ -6310,64 +6436,80 @@ function GiftCardsView() {
6310
6436
  money3(issued.total),
6311
6437
  " em valor de face. Eles nascem sem saldo: ative cada um na venda."
6312
6438
  ] }),
6313
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-end gap-3 rounded-2xl border bg-card px-4 py-3", children: [
6314
- /* @__PURE__ */ jsx(Field, { label: "Buscar c\xF3digo", children: /* @__PURE__ */ jsx(
6439
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-2 rounded-2xl border bg-card px-3 py-2", children: [
6440
+ /* @__PURE__ */ jsx("div", { className: "min-w-[10rem] flex-1", children: /* @__PURE__ */ jsx(
6315
6441
  Input,
6316
6442
  {
6317
6443
  value: search,
6318
- placeholder: "D\xEDgitos",
6319
- className: "w-40 text-sm",
6444
+ placeholder: "Buscar c\xF3digo",
6445
+ className: "border-0 bg-transparent px-1 text-sm shadow-none focus-visible:ring-0",
6320
6446
  onChange: (e) => setSearch(e.target.value)
6321
6447
  }
6322
6448
  ) }),
6323
- /* @__PURE__ */ jsx(Field, { label: "Status", children: /* @__PURE__ */ jsxs(
6324
- "select",
6325
- {
6326
- value: state,
6327
- onChange: (e) => setState(e.target.value),
6328
- className: "mt-1 block rounded-input border border-input bg-card px-2 py-1.5 text-sm",
6329
- children: [
6330
- /* @__PURE__ */ jsx("option", { value: "", children: "Todos" }),
6331
- Object.keys(STATE_META).map((s) => /* @__PURE__ */ jsx("option", { value: s, children: STATE_META[s].label }, s))
6332
- ]
6333
- }
6334
- ) }),
6335
- /* @__PURE__ */ jsx(Field, { label: "Valor de", children: /* @__PURE__ */ jsx(
6336
- Input,
6337
- {
6338
- type: "number",
6339
- value: minValue,
6340
- className: "w-28 text-sm",
6341
- onChange: (e) => setMinValue(e.target.value)
6342
- }
6343
- ) }),
6344
- /* @__PURE__ */ jsx(Field, { label: "at\xE9", children: /* @__PURE__ */ jsx(
6345
- Input,
6346
- {
6347
- type: "number",
6348
- value: maxValue,
6349
- className: "w-28 text-sm",
6350
- onChange: (e) => setMaxValue(e.target.value)
6351
- }
6352
- ) }),
6353
- /* @__PURE__ */ jsx(Field, { label: "Emiss\xE3o de", children: /* @__PURE__ */ jsx(
6354
- Input,
6355
- {
6356
- type: "date",
6357
- value: from,
6358
- className: "w-40 text-sm",
6359
- onChange: (e) => setFrom(e.target.value)
6360
- }
6361
- ) }),
6362
- /* @__PURE__ */ jsx(Field, { label: "at\xE9", children: /* @__PURE__ */ jsx(
6363
- Input,
6449
+ /* @__PURE__ */ jsx(
6450
+ ListFilterBar,
6364
6451
  {
6365
- type: "date",
6366
- value: to,
6367
- className: "w-40 text-sm",
6368
- onChange: (e) => setTo(e.target.value)
6452
+ className: "ml-auto",
6453
+ filters: [{
6454
+ id: "state",
6455
+ label: "Status",
6456
+ allLabel: "Todos os status",
6457
+ options: Object.keys(STATE_META).map((st) => ({ value: st, label: STATE_META[st].label })),
6458
+ value: state || void 0,
6459
+ onChange: (next) => setState(next ?? "")
6460
+ }],
6461
+ extraFields: [
6462
+ {
6463
+ id: "value",
6464
+ label: "Valor",
6465
+ activeCount: (minValue ? 1 : 0) + (maxValue ? 1 : 0),
6466
+ render: () => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 p-1", children: [
6467
+ /* @__PURE__ */ jsx(
6468
+ Input,
6469
+ {
6470
+ type: "number",
6471
+ value: minValue,
6472
+ placeholder: "de",
6473
+ className: "h-8 w-24 text-xs",
6474
+ onChange: (e) => setMinValue(e.target.value)
6475
+ }
6476
+ ),
6477
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "\u2013" }),
6478
+ /* @__PURE__ */ jsx(
6479
+ Input,
6480
+ {
6481
+ type: "number",
6482
+ value: maxValue,
6483
+ placeholder: "at\xE9",
6484
+ className: "h-8 w-24 text-xs",
6485
+ onChange: (e) => setMaxValue(e.target.value)
6486
+ }
6487
+ )
6488
+ ] })
6489
+ },
6490
+ {
6491
+ id: "issued",
6492
+ label: "Emiss\xE3o",
6493
+ activeCount: (from ? 1 : 0) + (to ? 1 : 0),
6494
+ // DatePicker `chip`, não `<input type=date>`: é o mesmo campo
6495
+ // de data de todo filtro do produto, e o nativo muda de cara em
6496
+ // cada navegador.
6497
+ render: () => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5 p-1", children: [
6498
+ /* @__PURE__ */ jsx(DatePicker, { variant: "chip", format: "numeric", value: from, onChange: setFrom }),
6499
+ /* @__PURE__ */ jsx(DatePicker, { variant: "chip", format: "numeric", value: to, onChange: setTo })
6500
+ ] })
6501
+ }
6502
+ ],
6503
+ activeCount: (state ? 1 : 0) + (minValue || maxValue ? 1 : 0) + (from || to ? 1 : 0),
6504
+ onClear: () => {
6505
+ setState("");
6506
+ setMinValue("");
6507
+ setMaxValue("");
6508
+ setFrom("");
6509
+ setTo("");
6510
+ }
6369
6511
  }
6370
- ) })
6512
+ )
6371
6513
  ] }),
6372
6514
  error && /* @__PURE__ */ jsx("div", { className: "mt-3 rounded-card border border-destructive/30 bg-destructive/5 px-4 py-3 text-xs text-destructive", children: error }),
6373
6515
  /* @__PURE__ */ jsx("div", { className: "mt-3 overflow-x-auto rounded-card border bg-card", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
@@ -6443,12 +6585,6 @@ function GiftCardsView() {
6443
6585
  )
6444
6586
  ] }) });
6445
6587
  }
6446
- function Field({ label, children }) {
6447
- return /* @__PURE__ */ jsxs("label", { className: "text-xs font-medium text-muted-foreground", children: [
6448
- label,
6449
- children
6450
- ] });
6451
- }
6452
6588
  function IssueDialog({
6453
6589
  money: money3,
6454
6590
  onClose,
@@ -6628,13 +6764,22 @@ function ReconciliationView() {
6628
6764
  ] });
6629
6765
  }
6630
6766
  const selected = lines.find((l) => l.id === selectedId) ?? null;
6767
+ const RefreshIcon = React15__default.useMemo(
6768
+ () => ({ className }) => /* @__PURE__ */ jsx(RefreshCw, { className: `${className ?? ""} ${loading ? "animate-spin" : ""}` }),
6769
+ [loading]
6770
+ );
6771
+ const headerActions = React15__default.useMemo(() => [{
6772
+ id: "refresh",
6773
+ label: t2("financial.reconciliation.refresh"),
6774
+ icon: RefreshIcon,
6775
+ intent: "utility",
6776
+ disabled: loading || busy,
6777
+ onSelect: () => void load()
6778
+ }], [t2, RefreshIcon, loading, busy, load]);
6631
6779
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
6632
6780
  /* @__PURE__ */ jsxs(PageHeaderActions, { children: [
6633
6781
  /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: lines.length > 0 ? t2("financial.reconciliation.pending", { count: lines.length }) : t2("financial.reconciliation.allDone") }),
6634
- /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", onClick: () => void load(), disabled: loading || busy, children: [
6635
- /* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }),
6636
- t2("financial.reconciliation.refresh")
6637
- ] })
6782
+ /* @__PURE__ */ jsx(PageActions, { actions: headerActions })
6638
6783
  ] }),
6639
6784
  lines.length === 0 && !loading ? /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card shadow-sm py-16 text-center", children: [
6640
6785
  /* @__PURE__ */ jsx(Check, { className: "h-8 w-8 text-success/40 mx-auto mb-2" }),
@@ -6802,16 +6947,22 @@ function NotasFiscaisView() {
6802
6947
  /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-muted-foreground", children: "Este provedor financeiro ainda n\xE3o exp\xF5e notas fiscais." })
6803
6948
  ] });
6804
6949
  }
6950
+ const headerActions = React15__default.useMemo(() => canCreateManually ? [{
6951
+ id: "new-nota",
6952
+ label: "Nova nota",
6953
+ icon: Plus,
6954
+ intent: "primary",
6955
+ creates: true,
6956
+ permission: { feature: "financial", action: "create" },
6957
+ onSelect: () => setCreating(true)
6958
+ }] : [], [canCreateManually]);
6805
6959
  return /* @__PURE__ */ jsxs("div", { className: "p-1", children: [
6806
6960
  /* @__PURE__ */ jsx(
6807
6961
  SubpageHeader,
6808
6962
  {
6809
6963
  title: "Notas Fiscais",
6810
6964
  subtitle: "Emiss\xE3o e acompanhamento de NF-e / NFC-e / NFS-e",
6811
- actions: /* @__PURE__ */ jsx(Fragment, { children: canCreateManually && /* @__PURE__ */ jsx(PermissionGate, { feature: "financial", action: "create", children: /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => setCreating(true), children: [
6812
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
6813
- "Nova nota"
6814
- ] }) }) })
6965
+ actions: headerActions.length > 0 ? /* @__PURE__ */ jsx(PageActions, { actions: headerActions }) : void 0
6815
6966
  }
6816
6967
  ),
6817
6968
  fiscalProvider ? /* @__PURE__ */ jsxs("div", { className: "mt-3 rounded-md border border-emerald-300/50 bg-emerald-50 px-3 py-2 text-xs text-emerald-800 dark:bg-emerald-950/30 dark:text-emerald-300", children: [
@@ -6895,6 +7046,9 @@ function CreateForm({ onCancel, onCreate }) {
6895
7046
  ] })
6896
7047
  ] });
6897
7048
  }
7049
+ var FINANCIAL_MODULE_DEFAULTS = {
7050
+ commissions: true
7051
+ };
6898
7052
  var FINANCIAL_DEFAULTS = {
6899
7053
  autoInstallments: true,
6900
7054
  requireDoc: false,
@@ -6904,10 +7058,20 @@ var FINANCIAL_DEFAULTS = {
6904
7058
  overdueAlerts: true,
6905
7059
  dailySummary: false
6906
7060
  };
6907
- function FinancialGeneralSettings() {
7061
+ function FinancialGeneralSettings({ config }) {
6908
7062
  const t2 = useTranslation();
6909
7063
  const s = useTenantPluginSettings("financial", FINANCIAL_DEFAULTS);
7064
+ const mods = useTenantPluginSettings("financial", FINANCIAL_MODULE_DEFAULTS);
6910
7065
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
7066
+ config?.modules.commissions && /* @__PURE__ */ jsx(SettingsGroup, { title: t2("financial.settings.modules"), description: t2("financial.settings.modulesDesc"), children: /* @__PURE__ */ jsx(
7067
+ ToggleRow,
7068
+ {
7069
+ label: t2("financial.settings.commissions"),
7070
+ description: t2("financial.settings.commissionsDesc"),
7071
+ checked: mods.get("commissions"),
7072
+ onChange: (v) => mods.set("commissions", v)
7073
+ }
7074
+ ) }),
6911
7075
  /* @__PURE__ */ jsxs(SettingsGroup, { title: t2("financial.settings.invoices"), description: t2("financial.settings.invoicesDesc"), children: [
6912
7076
  /* @__PURE__ */ jsx(ToggleRow, { label: t2("financial.settings.autoInstallments"), description: t2("financial.settings.autoInstallmentsDesc"), checked: s.get("autoInstallments"), onChange: (v) => s.set("autoInstallments", v) }),
6913
7077
  /* @__PURE__ */ jsx(ToggleRow, { label: t2("financial.settings.requireDoc"), description: t2("financial.settings.requireDocDesc"), checked: s.get("requireDoc"), onChange: (v) => s.set("requireDoc", v) }),
@@ -7101,7 +7265,13 @@ function buildNav(config, activeView, navigate, t2) {
7101
7265
  }
7102
7266
  return items;
7103
7267
  }
7104
- function FinancialPage({ config, provider, store: store6, registries }) {
7268
+ function FinancialPage({ config: appConfig, provider, store: store6, registries }) {
7269
+ const houseModules = useTenantPluginSettings("financial", FINANCIAL_MODULE_DEFAULTS);
7270
+ const commissionsOn = houseModules.get("commissions");
7271
+ const config = useMemo(
7272
+ () => appConfig.modules.commissions && commissionsOn ? appConfig : { ...appConfig, modules: { ...appConfig.modules, commissions: false } },
7273
+ [appConfig, commissionsOn]
7274
+ );
7105
7275
  const { view, direction, navigate, previousView, back } = useModuleNavigation("/financial", {
7106
7276
  summary: 0,
7107
7277
  "payables-list": 0,
@@ -7188,7 +7358,7 @@ function FinancialPage({ config, provider, store: store6, registries }) {
7188
7358
  PluginSettingsPanel,
7189
7359
  {
7190
7360
  title: t2("financial.settingsPage.title"),
7191
- generalSettings: /* @__PURE__ */ jsx(FinancialGeneralSettings, {}),
7361
+ generalSettings: /* @__PURE__ */ jsx(FinancialGeneralSettings, { config: appConfig }),
7192
7362
  registries,
7193
7363
  hostPluginId: "financial",
7194
7364
  routeBase: "/settings/financial"
@@ -8378,29 +8548,6 @@ function getClients() {
8378
8548
  if (!supabase) throw new Error("Supabase not initialized");
8379
8549
  return { core: supabase, pub: supabase };
8380
8550
  }
8381
- var _overdueSwept = false;
8382
- async function sweepOverdue() {
8383
- if (_overdueSwept) return;
8384
- _overdueSwept = true;
8385
- try {
8386
- const { core, pub } = getClients();
8387
- const today4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8388
- const { data: overdueMovs } = await pub.from(T.movements).update({ status: "overdue" }).in("status", ["pending", "partial"]).lt("due_date", today4).select("invoice_id");
8389
- if (overdueMovs && overdueMovs.length > 0) {
8390
- const invoiceIds = [...new Set(overdueMovs.map((m) => m.invoice_id).filter(Boolean))];
8391
- for (const invId of invoiceIds) {
8392
- const { data: allMovs } = await pub.from(T.bills).select("status").eq("order_id", invId);
8393
- const movs = allMovs ?? [];
8394
- const hasOverdue = movs.some((m) => m.status === "overdue");
8395
- const allPaid = movs.every((m) => m.status === "paid");
8396
- const invoiceStatus = allPaid ? "paid" : hasOverdue ? "overdue" : "open";
8397
- const invoiceStage = allPaid ? "paid" : hasOverdue ? "overdue" : "invoiced";
8398
- await core.from("orders").update({ stage: invoiceStage }).eq("id", invId);
8399
- }
8400
- }
8401
- } catch {
8402
- }
8403
- }
8404
8551
  function round23(n) {
8405
8552
  return Math.round(n * 100) / 100;
8406
8553
  }
@@ -8849,6 +8996,29 @@ function createSupabaseFinancialProvider() {
8849
8996
  * por fatura, então os indicadores são uma soma — não uma segunda contagem
8850
8997
  * que poderia divergir da lista logo abaixo dela.
8851
8998
  */
8999
+ async searchSellableServices(term) {
9000
+ const { pub } = getClients();
9001
+ let q = pub.from("products").select("id, name, price").eq("kind", "service").not("is_active", "is", false).order("name").limit(20);
9002
+ if (term.trim()) q = q.ilike("name", `%${term.trim()}%`);
9003
+ const { data, error } = await q;
9004
+ if (error) throw error;
9005
+ return (data ?? []).map((p) => ({ id: p.id, name: p.name, price: Number(p.price ?? 0) }));
9006
+ },
9007
+ async prepayServices(personId, lines) {
9008
+ const { pub } = getClients();
9009
+ const { data, error } = await pub.rpc("clinical_prepay_services", {
9010
+ p_party: personId,
9011
+ p_lines: lines.map((l) => ({
9012
+ service_id: l.serviceId,
9013
+ name: l.name,
9014
+ quantity: l.quantity,
9015
+ unit_price: l.unitPrice
9016
+ }))
9017
+ });
9018
+ if (error) throw error;
9019
+ const r = data ?? {};
9020
+ return { ok: r.ok === true, reason: r.reason, orderId: r.order_id, total: r.total };
9021
+ },
8852
9022
  async getClientAccount(personId) {
8853
9023
  const { pub } = getClients();
8854
9024
  const { data, error } = await pub.from("v_financial_invoice_balances").select("id, number, order_id, competence_date, total, paid, balance, status, description").eq("counterparty_person_id", personId).eq("direction", "receivable").is("cancelled_at", null).order("competence_date", { ascending: false });
@@ -8865,11 +9035,23 @@ function createSupabaseFinancialProvider() {
8865
9035
  status: r.status,
8866
9036
  description: r.description
8867
9037
  }));
9038
+ let paidToExecute;
9039
+ let openToExecute;
9040
+ try {
9041
+ const { data: exec } = await pub.from("v_clinical_to_execute").select("paid_to_execute, open_to_execute").eq("party_id", personId).maybeSingle();
9042
+ if (exec) {
9043
+ paidToExecute = Number(exec.paid_to_execute) || 0;
9044
+ openToExecute = Number(exec.open_to_execute) || 0;
9045
+ }
9046
+ } catch {
9047
+ }
8868
9048
  return {
8869
9049
  personId,
8870
9050
  contracted: invoices.reduce((a, i) => a + i.total, 0),
8871
9051
  paid: invoices.reduce((a, i) => a + i.paid, 0),
8872
9052
  open: invoices.reduce((a, i) => a + i.balance, 0),
9053
+ paidToExecute,
9054
+ openToExecute,
8873
9055
  invoices
8874
9056
  };
8875
9057
  },
@@ -9747,39 +9929,24 @@ function createSupabaseFinancialProvider() {
9747
9929
  if (error) throw error;
9748
9930
  },
9749
9931
  async getSummary(dateRange) {
9750
- sweepOverdue();
9751
9932
  const { pub } = getClients();
9752
- const [{ data: accounts, error: accountsError }, { data: movs, error: movementsError }, { data: cashMovs, error: cashError }] = await Promise.all([
9753
- pub.from(T.accountBalances).select("balance").eq("is_active", true).in("kind", ["bank", "cash_physical", "cash_session", "card_receivable_credit", "card_receivable_debit"]),
9754
- pub.from(T.movements).select("direction, movement_kind, amount, paid_amount, status, due_date, payment_date"),
9755
- pub.from(T.cashMovements).select("direction, movement_kind, paid_amount, payment_date")
9756
- ]);
9757
- if (accountsError) throw accountsError;
9758
- if (movementsError) throw movementsError;
9759
- if (cashError) throw cashError;
9760
- const totalBalance = (accounts ?? []).reduce((s, a) => s + (Number(a.balance) || 0), 0);
9761
- const allMovs = movs ?? [];
9762
- const unpaid = allMovs.filter((m) => ["pending", "partial", "overdue"].includes(m.status));
9763
- const today4 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9764
- const unpaidCredit = unpaid.filter((m) => m.direction === "credit");
9765
- const unpaidDebit = unpaid.filter((m) => m.direction === "debit");
9766
- const overdueCredit = unpaidCredit.filter((m) => m.status === "overdue" || m.due_date < today4);
9767
- const overdueDebit = unpaidDebit.filter((m) => m.status === "overdue" || m.due_date < today4);
9768
- const monthAgo = /* @__PURE__ */ new Date();
9769
- monthAgo.setDate(monthAgo.getDate() - 30);
9770
- const monthStart = monthAgo.toISOString().slice(0, 10);
9771
- const period = dateRange ?? { from: monthStart, to: today4 };
9772
- const monthPaid = (cashMovs ?? []).filter((m) => m.movement_kind === "payment" && m.payment_date >= period.from && m.payment_date <= period.to);
9933
+ const { data, error } = await pub.rpc("financial_summary", {
9934
+ p_from: dateRange?.from ?? null,
9935
+ p_to: dateRange?.to ?? null
9936
+ });
9937
+ if (error) throw error;
9938
+ const row = (Array.isArray(data) ? data[0] : data) ?? {};
9939
+ const n = (v) => Number(v) || 0;
9773
9940
  return {
9774
- totalBalance,
9775
- totalReceivable: unpaidCredit.reduce((s, m) => s + m.amount - m.paid_amount, 0),
9776
- totalPayable: unpaidDebit.reduce((s, m) => s + m.amount - m.paid_amount, 0),
9777
- monthlyInflow: monthPaid.filter((m) => m.direction === "credit").reduce((s, m) => s + m.paid_amount, 0),
9778
- monthlyOutflow: monthPaid.filter((m) => m.direction === "debit").reduce((s, m) => s + m.paid_amount, 0),
9779
- overdueReceivableCount: overdueCredit.length,
9780
- overdueReceivableAmount: overdueCredit.reduce((s, m) => s + m.amount - m.paid_amount, 0),
9781
- overduePayableCount: overdueDebit.length,
9782
- overduePayableAmount: overdueDebit.reduce((s, m) => s + m.amount - m.paid_amount, 0)
9941
+ totalBalance: n(row.total_balance),
9942
+ totalReceivable: n(row.total_receivable),
9943
+ totalPayable: n(row.total_payable),
9944
+ monthlyInflow: n(row.monthly_inflow),
9945
+ monthlyOutflow: n(row.monthly_outflow),
9946
+ overdueReceivableCount: n(row.overdue_receivable_count),
9947
+ overdueReceivableAmount: n(row.overdue_receivable_amount),
9948
+ overduePayableCount: n(row.overdue_payable_count),
9949
+ overduePayableAmount: n(row.overdue_payable_amount)
9783
9950
  };
9784
9951
  },
9785
9952
  // --- Reconciliation (conciliação) ---
@@ -11255,6 +11422,9 @@ var en = {
11255
11422
  "financial.invoice.summaryReceivable": "Receivable",
11256
11423
  "financial.invoice.summaryPaid": "Paid",
11257
11424
  "financial.invoice.summaryPayable": "Payable",
11425
+ "financial.invoice.loadFailed": "Could not load",
11426
+ "financial.invoice.loadFailedDesc": "Reading this invoice failed. The data is still there \u2014 it just did not reach the screen.",
11427
+ "financial.invoice.retry": "Try again",
11258
11428
  "financial.invoice.notFound": "Invoice not found",
11259
11429
  "financial.invoice.notFoundDesc": "This invoice could not be loaded.",
11260
11430
  "financial.invoice.notes": "Notes",
@@ -11341,6 +11511,8 @@ var en = {
11341
11511
  "financial.invoice.filterThisMonth": "This month",
11342
11512
  "financial.invoice.filterLastMonth": "Last month",
11343
11513
  "financial.invoice.filterLast30": "Last 30 days",
11514
+ "financial.invoice.filterLast90": "Last 90 days",
11515
+ "financial.invoice.filterMaxRange": "90-day maximum range",
11344
11516
  "financial.invoice.filterThisYear": "This year",
11345
11517
  "financial.invoice.filterCustom": "Custom",
11346
11518
  "financial.invoice.filterFrom": "From",
@@ -11411,6 +11583,10 @@ var en = {
11411
11583
  "financial.settings.cashRegisterDesc": "Cash session behavior",
11412
11584
  "financial.settings.dailySummary": "Daily financial summary",
11413
11585
  "financial.settings.dailySummaryDesc": "Send a daily email summary of receivables and payables",
11586
+ "financial.settings.modules": "Modules",
11587
+ "financial.settings.modulesDesc": "What this house shows inside Financial",
11588
+ "financial.settings.commissions": "Commissions",
11589
+ "financial.settings.commissionsDesc": "The tab that settles what the team earns per sale",
11414
11590
  "financial.settings.invoices": "Invoices",
11415
11591
  "financial.settings.invoicesDesc": "How invoices and payments are handled",
11416
11592
  "financial.settings.notifications": "Notifications",
@@ -11829,6 +12005,9 @@ var ptBR = {
11829
12005
  "financial.invoice.summaryReceivable": "A Receber",
11830
12006
  "financial.invoice.summaryPaid": "Pago",
11831
12007
  "financial.invoice.summaryPayable": "A Pagar",
12008
+ "financial.invoice.loadFailed": "N\xE3o foi poss\xEDvel carregar",
12009
+ "financial.invoice.loadFailedDesc": "A leitura desta fatura falhou. O dado continua no lugar \u2014 s\xF3 n\xE3o chegou at\xE9 aqui.",
12010
+ "financial.invoice.retry": "Tentar de novo",
11832
12011
  "financial.invoice.notFound": "Fatura n\xE3o encontrada",
11833
12012
  "financial.invoice.notFoundDesc": "Esta fatura n\xE3o p\xF4de ser carregada.",
11834
12013
  "financial.invoice.notes": "Observa\xE7\xF5es",
@@ -11918,6 +12097,8 @@ var ptBR = {
11918
12097
  "financial.invoice.filterThisMonth": "Este m\xEAs",
11919
12098
  "financial.invoice.filterLastMonth": "M\xEAs passado",
11920
12099
  "financial.invoice.filterLast30": "\xDAltimos 30 dias",
12100
+ "financial.invoice.filterLast90": "\xDAltimos 90 dias",
12101
+ "financial.invoice.filterMaxRange": "Per\xEDodo m\xE1ximo de 90 dias",
11921
12102
  "financial.invoice.filterThisYear": "Este ano",
11922
12103
  "financial.invoice.filterCustom": "Personalizado",
11923
12104
  "financial.invoice.filterFrom": "De",
@@ -11988,6 +12169,10 @@ var ptBR = {
11988
12169
  "financial.settings.cashRegisterDesc": "Comportamento da sess\xE3o de caixa",
11989
12170
  "financial.settings.dailySummary": "Resumo financeiro di\xE1rio",
11990
12171
  "financial.settings.dailySummaryDesc": "Enviar um resumo di\xE1rio por e-mail de contas a receber e a pagar",
12172
+ "financial.settings.modules": "M\xF3dulos",
12173
+ "financial.settings.modulesDesc": "O que desta casa aparece no Financeiro",
12174
+ "financial.settings.commissions": "Comiss\xF5es",
12175
+ "financial.settings.commissionsDesc": "A aba de apura\xE7\xE3o do que a equipe recebe por venda",
11991
12176
  "financial.settings.invoices": "Faturas",
11992
12177
  "financial.settings.invoicesDesc": "Como faturas e pagamentos s\xE3o processados",
11993
12178
  "financial.settings.notifications": "Notifica\xE7\xF5es",
@@ -12441,7 +12626,7 @@ function CommissionRuleDialog({
12441
12626
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Crit\xE9rio vazio vale para todos. Quanto mais crit\xE9rios casarem com o atendimento, mais espec\xEDfica \u2014 e mais forte \u2014 a regra." })
12442
12627
  ] }),
12443
12628
  /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2", children: [
12444
- /* @__PURE__ */ jsx(Field2, { label: "Servi\xE7os", children: /* @__PURE__ */ jsx(
12629
+ /* @__PURE__ */ jsx(Field, { label: "Servi\xE7os", children: /* @__PURE__ */ jsx(
12445
12630
  MultiSelect,
12446
12631
  {
12447
12632
  allLabel: "Todos os servi\xE7os",
@@ -12450,7 +12635,7 @@ function CommissionRuleDialog({
12450
12635
  options: catalogs.services.map((s) => ({ value: s.id, label: s.name }))
12451
12636
  }
12452
12637
  ) }),
12453
- /* @__PURE__ */ jsx(Field2, { label: "Grupos de servi\xE7o", children: /* @__PURE__ */ jsx(
12638
+ /* @__PURE__ */ jsx(Field, { label: "Grupos de servi\xE7o", children: /* @__PURE__ */ jsx(
12454
12639
  MultiSelect,
12455
12640
  {
12456
12641
  allLabel: "Todos os grupos",
@@ -12459,7 +12644,7 @@ function CommissionRuleDialog({
12459
12644
  options: catalogs.categories.map((c) => ({ value: c.id, label: c.name }))
12460
12645
  }
12461
12646
  ) }),
12462
- /* @__PURE__ */ jsx(Field2, { label: "Profissionais", children: /* @__PURE__ */ jsx(
12647
+ /* @__PURE__ */ jsx(Field, { label: "Profissionais", children: /* @__PURE__ */ jsx(
12463
12648
  MultiSelect,
12464
12649
  {
12465
12650
  allLabel: "Todos os profissionais",
@@ -12468,7 +12653,7 @@ function CommissionRuleDialog({
12468
12653
  options: catalogs.professionals.map((p) => ({ value: p.id, label: p.name }))
12469
12654
  }
12470
12655
  ) }),
12471
- /* @__PURE__ */ jsx(Field2, { label: "Unidades", children: /* @__PURE__ */ jsx(
12656
+ /* @__PURE__ */ jsx(Field, { label: "Unidades", children: /* @__PURE__ */ jsx(
12472
12657
  MultiSelect,
12473
12658
  {
12474
12659
  allLabel: "Todas as unidades",
@@ -12478,7 +12663,7 @@ function CommissionRuleDialog({
12478
12663
  }
12479
12664
  ) })
12480
12665
  ] }),
12481
- /* @__PURE__ */ jsx(Field2, { label: "Dias da semana", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-3 rounded-input border border-input bg-card px-3 py-2", children: WEEKDAYS.map((d) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-xs", children: [
12666
+ /* @__PURE__ */ jsx(Field, { label: "Dias da semana", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-3 rounded-input border border-input bg-card px-3 py-2", children: WEEKDAYS.map((d) => /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-1.5 text-xs", children: [
12482
12667
  /* @__PURE__ */ jsx(
12483
12668
  Checkbox,
12484
12669
  {
@@ -12488,7 +12673,7 @@ function CommissionRuleDialog({
12488
12673
  ),
12489
12674
  d.label
12490
12675
  ] }, d.value)) }) }),
12491
- /* @__PURE__ */ jsx(Field2, { label: "Faixas de hor\xE1rio", children: /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-input border border-input bg-card px-3 py-2", children: [
12676
+ /* @__PURE__ */ jsx(Field, { label: "Faixas de hor\xE1rio", children: /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-input border border-input bg-card px-3 py-2", children: [
12492
12677
  ranges.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: "Sem faixa, a regra vale em qualquer hor\xE1rio." }),
12493
12678
  ranges.map((r, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
12494
12679
  /* @__PURE__ */ jsx(
@@ -12550,7 +12735,7 @@ var LABEL = {
12550
12735
  professional_group: "Grupos de Profissionais",
12551
12736
  payment_method_type: "Formas de Pagamento"
12552
12737
  };
12553
- function Field2({ label, children }) {
12738
+ function Field({ label, children }) {
12554
12739
  return /* @__PURE__ */ jsxs("div", { children: [
12555
12740
  /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium uppercase text-muted-foreground", children: label }),
12556
12741
  /* @__PURE__ */ jsx("div", { className: "mt-0.5", children })
@@ -12621,7 +12806,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
12621
12806
  /* @__PURE__ */ jsx(ModalBody, { className: "fz-container px-5 py-4", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
12622
12807
  functions.map((fn, i) => /* @__PURE__ */ jsxs("div", { className: "rounded-card border bg-card p-3", children: [
12623
12808
  /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-3", children: [
12624
- /* @__PURE__ */ jsx(Field3, { label: "Fun\xE7\xE3o", children: /* @__PURE__ */ jsx(
12809
+ /* @__PURE__ */ jsx(Field2, { label: "Fun\xE7\xE3o", children: /* @__PURE__ */ jsx(
12625
12810
  Input,
12626
12811
  {
12627
12812
  value: fn.functionName,
@@ -12630,7 +12815,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
12630
12815
  onChange: (e) => patch(i, { functionName: e.target.value })
12631
12816
  }
12632
12817
  ) }),
12633
- /* @__PURE__ */ jsx(Field3, { label: "Conta padr\xE3o", children: /* @__PURE__ */ jsx(
12818
+ /* @__PURE__ */ jsx(Field2, { label: "Conta padr\xE3o", children: /* @__PURE__ */ jsx(
12634
12819
  NativeSelect,
12635
12820
  {
12636
12821
  value: fn.accountType,
@@ -12638,7 +12823,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
12638
12823
  options: ACCOUNT_TYPES
12639
12824
  }
12640
12825
  ) }),
12641
- /* @__PURE__ */ jsx(Field3, { label: "Sobre", children: /* @__PURE__ */ jsx(
12826
+ /* @__PURE__ */ jsx(Field2, { label: "Sobre", children: /* @__PURE__ */ jsx(
12642
12827
  NativeSelect,
12643
12828
  {
12644
12829
  value: fn.calculationBase,
@@ -12648,7 +12833,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
12648
12833
  ) })
12649
12834
  ] }),
12650
12835
  /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2 mt-3", children: [
12651
- /* @__PURE__ */ jsx(Field3, { label: "Faixa de produ\xE7\xE3o", children: /* @__PURE__ */ jsx(
12836
+ /* @__PURE__ */ jsx(Field2, { label: "Faixa de produ\xE7\xE3o", children: /* @__PURE__ */ jsx(
12652
12837
  NativeSelect,
12653
12838
  {
12654
12839
  value: fn.tierMode,
@@ -12656,7 +12841,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
12656
12841
  options: TIER_MODES
12657
12842
  }
12658
12843
  ) }),
12659
- (fn.tierMode === "none" || fn.tierMode === "guaranteed_min") && /* @__PURE__ */ jsx(Field3, { label: "Valor", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
12844
+ (fn.tierMode === "none" || fn.tierMode === "guaranteed_min") && /* @__PURE__ */ jsx(Field2, { label: "Valor", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
12660
12845
  /* @__PURE__ */ jsx(
12661
12846
  Input,
12662
12847
  {
@@ -12677,7 +12862,7 @@ function CommissionFunctionsDialog({ rule, provider, onClose, onSaved }) {
12677
12862
  )
12678
12863
  ] }) })
12679
12864
  ] }),
12680
- fn.tierMode === "guaranteed_min" && /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs(Field3, { label: "Piso mensal garantido (R$)", children: [
12865
+ fn.tierMode === "guaranteed_min" && /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs(Field2, { label: "Piso mensal garantido (R$)", children: [
12681
12866
  /* @__PURE__ */ jsx(
12682
12867
  Input,
12683
12868
  {
@@ -12806,7 +12991,7 @@ function TierTable({ tiers, onChange }) {
12806
12991
  /* @__PURE__ */ jsx("p", { className: "mt-2 text-[10px] text-muted-foreground", children: "Ex.: 0\u20131.800 fixo R$ 180; acima de 1.800 \u2192 20% s\xF3 do que passar de 1.800." })
12807
12992
  ] });
12808
12993
  }
12809
- function Field3({ label, children }) {
12994
+ function Field2({ label, children }) {
12810
12995
  return /* @__PURE__ */ jsxs("div", { children: [
12811
12996
  /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium uppercase text-muted-foreground", children: label }),
12812
12997
  /* @__PURE__ */ jsx("div", { className: "mt-0.5", children })
@@ -12927,15 +13112,20 @@ function CommissionRulesView({ provider }) {
12927
13112
  await provider.duplicateCommissionRule?.(rule.id);
12928
13113
  await load();
12929
13114
  }
13115
+ const headerActions = React15__default.useMemo(() => [{
13116
+ id: "new-rule",
13117
+ label: "Adicionar regra",
13118
+ icon: Plus,
13119
+ intent: "primary",
13120
+ creates: true,
13121
+ onSelect: () => setEditing("new")
13122
+ }], []);
12930
13123
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
12931
13124
  /* @__PURE__ */ jsx(
12932
13125
  SubpageHeader,
12933
13126
  {
12934
13127
  title: "Regras de Comiss\xE3o",
12935
- actions: /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: () => setEditing("new"), children: [
12936
- /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
12937
- " Adicionar"
12938
- ] }) })
13128
+ actions: /* @__PURE__ */ jsx(PageActions, { actions: headerActions })
12939
13129
  }
12940
13130
  ),
12941
13131
  /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground", children: [
@@ -31908,110 +32098,1453 @@ REVOKE ALL ON FUNCTION migration.upsert_fin_movement(uuid, uuid, jsonb, jsonb)
31908
32098
  REVOKE ALL ON FUNCTION migration._fin_installment_from_bill(uuid, uuid, uuid, uuid, integer, date, numeric, boolean, boolean)
31909
32099
  FROM PUBLIC, anon, authenticated;
31910
32100
  `;
31911
- var MIGRATIONS = [
31912
- { id: "000_baseline", sql: MIGRATION_000_BASELINE },
31913
- { id: "001_the_comanda_becomes_a_receivable_when_it_closes", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },
31914
- { id: "002_payment_types_are_the_country_not_the_tenant", sql: MIGRATION_002_PAYMENT_TYPES_ARE_THE_COUNTRY_NOT_THE_TENANT },
31915
- { id: "003_the_commission_rule_gets_a_table", sql: MIGRATION_003_THE_COMMISSION_RULE_GETS_A_TABLE },
31916
- { id: "004_one_bridge_for_three_doors", sql: MIGRATION_004_ONE_BRIDGE_FOR_THREE_DOORS },
31917
- { id: "005_the_appointment_closes_like_the_comanda", sql: MIGRATION_005_THE_APPOINTMENT_CLOSES_LIKE_THE_COMANDA },
31918
- { id: "006_the_month_can_be_closed_and_the_book_can_be_proven", sql: MIGRATION_006_THE_MONTH_CAN_BE_CLOSED_AND_THE_BOOK_CAN_BE_PROVEN },
31919
- { id: "007_the_invoice_knows_who_issued_it_and_what_it_owes", sql: MIGRATION_007_THE_INVOICE_KNOWS_WHO_ISSUED_IT_AND_WHAT_IT_OWES },
31920
- { id: "008_the_commission_rule_learns_to_choose_and_to_split", sql: MIGRATION_008_THE_COMMISSION_RULE_LEARNS_TO_CHOOSE_AND_TO_SPLIT },
31921
- { id: "009_the_commission_is_calculated_line_by_line", sql: MIGRATION_009_THE_COMMISSION_IS_CALCULATED_LINE_BY_LINE },
31922
- { id: "010_the_invoice_raised_from_a_sale_gets_its_own_lines", sql: MIGRATION_010_THE_INVOICE_RAISED_FROM_A_SALE_GETS_ITS_OWN_LINES },
31923
- { id: "011_a_new_tenant_can_take_money_on_the_first_day", sql: MIGRATION_011_A_NEW_TENANT_CAN_TAKE_MONEY_ON_THE_FIRST_DAY },
31924
- { id: "012_the_commissions_become_one_bill_per_person", sql: MIGRATION_012_THE_COMMISSIONS_BECOME_ONE_BILL_PER_PERSON },
31925
- { id: "013_a_discount_has_a_reason_and_someone_who_allowed_it", sql: MIGRATION_013_A_DISCOUNT_HAS_A_REASON_AND_SOMEONE_WHO_ALLOWED_IT },
31926
- { id: "014_the_bill_that_comes_back_every_month", sql: MIGRATION_014_THE_BILL_THAT_COMES_BACK_EVERY_MONTH },
31927
- { id: "015_the_sale_knows_what_it_cost", sql: MIGRATION_015_THE_SALE_KNOWS_WHAT_IT_COST },
31928
- { id: "016_the_overdue_bill_gets_a_letter", sql: MIGRATION_016_THE_OVERDUE_BILL_GETS_A_LETTER },
31929
- { id: "017_the_receipt_is_a_query_not_a_layout", sql: MIGRATION_017_THE_RECEIPT_IS_A_QUERY_NOT_A_LAYOUT },
31930
- { id: "018_the_caixa_shows_what_it_should_have", sql: MIGRATION_018_THE_CAIXA_SHOWS_WHAT_IT_SHOULD_HAVE },
31931
- { id: "019_who_did_it_is_already_known", sql: MIGRATION_019_WHO_DID_IT_IS_ALREADY_KNOWN },
31932
- { id: "020_a_rule_with_nobody_to_pay_pays_the_executor", sql: MIGRATION_020_A_RULE_WITH_NOBODY_TO_PAY_PAYS_THE_EXECUTOR },
31933
- { id: "021_the_most_specific_rule_wins", sql: MIGRATION_021_THE_MOST_SPECIFIC_RULE_WINS },
31934
- { id: "022_an_unknown_attribute_does_not_match", sql: MIGRATION_022_AN_UNKNOWN_ATTRIBUTE_DOES_NOT_MATCH },
31935
- { id: "023_reapurar_nao_cria_um_lote_orfao", sql: MIGRATION_023_REAPURAR_NAO_CRIA_UM_LOTE_ORFAO },
31936
- { id: "024_o_bordero_paga_o_que_ele_soma", sql: MIGRATION_024_O_BORDERO_PAGA_O_QUE_ELE_SOMA },
31937
- { id: "025_a_meta_do_mes_tem_tres_degraus", sql: MIGRATION_025_A_META_DO_MES_TEM_TRES_DEGRAUS },
31938
- { id: "026_quem_fecha_o_caixa_nao_e_quem_confere", sql: MIGRATION_026_QUEM_FECHA_O_CAIXA_NAO_E_QUEM_CONFERE },
31939
- { id: "027_o_vale_presente_nasce_em_lote_e_e_um_passivo", sql: MIGRATION_027_O_VALE_PRESENTE_NASCE_EM_LOTE_E_E_UM_PASSIVO },
31940
- { id: "028_o_cartao_tem_cor", sql: MIGRATION_028_O_CARTAO_TEM_COR },
31941
- { id: "029_the_kiosk_sale_is_a_sale_too", sql: MIGRATION_029_THE_KIOSK_SALE_IS_A_SALE_TOO },
31942
- { id: "030_the_census_can_see_the_kiosk", sql: MIGRATION_030_THE_CENSUS_CAN_SEE_THE_KIOSK },
31943
- { id: "031_the_bank_line_is_evidence_not_a_second_payment", sql: MIGRATION_031_THE_BANK_LINE_IS_EVIDENCE_NOT_A_SECOND_PAYMENT },
31944
- { id: "032_contas_a_pagar_ganham_onde_morar", sql: MIGRATION_032_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR },
31945
- { id: "033_o_vencimento_aponta_para_o_titulo", sql: MIGRATION_033_O_VENCIMENTO_APONTA_PARA_O_TITULO },
31946
- { id: "034_a_unidade_tem_conta_e_a_pessoa_tem_uma_so", sql: MIGRATION_034_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO },
31947
- { id: "035_a_classificacao_contabil_chega_inteira", sql: MIGRATION_035_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA },
31948
- { id: "036_a_fusao_para_de_dobrar_a_propria_memoria", sql: MIGRATION_036_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA },
31949
- { id: "037_a_venda_de_balcao_tem_conta", sql: MIGRATION_037_A_VENDA_DE_BALCAO_TEM_CONTA },
31950
- { id: "038_a_linha_da_fatura_alcanca_quem_veio_pelo_pedido", sql: MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO },
31951
- { id: "039_o_titulo_de_uma_venda_cancelada_nasce_cancelado", sql: MIGRATION_039_O_TITULO_DE_UMA_VENDA_CANCELADA_NASCE_CANCELADO },
31952
- { id: "040_a_dre_le_a_linha_da_venda", sql: MIGRATION_040_A_DRE_LE_A_LINHA_DA_VENDA },
31953
- { id: "041_o_caixa_do_v1_nao_segura_o_movimento", sql: MIGRATION_041_O_CAIXA_DO_V1_NAO_SEGURA_O_MOVIMENTO },
31954
- { id: "042_o_cancelado_do_v1_e_o_cancelled_do_v2", sql: MIGRATION_042_O_CANCELADO_DO_V1_E_O_CANCELLED_DO_V2 },
31955
- { id: "043_o_movimento_sabe_de_que_unidade_ele_e", sql: MIGRATION_043_O_MOVIMENTO_SABE_DE_QUE_UNIDADE_ELE_E },
31956
- { id: "044_a_dre_nao_paga_pedagio_por_item", sql: MIGRATION_044_A_DRE_NAO_PAGA_PEDAGIO_POR_ITEM },
31957
- { id: "045_a_conta_so_classifica_se_a_natureza_casa", sql: MIGRATION_045_A_CONTA_SO_CLASSIFICA_SE_A_NATUREZA_CASA },
31958
- { id: "046_o_demonstrativo_mostra_o_plano_inteiro", sql: MIGRATION_046_O_DEMONSTRATIVO_MOSTRA_O_PLANO_INTEIRO },
31959
- { id: "047_a_conta_reconhece_a_contraparte_na_segunda_oferta", sql: MIGRATION_047_A_CONTA_RECONHECE_A_CONTRAPARTE_NA_SEGUNDA_OFERTA },
31960
- { id: "048_a_parcela_e_a_conta_do_v1_e_a_baixa_e_a_alocacao", sql: MIGRATION_048_A_PARCELA_E_A_CONTA_DO_V1_E_A_BAIXA_E_A_ALOCACAO },
31961
- { id: "049_a_parcela_carrega_o_cancelamento", sql: MIGRATION_049_A_PARCELA_CARREGA_O_CANCELAMENTO },
31962
- { id: "050_o_saldo_do_titulo_se_calcula_uma_vez", sql: MIGRATION_050_O_SALDO_DO_TITULO_SE_CALCULA_UMA_VEZ },
31963
- { id: "051_o_saldo_se_calcula_so_na_linha_que_a_tela_mostra", sql: MIGRATION_051_O_SALDO_SE_CALCULA_SO_NA_LINHA_QUE_A_TELA_MOSTRA },
31964
- { id: "052_a_lista_de_contas_fala_vencimento", sql: MIGRATION_052_A_LISTA_DE_CONTAS_FALA_VENCIMENTO },
31965
- { id: "053_um_titulo_parcelado_vence_varias_vezes", sql: MIGRATION_053_UM_TITULO_PARCELADO_VENCE_VARIAS_VEZES },
31966
- { id: "054_as_datas_em_que_o_titulo_vence", sql: MIGRATION_054_AS_DATAS_EM_QUE_O_TITULO_VENCE },
31967
- { id: "055_a_lista_de_contas_lista_parcelas", sql: MIGRATION_055_A_LISTA_DE_CONTAS_LISTA_PARCELAS },
31968
- { id: "056_what_the_port_left_open", sql: MIGRATION_056_WHAT_THE_PORT_LEFT_OPEN },
31969
- { id: "057_a_linha_diz_que_tem_recibo", sql: MIGRATION_057_A_LINHA_DIZ_QUE_TEM_RECIBO },
31970
- { id: "058_o_clipe_usa_o_indice", sql: MIGRATION_058_O_CLIPE_USA_O_INDICE },
31971
- { id: "059_o_clipe_sai_da_view", sql: MIGRATION_059_O_CLIPE_SAI_DA_VIEW },
31972
- { id: "060_o_recebivel_volta_da_arvore_derrubada", sql: MIGRATION_060_O_RECEBIVEL_VOLTA_DA_ARVORE_DERRUBADA },
31973
- { id: "061_contas_a_pagar_ganham_onde_morar", sql: MIGRATION_061_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR },
31974
- { id: "062_o_vencimento_aponta_para_o_titulo", sql: MIGRATION_062_O_VENCIMENTO_APONTA_PARA_O_TITULO },
31975
- { id: "063_a_unidade_tem_conta_e_a_pessoa_tem_uma_so", sql: MIGRATION_063_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO },
31976
- { id: "064_a_classificacao_contabil_chega_inteira", sql: MIGRATION_064_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA },
31977
- { id: "065_a_fusao_para_de_dobrar_a_propria_memoria", sql: MIGRATION_065_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA },
31978
- { id: "066_a_venda_de_balcao_tem_conta", sql: MIGRATION_066_A_VENDA_DE_BALCAO_TEM_CONTA },
31979
- { id: "067_o_recibo_da_conta_atravessa", sql: MIGRATION_067_O_RECIBO_DA_CONTA_ATRAVESSA },
31980
- { id: "068_a_unidade_entra_com_a_linha_nao_depois_dela", sql: MIGRATION_068_A_UNIDADE_ENTRA_COM_A_LINHA_NAO_DEPOIS_DELA },
31981
- { id: "069_o_titulo_que_ja_existe_esta_migrado", sql: MIGRATION_069_O_TITULO_QUE_JA_EXISTE_ESTA_MIGRADO },
31982
- { id: "070_a_parcela_nasce_antes_do_movimento", sql: MIGRATION_070_A_PARCELA_NASCE_ANTES_DO_MOVIMENTO },
31983
- { id: "071_a_parcela_tem_uma_assinatura_so", sql: MIGRATION_071_A_PARCELA_TEM_UMA_ASSINATURA_SO },
31984
- { id: "072_o_caixa_do_v1_guarda_quem_o_abriu", sql: MIGRATION_072_O_CAIXA_DO_V1_GUARDA_QUEM_O_ABRIU },
31985
- { id: "073_o_caixa_pertence_a_conta_que_o_v1_lhe_deu", sql: MIGRATION_073_O_CAIXA_PERTENCE_A_CONTA_QUE_O_V1_LHE_DEU },
31986
- { id: "074_o_movimento_volta_a_apontar_para_o_caixa", sql: MIGRATION_074_O_MOVIMENTO_VOLTA_A_APONTAR_PARA_O_CAIXA },
31987
- { id: "075_a_venda_cancelada_cancela_o_titulo_dela", sql: MIGRATION_075_A_VENDA_CANCELADA_CANCELA_O_TITULO_DELA },
31988
- { id: "076_o_plano_de_contas_do_cliente_vence_o_do_modelo", sql: MIGRATION_076_O_PLANO_DE_CONTAS_DO_CLIENTE_VENCE_O_DO_MODELO },
31989
- { id: "077_o_resumo_financeiro_se_soma_no_banco", sql: MIGRATION_077_O_RESUMO_FINANCEIRO_SE_SOMA_NO_BANCO },
31990
- { id: "078_o_saldo_da_conta_se_calcula_so_na_conta_pedida", sql: MIGRATION_078_O_SALDO_DA_CONTA_SE_CALCULA_SO_NA_CONTA_PEDIDA },
31991
- { id: "079_as_portas_de_porte_novas_nao_nascem_abertas", sql: MIGRATION_079_AS_PORTAS_DE_PORTE_NOVAS_NAO_NASCEM_ABERTAS }
31992
- ];
31993
- var STANDALONE_CONFIG = {
31994
- currency: { code: "BRL", locale: "pt-BR", symbol: "R$" }
31995
- };
31996
- registerTranslations(financialLocales);
31997
- function ReceivePaymentModal({
31998
- installment,
31999
- receiptUrl,
32000
- onClose,
32001
- onPaid
32002
- }) {
32003
- const provider = React15__default.useMemo(() => createSafeFinancialProvider(), []);
32004
- const store6 = React15__default.useMemo(() => createFinancialStore(provider), [provider]);
32005
- return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
32006
- PaymentModal,
32007
- {
32008
- movement: installment,
32009
- receiptUrl,
32010
- onClose,
32011
- onPaid
32012
- }
32013
- ) });
32014
- }
32101
+ var MIGRATION_080_A_REMESSA_E_A_COMISSAO_A_PAGAR_TEM_ONDE_MORAR = `-- A remessa banc\xE1ria e a comiss\xE3o a pagar t\xEAm onde morar.
32102
+ --
32103
+ -- Cinco tabelas do V1 sem destino, dois assuntos:
32104
+ --
32105
+ -- cnab_remessas 59 o arquivo de remessa enviado ao banco
32106
+ -- cnab_remessa_items 700 o pagamento dentro dele
32107
+ -- commission_payables 1 a comiss\xE3o apurada virando conta a pagar
32108
+ -- commission_payable_items 3 a linha da apura\xE7\xE3o dentro dela
32109
+ -- commission_payee_prefs 1 para qual conta cont\xE1bil vai a comiss\xE3o
32110
+ --
32111
+ -- O volume \xE9 pequeno \u2014 CNAB \xE9 do Pertinho do C\xE9u, comiss\xE3o a pagar mal come\xE7ou
32112
+ -- no Espa\xE7o Facial. Mas nenhuma das duas \xE9 deriv\xE1vel: a remessa \xE9 um arquivo
32113
+ -- que j\xE1 saiu para o banco com um n\xFAmero de sequ\xEAncia que n\xE3o pode repetir, e a
32114
+ -- comiss\xE3o a pagar \xE9 a ponte entre o que foi APURADO e o T\xEDtulo que algu\xE9m vai
32115
+ -- pagar. Reconstituir qualquer uma seria inventar.
32116
+ --
32117
+ -- \u2500\u2500 O que o V2 j\xE1 tinha, e por isso n\xE3o se repete aqui \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32118
+ --
32119
+ -- \`plg_financial_commission_entries\`, \`_generations\`, \`_rules\`,
32120
+ -- \`_rule_criteria\` e \`_functions\` j\xE1 existem. O que faltava era o PAG\xC1VEL \u2014 o
32121
+ -- elo entre a apura\xE7\xE3o e o contas a pagar. Por isso estas duas tabelas e n\xE3o um
32122
+ -- dom\xEDnio inteiro.
32123
+ --
32124
+ -- \u2500\u2500 O arquivo de remessa \xE9 arquivo \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
32125
+ --
32126
+ -- \`cnab_remessas.file_content\` guarda o CNAB inteiro numa coluna de texto.
32127
+ -- Mesma decis\xE3o da caixa de entrada da NF-e: arquivo mora em
32128
+ -- \`public.documents\`, e aqui fica o ponteiro. O que N\xC3O sai da linha \xE9
32129
+ -- \`sequence_number\`: ele \xE9 do banco, n\xE3o do arquivo \u2014 dois arquivos com o mesmo
32130
+ -- n\xFAmero s\xE3o uma remessa rejeitada.
32131
+ --
32132
+ -- Idempotente.
32133
+
32134
+ CREATE TABLE IF NOT EXISTS public.plg_financial_cnab_batches (
32135
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
32136
+ tenant_id uuid NOT NULL,
32137
+ unit_id uuid,
32138
+ bank_account_id uuid,
32139
+ layout text,
32140
+ -- O n\xFAmero que o banco conta. \xDAnico por conta: repetir \xE9 remessa rejeitada.
32141
+ sequence_number integer,
32142
+ total_amount numeric(14,2) DEFAULT 0 NOT NULL,
32143
+ items_count integer DEFAULT 0 NOT NULL,
32144
+ file_name text,
32145
+ -- O CNAB em si vive em public.documents; aqui fica o ponteiro.
32146
+ document_id uuid,
32147
+ generated_at timestamp with time zone DEFAULT now() NOT NULL,
32148
+ owner_id uuid,
32149
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
32150
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
32151
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
32152
+ );
32153
+
32154
+ COMMENT ON TABLE public.plg_financial_cnab_batches IS
32155
+ 'O arquivo de remessa enviado ao banco. V1: cnab_remessas. O arquivo mora em public.documents; o n\xFAmero de sequ\xEAncia mora aqui, porque \xE9 do banco.';
32156
+
32157
+ CREATE TABLE IF NOT EXISTS public.plg_financial_cnab_items (
32158
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
32159
+ tenant_id uuid NOT NULL,
32160
+ batch_id uuid NOT NULL,
32161
+ movement_id uuid,
32162
+ payment_form text,
32163
+ payment_date date,
32164
+ amount numeric(14,2) DEFAULT 0 NOT NULL,
32165
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
32166
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
32167
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
32168
+ );
32169
+
32170
+ COMMENT ON TABLE public.plg_financial_cnab_items IS
32171
+ 'O pagamento dentro da remessa, ligado ao movimento financeiro que ele quita. V1: cnab_remessa_items.';
32172
+
32173
+ CREATE TABLE IF NOT EXISTS public.plg_financial_commission_payables (
32174
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
32175
+ tenant_id uuid NOT NULL,
32176
+ unit_id uuid,
32177
+ -- O T\xEDtulo que a apura\xE7\xE3o gerou. \xC9 este elo que faltava no V2.
32178
+ invoice_id uuid,
32179
+ beneficiary_party_id uuid,
32180
+ chart_account_id uuid,
32181
+ due_date date,
32182
+ total_amount numeric(14,2) DEFAULT 0 NOT NULL,
32183
+ entries_count integer DEFAULT 0 NOT NULL,
32184
+ status text DEFAULT 'pending'::text NOT NULL,
32185
+ launched_at timestamp with time zone,
32186
+ canceled_at timestamp with time zone,
32187
+ -- Quem cancelou: a tela, o estorno do T\xEDtulo, ou a reapura\xE7\xE3o. O V1 guarda
32188
+ -- a palavra e ela explica por que a comiss\xE3o sumiu do relat\xF3rio.
32189
+ cancel_source text,
32190
+ owner_id uuid,
32191
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
32192
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
32193
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
32194
+ );
32195
+
32196
+ COMMENT ON TABLE public.plg_financial_commission_payables IS
32197
+ 'A comiss\xE3o apurada virando conta a pagar \u2014 o elo entre plg_financial_commission_entries e o T\xEDtulo. V1: commission_payables.';
32198
+
32199
+ CREATE TABLE IF NOT EXISTS public.plg_financial_commission_payable_items (
32200
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
32201
+ tenant_id uuid NOT NULL,
32202
+ payable_id uuid NOT NULL,
32203
+ -- A linha da venda que gerou a comiss\xE3o, e a entrada de apura\xE7\xE3o dela.
32204
+ source_item_id uuid,
32205
+ item_id uuid,
32206
+ aggregate_entry_id uuid,
32207
+ description text,
32208
+ amount numeric(14,2) DEFAULT 0 NOT NULL,
32209
+ -- \`entry_ids\` do V1 \xE9 um array de apura\xE7\xF5es agregadas numa linha s\xF3. Vem
32210
+ -- como jsonb de refer\xEAncias: s\xE3o ids do V1 que s\xF3 resolvem depois que
32211
+ -- commission_entries atravessar, e um array de uuid faria a linha mentir
32212
+ -- enquanto isso.
32213
+ entry_refs jsonb DEFAULT '[]'::jsonb NOT NULL,
32214
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
32215
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
32216
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
32217
+ );
32218
+
32219
+ CREATE TABLE IF NOT EXISTS public.plg_financial_commission_payee_prefs (
32220
+ id uuid DEFAULT gen_random_uuid() NOT NULL,
32221
+ tenant_id uuid NOT NULL,
32222
+ party_id uuid NOT NULL,
32223
+ chart_account_id uuid,
32224
+ metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
32225
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
32226
+ updated_at timestamp with time zone DEFAULT now() NOT NULL
32227
+ );
32228
+
32229
+ COMMENT ON TABLE public.plg_financial_commission_payee_prefs IS
32230
+ 'Para qual conta cont\xE1bil a comiss\xE3o daquela pessoa vai. Uma por pessoa. V1: commission_payee_prefs.';
32231
+
32232
+ -- \u2500\u2500 chaves \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32233
+ DO $$
32234
+ DECLARE t text;
32235
+ BEGIN
32236
+ FOREACH t IN ARRAY ARRAY['plg_financial_cnab_batches', 'plg_financial_cnab_items',
32237
+ 'plg_financial_commission_payables', 'plg_financial_commission_payable_items',
32238
+ 'plg_financial_commission_payee_prefs'] LOOP
32239
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = ('public.' || t)::regclass AND contype = 'p') THEN
32240
+ EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I PRIMARY KEY (id)', t, t || '_pkey');
32241
+ END IF;
32242
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_id_id_key') THEN
32243
+ EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I UNIQUE (tenant_id, id)', t, t || '_tenant_id_id_key');
32244
+ END IF;
32245
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = t || '_tenant_fkey') THEN
32246
+ EXECUTE format('ALTER TABLE ONLY public.%I ADD CONSTRAINT %I FOREIGN KEY (tenant_id) REFERENCES public.tenants(id) ON DELETE CASCADE', t, t || '_tenant_fkey');
32247
+ END IF;
32248
+ END LOOP;
32249
+ END $$;
32250
+
32251
+ DO $$ BEGIN
32252
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_unit_fkey') THEN
32253
+ ALTER TABLE ONLY public.plg_financial_cnab_batches ADD CONSTRAINT plg_financial_cnab_batches_unit_fkey
32254
+ FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
32255
+ END IF;
32256
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_account_fkey') THEN
32257
+ ALTER TABLE ONLY public.plg_financial_cnab_batches ADD CONSTRAINT plg_financial_cnab_batches_account_fkey
32258
+ FOREIGN KEY (bank_account_id) REFERENCES public.plg_financial_bank_accounts(id) ON DELETE SET NULL;
32259
+ END IF;
32260
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_document_fkey') THEN
32261
+ ALTER TABLE ONLY public.plg_financial_cnab_batches ADD CONSTRAINT plg_financial_cnab_batches_document_fkey
32262
+ FOREIGN KEY (document_id) REFERENCES public.documents(id) ON DELETE SET NULL;
32263
+ END IF;
32264
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_items_batch_fkey') THEN
32265
+ ALTER TABLE ONLY public.plg_financial_cnab_items ADD CONSTRAINT plg_financial_cnab_items_batch_fkey
32266
+ FOREIGN KEY (tenant_id, batch_id) REFERENCES public.plg_financial_cnab_batches(tenant_id, id) ON DELETE CASCADE;
32267
+ END IF;
32268
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_items_movement_fkey') THEN
32269
+ ALTER TABLE ONLY public.plg_financial_cnab_items ADD CONSTRAINT plg_financial_cnab_items_movement_fkey
32270
+ FOREIGN KEY (movement_id) REFERENCES public.plg_financial_movements(id) ON DELETE SET NULL;
32271
+ END IF;
32272
+
32273
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_unit_fkey') THEN
32274
+ ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_unit_fkey
32275
+ FOREIGN KEY (tenant_id, unit_id) REFERENCES app.units(tenant_id, id) ON DELETE SET NULL;
32276
+ END IF;
32277
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_invoice_fkey') THEN
32278
+ ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_invoice_fkey
32279
+ FOREIGN KEY (invoice_id) REFERENCES public.plg_financial_invoices(id) ON DELETE SET NULL;
32280
+ END IF;
32281
+ -- S\xF3 enquanto a coluna existe com ESTE nome. A 082 renomeia
32282
+ -- \`beneficiary_party_id\` para \`beneficiary_account_id\` (o benefici\xE1rio da
32283
+ -- comiss\xE3o \xE9 uma CONTA, n\xE3o uma pessoa), e num replay este arquivo roda de
32284
+ -- novo sobre a tabela j\xE1 corrigida: sem a guarda, ele estoura com "column
32285
+ -- beneficiary_party_id referenced in foreign key constraint does not exist" e
32286
+ -- derruba a corrente inteira.
32287
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_party_fkey')
32288
+ AND EXISTS (SELECT 1 FROM information_schema.columns
32289
+ WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payables'
32290
+ AND column_name = 'beneficiary_party_id') THEN
32291
+ ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_party_fkey
32292
+ FOREIGN KEY (beneficiary_party_id) REFERENCES public.people(id) ON DELETE SET NULL;
32293
+ END IF;
32294
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_chart_fkey') THEN
32295
+ ALTER TABLE ONLY public.plg_financial_commission_payables ADD CONSTRAINT plg_financial_commission_payables_chart_fkey
32296
+ FOREIGN KEY (chart_account_id) REFERENCES public.plg_financial_chart_of_accounts(id) ON DELETE SET NULL;
32297
+ END IF;
32298
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payable_items_payable_fkey') THEN
32299
+ ALTER TABLE ONLY public.plg_financial_commission_payable_items ADD CONSTRAINT plg_financial_commission_payable_items_payable_fkey
32300
+ FOREIGN KEY (tenant_id, payable_id) REFERENCES public.plg_financial_commission_payables(tenant_id, id) ON DELETE CASCADE;
32301
+ END IF;
32302
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payable_items_item_fkey') THEN
32303
+ ALTER TABLE ONLY public.plg_financial_commission_payable_items ADD CONSTRAINT plg_financial_commission_payable_items_item_fkey
32304
+ FOREIGN KEY (item_id) REFERENCES public.items(id) ON DELETE SET NULL;
32305
+ END IF;
32306
+ -- Mesma hist\xF3ria: a 082 renomeia \`party_id\` para \`account_id\` aqui.
32307
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_party_fkey')
32308
+ AND EXISTS (SELECT 1 FROM information_schema.columns
32309
+ WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payee_prefs'
32310
+ AND column_name = 'party_id') THEN
32311
+ ALTER TABLE ONLY public.plg_financial_commission_payee_prefs ADD CONSTRAINT plg_financial_commission_payee_prefs_party_fkey
32312
+ FOREIGN KEY (party_id) REFERENCES public.people(id) ON DELETE CASCADE;
32313
+ END IF;
32314
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_chart_fkey') THEN
32315
+ ALTER TABLE ONLY public.plg_financial_commission_payee_prefs ADD CONSTRAINT plg_financial_commission_payee_prefs_chart_fkey
32316
+ FOREIGN KEY (chart_account_id) REFERENCES public.plg_financial_chart_of_accounts(id) ON DELETE SET NULL;
32317
+ END IF;
32318
+ END $$;
32319
+
32320
+ -- Dois arquivos de remessa com o mesmo n\xFAmero na mesma conta \xE9 remessa
32321
+ -- rejeitada pelo banco. O \xEDndice \xE9 a trava que o V1 n\xE3o tinha.
32322
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_cnab_batches_sequence_uidx
32323
+ ON public.plg_financial_cnab_batches USING btree (tenant_id, bank_account_id, sequence_number)
32324
+ WHERE bank_account_id IS NOT NULL AND sequence_number IS NOT NULL;
32325
+ CREATE INDEX IF NOT EXISTS plg_financial_cnab_items_batch_idx
32326
+ ON public.plg_financial_cnab_items USING btree (tenant_id, batch_id);
32327
+ DO $$ BEGIN
32328
+ IF EXISTS (SELECT 1 FROM information_schema.columns
32329
+ WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payables'
32330
+ AND column_name = 'beneficiary_party_id') THEN
32331
+ CREATE INDEX IF NOT EXISTS plg_financial_commission_payables_party_idx
32332
+ ON public.plg_financial_commission_payables USING btree (tenant_id, beneficiary_party_id, due_date DESC);
32333
+ END IF;
32334
+ END $$;
32335
+ CREATE INDEX IF NOT EXISTS plg_financial_commission_payables_open_idx
32336
+ ON public.plg_financial_commission_payables USING btree (tenant_id, unit_id, due_date)
32337
+ WHERE canceled_at IS NULL;
32338
+ CREATE INDEX IF NOT EXISTS plg_financial_commission_payable_items_payable_idx
32339
+ ON public.plg_financial_commission_payable_items USING btree (tenant_id, payable_id);
32340
+ -- Uma prefer\xEAncia por pessoa: duas seriam duas respostas para "onde lan\xE7ar".
32341
+ -- Guardado pelo mesmo motivo da FK acima: a 082 renomeia esta coluna.
32342
+ DO $$ BEGIN
32343
+ IF EXISTS (SELECT 1 FROM information_schema.columns
32344
+ WHERE table_schema = 'public' AND table_name = 'plg_financial_commission_payee_prefs'
32345
+ AND column_name = 'party_id') THEN
32346
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_commission_payee_prefs_party_uidx
32347
+ ON public.plg_financial_commission_payee_prefs USING btree (tenant_id, party_id);
32348
+ END IF;
32349
+ END $$;
32350
+
32351
+ -- \u2500\u2500 gatilho de updated_at \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
32352
+ DO $$
32353
+ DECLARE t text;
32354
+ BEGIN
32355
+ FOREACH t IN ARRAY ARRAY['plg_financial_cnab_batches', 'plg_financial_cnab_items',
32356
+ 'plg_financial_commission_payables', 'plg_financial_commission_payable_items',
32357
+ 'plg_financial_commission_payee_prefs'] LOOP
32358
+ EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', t || '_updated_at', t);
32359
+ EXECUTE format('CREATE TRIGGER %I BEFORE UPDATE ON public.%I FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at()', t || '_updated_at', t);
32360
+ END LOOP;
32361
+ END $$;
32362
+
32363
+ -- \u2500\u2500 quem enxerga: o scaffold, e n\xE3o policy escrita \xE0 m\xE3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32364
+ --
32365
+ -- \`app.scaffold_table\` \xE9 a porta \xFAnica de autoriza\xE7\xE3o do financeiro: ele
32366
+ -- registra a tabela em \`app.scaffold_registry\`, gera as pol\xEDticas com o nome
32367
+ -- que o gate cobra (\`*_authz_*\`) e liga a permiss\xE3o do plugin junto com as
32368
+ -- escotilhas de compartilhamento. Escrever a policy \xE0 m\xE3o criaria a SEGUNDA
32369
+ -- regra de autoriza\xE7\xE3o do financeiro \u2014 e foi exatamente o que este arquivo fez
32370
+ -- na primeira vers\xE3o: 41 asser\xE7\xF5es de \`004_negative_authz\` viraram 40, porque
32371
+ -- as cinco tabelas tinham pol\xEDtica e n\xE3o tinham declara\xE7\xE3o.
32372
+ --
32373
+ -- \`unit_optional\` nas que t\xEAm \`unit_id\` anul\xE1vel (a remessa pode ser da rede, a
32374
+ -- comiss\xE3o pode n\xE3o ter filial); \`tenant\` nas tr\xEAs que n\xE3o t\xEAm unidade nenhuma
32375
+ -- \u2014 declarar \`unit\` numa coluna que n\xE3o existe esconde a tabela inteira.
32376
+ SELECT app.scaffold_table('public.plg_financial_cnab_batches'::regclass,
32377
+ 'financial.cnab_batch', 'financial', false, 'enforce');
32378
+ SELECT app.scaffold_scope('public.plg_financial_cnab_batches'::regclass, 'unit_optional', true);
32379
+
32380
+ SELECT app.scaffold_table('public.plg_financial_cnab_items'::regclass,
32381
+ 'financial.cnab_item', 'financial', false, 'enforce');
32382
+ SELECT app.scaffold_scope('public.plg_financial_cnab_items'::regclass, 'tenant', true);
32383
+
32384
+ SELECT app.scaffold_table('public.plg_financial_commission_payables'::regclass,
32385
+ 'financial.commission_payable', 'financial', false, 'enforce');
32386
+ SELECT app.scaffold_scope('public.plg_financial_commission_payables'::regclass, 'unit_optional', true);
32387
+
32388
+ SELECT app.scaffold_table('public.plg_financial_commission_payable_items'::regclass,
32389
+ 'financial.commission_payable_item', 'financial', false, 'enforce');
32390
+ SELECT app.scaffold_scope('public.plg_financial_commission_payable_items'::regclass, 'tenant', true);
32391
+
32392
+ SELECT app.scaffold_table('public.plg_financial_commission_payee_prefs'::regclass,
32393
+ 'financial.commission_payee_pref', 'financial', false, 'enforce');
32394
+ SELECT app.scaffold_scope('public.plg_financial_commission_payee_prefs'::regclass, 'tenant', true);
32395
+ `;
32396
+ var MIGRATION_081_A_REMESSA_E_A_COMISSAO_ATRAVESSAM = `-- A remessa e a comiss\xE3o atravessam.
32397
+ --
32398
+ -- A 080 fez as cinco tabelas. Esta escreve o de-para, com o writer gen\xE9rico
32399
+ -- \`by_shape\` (spine 123) \u2014 as colunas do destino t\xEAm o nome que o de-para
32400
+ -- produz, ent\xE3o o que falta \xE9 a tradu\xE7\xE3o, n\xE3o uma fun\xE7\xE3o por tabela.
32401
+ --
32402
+ -- \u2500\u2500 O arquivo de remessa n\xE3o vem por aqui \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
32403
+ --
32404
+ -- \`cnab_remessas.file_content\` guarda o CNAB inteiro em texto. Sai com \`-\`:
32405
+ -- arquivo atravessa pelo est\xE1gio de assets e vira \`public.documents\`. O que
32406
+ -- FICA na linha \xE9 \`sequence_number\`, porque ele \xE9 do banco \u2014 dois arquivos com
32407
+ -- o mesmo n\xFAmero s\xE3o uma remessa rejeitada, e isso \xE9 dado, n\xE3o anexo.
32408
+ --
32409
+ -- \u2500\u2500 As apura\xE7\xF5es agregadas ficam como refer\xEAncia \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
32410
+ --
32411
+ -- \`commission_payable_items.entry_ids\` \xE9 um array de ids de apura\xE7\xE3o agregados
32412
+ -- numa linha s\xF3. Vem como \`entry_refs\` jsonb com os ids do V1 dentro: eles s\xF3
32413
+ -- resolvem depois que \`commission_entries\` atravessar, e um array de uuid faria
32414
+ -- a linha mentir enquanto isso.
32415
+ --
32416
+ -- Idempotente.
32417
+
32418
+ -- \u2500\u2500 \`by_shape\` volta ao vocabul\xE1rio \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
32419
+ --
32420
+ -- O spine 123 acrescenta \`by_shape\` ao \`allowlist_writer_check\`. Ele N\xC3O
32421
+ -- sobrevive at\xE9 aqui: \`financial/061\` reescreve a restri\xE7\xE3o com uma lista
32422
+ -- LITERAL, e a lista dele foi escrita antes de o spine 123 existir. Toda
32423
+ -- palavra que o spine acrescenta morre na passagem por 061 \u2014 foi assim que a
32424
+ -- corrente do zero parou nesta linha com
32425
+ -- \`new row for relation "allowlist" violates check constraint\`.
32426
+ --
32427
+ -- A ironia est\xE1 no cabe\xE7alho do pr\xF3prio 061: "A LISTA \xC9 A UNI\xC3O, E N\xC3O UMA
32428
+ -- LISTA LITERAL". Ele descreve a regra e faz o contr\xE1rio.
32429
+ --
32430
+ -- O acr\xE9scimo \xE9 refeito aqui porque este \xE9 o PRIMEIRO consumidor de \`by_shape\`
32431
+ -- depois de 061 \u2014 fiscal_br/003, inventory/007 e tables/007 v\xEAm adiante e
32432
+ -- herdam o conserto. A corre\xE7\xE3o de verdade \xE9 converter 061 para o splice, e
32433
+ -- isso \xE9 do dono daquele arquivo.
32434
+ DO $$
32435
+ DECLARE v_def text;
32436
+ BEGIN
32437
+ SELECT pg_get_constraintdef(oid) INTO v_def
32438
+ FROM pg_constraint WHERE conname = 'allowlist_writer_check';
32439
+ IF v_def IS NULL OR strpos(v_def, 'by_shape') > 0 THEN RETURN; END IF;
32440
+ ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
32441
+ EXECUTE replace(
32442
+ 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,
32443
+ '''person''::text', '''person''::text, ''by_shape''::text');
32444
+ END $$;
32445
+
32446
+ SET LOCAL migration.allowlist_unlock = 'on';
32447
+
32448
+ SELECT migration._allow(
32449
+ 'cnab_remessas', 'public.plg_financial_cnab_batches', 'financial', 'by_shape',
32450
+ jsonb_build_object('target', 'public.plg_financial_cnab_batches'),
32451
+ NULL::text[],
32452
+ jsonb_build_object(
32453
+ 'uuid', 'id',
32454
+ 'unit_id', 'unit_id@companies',
32455
+ 'bank_account_id', 'bank_account_id@bank_accounts',
32456
+ 'layout', 'layout',
32457
+ 'sequence_number', 'sequence_number',
32458
+ 'total_amount', 'total_amount',
32459
+ 'items_count', 'items_count',
32460
+ 'file_name', 'file_name',
32461
+ 'created_at', 'generated_at',
32462
+ 'updated_at', 'updated_at',
32463
+ 'tenant_id', '-',
32464
+ 'generated_by', '-',
32465
+ 'file_content', '-',
32466
+ 'id', 'jsonb:financial.v1_ref'),
32467
+ 'O arquivo de remessa enviado ao banco. O CNAB em si vai pelo est\xE1gio de '
32468
+ 'assets; o n\xFAmero de sequ\xEAncia fica na linha porque \xE9 do banco.'
32469
+ );
32470
+
32471
+ SELECT migration._allow(
32472
+ 'cnab_remessa_items', 'public.plg_financial_cnab_items', 'financial', 'by_shape',
32473
+ jsonb_build_object('target', 'public.plg_financial_cnab_items'),
32474
+ NULL::text[],
32475
+ jsonb_build_object(
32476
+ 'remessa_id', 'batch_id@cnab_remessas',
32477
+ 'financial_movement_id', 'movement_id@financial_movements',
32478
+ 'payment_form', 'payment_form',
32479
+ 'payment_date', 'payment_date',
32480
+ 'amount', 'amount',
32481
+ 'created_at', 'created_at',
32482
+ 'tenant_id', '-',
32483
+ 'id', 'jsonb:financial.v1_ref'),
32484
+ 'O pagamento dentro da remessa. Sem \`uuid\` na origem \u2014 o id do destino nasce '
32485
+ 'determin\xEDstico da chave num\xE9rica.'
32486
+ );
32487
+
32488
+ SELECT migration._allow(
32489
+ 'commission_payables', 'public.plg_financial_commission_payables', 'financial', 'by_shape',
32490
+ jsonb_build_object('target', 'public.plg_financial_commission_payables'),
32491
+ NULL::text[],
32492
+ jsonb_build_object(
32493
+ 'uuid', 'id',
32494
+ 'unit_id', 'unit_id@companies',
32495
+ 'invoice_id', 'invoice_id@invoices',
32496
+ 'beneficiary_account_central_id', 'beneficiary_party_id@account_central',
32497
+ 'chart_account_id', 'chart_account_id@chart_of_accounts',
32498
+ 'due_date', 'due_date',
32499
+ 'total_amount', 'total_amount',
32500
+ 'entries_count', 'entries_count',
32501
+ 'status', 'status',
32502
+ 'launched_at', 'launched_at',
32503
+ 'canceled_at', 'canceled_at',
32504
+ 'cancel_source', 'cancel_source',
32505
+ 'created_at', 'created_at',
32506
+ 'updated_at', 'updated_at',
32507
+ 'tenant_id', '-',
32508
+ 'invoice_uuid', '-',
32509
+ 'launched_by', '-',
32510
+ 'id', 'jsonb:financial.v1_ref'),
32511
+ 'A comiss\xE3o apurada virando conta a pagar \u2014 o elo entre a apura\xE7\xE3o e o '
32512
+ 'T\xEDtulo, que era o que faltava no V2. \`cancel_source\` vem junto porque \xE9 ele '
32513
+ 'que explica por que a comiss\xE3o sumiu do relat\xF3rio.'
32514
+ );
32515
+
32516
+ SELECT migration._allow(
32517
+ 'commission_payable_items', 'public.plg_financial_commission_payable_items', 'financial', 'by_shape',
32518
+ jsonb_build_object('target', 'public.plg_financial_commission_payable_items'),
32519
+ NULL::text[],
32520
+ jsonb_build_object(
32521
+ 'uuid', 'id',
32522
+ 'payable_id', 'payable_id@commission_payables',
32523
+ 'source_invoice_item_id', 'source_item_id@invoice_items',
32524
+ 'invoice_item_id', 'item_id@invoice_items',
32525
+ 'description', 'description',
32526
+ 'amount', 'amount',
32527
+ 'entry_ids', 'entry_refs',
32528
+ 'aggregate_entry_id', 'jsonb:financial.v1_aggregate_entry_ref',
32529
+ 'created_at', 'created_at',
32530
+ 'updated_at', 'updated_at',
32531
+ 'tenant_id', '-',
32532
+ 'id', 'jsonb:financial.v1_ref'),
32533
+ 'A linha da apura\xE7\xE3o dentro do pag\xE1vel. \`entry_ids\` vira \`entry_refs\` jsonb '
32534
+ 'com os ids do V1: eles s\xF3 resolvem quando commission_entries atravessar.'
32535
+ );
32536
+
32537
+ SELECT migration._allow(
32538
+ 'commission_payee_prefs', 'public.plg_financial_commission_payee_prefs', 'financial', 'by_shape',
32539
+ jsonb_build_object('target', 'public.plg_financial_commission_payee_prefs'),
32540
+ NULL::text[],
32541
+ jsonb_build_object(
32542
+ 'account_central_id', 'party_id@account_central',
32543
+ 'chart_account_id', 'chart_account_id@chart_of_accounts',
32544
+ 'updated_at', 'updated_at',
32545
+ 'tenant_id', '-',
32546
+ 'id', 'jsonb:financial.v1_ref'),
32547
+ 'Para qual conta cont\xE1bil a comiss\xE3o daquela pessoa vai. Uma por pessoa \u2014 '
32548
+ 'duas seriam duas respostas para "onde lan\xE7ar".'
32549
+ );
32550
+
32551
+ DELETE FROM migration.excluded_tables
32552
+ WHERE source_table IN ('cnab_remessas', 'cnab_remessa_items', 'commission_payables',
32553
+ 'commission_payable_items', 'commission_payee_prefs');
32554
+
32555
+ RESET migration.allowlist_unlock;
32556
+ `;
32557
+ var MIGRATION_082_O_BENEFICIARIO_DA_COMISSAO_E_UMA_CONTA = `-- O benefici\xE1rio da comiss\xE3o \xE9 uma CONTA, n\xE3o uma pessoa.
32558
+ --
32559
+ -- A 080 declarou \`beneficiary_party_id uuid \u2192 public.people\` e a 081 mapeou
32560
+ -- \`commission_payables.beneficiary_account_central_id\` para ela. As duas
32561
+ -- estavam erradas, e a FK cobrou na primeira onda:
32562
+ --
32563
+ -- 23503: insert or update on table "plg_financial_commission_payables"
32564
+ -- violates foreign key constraint "plg_financial_commission_payables_party_fkey"
32565
+ --
32566
+ -- \`account_central\` do V1 n\xE3o \xE9 pessoa: \xE9 o raz\xE3o de QUEM RECEBE, e ele
32567
+ -- atravessa para \`public.plg_financial_accounts\` (writer \`fin_account\`). O id
32568
+ -- que o ledger devolveu \xE9 de uma conta, e a coluna exigia uma pessoa.
32569
+ --
32570
+ -- N\xE3o \xE9 um detalhe de nome. Uma comiss\xE3o pode ser paga a uma conta que n\xE3o \xE9 de
32571
+ -- ningu\xE9m \u2014 um centro, um fundo, a pr\xF3pria casa \u2014 e for\xE7ar uma pessoa ali faria
32572
+ -- essas linhas n\xE3o existirem. A mesma corre\xE7\xE3o vale para
32573
+ -- \`commission_payee_prefs\`, que responde "para qual conta cont\xE1bil a comiss\xE3o
32574
+ -- daquele benefici\xE1rio vai".
32575
+ --
32576
+ -- S\xE3o 5 linhas no Espa\xE7o Facial. O volume \xE9 irrelevante; o modelo, n\xE3o.
32577
+ --
32578
+ -- Idempotente.
32579
+
32580
+ DO $$ BEGIN
32581
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_party_fkey') THEN
32582
+ ALTER TABLE public.plg_financial_commission_payables
32583
+ DROP CONSTRAINT plg_financial_commission_payables_party_fkey;
32584
+ END IF;
32585
+ IF EXISTS (SELECT 1 FROM pg_attribute a
32586
+ WHERE a.attrelid = 'public.plg_financial_commission_payables'::regclass
32587
+ AND a.attname = 'beneficiary_party_id' AND NOT a.attisdropped) THEN
32588
+ ALTER TABLE public.plg_financial_commission_payables
32589
+ RENAME COLUMN beneficiary_party_id TO beneficiary_account_id;
32590
+ END IF;
32591
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payables_account_fkey') THEN
32592
+ ALTER TABLE public.plg_financial_commission_payables
32593
+ ADD CONSTRAINT plg_financial_commission_payables_account_fkey
32594
+ FOREIGN KEY (beneficiary_account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE SET NULL;
32595
+ END IF;
32596
+
32597
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_party_fkey') THEN
32598
+ ALTER TABLE public.plg_financial_commission_payee_prefs
32599
+ DROP CONSTRAINT plg_financial_commission_payee_prefs_party_fkey;
32600
+ END IF;
32601
+ IF EXISTS (SELECT 1 FROM pg_attribute a
32602
+ WHERE a.attrelid = 'public.plg_financial_commission_payee_prefs'::regclass
32603
+ AND a.attname = 'party_id' AND NOT a.attisdropped) THEN
32604
+ ALTER TABLE public.plg_financial_commission_payee_prefs
32605
+ RENAME COLUMN party_id TO account_id;
32606
+ END IF;
32607
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_commission_payee_prefs_account_fkey') THEN
32608
+ ALTER TABLE public.plg_financial_commission_payee_prefs
32609
+ ADD CONSTRAINT plg_financial_commission_payee_prefs_account_fkey
32610
+ FOREIGN KEY (account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE CASCADE;
32611
+ END IF;
32612
+ END $$;
32613
+
32614
+ DROP INDEX IF EXISTS public.plg_financial_commission_payables_party_idx;
32615
+ DROP INDEX IF EXISTS public.plg_financial_commission_payee_prefs_party_uidx;
32616
+ CREATE INDEX IF NOT EXISTS plg_financial_commission_payables_account_idx
32617
+ ON public.plg_financial_commission_payables USING btree (tenant_id, beneficiary_account_id, due_date DESC);
32618
+ CREATE UNIQUE INDEX IF NOT EXISTS plg_financial_commission_payee_prefs_account_uidx
32619
+ ON public.plg_financial_commission_payee_prefs USING btree (tenant_id, account_id);
32620
+
32621
+ COMMENT ON COLUMN public.plg_financial_commission_payables.beneficiary_account_id IS
32622
+ 'V1: commission_payables.beneficiary_account_central_id \u2014 o raz\xE3o de quem recebe, que pode n\xE3o ser uma pessoa.';
32623
+
32624
+ SET LOCAL migration.allowlist_unlock = 'on';
32625
+
32626
+ UPDATE migration.allowlist
32627
+ SET columns = columns
32628
+ - 'beneficiary_account_central_id'
32629
+ || jsonb_build_object('beneficiary_account_central_id', 'beneficiary_account_id@account_central')
32630
+ WHERE source_table = 'commission_payables';
32631
+
32632
+ UPDATE migration.allowlist
32633
+ SET columns = columns
32634
+ - 'account_central_id'
32635
+ || jsonb_build_object('account_central_id', 'account_id@account_central')
32636
+ WHERE source_table = 'commission_payee_prefs';
32637
+
32638
+ RESET migration.allowlist_unlock;
32639
+ `;
32640
+ var MIGRATION_083_A_CONTA_DO_BANCO_E_UMA_CONTA_E_A_FATURA_TEM_DOIS_DESTINOS = `-- A conta do banco \xE9 uma conta, e a fatura tem dois destinos.
32641
+ --
32642
+ -- Duas corre\xE7\xF5es que a primeira onda cobrou, das mesmas 59 + 1 linhas.
32643
+ -- (A terceira, do \xEDndice de grupo fiscal, mudou de arquivo: ela toca uma
32644
+ -- tabela do fiscal_br, e o fiscal_br roda DEPOIS do financeiro na corrente \u2014
32645
+ -- um arquivo do financeiro que mexe na tabela de outro plugin quebra do zero.
32646
+ -- Ver fiscal_br/004.)
32647
+ --
32648
+ -- \u2500\u2500 1. \`bank_accounts\` do V1 n\xE3o vira \`plg_financial_bank_accounts\` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32649
+ --
32650
+ -- A 080 declarou \`plg_financial_cnab_batches.bank_account_id\` apontando para
32651
+ -- \`public.plg_financial_bank_accounts\`. Mas o de-para manda \`bank_accounts\` do
32652
+ -- V1 para \`public.plg_financial_accounts\` \u2014 o mesmo destino de
32653
+ -- \`account_central\`, porque no V2 a conta banc\xE1ria \xE9 uma CONTA do raz\xE3o, n\xE3o
32654
+ -- uma tabela paralela. As 59 remessas do Pertinho do C\xE9u bateram na FK.
32655
+ --
32656
+ -- \xC9 o mesmo erro da 082, no arquivo ao lado: supor o destino pelo nome da
32657
+ -- tabela de origem em vez de ler a allowlist.
32658
+ --
32659
+ -- \u2500\u2500 2. \`invoices\` resolve para DOIS destinos \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
32660
+ --
32661
+ -- \`invoices\` do V1 tem duas linhas de de-para: \`public.orders\` (a Venda) e
32662
+ -- \`public.plg_financial_invoices\` (o T\xEDtulo). \`migration.resolve_ref\` sem alvo
32663
+ -- declarado l\xEA \`SELECT target_table ... WHERE source_table = 'invoices'\` e pega
32664
+ -- UMA das duas, sem ordem \u2014 ent\xE3o \`invoice_id@invoices\` devolvia \xE0s vezes o id
32665
+ -- de uma Venda para uma coluna que exige um T\xEDtulo.
32666
+ --
32667
+ -- O de-para j\xE1 tem a sintaxe para isso e a 081 n\xE3o a usou:
32668
+ -- \`coluna@tabela_de_origem:tabela_de_destino\`. Uma refer\xEAncia a uma origem de
32669
+ -- dois destinos SEM o alvo \xE9 amb\xEDgua por constru\xE7\xE3o, e acerta por acidente
32670
+ -- enquanto houver sorte.
32671
+ --
32672
+ -- Idempotente.
32673
+
32674
+ DO $$ BEGIN
32675
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_account_fkey') THEN
32676
+ ALTER TABLE public.plg_financial_cnab_batches DROP CONSTRAINT plg_financial_cnab_batches_account_fkey;
32677
+ END IF;
32678
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'plg_financial_cnab_batches_ledger_account_fkey') THEN
32679
+ ALTER TABLE public.plg_financial_cnab_batches
32680
+ ADD CONSTRAINT plg_financial_cnab_batches_ledger_account_fkey
32681
+ FOREIGN KEY (bank_account_id) REFERENCES public.plg_financial_accounts(id) ON DELETE SET NULL;
32682
+ END IF;
32683
+ END $$;
32684
+
32685
+ COMMENT ON COLUMN public.plg_financial_cnab_batches.bank_account_id IS
32686
+ 'A conta do raz\xE3o de onde a remessa sai. V1: cnab_remessas.bank_account_id \u2192 bank_accounts, que atravessa para plg_financial_accounts.';
32687
+
32688
+ SET LOCAL migration.allowlist_unlock = 'on';
32689
+
32690
+ -- \`:public.plg_financial_invoices\` \xE9 o alvo declarado. Sem ele a resolu\xE7\xE3o
32691
+ -- escolhe entre a Venda e o T\xEDtulo, e escolher n\xE3o \xE9 resolver.
32692
+ UPDATE migration.allowlist
32693
+ SET columns = columns - 'invoice_id'
32694
+ || jsonb_build_object('invoice_id', 'invoice_id@invoices:public.plg_financial_invoices')
32695
+ WHERE source_table = 'commission_payables';
32696
+
32697
+ RESET migration.allowlist_unlock;
32698
+ `;
32699
+ var MIGRATION_084_A_NOTA_DE_ENTRADA_TEM_UM_TITULO_A_PAGAR = `-- A nota de entrada tem um t\xEDtulo a pagar.
32700
+ --
32701
+ -- \`danfe_payments\` \xE9 o plano de pagamento que o operador montou ao importar a
32702
+ -- DANFE: 895 linhas (657 no Pertinho do C\xE9u, 238 no Espa\xE7o Facial),
32703
+ -- R$ 1.293.490,86, e nenhuma delas atravessava. \xC9 a \xFAltima das quatro tabelas
32704
+ -- da entrada de notas; as outras tr\xEAs v\xE3o para o invent\xE1rio
32705
+ -- (plugin-inventory/010). Esta mora aqui porque o destino dela \xE9 uma tabela do
32706
+ -- FINANCEIRO, e o financeiro roda ANTES do invent\xE1rio na corrente
32707
+ -- (packages/db/chain.json) \u2014 um arquivo do invent\xE1rio que criasse a parcela
32708
+ -- quebraria a aplica\xE7\xE3o do zero.
32709
+ --
32710
+ -- \u2500\u2500 O t\xEDtulo n\xE3o existe no V1, e a parcela n\xE3o vive sem ele \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32711
+ --
32712
+ -- \`plg_financial_invoice_installments.invoice_id\` \xE9 NOT NULL, e no V1 n\xE3o h\xE1
32713
+ -- nada para resolver: \`danfe_payments\` aponta para \`danfe_imports\`, e nenhuma
32714
+ -- linha de \`invoices\` referencia uma nota de entrada \u2014 as tr\xEAs FKs que chegam
32715
+ -- em \`danfe_imports\` s\xE3o \`danfe_import_items\`, \`danfe_payments\` e \`dfe_inbox\`.
32716
+ -- No V1 a conta a pagar do fornecedor simplesmente n\xE3o \xE9 um t\xEDtulo: \xE9 uma lista
32717
+ -- de vencimentos pendurada na tela de importa\xE7\xE3o.
32718
+ --
32719
+ -- Ent\xE3o o t\xEDtulo nasce aqui, um por nota, determin\xEDstico:
32720
+ --
32721
+ -- id md5('danfe-invoice:' || uuid da nota)::uuid
32722
+ -- direction payable
32723
+ -- idempotency_key danfe:<chave de acesso>
32724
+ --
32725
+ -- \xC9 o mesmo padr\xE3o de id derivado que o porte j\xE1 usa para o anexo (spine 054).
32726
+ -- Determin\xEDstico importa duas vezes: a segunda parcela da mesma nota encontra o
32727
+ -- t\xEDtulo que a primeira criou, e a segunda ONDA encontra o mesmo id.
32728
+ --
32729
+ -- \u2500\u2500 O total do t\xEDtulo \xE9 a soma das PARCELAS, n\xE3o o total da nota \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32730
+ --
32731
+ -- Medido em 21/09/2026: em 38 das 607 notas (6,3%) a soma das parcelas n\xE3o bate
32732
+ -- com \`danfe_imports.total_value\` \u2014 R$ 926.668,06 de notas contra R$ 850.454,65
32733
+ -- de parcelas no tenant 8. Um t\xEDtulo cujo total n\xE3o \xE9 a soma das parcelas nasce
32734
+ -- com saldo em aberto errado em toda tela que faz "total \u2212 pago", e o total da
32735
+ -- nota continua leg\xEDvel em \`metadata.danfe.total_value\` e na caixa de entrada
32736
+ -- fiscal. A soma vem do extrator (\`note_payments_total\`), j\xE1 pronta na primeira
32737
+ -- parcela, porque \`plg_financial_invoices\` \xE9 IMUT\xC1VEL depois de emitido: o
32738
+ -- gatilho recusa alterar \`total\`, e acumular parcela a parcela n\xE3o \xE9 op\xE7\xE3o.
32739
+ --
32740
+ -- \u2500\u2500 \`number\` do t\xEDtulo fica nulo de prop\xF3sito \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
32741
+ --
32742
+ -- \`plg_financial_invoices_number_uq\` \xE9 UNIQUE (tenant_id, number). O n\xFAmero da
32743
+ -- NF-e do fornecedor n\xE3o \xE9 \xFAnico dentro do tenant \u2014 dois fornecedores emitem a
32744
+ -- nota 1234 no mesmo ano, e ele ainda colidiria com o n\xFAmero das vendas. O
32745
+ -- n\xFAmero vai para \`description\` e para \`metadata\`, e a identidade \xFAnica \xE9 a
32746
+ -- chave de acesso, em \`idempotency_key\`.
32747
+ --
32748
+ -- \u2500\u2500 \`number\` da PARCELA \xE9 a ordem dentro da nota \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
32749
+ --
32750
+ -- \`(invoice_id, number)\` \xE9 UNIQUE, e 251 das 895 parcelas n\xE3o t\xEAm
32751
+ -- \`installment_number\` no V1 (o pagamento \xE0 vista \u2014 pix, dinheiro \u2014 n\xE3o numera).
32752
+ -- Usar \`coalesce(installment_number, 1)\` colidiria na nota que tem pix E
32753
+ -- boleto 1/2. O extrator traz a ordem pelo id da pr\xF3pria origem
32754
+ -- (\`payment_rank\`), que \xE9 \xFAnica por constru\xE7\xE3o, e o \`installment_number\` do V1
32755
+ -- fica em metadata.
32756
+ --
32757
+ -- E 58 das 895 n\xE3o t\xEAm vencimento: o pagamento \xE0 vista vence no dia da nota, e
32758
+ -- \xE9 a data de emiss\xE3o que entra \u2014 \`due_date\` \xE9 NOT NULL no destino.
32759
+ --
32760
+ -- Idempotente.
32761
+
32762
+ -- \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\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
32763
+ -- 1. O writer
32764
+ -- \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\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
32765
+
32766
+ CREATE OR REPLACE FUNCTION migration.upsert_fin_danfe_installment(
32767
+ p_tenant uuid, p_id uuid, p_row jsonb, p_args jsonb DEFAULT '{}'::jsonb
32768
+ ) RETURNS jsonb
32769
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path TO ''
32770
+ AS $function$
32771
+ DECLARE
32772
+ v_meta jsonb := coalesce(p_row -> 'metadata', '{}'::jsonb);
32773
+ v_danfe jsonb := coalesce(v_meta -> 'danfe', '{}'::jsonb);
32774
+ v_note uuid;
32775
+ v_unit uuid := (p_row ->> 'unit_id')::uuid;
32776
+ v_invoice uuid;
32777
+ v_issue date;
32778
+ v_due date;
32779
+ v_amount numeric := round(coalesce((p_row ->> 'amount')::numeric, 0), 2);
32780
+ v_number integer := coalesce((p_row ->> 'number')::integer, 1);
32781
+ v_total numeric;
32782
+ v_desc text;
32783
+ v_existing uuid;
32784
+ v_created boolean := false;
32785
+ v_standalone boolean := migration._standalone();
32786
+ BEGIN
32787
+ IF v_standalone THEN PERFORM migration._assert_fence(p_tenant); PERFORM set_config('migration.writer', 'on', true); END IF;
32788
+
32789
+ IF p_id IS NULL THEN
32790
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
32791
+ 'reason', 'uma parcela precisa de id');
32792
+ END IF;
32793
+
32794
+ BEGIN v_note := (v_danfe ->> 'import_uuid')::uuid; EXCEPTION WHEN OTHERS THEN v_note := NULL; END;
32795
+ IF v_note IS NULL THEN
32796
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'state',
32797
+ 'reason', 'parcela de nota de entrada sem a nota \u2014 o t\xEDtulo dela n\xE3o tem de que nascer');
32798
+ END IF;
32799
+
32800
+ -- A emiss\xE3o da nota \xE9 timestamptz na origem e date no destino. Data que n\xE3o
32801
+ -- converte vira nulo aqui e o vencimento cai para hoje, em vez de derrubar a
32802
+ -- parcela inteira: o valor e a nota s\xE3o o dado, a data \xE9 o enfeite.
32803
+ BEGIN v_issue := (v_danfe ->> 'issue_date')::timestamptz::date; EXCEPTION WHEN OTHERS THEN v_issue := NULL; END;
32804
+ BEGIN v_due := (p_row ->> 'due_date')::date; EXCEPTION WHEN OTHERS THEN v_due := NULL; END;
32805
+ v_due := coalesce(v_due, v_issue, current_date);
32806
+
32807
+ IF v_amount < 0 THEN
32808
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'value',
32809
+ 'reason', format('parcela de valor negativo (%s) \u2014 o destino exige amount >= 0', v_amount));
32810
+ END IF;
32811
+
32812
+ -- O T\xCDTULO. \`total\` \xE9 a soma das parcelas da nota (vem do extrator); sem ela,
32813
+ -- o valor desta parcela, que \xE9 o m\xEDnimo verdadeiro.
32814
+ v_total := round(coalesce((v_danfe ->> 'payments_total')::numeric, v_amount), 2);
32815
+ v_desc := btrim('NF-e ' || coalesce(v_danfe ->> 'nfe_number', 's/n')
32816
+ || coalesce('/' || (v_danfe ->> 'nfe_series'), '')
32817
+ || coalesce(' \u2014 ' || (v_danfe ->> 'supplier_name'), ''));
32818
+ v_invoice := md5('danfe-invoice:' || v_note::text)::uuid;
32819
+
32820
+ INSERT INTO public.plg_financial_invoices
32821
+ (id, tenant_id, unit_id, direction, number, competence_date, issue_date, total,
32822
+ description, idempotency_key, metadata)
32823
+ VALUES (v_invoice, p_tenant, v_unit, 'payable', NULL,
32824
+ coalesce(v_issue, v_due), coalesce(v_issue, v_due), v_total,
32825
+ nullif(v_desc, ''),
32826
+ 'danfe:' || coalesce(v_danfe ->> 'access_key', v_note::text),
32827
+ jsonb_build_object('danfe', v_danfe - 'payment'))
32828
+ ON CONFLICT (id) DO NOTHING;
32829
+
32830
+ SELECT tenant_id INTO v_existing FROM public.plg_financial_invoice_installments WHERE id = p_id;
32831
+ IF v_existing IS NOT NULL AND v_existing <> p_tenant THEN
32832
+ RETURN jsonb_build_object('status', 'quarantined', 'kind', 'tenant',
32833
+ 'reason', format('a parcela %s j\xE1 existe em outro tenant', p_id));
32834
+ END IF;
32835
+ v_created := v_existing IS NULL;
32836
+
32837
+ INSERT INTO public.plg_financial_invoice_installments
32838
+ (id, tenant_id, unit_id, invoice_id, number, due_date, amount, metadata, created_at)
32839
+ VALUES (p_id, p_tenant, v_unit, v_invoice, v_number, v_due, v_amount, v_meta,
32840
+ coalesce((p_row ->> 'created_at')::timestamptz, now()))
32841
+ ON CONFLICT (id) DO UPDATE SET
32842
+ unit_id = coalesce(EXCLUDED.unit_id, public.plg_financial_invoice_installments.unit_id),
32843
+ invoice_id = EXCLUDED.invoice_id,
32844
+ number = EXCLUDED.number,
32845
+ due_date = EXCLUDED.due_date,
32846
+ amount = EXCLUDED.amount,
32847
+ metadata = migration._merge_meta(public.plg_financial_invoice_installments.metadata, EXCLUDED.metadata),
32848
+ updated_at = now();
32849
+
32850
+ IF v_standalone THEN
32851
+ PERFORM migration._audit(p_tenant, 'public.plg_financial_invoice_installments', p_id,
32852
+ jsonb_build_object('reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END,
32853
+ 'invoice_id', v_invoice));
32854
+ END IF;
32855
+ RETURN jsonb_build_object('status', 'migrated', 'target_id', p_id,
32856
+ 'reason', CASE WHEN v_created THEN 'created' ELSE 'updated' END);
32857
+ END $function$;
32858
+
32859
+ COMMENT ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) IS
32860
+ 'A parcela do plano de pagamento de uma DANFE importada. Cria, uma vez por nota '
32861
+ 'e com id derivado da nota, o t\xEDtulo a pagar que o V1 n\xE3o tem \u2014 sem ele a '
32862
+ 'parcela n\xE3o existe, porque invoice_id \xE9 NOT NULL.';
32863
+
32864
+ -- \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\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
32865
+ -- 2. O vocabul\xE1rio de writers aceita o nome novo
32866
+ -- \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\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
32867
+ --
32868
+ -- Lendo e costurando o CHECK que est\xE1 l\xE1: reescrev\xEA-lo com lista literal
32869
+ -- apagaria as palavras que os outros passos da corrente acrescentaram.
32870
+
32871
+ DO $$
32872
+ DECLARE v_def text;
32873
+ BEGIN
32874
+ SELECT pg_get_constraintdef(oid) INTO v_def
32875
+ FROM pg_constraint WHERE conname = 'allowlist_writer_check';
32876
+ IF v_def IS NULL OR strpos(v_def, 'fin_danfe_installment') > 0 THEN RETURN; END IF;
32877
+ ALTER TABLE migration.allowlist DROP CONSTRAINT allowlist_writer_check;
32878
+ EXECUTE replace(
32879
+ 'ALTER TABLE migration.allowlist ADD CONSTRAINT allowlist_writer_check ' || v_def,
32880
+ '''person''::text', '''person''::text, ''fin_danfe_installment''::text');
32881
+ END $$;
32882
+
32883
+ -- \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\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
32884
+ -- 3. O de-para
32885
+ -- \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\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
32886
+ --
32887
+ -- Tudo que o t\xEDtulo precisa chega pelo augment do extrator, a partir da pr\xF3pria
32888
+ -- nota. \xC9 de prop\xF3sito: este writer \xE9 do financeiro, e o financeiro roda antes
32889
+ -- do invent\xE1rio \u2014 ler \`plg_inventory_operations\` daqui seria depender de uma
32890
+ -- tabela que ainda n\xE3o existe quando este arquivo \xE9 aplicado.
32891
+
32892
+ SET LOCAL migration.allowlist_unlock = 'on';
32893
+
32894
+ SELECT migration._allow(
32895
+ 'danfe_payments', 'public.plg_financial_invoice_installments', 'financial',
32896
+ 'fin_danfe_installment', '{}'::jsonb, ARRAY['amount'],
32897
+ jsonb_build_object(
32898
+ 'uuid', 'id',
32899
+ 'note_unit_id', 'unit_id@companies',
32900
+ 'payment_rank', 'number',
32901
+ 'due_date', 'due_date',
32902
+ 'value', 'amount',
32903
+ 'created_at', 'created_at',
32904
+ 'tenant_id', '-',
32905
+ -- O que o writer l\xEA para montar o t\xEDtulo. Vem em metadata e n\xE3o em coluna:
32906
+ -- \xE9 a nota, n\xE3o a parcela, e duplic\xE1-la em coluna daria duas respostas para
32907
+ -- "de que nota \xE9 este vencimento".
32908
+ 'note_uuid', 'jsonb:danfe.import_uuid',
32909
+ 'note_access_key', 'jsonb:danfe.access_key',
32910
+ 'note_number', 'jsonb:danfe.nfe_number',
32911
+ 'note_series', 'jsonb:danfe.nfe_series',
32912
+ 'note_issue_date', 'jsonb:danfe.issue_date',
32913
+ 'note_supplier', 'jsonb:danfe.supplier_name',
32914
+ 'note_total', 'jsonb:danfe.total_value',
32915
+ 'note_payments_total', 'jsonb:danfe.payments_total',
32916
+ 'import_id', 'jsonb:danfe.v1_import_ref',
32917
+ 'payment_type', 'jsonb:danfe.payment.type',
32918
+ 'payment_type_code', 'jsonb:danfe.payment.type_code',
32919
+ 'installment_number', 'jsonb:danfe.payment.v1_installment_number',
32920
+ 'installment_ref', 'jsonb:danfe.payment.v1_installment_ref',
32921
+ 'barcode', 'jsonb:danfe.payment.barcode',
32922
+ 'pix_key', 'jsonb:danfe.payment.pix_key',
32923
+ 'raw_json', 'jsonb:danfe.payment.raw',
32924
+ 'id', 'jsonb:danfe.payment.v1_ref'),
32925
+ 'O plano de pagamento da nota de entrada: 895 parcelas, R$ 1.293.490,86, que '
32926
+ 'hoje n\xE3o chegam a lugar nenhum. O t\xEDtulo a pagar nasce com a primeira '
32927
+ 'parcela, um por nota, com id derivado do uuid dela \u2014 no V1 ele n\xE3o existe, e '
32928
+ 'invoice_id \xE9 NOT NULL. O boleto e a chave pix ficam em metadata: cobran\xE7a de '
32929
+ 'fornecedor \xE9 evid\xEAncia, n\xE3o mais uma coluna do t\xEDtulo.'
32930
+ );
32931
+
32932
+ DELETE FROM migration.excluded_tables WHERE source_table = 'danfe_payments';
32933
+
32934
+ RESET migration.allowlist_unlock;
32935
+
32936
+
32937
+ -- \u2500\u2500 quem pode executar \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\u2500
32938
+ --
32939
+ -- Fun\xE7\xE3o nova nasce execut\xE1vel por PUBLIC, e \`anon\`/\`authenticated\` herdam
32940
+ -- dali. Esta \xE9 de PORTE: SECURITY DEFINER, escreve dados de qualquer inquilino,
32941
+ -- e alcan\xE7\xE1vel pela chave an\xF4nima seria a base inteira alcan\xE7\xE1vel pela chave
32942
+ -- an\xF4nima. S\xF3 o service_role, que \xE9 quem roda a onda.
32943
+ REVOKE ALL ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) FROM PUBLIC, anon, authenticated;
32944
+ GRANT EXECUTE ON FUNCTION migration.upsert_fin_danfe_installment(uuid, uuid, jsonb, jsonb) TO service_role;
32945
+ `;
32946
+ var MIGRATION_085_O_RESUMO_LE_CINCO_MIL_LINHAS_E_NAO_DUZENTAS_E_OITENTA_MIL = `-- O resumo l\xEA cinco mil linhas, n\xE3o duzentas e oitenta mil.
32947
+ --
32948
+ -- \`financial_summary\` (migra\xE7\xE3o 077) j\xE1 soma no banco em vez de baixar a tabela
32949
+ -- para o navegador, e ainda assim o painel devolvia 500: "canceling statement
32950
+ -- due to statement timeout". Somar no lugar certo n\xE3o basta se para somar for
32951
+ -- preciso LER tudo.
32952
+ --
32953
+ -- O que a fun\xE7\xE3o pede, e o que a tabela obrigava:
32954
+ --
32955
+ -- \xB7 aberto \u2192 \`status IN ('pending','partial','overdue')\`: 5.100 linhas de
32956
+ -- 281.174. Nenhum \xEDndice come\xE7a por \`status\`, ent\xE3o era varredura
32957
+ -- sequencial em 287 MB.
32958
+ -- \xB7 caixa \u2192 \`movement_kind = 'payment'\` numa janela de 30 dias: 870 linhas.
32959
+ -- O \xEDndice \`(tenant_id, payment_date)\` existente \xE9 parcial e vale
32960
+ -- s\xF3 para concilia\xE7\xE3o banc\xE1ria (\`external_source IS NOT NULL\`),
32961
+ -- ent\xE3o este caminho tamb\xE9m varria tudo.
32962
+ -- \xB7 a view \`v_financial_cash_movements\` faz LEFT JOIN da tabela COM ELA MESMA
32963
+ -- em \`reverses_id\`, e n\xE3o havia \xEDndice nessa coluna \u2014 a segunda
32964
+ -- varredura dos mesmos 287 MB, por chamada.
32965
+ --
32966
+ -- E a cerca de RLS cobra por linha: \`app.has_unit(unit_id)\` e
32967
+ -- \`app.has_permission('financial.read', unit_id)\` dependem da LINHA, ent\xE3o n\xE3o
32968
+ -- saem do la\xE7o. Com 281 mil linhas a pergunta \xE9 feita 281 mil vezes; com 5 mil,
32969
+ -- cinco mil. O \xEDndice n\xE3o muda a pol\xEDtica, muda quantas vezes ela \xE9 consultada
32970
+ -- \u2014 \xE9 a mesma corre\xE7\xE3o que levou /clients de 10s para 15ms.
32971
+ --
32972
+ -- \`INCLUDE\` carrega as colunas que a soma usa (valor, pago, e as duas que a RLS
32973
+ -- precisa ler) para a varredura terminar no \xEDndice e n\xE3o voltar \xE0 heap 5.100
32974
+ -- vezes.
32975
+ --
32976
+ -- S\xF3 \xEDndices: nenhuma pol\xEDtica, coluna ou dado muda. Idempotente.
32977
+
32978
+ -- T\xEDtulos em aberto \u2014 o "a receber", o "a pagar" e os dois vencidos.
32979
+ CREATE INDEX IF NOT EXISTS plg_financial_movements_open_idx
32980
+ ON public.plg_financial_movements (tenant_id, direction, due_date)
32981
+ INCLUDE (amount, paid_amount, status, unit_id, owner_id)
32982
+ WHERE status IN ('pending', 'partial', 'overdue');
32983
+
32984
+ -- Eventos de dinheiro na janela \u2014 a entrada e a sa\xEDda do per\xEDodo.
32985
+ CREATE INDEX IF NOT EXISTS plg_financial_movements_payment_idx
32986
+ ON public.plg_financial_movements (tenant_id, payment_date, direction)
32987
+ INCLUDE (paid_amount, unit_id, owner_id)
32988
+ WHERE movement_kind = 'payment';
32989
+
32990
+ -- A auto-jun\xE7\xE3o da view de caixa. Parcial porque estorno \xE9 exce\xE7\xE3o: o \xEDndice
32991
+ -- fica pequeno e o planejador o usa para o lado que de fato tem valor.
32992
+ CREATE INDEX IF NOT EXISTS plg_financial_movements_reverses_idx
32993
+ ON public.plg_financial_movements (reverses_id)
32994
+ WHERE reverses_id IS NOT NULL;
32995
+
32996
+ -- O saldo por conta. \`v_financial_account_balances\` soma, POR CONTA, os
32997
+ -- movimentos que entram e os que saem dela. Os \xEDndices existentes s\xE3o de uma
32998
+ -- coluna s\xF3 (\`debit_account_id\`, \`credit_account_id\`), ent\xE3o o planejador
32999
+ -- cruzava um deles com o \xEDndice de \`tenant_id\` \u2014 46.862 linhas de bitmap por
33000
+ -- conta, vezes o n\xFAmero de contas. Compostos e com as colunas da soma dentro,
33001
+ -- cada conta \xE9 uma varredura curta do pr\xF3prio \xEDndice.
33002
+ CREATE INDEX IF NOT EXISTS plg_financial_movements_debit_balance_idx
33003
+ ON public.plg_financial_movements (tenant_id, debit_account_id)
33004
+ INCLUDE (paid_amount, amount, unit_id, owner_id, reverses_id)
33005
+ WHERE debit_account_id IS NOT NULL
33006
+ AND status <> 'cancelled'
33007
+ AND movement_kind IN ('payment', 'transfer');
33008
+
33009
+ CREATE INDEX IF NOT EXISTS plg_financial_movements_credit_balance_idx
33010
+ ON public.plg_financial_movements (tenant_id, credit_account_id)
33011
+ INCLUDE (paid_amount, amount, unit_id, owner_id, reverses_id)
33012
+ WHERE credit_account_id IS NOT NULL
33013
+ AND status <> 'cancelled'
33014
+ AND movement_kind IN ('payment', 'transfer');
33015
+ `;
33016
+ var MIGRATION_086_A_CERCA_SE_PERGUNTA_UMA_VEZ_E_NAO_UMA_VEZ_POR_LINHA = `-- A cerca se pergunta uma vez, n\xE3o uma vez por linha.
33017
+ --
33018
+ -- \`financial_summary\` (077) j\xE1 soma no banco, e os \xEDndices (085) j\xE1 a fazem
33019
+ -- ler 5.100 linhas em vez de 281.174. Continuava dando 57014 \u2014 "canceling
33020
+ -- statement due to statement timeout" \u2014 e o custo que sobrou n\xE3o \xE9 varredura,
33021
+ -- \xE9 a POL\xCDTICA.
33022
+ --
33023
+ -- A pol\xEDtica de SELECT de \`plg_financial_movements\` faz, POR LINHA:
33024
+ --
33025
+ -- (unit_id IS NULL OR app.has_unit(unit_id))
33026
+ -- AND CASE unit_id \u2026 app.has_permission('financial.read', unit_id) END
33027
+ -- AND (\u2026 app.has_permission('financial.read_all', unit_id) \u2026)
33028
+ --
33029
+ -- As tr\xEAs dependem da LINHA, ent\xE3o n\xE3o saem do la\xE7o (as que n\xE3o dependem j\xE1
33030
+ -- est\xE3o i\xE7adas em \`(SELECT \u2026)\`). \`has_unit\` e \`has_permission\` s\xE3o memoizadas
33031
+ -- num GUC, mas memoizado n\xE3o \xE9 gr\xE1tis: cada chamada monta a chave e faz o
33032
+ -- parse de um JSONB que cresce. Vezes tr\xEAs, vezes o n\xFAmero de linhas \u2014 e o
33033
+ -- saldo por conta l\xEA os movimentos de cada conta, ent\xE3o ali s\xE3o milh\xF5es de
33034
+ -- avalia\xE7\xF5es.
33035
+ --
33036
+ -- A mesma pergunta, feita como CONJUNTO, custa uma vez:
33037
+ --
33038
+ -- app.visible_units() -- exatamente o que has_unit aceita
33039
+ -- \xD7 app.has_permission(perm, u) por UNIDADE -- meia d\xFAzia, n\xE3o 281 mil
33040
+ --
33041
+ -- Da\xED em diante o filtro da linha \xE9 \`unit_id = ANY(array)\`, que \xE9 compara\xE7\xE3o,
33042
+ -- n\xE3o chamada de fun\xE7\xE3o. \xC9 a corre\xE7\xE3o que levou /clients de 10s para 15ms.
33043
+ --
33044
+ -- \u2500\u2500 O que N\xC3O muda \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\u2500\u2500\u2500\u2500\u2500\u2500
33045
+ --
33046
+ -- A fun\xE7\xE3o passa a \`SECURITY DEFINER\`, ent\xE3o a cerca \xE9 responsabilidade DELA, e
33047
+ -- est\xE1 escrita abaixo como tradu\xE7\xE3o literal das duas pol\xEDticas (movimento e
33048
+ -- conta) \u2014 mesma ordem, mesmos recursos, mesmas permiss\xF5es:
33049
+ --
33050
+ -- \xB7 tenant \u2192 \`tenant_id = app.current_tenant_id()\`
33051
+ -- \xB7 unidade \u2192 \`unit_id IS NULL\` com \`financial.read\` sem unidade, ou
33052
+ -- unidade dentro de \`visible_units()\` COM \`financial.read\` nela
33053
+ -- \xB7 dono \u2192 s\xF3 quando \`app.owner_scoped(<recurso>)\`; a\xED valem, na ordem da
33054
+ -- pol\xEDtica, \`owner_id IS NULL\`, \`owner_id = auth.uid()\`, o
33055
+ -- compartilhamento (\`has_grant\`, por linha \u2014 \xE9 o \xFAnico que n\xE3o
33056
+ -- tem forma de conjunto, e s\xF3 \xE9 avaliado quando o escopo por
33057
+ -- dono est\xE1 ligado, que \xE9 a exce\xE7\xE3o) e \`financial.read_all\`.
33058
+ --
33059
+ -- O estorno (\`v_financial_cash_movements\`) \xE9 reescrito aqui como uma coluna
33060
+ -- calculada, em vez de LEFT JOIN da tabela com ela mesma: mesma regra, sem a
33061
+ -- segunda varredura dos mesmos 287 MB por chamada.
33062
+ --
33063
+ -- Idempotente. Nenhuma pol\xEDtica, coluna ou dado muda.
33064
+
33065
+ CREATE OR REPLACE FUNCTION public.financial_summary(
33066
+ p_from date DEFAULT NULL,
33067
+ p_to date DEFAULT NULL
33068
+ ) RETURNS TABLE (
33069
+ total_balance numeric,
33070
+ total_receivable numeric,
33071
+ total_payable numeric,
33072
+ monthly_inflow numeric,
33073
+ monthly_outflow numeric,
33074
+ overdue_receivable_count integer,
33075
+ overdue_receivable_amount numeric,
33076
+ overdue_payable_count integer,
33077
+ overdue_payable_amount numeric
33078
+ )
33079
+ LANGUAGE sql
33080
+ STABLE
33081
+ SECURITY DEFINER
33082
+ SET search_path TO ''
33083
+ AS $function$
33084
+ WITH ctx AS (
33085
+ SELECT app.current_tenant_id() AS tenant,
33086
+ auth.uid() AS me,
33087
+ app.visible_units() AS units,
33088
+ app.has_permission('financial.read', NULL::uuid) AS read_null,
33089
+ app.has_permission('financial.read_all', NULL::uuid) AS read_all_null,
33090
+ app.owner_scoped('financial.movement') AS own_mov,
33091
+ app.owner_scoped('financial.account') AS own_acc,
33092
+ app.grants_exist('financial.movement', ARRAY['shared_with']) AS grants_mov,
33093
+ app.grants_exist('financial.account', ARRAY['shared_with']) AS grants_acc,
33094
+ coalesce(p_from, (current_date - 30)) AS de,
33095
+ coalesce(p_to, current_date) AS ate
33096
+ ),
33097
+ -- Uma pergunta por unidade alcan\xE7\xE1vel, n\xE3o uma por linha.
33098
+ u_read AS (
33099
+ SELECT u FROM ctx, unnest(ctx.units) u
33100
+ WHERE app.has_permission('financial.read', u)
33101
+ ),
33102
+ u_read_all AS (
33103
+ SELECT u FROM ctx, unnest(ctx.units) u
33104
+ WHERE app.has_permission('financial.read_all', u)
33105
+ ),
33106
+ -- Movimentos que esta pessoa veria na lista: o WHERE abaixo \xE9 a pol\xEDtica de
33107
+ -- SELECT de plg_financial_movements, termo por termo.
33108
+ -- NOT MATERIALIZED de prop\xF3sito: a partir do PG12 uma CTE citada mais de uma
33109
+ -- vez \xE9 materializada, e materializar a cerca inteira \xE9 justamente juntar as
33110
+ -- 281 mil linhas numa tabela tempor\xE1ria. Inlinada, cada uso leva o pr\xF3prio
33111
+ -- \xEDndice (aberto, pagamento, saldo por conta).
33112
+ --
33113
+ -- \`caixa_visivel\` \xE9 a regra de estorno de v_financial_cash_movements dita
33114
+ -- sem a auto-jun\xE7\xE3o: quando o movimento estorna outro, quem decide \xE9 o
33115
+ -- estornado; se n\xE3o estorna ningu\xE9m, decide ele mesmo. \`reverses_id IS NULL\`
33116
+ -- \xE9 a esmagadora maioria, ent\xE3o a subconsulta quase nunca roda \u2014 e quando
33117
+ -- roda, \xE9 pela chave prim\xE1ria.
33118
+ mov AS NOT MATERIALIZED (
33119
+ SELECT m.*,
33120
+ CASE
33121
+ WHEN m.reverses_id IS NULL THEN
33122
+ (m.external_source IS NULL
33123
+ OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL))
33124
+ ELSE coalesce(
33125
+ (SELECT r.external_source IS NULL
33126
+ OR (r.reconciled_at IS NOT NULL AND r.matched_movement_id IS NULL)
33127
+ FROM public.plg_financial_movements r
33128
+ WHERE r.id = m.reverses_id),
33129
+ (m.external_source IS NULL
33130
+ OR (m.reconciled_at IS NOT NULL AND m.matched_movement_id IS NULL)))
33131
+ END AS caixa_visivel
33132
+ FROM public.plg_financial_movements m, ctx
33133
+ WHERE m.tenant_id = ctx.tenant
33134
+ AND (CASE WHEN m.unit_id IS NULL THEN ctx.read_null
33135
+ ELSE m.unit_id IN (SELECT u FROM u_read) END)
33136
+ AND (NOT ctx.own_mov
33137
+ OR m.owner_id IS NULL
33138
+ OR m.owner_id = ctx.me
33139
+ OR (ctx.grants_mov
33140
+ AND app.has_grant('financial.movement', m.id, ARRAY['shared_with']))
33141
+ OR (CASE WHEN m.unit_id IS NULL THEN ctx.read_all_null
33142
+ ELSE m.unit_id IN (SELECT u FROM u_read_all) END))
33143
+ ),
33144
+ -- O mesmo, para as contas.
33145
+ acc AS (
33146
+ SELECT a.id, a.kind
33147
+ FROM public.plg_financial_accounts a, ctx
33148
+ WHERE a.tenant_id = ctx.tenant
33149
+ AND a.is_active
33150
+ AND a.kind IN ('bank', 'cash_physical', 'cash_session',
33151
+ 'card_receivable_credit', 'card_receivable_debit')
33152
+ AND (CASE WHEN a.unit_id IS NULL THEN ctx.read_null
33153
+ ELSE a.unit_id IN (SELECT u FROM u_read) END)
33154
+ AND (NOT ctx.own_acc
33155
+ OR a.owner_id IS NULL
33156
+ OR a.owner_id = ctx.me
33157
+ OR (ctx.grants_acc
33158
+ AND app.has_grant('financial.account', a.id, ARRAY['shared_with']))
33159
+ OR (CASE WHEN a.unit_id IS NULL THEN ctx.read_all_null
33160
+ ELSE a.unit_id IN (SELECT u FROM u_read_all) END))
33161
+ ),
33162
+ -- O saldo de uma conta \xE9 o que entrou menos o que saiu dela, e s\xF3 conta
33163
+ -- pagamento e transfer\xEAncia \u2014 a obriga\xE7\xE3o n\xE3o \xE9 dinheiro. Para os cinco
33164
+ -- \`kind\` de caixa que o cart\xE3o soma, v_financial_account_balances calcula
33165
+ -- \`debit_total - credit_total\`; \xE9 esse sinal que est\xE1 aqui.
33166
+ saldo AS (
33167
+ SELECT coalesce(sum(
33168
+ coalesce((SELECT sum(d.amount) FROM mov d
33169
+ WHERE d.debit_account_id = acc.id
33170
+ AND d.status <> 'cancelled'
33171
+ AND d.movement_kind IN ('payment', 'transfer')
33172
+ AND d.caixa_visivel), 0)
33173
+ - coalesce((SELECT sum(c.amount) FROM mov c
33174
+ WHERE c.credit_account_id = acc.id
33175
+ AND c.status <> 'cancelled'
33176
+ AND c.movement_kind IN ('payment', 'transfer')
33177
+ AND c.caixa_visivel), 0)
33178
+ ), 0) AS v
33179
+ FROM acc
33180
+ ),
33181
+ aberto AS (
33182
+ SELECT
33183
+ coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS receber,
33184
+ coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS pagar,
33185
+ count(*) FILTER (WHERE m.direction = 'credit'
33186
+ AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_receber_n,
33187
+ coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'credit'
33188
+ AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_receber_v,
33189
+ count(*) FILTER (WHERE m.direction = 'debit'
33190
+ AND (m.status = 'overdue' OR m.due_date < current_date)) AS venc_pagar_n,
33191
+ coalesce(sum(m.amount - m.paid_amount) FILTER (WHERE m.direction = 'debit'
33192
+ AND (m.status = 'overdue' OR m.due_date < current_date)), 0) AS venc_pagar_v
33193
+ FROM mov m
33194
+ WHERE m.status IN ('pending', 'partial', 'overdue')
33195
+ ),
33196
+ caixa AS (
33197
+ SELECT
33198
+ coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'credit'), 0) AS entrada,
33199
+ coalesce(sum(m.paid_amount) FILTER (WHERE m.direction = 'debit'), 0) AS saida
33200
+ FROM mov m, ctx
33201
+ WHERE m.movement_kind = 'payment'
33202
+ AND m.payment_date >= ctx.de AND m.payment_date <= ctx.ate
33203
+ AND m.caixa_visivel
33204
+ )
33205
+ SELECT saldo.v, aberto.receber, aberto.pagar, caixa.entrada, caixa.saida,
33206
+ aberto.venc_receber_n::integer, aberto.venc_receber_v,
33207
+ aberto.venc_pagar_n::integer, aberto.venc_pagar_v
33208
+ FROM saldo, aberto, caixa;
33209
+ $function$;
33210
+
33211
+ COMMENT ON FUNCTION public.financial_summary(date, date) IS
33212
+ 'Os nove numeros do cartao de resumo, somados no banco, com a cerca de '
33213
+ 'tenant/unidade/dono perguntada UMA vez em vez de uma vez por linha. '
33214
+ 'SECURITY DEFINER: a politica de plg_financial_movements e de '
33215
+ 'plg_financial_accounts esta reescrita aqui, termo por termo, em forma de '
33216
+ 'conjunto - ver o cabecalho da migracao 086.';
33217
+
33218
+ GRANT EXECUTE ON FUNCTION public.financial_summary(date, date) TO authenticated, service_role;
33219
+ `;
33220
+ var MIGRATION_087_O_SALDO_DE_UMA_FATURA_NAO_SOMA_O_RAZAO_INTEIRO = `-- O saldo de uma fatura n\xE3o soma o raz\xE3o inteiro.
33221
+ --
33222
+ -- \`v_financial_installment_balances\` abre com uma CTE que agrega TUDO:
33223
+ --
33224
+ -- WITH paid AS (
33225
+ -- SELECT a.installment_id, sum(a.amount)
33226
+ -- FROM plg_financial_allocations a
33227
+ -- JOIN plg_financial_movements m ON m.id = a.payment_movement_id
33228
+ -- WHERE m.status <> 'cancelled' AND m.reversed_by_id IS NULL
33229
+ -- GROUP BY a.installment_id)
33230
+ --
33231
+ -- Nenhum predicado do chamador alcan\xE7a essa CTE: ela agrupa por
33232
+ -- \`installment_id\`, e o \`LEFT JOIN paid p ON p.installment_id = i.id\` s\xF3
33233
+ -- acontece DEPOIS. O planejador n\xE3o tem como empurrar \`invoice_id = X\` para
33234
+ -- dentro, ent\xE3o materializa o raz\xE3o inteiro \u2014 toda vez, para qualquer pergunta.
33235
+ --
33236
+ -- Medido no cluster, pedindo UMA fatura:
33237
+ --
33238
+ -- Index Scan ... plg_financial_invoice_installments 1 linha 0,077 ms
33239
+ -- Seq Scan plg_financial_movements 281.113 linhas
33240
+ -- Seq Scan plg_financial_allocations 140.073 linhas
33241
+ -- HashAggregate 9 lotes, 7.288 kB em disco
33242
+ -- \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
33243
+ -- CTE paid 2.251 ms
33244
+ -- total 2.627 ms
33245
+ --
33246
+ -- 2.627 ms como superusu\xE1rio. Pelo PostgREST, com RLS por cima, passa dos 8 s
33247
+ -- e a tela recebe 500 / 57014. A parcela que a pessoa pediu custou 0,077 ms; o
33248
+ -- resto \xE9 o raz\xE3o de dois tenants sendo somado para ser jogado fora.
33249
+ --
33250
+ -- \u2500\u2500 O conserto \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
33251
+ --
33252
+ -- LATERAL correlacionado em \`i.id\`. A soma passa a acontecer POR PARCELA, e
33253
+ -- \`plg_financial_allocations_installment_idx\` \u2014 que j\xE1 existe \u2014 a resolve com
33254
+ -- um index scan de poucas linhas. Uma fatura com 8 parcelas faz 8 buscas
33255
+ -- pequenas em vez de uma agrega\xE7\xE3o de 140 mil linhas.
33256
+ --
33257
+ -- \xC9 o mesmo movimento da 085 e da 086: a pergunta \xE9 de UMA linha, e a resposta
33258
+ -- n\xE3o pode ter custo de tabela inteira.
33259
+ --
33260
+ -- \u2500\u2500 O que N\xC3O muda \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\u2500\u2500\u2500\u2500\u2500\u2500
33261
+ --
33262
+ -- As colunas, na mesma ordem e com os mesmos tipos \u2014 \`CREATE OR REPLACE VIEW\`
33263
+ -- exige isso, e quem l\xEA a vista n\xE3o percebe diferen\xE7a nenhuma al\xE9m do tempo.
33264
+ -- \`security_invoker\` continua ligado: a vista \xE9 uma lente, a RLS \xE9 das tabelas.
33265
+ --
33266
+ -- Idempotente.
33267
+
33268
+ CREATE OR REPLACE VIEW public.v_financial_installment_balances
33269
+ WITH (security_invoker = true) AS
33270
+ SELECT i.id AS installment_id,
33271
+ i.tenant_id,
33272
+ i.unit_id,
33273
+ i.invoice_id,
33274
+ t.number AS invoice_number,
33275
+ t.direction,
33276
+ t.counterparty_person_id,
33277
+ t.order_id,
33278
+ i.number,
33279
+ i.due_date,
33280
+ i.amount,
33281
+ COALESCE(p.paid, 0::numeric)::numeric(14,2) AS paid,
33282
+ CASE
33283
+ WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 0::numeric
33284
+ ELSE GREATEST(i.amount - COALESCE(p.paid, 0::numeric), 0::numeric)
33285
+ END::numeric(14,2) AS balance,
33286
+ CASE
33287
+ WHEN i.cancelled_at IS NOT NULL OR t.cancelled_at IS NOT NULL THEN 'cancelled'::text
33288
+ WHEN COALESCE(p.paid, 0::numeric) >= i.amount THEN 'paid'::text
33289
+ WHEN COALESCE(p.paid, 0::numeric) > 0::numeric THEN 'partial'::text
33290
+ WHEN i.due_date < CURRENT_DATE THEN 'overdue'::text
33291
+ ELSE 'pending'::text
33292
+ END AS status,
33293
+ i.cancelled_at,
33294
+ i.metadata,
33295
+ i.created_at,
33296
+ i.updated_at
33297
+ FROM plg_financial_invoice_installments i
33298
+ JOIN plg_financial_invoices t ON t.id = i.invoice_id
33299
+ -- A soma \xE9 POR PARCELA. \`ON true\` mant\xE9m o LEFT: parcela sem pagamento
33300
+ -- continua aparecendo, com \`paid\` nulo, que o COALESCE zera.
33301
+ LEFT JOIN LATERAL (
33302
+ SELECT sum(a.amount) AS paid
33303
+ FROM plg_financial_allocations a
33304
+ JOIN plg_financial_movements m ON m.id = a.payment_movement_id
33305
+ WHERE a.installment_id = i.id
33306
+ AND m.status <> 'cancelled'::text
33307
+ AND m.reversed_by_id IS NULL
33308
+ ) p ON true;
33309
+
33310
+ -- O movimento \xE9 olhado uma vez por aloca\xE7\xE3o, pelo id \u2014 e o filtro de vivo
33311
+ -- (\`status\`, \`reversed_by_id\`) cabe no \xEDndice, ent\xE3o a visita \xE0 heap s\xF3
33312
+ -- acontece para o que de fato soma.
33313
+ CREATE INDEX IF NOT EXISTS plg_financial_movements_alive_idx
33314
+ ON public.plg_financial_movements (id)
33315
+ WHERE status <> 'cancelled'::text AND reversed_by_id IS NULL;
33316
+ `;
33317
+ var MIGRATION_088_A_CONTA_SEM_PESSOA_E_DO_SISTEMA_NAO_DE_NINGUEM = `-- A conta sem pessoa \xE9 do sistema, n\xE3o de ningu\xE9m.
33318
+ --
33319
+ -- \`migration.upsert_fin_account\` inventa um valor que a tabela n\xE3o aceita:
33320
+ --
33321
+ -- IF v_kind = 'person' AND coalesce(v_ref_id, 0) = 0 THEN
33322
+ -- v_kind := 'unidentified';
33323
+ --
33324
+ -- Uma conta de PESSOA sem pessoa referenciada vira \`'unidentified'\`. S\xF3 que
33325
+ -- \`plg_financial_accounts_kind_check\` declara dez esp\xE9cies e essa n\xE3o \xE9 uma
33326
+ -- delas \u2014 a migration 063 recria o CHECK e ele estoura com \`23514\` em cima das
33327
+ -- linhas que o porte gravou. O efeito \xE9 pior do que parece: 063 \xE9 a 64\xAA de 88
33328
+ -- na cadeia do financeiro, ent\xE3o DUAS linhas travam a aplica\xE7\xE3o de todo o resto
33329
+ -- do plugin, migrations de desempenho inclusive.
33330
+ --
33331
+ -- \u2500\u2500 Quem s\xE3o as duas \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\u2500\u2500\u2500\u2500
33332
+ --
33333
+ -- Pertinho do C\xE9u, vindas de \`account_central\`:
33334
+ --
33335
+ -- 653309 Cliente n\xE3o identificado
33336
+ -- 654242 M\xFAltiplos participantes
33337
+ --
33338
+ -- 94 movimentos creditam nelas. N\xE3o s\xE3o erro de dado: s\xE3o o balc\xE3o an\xF4nimo e a
33339
+ -- conta dividida \u2014 dois baldes que a CASA mant\xE9m. Nenhuma das duas \xE9 pessoa, e
33340
+ -- nenhuma \xE9 banco.
33341
+ --
33342
+ -- \u2500\u2500 Por que \`system\` e n\xE3o mais um valor no CHECK \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
33343
+ --
33344
+ -- \`system\` j\xE1 existe no vocabul\xE1rio e significa exatamente isto: conta que o
33345
+ -- estabelecimento mant\xE9m e que n\xE3o \xE9 de ningu\xE9m. Acrescentar \`unidentified\`
33346
+ -- seria criar um d\xE9cimo primeiro nome para um conceito que j\xE1 tem o seu, e
33347
+ -- alargar um CHECK que outro dono fechou de prop\xF3sito.
33348
+ --
33349
+ -- O nome continua leg\xEDvel \u2014 "Cliente n\xE3o identificado" est\xE1 em \`name\`, que \xE9 o
33350
+ -- que a tela mostra. O que muda \xE9 a ESP\xC9CIE, que \xE9 o que o raz\xE3o classifica,
33351
+ -- mais um \`code\` est\xE1vel, que \xE9 como conta de sistema se procura.
33352
+ --
33353
+ -- \u2500\u2500 Ordem \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
33354
+ --
33355
+ -- Numa cadeia do zero isto nunca acontece: sem porte n\xE3o h\xE1 linha, e a 063
33356
+ -- passa. O conserto aqui \xE9 para o cluster onde o porte j\xE1 rodou \u2014 normaliza o
33357
+ -- que existe e, sobretudo, corrige o writer para n\xE3o produzir mais.
33358
+ --
33359
+ -- Idempotente.
33360
+
33361
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
33362
+ -- 1. O que j\xE1 est\xE1 gravado
33363
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
33364
+
33365
+ -- \`plg_financial_accounts_check1\` cobra \`code\` de toda conta de sistema, e
33366
+ -- cobra com raz\xE3o: conta de sistema se endere\xE7a por c\xF3digo est\xE1vel, n\xE3o por
33367
+ -- nome \u2014 o nome \xE9 o que a tela mostra, o c\xF3digo \xE9 o que o raz\xE3o procura.
33368
+ -- Os dois baldes ganham c\xF3digo no estilo dos que j\xE1 existem (\`legacy\`,
33369
+ -- \`payables\`, \`receivables\`).
33370
+ UPDATE public.plg_financial_accounts
33371
+ SET kind = 'system',
33372
+ code = coalesce(code, CASE
33373
+ WHEN name ILIKE '%%ltiplos%%' OR name ILIKE '%%multiple%%' THEN 'multiple_parties'
33374
+ ELSE 'unidentified_party'
33375
+ END),
33376
+ metadata = jsonb_set(coalesce(metadata, '{}'::jsonb), '{v1,account_kind}', '"unidentified"'::jsonb, true)
33377
+ WHERE kind = 'unidentified';
33378
+
33379
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
33380
+ -- 2. O writer para de produzir
33381
+ -- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
33382
+ --
33383
+ -- Duas trocas no corpo, nada mais: a atribui\xE7\xE3o e a lista de aceitos. Reescrever
33384
+ -- a fun\xE7\xE3o inteira aqui duplicaria 60 linhas que a 061 j\xE1 define \u2014 e a c\xF3pia
33385
+ -- envelheceria na pr\xF3xima vez que algu\xE9m mexesse l\xE1.
33386
+
33387
+ DO $$
33388
+ DECLARE
33389
+ v_src text;
33390
+ v_new text;
33391
+ BEGIN
33392
+ SELECT pg_get_functiondef(p.oid) INTO v_src
33393
+ FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
33394
+ WHERE n.nspname = 'migration' AND p.proname = 'upsert_fin_account'
33395
+ LIMIT 1;
33396
+
33397
+ IF v_src IS NULL THEN
33398
+ RAISE NOTICE 'migration.upsert_fin_account n\xE3o existe neste cluster \u2014 nada a corrigir';
33399
+ RETURN;
33400
+ END IF;
33401
+
33402
+ IF position('''unidentified''' in v_src) = 0 THEN
33403
+ RAISE NOTICE 'migration.upsert_fin_account j\xE1 n\xE3o produz unidentified';
33404
+ RETURN;
33405
+ END IF;
33406
+
33407
+ v_new := replace(v_src, 'v_kind := ''unidentified'';', 'v_kind := ''system'';');
33408
+ -- A mesma palavra aparece na lista de esp\xE9cies aceitas pelo writer; l\xE1 ela
33409
+ -- some, porque \`system\` j\xE1 est\xE1 na lista.
33410
+ v_new := replace(v_new, ',' || chr(10) || ' ''unidentified'')', ')');
33411
+ v_new := replace(v_new, ', ''unidentified'')', ')');
33412
+
33413
+ IF v_new = v_src THEN
33414
+ RAISE EXCEPTION 'upsert_fin_account mudou de forma: o conserto de unidentified n\xE3o casou. Conferir \xE0 m\xE3o.';
33415
+ END IF;
33416
+
33417
+ EXECUTE v_new;
33418
+ END $$;
33419
+
33420
+ -- A prova: nenhuma linha fora do vocabul\xE1rio, e o writer sem a palavra.
33421
+ DO $$
33422
+ BEGIN
33423
+ IF EXISTS (SELECT 1 FROM public.plg_financial_accounts WHERE kind = 'unidentified') THEN
33424
+ RAISE EXCEPTION 'ainda h\xE1 conta com kind=unidentified';
33425
+ END IF;
33426
+ IF EXISTS (
33427
+ SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
33428
+ WHERE n.nspname = 'migration' AND p.proname = 'upsert_fin_account'
33429
+ AND position('''unidentified''' in pg_get_functiondef(p.oid)) > 0
33430
+ ) THEN
33431
+ RAISE EXCEPTION 'upsert_fin_account ainda produz unidentified';
33432
+ END IF;
33433
+ END $$;
33434
+ `;
33435
+ var MIGRATIONS = [
33436
+ { id: "000_baseline", sql: MIGRATION_000_BASELINE },
33437
+ { id: "001_the_comanda_becomes_a_receivable_when_it_closes", sql: MIGRATION_001_THE_COMANDA_BECOMES_A_RECEIVABLE_WHEN_IT_CLOSES },
33438
+ { id: "002_payment_types_are_the_country_not_the_tenant", sql: MIGRATION_002_PAYMENT_TYPES_ARE_THE_COUNTRY_NOT_THE_TENANT },
33439
+ { id: "003_the_commission_rule_gets_a_table", sql: MIGRATION_003_THE_COMMISSION_RULE_GETS_A_TABLE },
33440
+ { id: "004_one_bridge_for_three_doors", sql: MIGRATION_004_ONE_BRIDGE_FOR_THREE_DOORS },
33441
+ { id: "005_the_appointment_closes_like_the_comanda", sql: MIGRATION_005_THE_APPOINTMENT_CLOSES_LIKE_THE_COMANDA },
33442
+ { id: "006_the_month_can_be_closed_and_the_book_can_be_proven", sql: MIGRATION_006_THE_MONTH_CAN_BE_CLOSED_AND_THE_BOOK_CAN_BE_PROVEN },
33443
+ { id: "007_the_invoice_knows_who_issued_it_and_what_it_owes", sql: MIGRATION_007_THE_INVOICE_KNOWS_WHO_ISSUED_IT_AND_WHAT_IT_OWES },
33444
+ { id: "008_the_commission_rule_learns_to_choose_and_to_split", sql: MIGRATION_008_THE_COMMISSION_RULE_LEARNS_TO_CHOOSE_AND_TO_SPLIT },
33445
+ { id: "009_the_commission_is_calculated_line_by_line", sql: MIGRATION_009_THE_COMMISSION_IS_CALCULATED_LINE_BY_LINE },
33446
+ { id: "010_the_invoice_raised_from_a_sale_gets_its_own_lines", sql: MIGRATION_010_THE_INVOICE_RAISED_FROM_A_SALE_GETS_ITS_OWN_LINES },
33447
+ { id: "011_a_new_tenant_can_take_money_on_the_first_day", sql: MIGRATION_011_A_NEW_TENANT_CAN_TAKE_MONEY_ON_THE_FIRST_DAY },
33448
+ { id: "012_the_commissions_become_one_bill_per_person", sql: MIGRATION_012_THE_COMMISSIONS_BECOME_ONE_BILL_PER_PERSON },
33449
+ { id: "013_a_discount_has_a_reason_and_someone_who_allowed_it", sql: MIGRATION_013_A_DISCOUNT_HAS_A_REASON_AND_SOMEONE_WHO_ALLOWED_IT },
33450
+ { id: "014_the_bill_that_comes_back_every_month", sql: MIGRATION_014_THE_BILL_THAT_COMES_BACK_EVERY_MONTH },
33451
+ { id: "015_the_sale_knows_what_it_cost", sql: MIGRATION_015_THE_SALE_KNOWS_WHAT_IT_COST },
33452
+ { id: "016_the_overdue_bill_gets_a_letter", sql: MIGRATION_016_THE_OVERDUE_BILL_GETS_A_LETTER },
33453
+ { id: "017_the_receipt_is_a_query_not_a_layout", sql: MIGRATION_017_THE_RECEIPT_IS_A_QUERY_NOT_A_LAYOUT },
33454
+ { id: "018_the_caixa_shows_what_it_should_have", sql: MIGRATION_018_THE_CAIXA_SHOWS_WHAT_IT_SHOULD_HAVE },
33455
+ { id: "019_who_did_it_is_already_known", sql: MIGRATION_019_WHO_DID_IT_IS_ALREADY_KNOWN },
33456
+ { id: "020_a_rule_with_nobody_to_pay_pays_the_executor", sql: MIGRATION_020_A_RULE_WITH_NOBODY_TO_PAY_PAYS_THE_EXECUTOR },
33457
+ { id: "021_the_most_specific_rule_wins", sql: MIGRATION_021_THE_MOST_SPECIFIC_RULE_WINS },
33458
+ { id: "022_an_unknown_attribute_does_not_match", sql: MIGRATION_022_AN_UNKNOWN_ATTRIBUTE_DOES_NOT_MATCH },
33459
+ { id: "023_reapurar_nao_cria_um_lote_orfao", sql: MIGRATION_023_REAPURAR_NAO_CRIA_UM_LOTE_ORFAO },
33460
+ { id: "024_o_bordero_paga_o_que_ele_soma", sql: MIGRATION_024_O_BORDERO_PAGA_O_QUE_ELE_SOMA },
33461
+ { id: "025_a_meta_do_mes_tem_tres_degraus", sql: MIGRATION_025_A_META_DO_MES_TEM_TRES_DEGRAUS },
33462
+ { id: "026_quem_fecha_o_caixa_nao_e_quem_confere", sql: MIGRATION_026_QUEM_FECHA_O_CAIXA_NAO_E_QUEM_CONFERE },
33463
+ { id: "027_o_vale_presente_nasce_em_lote_e_e_um_passivo", sql: MIGRATION_027_O_VALE_PRESENTE_NASCE_EM_LOTE_E_E_UM_PASSIVO },
33464
+ { id: "028_o_cartao_tem_cor", sql: MIGRATION_028_O_CARTAO_TEM_COR },
33465
+ { id: "029_the_kiosk_sale_is_a_sale_too", sql: MIGRATION_029_THE_KIOSK_SALE_IS_A_SALE_TOO },
33466
+ { id: "030_the_census_can_see_the_kiosk", sql: MIGRATION_030_THE_CENSUS_CAN_SEE_THE_KIOSK },
33467
+ { id: "031_the_bank_line_is_evidence_not_a_second_payment", sql: MIGRATION_031_THE_BANK_LINE_IS_EVIDENCE_NOT_A_SECOND_PAYMENT },
33468
+ { id: "032_contas_a_pagar_ganham_onde_morar", sql: MIGRATION_032_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR },
33469
+ { id: "033_o_vencimento_aponta_para_o_titulo", sql: MIGRATION_033_O_VENCIMENTO_APONTA_PARA_O_TITULO },
33470
+ { id: "034_a_unidade_tem_conta_e_a_pessoa_tem_uma_so", sql: MIGRATION_034_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO },
33471
+ { id: "035_a_classificacao_contabil_chega_inteira", sql: MIGRATION_035_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA },
33472
+ { id: "036_a_fusao_para_de_dobrar_a_propria_memoria", sql: MIGRATION_036_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA },
33473
+ { id: "037_a_venda_de_balcao_tem_conta", sql: MIGRATION_037_A_VENDA_DE_BALCAO_TEM_CONTA },
33474
+ { id: "038_a_linha_da_fatura_alcanca_quem_veio_pelo_pedido", sql: MIGRATION_038_A_LINHA_DA_FATURA_ALCANCA_QUEM_VEIO_PELO_PEDIDO },
33475
+ { id: "039_o_titulo_de_uma_venda_cancelada_nasce_cancelado", sql: MIGRATION_039_O_TITULO_DE_UMA_VENDA_CANCELADA_NASCE_CANCELADO },
33476
+ { id: "040_a_dre_le_a_linha_da_venda", sql: MIGRATION_040_A_DRE_LE_A_LINHA_DA_VENDA },
33477
+ { id: "041_o_caixa_do_v1_nao_segura_o_movimento", sql: MIGRATION_041_O_CAIXA_DO_V1_NAO_SEGURA_O_MOVIMENTO },
33478
+ { id: "042_o_cancelado_do_v1_e_o_cancelled_do_v2", sql: MIGRATION_042_O_CANCELADO_DO_V1_E_O_CANCELLED_DO_V2 },
33479
+ { id: "043_o_movimento_sabe_de_que_unidade_ele_e", sql: MIGRATION_043_O_MOVIMENTO_SABE_DE_QUE_UNIDADE_ELE_E },
33480
+ { id: "044_a_dre_nao_paga_pedagio_por_item", sql: MIGRATION_044_A_DRE_NAO_PAGA_PEDAGIO_POR_ITEM },
33481
+ { id: "045_a_conta_so_classifica_se_a_natureza_casa", sql: MIGRATION_045_A_CONTA_SO_CLASSIFICA_SE_A_NATUREZA_CASA },
33482
+ { id: "046_o_demonstrativo_mostra_o_plano_inteiro", sql: MIGRATION_046_O_DEMONSTRATIVO_MOSTRA_O_PLANO_INTEIRO },
33483
+ { id: "047_a_conta_reconhece_a_contraparte_na_segunda_oferta", sql: MIGRATION_047_A_CONTA_RECONHECE_A_CONTRAPARTE_NA_SEGUNDA_OFERTA },
33484
+ { id: "048_a_parcela_e_a_conta_do_v1_e_a_baixa_e_a_alocacao", sql: MIGRATION_048_A_PARCELA_E_A_CONTA_DO_V1_E_A_BAIXA_E_A_ALOCACAO },
33485
+ { id: "049_a_parcela_carrega_o_cancelamento", sql: MIGRATION_049_A_PARCELA_CARREGA_O_CANCELAMENTO },
33486
+ { id: "050_o_saldo_do_titulo_se_calcula_uma_vez", sql: MIGRATION_050_O_SALDO_DO_TITULO_SE_CALCULA_UMA_VEZ },
33487
+ { id: "051_o_saldo_se_calcula_so_na_linha_que_a_tela_mostra", sql: MIGRATION_051_O_SALDO_SE_CALCULA_SO_NA_LINHA_QUE_A_TELA_MOSTRA },
33488
+ { id: "052_a_lista_de_contas_fala_vencimento", sql: MIGRATION_052_A_LISTA_DE_CONTAS_FALA_VENCIMENTO },
33489
+ { id: "053_um_titulo_parcelado_vence_varias_vezes", sql: MIGRATION_053_UM_TITULO_PARCELADO_VENCE_VARIAS_VEZES },
33490
+ { id: "054_as_datas_em_que_o_titulo_vence", sql: MIGRATION_054_AS_DATAS_EM_QUE_O_TITULO_VENCE },
33491
+ { id: "055_a_lista_de_contas_lista_parcelas", sql: MIGRATION_055_A_LISTA_DE_CONTAS_LISTA_PARCELAS },
33492
+ { id: "056_what_the_port_left_open", sql: MIGRATION_056_WHAT_THE_PORT_LEFT_OPEN },
33493
+ { id: "057_a_linha_diz_que_tem_recibo", sql: MIGRATION_057_A_LINHA_DIZ_QUE_TEM_RECIBO },
33494
+ { id: "058_o_clipe_usa_o_indice", sql: MIGRATION_058_O_CLIPE_USA_O_INDICE },
33495
+ { id: "059_o_clipe_sai_da_view", sql: MIGRATION_059_O_CLIPE_SAI_DA_VIEW },
33496
+ { id: "060_o_recebivel_volta_da_arvore_derrubada", sql: MIGRATION_060_O_RECEBIVEL_VOLTA_DA_ARVORE_DERRUBADA },
33497
+ { id: "061_contas_a_pagar_ganham_onde_morar", sql: MIGRATION_061_CONTAS_A_PAGAR_GANHAM_ONDE_MORAR },
33498
+ { id: "062_o_vencimento_aponta_para_o_titulo", sql: MIGRATION_062_O_VENCIMENTO_APONTA_PARA_O_TITULO },
33499
+ { id: "063_a_unidade_tem_conta_e_a_pessoa_tem_uma_so", sql: MIGRATION_063_A_UNIDADE_TEM_CONTA_E_A_PESSOA_TEM_UMA_SO },
33500
+ { id: "064_a_classificacao_contabil_chega_inteira", sql: MIGRATION_064_A_CLASSIFICACAO_CONTABIL_CHEGA_INTEIRA },
33501
+ { id: "065_a_fusao_para_de_dobrar_a_propria_memoria", sql: MIGRATION_065_A_FUSAO_PARA_DE_DOBRAR_A_PROPRIA_MEMORIA },
33502
+ { id: "066_a_venda_de_balcao_tem_conta", sql: MIGRATION_066_A_VENDA_DE_BALCAO_TEM_CONTA },
33503
+ { id: "067_o_recibo_da_conta_atravessa", sql: MIGRATION_067_O_RECIBO_DA_CONTA_ATRAVESSA },
33504
+ { id: "068_a_unidade_entra_com_a_linha_nao_depois_dela", sql: MIGRATION_068_A_UNIDADE_ENTRA_COM_A_LINHA_NAO_DEPOIS_DELA },
33505
+ { id: "069_o_titulo_que_ja_existe_esta_migrado", sql: MIGRATION_069_O_TITULO_QUE_JA_EXISTE_ESTA_MIGRADO },
33506
+ { id: "070_a_parcela_nasce_antes_do_movimento", sql: MIGRATION_070_A_PARCELA_NASCE_ANTES_DO_MOVIMENTO },
33507
+ { id: "071_a_parcela_tem_uma_assinatura_so", sql: MIGRATION_071_A_PARCELA_TEM_UMA_ASSINATURA_SO },
33508
+ { id: "072_o_caixa_do_v1_guarda_quem_o_abriu", sql: MIGRATION_072_O_CAIXA_DO_V1_GUARDA_QUEM_O_ABRIU },
33509
+ { id: "073_o_caixa_pertence_a_conta_que_o_v1_lhe_deu", sql: MIGRATION_073_O_CAIXA_PERTENCE_A_CONTA_QUE_O_V1_LHE_DEU },
33510
+ { id: "074_o_movimento_volta_a_apontar_para_o_caixa", sql: MIGRATION_074_O_MOVIMENTO_VOLTA_A_APONTAR_PARA_O_CAIXA },
33511
+ { id: "075_a_venda_cancelada_cancela_o_titulo_dela", sql: MIGRATION_075_A_VENDA_CANCELADA_CANCELA_O_TITULO_DELA },
33512
+ { id: "076_o_plano_de_contas_do_cliente_vence_o_do_modelo", sql: MIGRATION_076_O_PLANO_DE_CONTAS_DO_CLIENTE_VENCE_O_DO_MODELO },
33513
+ { id: "077_o_resumo_financeiro_se_soma_no_banco", sql: MIGRATION_077_O_RESUMO_FINANCEIRO_SE_SOMA_NO_BANCO },
33514
+ { id: "078_o_saldo_da_conta_se_calcula_so_na_conta_pedida", sql: MIGRATION_078_O_SALDO_DA_CONTA_SE_CALCULA_SO_NA_CONTA_PEDIDA },
33515
+ { id: "079_as_portas_de_porte_novas_nao_nascem_abertas", sql: MIGRATION_079_AS_PORTAS_DE_PORTE_NOVAS_NAO_NASCEM_ABERTAS },
33516
+ { id: "080_a_remessa_e_a_comissao_a_pagar_tem_onde_morar", sql: MIGRATION_080_A_REMESSA_E_A_COMISSAO_A_PAGAR_TEM_ONDE_MORAR },
33517
+ { id: "081_a_remessa_e_a_comissao_atravessam", sql: MIGRATION_081_A_REMESSA_E_A_COMISSAO_ATRAVESSAM },
33518
+ { id: "082_o_beneficiario_da_comissao_e_uma_conta", sql: MIGRATION_082_O_BENEFICIARIO_DA_COMISSAO_E_UMA_CONTA },
33519
+ { id: "083_a_conta_do_banco_e_uma_conta_e_a_fatura_tem_dois_destinos", sql: MIGRATION_083_A_CONTA_DO_BANCO_E_UMA_CONTA_E_A_FATURA_TEM_DOIS_DESTINOS },
33520
+ { id: "084_a_nota_de_entrada_tem_um_titulo_a_pagar", sql: MIGRATION_084_A_NOTA_DE_ENTRADA_TEM_UM_TITULO_A_PAGAR },
33521
+ { id: "085_o_resumo_le_cinco_mil_linhas_e_nao_duzentas_e_oitenta_mil", sql: MIGRATION_085_O_RESUMO_LE_CINCO_MIL_LINHAS_E_NAO_DUZENTAS_E_OITENTA_MIL },
33522
+ { id: "086_a_cerca_se_pergunta_uma_vez_e_nao_uma_vez_por_linha", sql: MIGRATION_086_A_CERCA_SE_PERGUNTA_UMA_VEZ_E_NAO_UMA_VEZ_POR_LINHA },
33523
+ { id: "087_o_saldo_de_uma_fatura_nao_soma_o_razao_inteiro", sql: MIGRATION_087_O_SALDO_DE_UMA_FATURA_NAO_SOMA_O_RAZAO_INTEIRO },
33524
+ { id: "088_a_conta_sem_pessoa_e_do_sistema_nao_de_ninguem", sql: MIGRATION_088_A_CONTA_SEM_PESSOA_E_DO_SISTEMA_NAO_DE_NINGUEM }
33525
+ ];
33526
+ var STANDALONE_CONFIG = {
33527
+ currency: { code: "BRL", locale: "pt-BR", symbol: "R$" }
33528
+ };
33529
+ registerTranslations(financialLocales);
33530
+ function ReceivePaymentModal({
33531
+ installment,
33532
+ receiptUrl,
33533
+ onClose,
33534
+ onPaid
33535
+ }) {
33536
+ const provider = React15__default.useMemo(() => createSafeFinancialProvider(), []);
33537
+ const store6 = React15__default.useMemo(() => createFinancialStore(provider), [provider]);
33538
+ return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
33539
+ PaymentModal,
33540
+ {
33541
+ movement: installment,
33542
+ receiptUrl,
33543
+ onClose,
33544
+ onPaid
33545
+ }
33546
+ ) });
33547
+ }
32015
33548
  var RECEIVING_REGISTRIES = ["bank-accounts", "payment-method-types", "payment-methods", "card-brands"];
32016
33549
  function createFinancialSettingsPlugin(options) {
32017
33550
  registerTranslations(financialLocales);
@@ -32053,7 +33586,21 @@ function createFinancialSettingsPlugin(options) {
32053
33586
  locales: financialLocales
32054
33587
  };
32055
33588
  }
33589
+ var RECUSA_COMPRA = {
33590
+ sem_sessao: "Fa\xE7a login para registrar a compra.",
33591
+ sem_tenant: "Escolha a conta antes de registrar.",
33592
+ cliente_nao_encontrado: "Este cliente n\xE3o est\xE1 nesta conta.",
33593
+ nada_escolhido: "Escolha pelo menos um servi\xE7o."
33594
+ };
32056
33595
  var BRL = { code: "BRL", locale: "pt-BR", symbol: "R$" };
33596
+ var STATUS_CHIP = {
33597
+ paid: { label: "Pago", cls: "bg-success-soft text-success-soft-foreground" },
33598
+ partial: { label: "Parcial", cls: "bg-warning-soft text-warning-soft-foreground" },
33599
+ pending: { label: "Aberto", cls: "bg-info-soft text-info-soft-foreground" },
33600
+ open: { label: "Aberto", cls: "bg-info-soft text-info-soft-foreground" },
33601
+ overdue: { label: "Vencida", cls: "bg-destructive-soft text-destructive-soft-foreground" },
33602
+ cancelled: { label: "Cancelada", cls: "bg-muted text-muted-foreground" }
33603
+ };
32057
33604
  function ClientAccountPanel({
32058
33605
  personId,
32059
33606
  orderId,
@@ -32064,12 +33611,13 @@ function ClientAccountPanel({
32064
33611
  const provider = useFinancialProvider();
32065
33612
  const [account, setAccount] = React15__default.useState(null);
32066
33613
  const [busy, setBusy] = React15__default.useState(false);
32067
- const [paying, setPaying] = React15__default.useState(null);
32068
- async function openPayment(invoiceId) {
32069
- const res = await provider.getMovements({ invoiceId, pageSize: 200 });
32070
- const open = (res.data ?? []).find((m) => m.movementKind === "bill" && m.amount - (m.paidAmount ?? 0) > 5e-3);
32071
- if (open) setPaying(open);
32072
- }
33614
+ const [viewInvoice, setViewInvoice] = React15__default.useState(null);
33615
+ const [comprando, setComprando] = React15__default.useState(false);
33616
+ const [buscaServico, setBuscaServico] = React15__default.useState("");
33617
+ const [catalogo, setCatalogo] = React15__default.useState([]);
33618
+ const [escolhidos, setEscolhidos] = React15__default.useState([]);
33619
+ const [comprandoBusy, setComprandoBusy] = React15__default.useState(false);
33620
+ const [erroCompra, setErroCompra] = React15__default.useState(null);
32073
33621
  const load = React15__default.useCallback(async () => {
32074
33622
  if (!personId) return;
32075
33623
  setAccount(await provider.getClientAccount(personId));
@@ -32079,18 +33627,165 @@ function ClientAccountPanel({
32079
33627
  }, [load]);
32080
33628
  const forThis = account?.invoices.filter((i) => orderId && i.orderId === orderId) ?? [];
32081
33629
  const hasForThis = forThis.length > 0;
33630
+ React15__default.useEffect(() => {
33631
+ if (!comprando) return;
33632
+ let vivo = true;
33633
+ const t2 = setTimeout(async () => {
33634
+ try {
33635
+ const r = await provider.searchSellableServices?.(buscaServico);
33636
+ if (vivo) setCatalogo(r ?? []);
33637
+ } catch {
33638
+ if (vivo) setCatalogo([]);
33639
+ }
33640
+ }, 250);
33641
+ return () => {
33642
+ vivo = false;
33643
+ clearTimeout(t2);
33644
+ };
33645
+ }, [comprando, buscaServico]);
33646
+ async function confirmarCompra() {
33647
+ if (!escolhidos.length) return;
33648
+ setComprandoBusy(true);
33649
+ try {
33650
+ const r = await provider.prepayServices?.(personId, escolhidos.map((e) => ({
33651
+ serviceId: e.id,
33652
+ name: e.name,
33653
+ quantity: e.qtd,
33654
+ unitPrice: e.price
33655
+ })));
33656
+ if (!r?.ok) {
33657
+ setErroCompra(RECUSA_COMPRA[r?.reason ?? ""] ?? "N\xE3o foi poss\xEDvel registrar a compra.");
33658
+ return;
33659
+ }
33660
+ setComprando(false);
33661
+ setEscolhidos([]);
33662
+ await load();
33663
+ } finally {
33664
+ setComprandoBusy(false);
33665
+ }
33666
+ }
32082
33667
  const tiles = [
32083
33668
  { label: "Contratado", value: account?.contracted ?? 0, hint: "Tudo que o cliente j\xE1 contratou" },
32084
33669
  { label: "Pago", value: account?.paid ?? 0 },
32085
33670
  { label: "Em aberto", value: account?.open ?? 0 }
32086
33671
  ];
32087
- return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
33672
+ const aExecutar = (account?.paidToExecute ?? 0) + (account?.openToExecute ?? 0);
33673
+ if (account?.paidToExecute !== void 0 || account?.openToExecute !== void 0) {
33674
+ tiles.push({
33675
+ label: "A executar",
33676
+ value: aExecutar,
33677
+ hint: (account?.paidToExecute ?? 0) > 0 ? `${formatCurrency(account?.paidToExecute ?? 0, currency)} j\xE1 pagos \u2014 servi\xE7o que a casa ainda deve prestar` : "Contratado e ainda n\xE3o executado"
33678
+ });
33679
+ }
33680
+ if (account === null) {
33681
+ return /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-3", "aria-busy": "true", "aria-label": "Carregando a conta do cliente", children: [
33682
+ /* @__PURE__ */ jsx("div", { className: "fz-tiles fz-tiles-3", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-xl border px-3 py-2", children: [
33683
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-16 animate-pulse rounded bg-muted" }),
33684
+ /* @__PURE__ */ jsx("div", { className: "h-5 w-20 animate-pulse rounded bg-muted/70" })
33685
+ ] }, i)) }),
33686
+ [0, 1, 2].map((i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded-xl border px-3 py-3", children: [
33687
+ /* @__PURE__ */ jsx("div", { className: "h-4 w-40 animate-pulse rounded bg-muted" }),
33688
+ /* @__PURE__ */ jsx("div", { className: "h-7 w-32 animate-pulse rounded-button bg-muted/70" })
33689
+ ] }, i))
33690
+ ] });
33691
+ }
33692
+ return /* @__PURE__ */ jsxs("div", { className: "fz-container space-y-3", children: [
32088
33693
  clientName && /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: clientName }),
32089
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2", children: tiles.map((t2) => /* @__PURE__ */ jsxs("div", { title: t2.hint, className: "rounded-xl border px-3 py-2", children: [
33694
+ /* @__PURE__ */ jsx("div", { className: `fz-tiles ${tiles.length > 3 ? "fz-tiles-4" : "fz-tiles-3"}`, children: tiles.map((t2) => /* @__PURE__ */ jsxs("div", { title: t2.hint, className: "rounded-xl border px-3 py-2", children: [
32090
33695
  /* @__PURE__ */ jsx("p", { className: "text-[10px] uppercase text-muted-foreground", children: t2.label }),
32091
33696
  /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-sm font-semibold tabular-nums", children: formatCurrency(t2.value, currency) })
32092
33697
  ] }, t2.label)) }),
32093
- orderId && !hasForThis && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-xl border border-dashed px-3 py-3", children: [
33698
+ provider.prepayServices && !comprando && /* @__PURE__ */ jsx(
33699
+ "button",
33700
+ {
33701
+ type: "button",
33702
+ onClick: () => setComprando(true),
33703
+ className: "w-full rounded-xl border border-dashed px-3 py-2 text-xs font-medium text-muted-foreground hover:bg-muted/40 hover:text-foreground",
33704
+ children: "+ Compra antecipada"
33705
+ }
33706
+ ),
33707
+ comprando && /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-xl border px-3 py-3", children: [
33708
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium", children: "Compra antecipada" }),
33709
+ /* @__PURE__ */ jsxs("p", { className: "text-[11px] text-muted-foreground", children: [
33710
+ "O servi\xE7o fica pago e ",
33711
+ /* @__PURE__ */ jsx("strong", { children: "a executar" }),
33712
+ ", esperando o dia. N\xE3o gera contrato nem proposta."
33713
+ ] }),
33714
+ escolhidos.length > 0 && /* @__PURE__ */ jsx("ul", { className: "divide-y rounded-lg border", children: escolhidos.map((e) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-2 px-2 py-1.5", children: [
33715
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs", children: e.name }),
33716
+ /* @__PURE__ */ jsx(
33717
+ "input",
33718
+ {
33719
+ type: "number",
33720
+ min: 1,
33721
+ value: e.qtd,
33722
+ onChange: (ev) => setEscolhidos((xs) => xs.map((x) => x.id === e.id ? { ...x, qtd: Math.max(1, Math.floor(Number(ev.target.value) || 1)) } : x)),
33723
+ className: "w-12 shrink-0 rounded-input border border-input bg-card px-1 py-0.5 text-center text-xs",
33724
+ "aria-label": `Quantidade de ${e.name}`
33725
+ }
33726
+ ),
33727
+ /* @__PURE__ */ jsx("span", { className: "w-20 shrink-0 text-right text-xs tabular-nums", children: formatCurrency(e.price * e.qtd, currency) }),
33728
+ /* @__PURE__ */ jsx(
33729
+ "button",
33730
+ {
33731
+ type: "button",
33732
+ onClick: () => setEscolhidos((xs) => xs.filter((x) => x.id !== e.id)),
33733
+ className: "shrink-0 text-[11px] text-destructive hover:underline",
33734
+ children: "Tirar"
33735
+ }
33736
+ )
33737
+ ] }, e.id)) }),
33738
+ /* @__PURE__ */ jsx(
33739
+ "input",
33740
+ {
33741
+ value: buscaServico,
33742
+ onChange: (e) => setBuscaServico(e.target.value),
33743
+ placeholder: "Buscar servi\xE7o\u2026",
33744
+ className: "w-full rounded-input border border-input bg-card px-2 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-ring"
33745
+ }
33746
+ ),
33747
+ /* @__PURE__ */ jsx("ul", { className: "max-h-40 space-y-0.5 overflow-y-auto", children: catalogo.filter((c) => !escolhidos.some((e) => e.id === c.id)).map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
33748
+ "button",
33749
+ {
33750
+ type: "button",
33751
+ onClick: () => setEscolhidos((xs) => [...xs, { ...c, qtd: 1 }]),
33752
+ className: "flex w-full items-center gap-2 rounded px-2 py-1 text-left hover:bg-muted",
33753
+ children: [
33754
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-xs", children: c.name }),
33755
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] tabular-nums text-muted-foreground", children: formatCurrency(c.price, currency) })
33756
+ ]
33757
+ }
33758
+ ) }, c.id)) }),
33759
+ erroCompra && /* @__PURE__ */ jsx("p", { className: "rounded-lg bg-destructive/10 px-2 py-1.5 text-[11px] text-destructive", children: erroCompra }),
33760
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 pt-1", children: [
33761
+ /* @__PURE__ */ jsx(
33762
+ "button",
33763
+ {
33764
+ type: "button",
33765
+ onClick: () => {
33766
+ setComprando(false);
33767
+ setEscolhidos([]);
33768
+ },
33769
+ className: "rounded-button px-2 py-1 text-xs text-muted-foreground hover:text-foreground",
33770
+ children: "Agora n\xE3o"
33771
+ }
33772
+ ),
33773
+ /* @__PURE__ */ jsx(
33774
+ "button",
33775
+ {
33776
+ type: "button",
33777
+ onClick: confirmarCompra,
33778
+ disabled: comprandoBusy || !escolhidos.length,
33779
+ className: "rounded-button bg-foreground px-3 py-1.5 text-xs font-semibold text-background disabled:opacity-40",
33780
+ children: comprandoBusy ? "Registrando\u2026" : `Registrar \xB7 ${formatCurrency(
33781
+ escolhidos.reduce((t2, e) => t2 + e.price * e.qtd, 0),
33782
+ currency
33783
+ )}`
33784
+ }
33785
+ )
33786
+ ] })
33787
+ ] }),
33788
+ onGenerate && !hasForThis && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-xl border border-dashed px-3 py-3", children: [
32094
33789
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
32095
33790
  /* @__PURE__ */ jsx("p", { className: "text-xs font-medium", children: "Este atendimento ainda n\xE3o tem cobran\xE7a" }),
32096
33791
  /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: "Gere o recebimento para cobrar os servi\xE7os deste atendimento." })
@@ -32138,16 +33833,19 @@ function ClientAccountPanel({
32138
33833
  /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold tabular-nums", children: formatCurrency(i.total, currency) }),
32139
33834
  /* @__PURE__ */ jsx("p", { className: `text-[10px] tabular-nums ${i.balance > 0 ? "text-destructive" : "text-success"}`, children: i.balance > 0 ? `resta ${formatCurrency(i.balance, currency)}` : "pago" })
32140
33835
  ] }),
32141
- i.balance > 0 && /* @__PURE__ */ jsxs(
33836
+ /* @__PURE__ */ jsx(
32142
33837
  "button",
32143
33838
  {
32144
33839
  type: "button",
32145
- onClick: () => void openPayment(i.id),
33840
+ onClick: () => setViewInvoice(i),
32146
33841
  className: "inline-flex shrink-0 items-center gap-1 rounded-button border border-input px-2.5 py-1.5 text-xs font-medium hover:bg-muted",
32147
- children: [
33842
+ children: i.balance > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
32148
33843
  /* @__PURE__ */ jsx(CircleDollarSign, { className: "h-3.5 w-3.5" }),
32149
33844
  " Receber"
32150
- ]
33845
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
33846
+ /* @__PURE__ */ jsx(FileText, { className: "h-3.5 w-3.5" }),
33847
+ " Ver fatura"
33848
+ ] })
32151
33849
  }
32152
33850
  )
32153
33851
  ]
@@ -32156,19 +33854,44 @@ function ClientAccountPanel({
32156
33854
  );
32157
33855
  }) })
32158
33856
  ] }),
32159
- paying && /* @__PURE__ */ jsx(
32160
- PaymentModal,
33857
+ viewInvoice && /* @__PURE__ */ jsx(
33858
+ ModalTask,
32161
33859
  {
32162
- movement: paying,
32163
- onClose: () => setPaying(null),
32164
- onPaid: () => {
32165
- setPaying(null);
33860
+ open: true,
33861
+ title: /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 items-center gap-2", children: [
33862
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: viewInvoice.number ?? "Recebimento" }),
33863
+ (() => {
33864
+ const s = STATUS_CHIP[viewInvoice.status];
33865
+ return s ? /* @__PURE__ */ jsx("span", { className: `shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium ${s.cls}`, children: s.label }) : null;
33866
+ })()
33867
+ ] }),
33868
+ hint: clientName ?? void 0,
33869
+ width: "max-w-3xl",
33870
+ onClose: () => {
33871
+ setViewInvoice(null);
32166
33872
  void load();
32167
- }
33873
+ },
33874
+ children: /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-4 py-3", children: /* @__PURE__ */ jsx(
33875
+ InvoiceDetailView,
33876
+ {
33877
+ embedded: true,
33878
+ invoiceId: viewInvoice.id,
33879
+ direction: "credit",
33880
+ onBack: () => {
33881
+ setViewInvoice(null);
33882
+ void load();
33883
+ }
33884
+ }
33885
+ ) })
32168
33886
  }
32169
33887
  )
32170
33888
  ] });
32171
33889
  }
33890
+ function ClientAccountInline(props) {
33891
+ const provider = React15__default.useMemo(() => createSafeFinancialProvider(), []);
33892
+ const store6 = React15__default.useMemo(() => createFinancialStore(provider), [provider]);
33893
+ return /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(ClientAccountPanel, { ...props }) });
33894
+ }
32172
33895
  function ClientAccountModal({
32173
33896
  personId,
32174
33897
  orderId,
@@ -32176,17 +33899,15 @@ function ClientAccountModal({
32176
33899
  onGenerate,
32177
33900
  onClose
32178
33901
  }) {
32179
- const provider = React15__default.useMemo(() => createSafeFinancialProvider(), []);
32180
- const store6 = React15__default.useMemo(() => createFinancialStore(provider), [provider]);
32181
- return /* @__PURE__ */ jsx(RightRailPage, { title: "Conta do cliente", open: true, onClose, width: "max-w-md", children: /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsx(FinancialContextProvider, { config: STANDALONE_CONFIG, provider, store: store6, children: /* @__PURE__ */ jsx(
32182
- ClientAccountPanel,
33902
+ return /* @__PURE__ */ jsx(RightRailPage, { title: "Conta do cliente", open: true, onClose, width: "max-w-md", children: /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-4 py-3", children: /* @__PURE__ */ jsx(
33903
+ ClientAccountInline,
32183
33904
  {
32184
33905
  personId,
32185
33906
  orderId,
32186
33907
  clientName,
32187
33908
  onGenerate
32188
33909
  }
32189
- ) }) }) });
33910
+ ) }) });
32190
33911
  }
32191
33912
 
32192
33913
  // src/index.ts
@@ -32551,7 +34272,7 @@ function createFinancialPlugin(options) {
32551
34272
  component: (() => {
32552
34273
  const FinancialSettingsTab = () => React15__default.createElement(PluginSettingsPanel, {
32553
34274
  title: "Financial Settings",
32554
- generalSettings: React15__default.createElement(FinancialGeneralSettings),
34275
+ generalSettings: React15__default.createElement(FinancialGeneralSettings, { config }),
32555
34276
  registries,
32556
34277
  hostPluginId: "financial",
32557
34278
  routeBase: "/settings/financial"
@@ -32613,6 +34334,6 @@ function createFinancialPlugin(options) {
32613
34334
  };
32614
34335
  }
32615
34336
 
32616
- export { ClientAccountModal, ClientAccountPanel, FINANCIAL_NAV_FEATURES, ReceivePaymentModal, buildFinancialDomainEvents, buildFinancialMessageDefaults, createFinancialPlugin, createFinancialSettingsPlugin, createMockFinancialProvider, createSafeFinancialProvider };
34337
+ export { ClientAccountInline, ClientAccountModal, ClientAccountPanel, FINANCIAL_NAV_FEATURES, ReceivePaymentModal, buildFinancialDomainEvents, buildFinancialMessageDefaults, createFinancialPlugin, createFinancialSettingsPlugin, createMockFinancialProvider, createSafeFinancialProvider };
32617
34338
  //# sourceMappingURL=index.js.map
32618
34339
  //# sourceMappingURL=index.js.map