@fayz-ai/plugin-financial 0.8.0 → 0.8.1

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.
@@ -34,6 +34,12 @@ export interface ResolvedFinancialConfig {
34
34
  entityLookups: EntityLookupMap;
35
35
  contactLookup?: EntityLookup;
36
36
  onBookingClick?: (orderId: string) => void;
37
+ /** Internal nav item id → access feature id, exactly as the APP declared it
38
+ * (`options.navFeatureMap`; empty when the app gates nothing — the plugin
39
+ * ships no defaults). Drives BOTH the sub-module link paywall
40
+ * (`useModuleNavAccess`) and the matching content gate, so the two can't
41
+ * drift. Unmapped ids are ungated. */
42
+ navFeatureMap: Record<string, string>;
37
43
  }
38
44
  export declare const FinancialContextProvider: import("react").FC<{
39
45
  config: ResolvedFinancialConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"FinancialContext.d.ts","sourceRoot":"","sources":["../src/FinancialContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AACjI,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACpE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,mGAAmG;IACnG,cAAc,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,oFAAoF;IACpF,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,sBAAsB,EAAE,OAAO,CAAA;IAC/B,aAAa,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACvD,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,aAAa,EAAE,eAAe,CAAA;IAC9B,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAC3C;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"FinancialContext.d.ts","sourceRoot":"","sources":["../src/FinancialContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,eAAe,CAAA;AACjI,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACpE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAM/C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,OAAO,CAAA;IACpB,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,mGAAmG;IACnG,cAAc,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,oFAAoF;IACpF,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,sBAAsB,EAAE,OAAO,CAAA;IAC/B,aAAa,EAAE;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IACvD,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,aAAa,EAAE,eAAe,CAAA;IAC9B,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C;;;;2CAIuC;IACvC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"FinancialPage.d.ts","sourceRoot":"","sources":["../src/FinancialPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAA;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAG/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,eAAe,CAAA;AAsBzE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;IACzC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAqHD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IACrE,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC,qBAuMA"}
1
+ {"version":3,"file":"FinancialPage.d.ts","sourceRoot":"","sources":["../src/FinancialPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAA;AACxE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAG/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,eAAe,CAAA;AAsBzE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,CAAA;IACzC,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AA6HD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IACrE,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC,qBA2OA"}
@@ -1 +1 @@
1
- {"version":3,"file":"FinancialGeneralSettings.d.ts","sourceRoot":"","sources":["../../src/components/FinancialGeneralSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,wBAAgB,wBAAwB,sBAqBvC"}
1
+ {"version":3,"file":"FinancialGeneralSettings.d.ts","sourceRoot":"","sources":["../../src/components/FinancialGeneralSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAgBzB,wBAAgB,wBAAwB,sBAsBvC"}
@@ -1 +1 @@
1
- {"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AA0JpD,wBAAgB,+BAA+B,IAAI,qBAAqB,CAytBvE"}
1
+ {"version":3,"file":"supabase.d.ts","sourceRoot":"","sources":["../../src/data/supabase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AA0JpD,wBAAgB,+BAA+B,IAAI,qBAAqB,CA8tBvE"}
package/dist/index.cjs CHANGED
@@ -273,6 +273,8 @@ function QuickTransactionForm({ open, onOpenChange, defaultType = "expense", def
273
273
  const { currency } = useFinancialConfig();
274
274
  const provider = useFinancialProvider();
275
275
  const createQuickTransaction = useFinancialStore((s) => s.createQuickTransaction);
276
+ const guardMovements = saas.useLimitGuard("movements_month");
277
+ const can = saas.usePermissionOptional();
276
278
  const [type, setType] = React17.useState(defaultType);
277
279
  const [amount, setAmount] = React17.useState(0);
278
280
  const [amountStr, setAmountStr] = React17.useState("");
@@ -358,12 +360,14 @@ function QuickTransactionForm({ open, onOpenChange, defaultType = "expense", def
358
360
  }
359
361
  const canSave = amount > 0 && !saving && (type === "transfer" ? !!accountId && !!toAccountId && accountId !== toAccountId : true);
360
362
  async function handleSave() {
363
+ if (!can("financial", "create")) return;
361
364
  if (!canSave) {
362
365
  if (type === "transfer" && accountId && accountId === toAccountId) {
363
366
  ui.toast.error(t2("financial.transfer.sameAccount"));
364
367
  }
365
368
  return;
366
369
  }
370
+ if (await guardMovements(type === "transfer" ? 2 : 1) === "blocked") return;
367
371
  setSaving(true);
368
372
  try {
369
373
  await createQuickTransaction({
@@ -378,6 +382,7 @@ function QuickTransactionForm({ open, onOpenChange, defaultType = "expense", def
378
382
  recurring: type === "transfer" ? false : recurring,
379
383
  receiptUrl: type === "transfer" ? void 0 : receiptUrl
380
384
  });
385
+ saas.invalidateLimit("movements_month");
381
386
  onOpenChange(false);
382
387
  } catch {
383
388
  } finally {
@@ -666,6 +671,23 @@ var STATUS_OPTIONS = [
666
671
  { value: "overdue", labelKey: "financial.invoice.statusOverdue", color: "bg-destructive-soft text-destructive-soft-foreground", icon: lucideReact.CircleAlert },
667
672
  { value: "cancelled", labelKey: "financial.invoice.statusCancelled", color: "bg-muted text-muted-foreground", icon: lucideReact.Ban }
668
673
  ];
674
+ var isoDate = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
675
+ function presetRange(preset) {
676
+ const now2 = /* @__PURE__ */ new Date();
677
+ switch (preset) {
678
+ case "thisMonth":
679
+ return { from: isoDate(new Date(now2.getFullYear(), now2.getMonth(), 1)), to: isoDate(new Date(now2.getFullYear(), now2.getMonth() + 1, 0)) };
680
+ case "lastMonth":
681
+ return { from: isoDate(new Date(now2.getFullYear(), now2.getMonth() - 1, 1)), to: isoDate(new Date(now2.getFullYear(), now2.getMonth(), 0)) };
682
+ case "last30": {
683
+ const from = new Date(now2);
684
+ from.setDate(from.getDate() - 29);
685
+ return { from: isoDate(from), to: isoDate(now2) };
686
+ }
687
+ case "thisYear":
688
+ return { from: isoDate(new Date(now2.getFullYear(), 0, 1)), to: isoDate(new Date(now2.getFullYear(), 11, 31)) };
689
+ }
690
+ }
669
691
  function StatusBadge({ status }) {
670
692
  const t2 = core.useTranslation();
671
693
  const opt = STATUS_OPTIONS.find((o) => o.value === status);
@@ -729,6 +751,8 @@ function useInvoiceColumns(currency) {
729
751
  }
730
752
  function InvoiceListView({ direction, onNew, onEdit }) {
731
753
  const t2 = core.useTranslation();
754
+ const can = saas.usePermissionOptional();
755
+ const onNewGated = can("financial", "create") ? onNew : void 0;
732
756
  const { currency } = useFinancialConfig();
733
757
  const provider = useFinancialProvider();
734
758
  const invoices = useFinancialStore((s) => s.invoices);
@@ -741,6 +765,25 @@ function InvoiceListView({ direction, onNew, onEdit }) {
741
765
  const [costCenterId, setCostCenterId] = React17.useState("");
742
766
  const [accounts, setAccounts] = React17.useState([]);
743
767
  const [costCenters, setCostCenters] = React17.useState([]);
768
+ const [datePreset, setDatePreset] = React17.useState("all");
769
+ const [dateFrom, setDateFrom] = React17.useState("");
770
+ const [dateTo, setDateTo] = React17.useState("");
771
+ const dateRange = React17.useMemo(() => {
772
+ if (!dateFrom && !dateTo) return void 0;
773
+ return { from: dateFrom || "0001-01-01", to: dateTo || "9999-12-31" };
774
+ }, [dateFrom, dateTo]);
775
+ const applyPreset = (value) => {
776
+ setDatePreset(value);
777
+ if (value === "all") {
778
+ setDateFrom("");
779
+ setDateTo("");
780
+ return;
781
+ }
782
+ if (value === "custom") return;
783
+ const range = presetRange(value);
784
+ setDateFrom(range.from);
785
+ setDateTo(range.to);
786
+ };
744
787
  React17.useEffect(() => {
745
788
  let active = true;
746
789
  Promise.all([provider.getChartOfAccounts(), provider.getCostCenters()]).then(([accountRows, costCenterRows]) => {
@@ -762,9 +805,10 @@ function InvoiceListView({ direction, onNew, onEdit }) {
762
805
  status: statusFilter.length > 0 ? statusFilter : void 0,
763
806
  search: search || void 0,
764
807
  accountId: accountId || void 0,
765
- costCenterId: costCenterId || void 0
808
+ costCenterId: costCenterId || void 0,
809
+ dateRange
766
810
  });
767
- }, [direction, statusFilter, search, accountId, costCenterId]);
811
+ }, [direction, statusFilter, search, accountId, costCenterId, dateRange]);
768
812
  const filtered = invoices.filter((inv) => inv.direction === direction);
769
813
  const listTitle = direction === "debit" ? t2("financial.invoice.accountsPayable") : t2("financial.invoice.accountsReceivable");
770
814
  const columns = useInvoiceColumns(currency);
@@ -794,7 +838,67 @@ function InvoiceListView({ direction, onNew, onEdit }) {
794
838
  subtitle: t2("financial.invoice.invoices", { count: filtered.length })
795
839
  }
796
840
  ),
797
- (accounts.length > 0 || costCenters.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-3 rounded-lg border bg-muted/20 px-4 py-3", children: [
841
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-3 rounded-lg border bg-muted/20 px-4 py-3", children: [
842
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-[180px] flex-col gap-1 text-xs font-medium text-muted-foreground", children: [
843
+ t2("financial.invoice.filterPeriod"),
844
+ /* @__PURE__ */ jsxRuntime.jsxs(
845
+ "select",
846
+ {
847
+ className: "h-9 rounded-md border bg-background px-3 text-sm text-foreground",
848
+ value: datePreset,
849
+ onChange: (event) => applyPreset(event.target.value),
850
+ children: [
851
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: t2("financial.invoice.filterAllPeriods") }),
852
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "thisMonth", children: t2("financial.invoice.filterThisMonth") }),
853
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "lastMonth", children: t2("financial.invoice.filterLastMonth") }),
854
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "last30", children: t2("financial.invoice.filterLast30") }),
855
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "thisYear", children: t2("financial.invoice.filterThisYear") }),
856
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "custom", children: t2("financial.invoice.filterCustom") })
857
+ ]
858
+ }
859
+ )
860
+ ] }),
861
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium text-muted-foreground", children: [
862
+ t2("financial.invoice.filterFrom"),
863
+ /* @__PURE__ */ jsxRuntime.jsx(
864
+ "input",
865
+ {
866
+ type: "date",
867
+ className: "h-9 rounded-md border bg-background px-3 text-sm text-foreground",
868
+ value: dateFrom,
869
+ max: dateTo || void 0,
870
+ onChange: (event) => {
871
+ setDateFrom(event.target.value);
872
+ setDatePreset("custom");
873
+ }
874
+ }
875
+ )
876
+ ] }),
877
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex flex-col gap-1 text-xs font-medium text-muted-foreground", children: [
878
+ t2("financial.invoice.filterTo"),
879
+ /* @__PURE__ */ jsxRuntime.jsx(
880
+ "input",
881
+ {
882
+ type: "date",
883
+ className: "h-9 rounded-md border bg-background px-3 text-sm text-foreground",
884
+ value: dateTo,
885
+ min: dateFrom || void 0,
886
+ onChange: (event) => {
887
+ setDateTo(event.target.value);
888
+ setDatePreset("custom");
889
+ }
890
+ }
891
+ )
892
+ ] }),
893
+ (dateFrom || dateTo) && /* @__PURE__ */ jsxRuntime.jsx(
894
+ "button",
895
+ {
896
+ type: "button",
897
+ className: "h-9 rounded-md px-2 text-xs font-medium text-muted-foreground hover:text-foreground",
898
+ onClick: () => applyPreset("all"),
899
+ children: t2("financial.invoice.filterClear")
900
+ }
901
+ ),
798
902
  accounts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground", children: [
799
903
  t2("financial.invoice.filterAccount"),
800
904
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -848,12 +952,12 @@ function InvoiceListView({ direction, onNew, onEdit }) {
848
952
  activeTag: statusFilter.length === 1 ? statusFilter[0] : void 0,
849
953
  onTagChange: (v) => setStatusFilter(v ? [v] : []),
850
954
  newLabel: t2("financial.invoice.new"),
851
- onNew,
955
+ onNew: onNewGated,
852
956
  onRowClick: (row) => onEdit?.(row.id),
853
957
  emptyIcon: lucideReact.FileText,
854
958
  emptyMessage: t2("financial.invoice.noInvoices"),
855
- emptyActionLabel: onNew ? t2("financial.invoice.createFirst") : void 0,
856
- onEmptyAction: onNew
959
+ emptyActionLabel: onNewGated ? t2("financial.invoice.createFirst") : void 0,
960
+ onEmptyAction: onNewGated
857
961
  }
858
962
  ),
859
963
  filtered.length > 0 && !invoicesLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-6 rounded-lg border bg-muted/30 px-5 py-3 text-sm", children: [
@@ -1193,6 +1297,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
1193
1297
  const hasItemLookups = itemTypes.some((tp) => config.entityLookups[tp.value]);
1194
1298
  const defaultItemKind = itemTypes.find((tp) => tp.value === "other")?.value ?? itemTypes.find((tp) => !config.entityLookups[tp.value])?.value ?? itemTypes[0]?.value ?? "other";
1195
1299
  const createInvoice = useFinancialStore((s) => s.createInvoice);
1300
+ const guardMovements = saas.useLimitGuard("movements_month");
1301
+ const can = saas.usePermissionOptional();
1196
1302
  const hasLocations = locations.length > 0;
1197
1303
  const isEdit = !!editId;
1198
1304
  const [loaded, setLoaded] = React17.useState(!editId);
@@ -1330,10 +1436,12 @@ function InvoiceFormView({ direction, editId, onSaved }) {
1330
1436
  setInstallments(inst);
1331
1437
  }
1332
1438
  async function handleSave() {
1439
+ if (!isEdit && !can("financial", "create")) return;
1333
1440
  if (items.length === 0) {
1334
1441
  ui.toast.error(t2("common.formIncomplete"));
1335
1442
  return;
1336
1443
  }
1444
+ if (!isEdit && await guardMovements(Math.max(1, installments.length)) === "blocked") return;
1337
1445
  setSaving(true);
1338
1446
  try {
1339
1447
  const movInputs = installments.map((inst, i) => ({
@@ -1375,6 +1483,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
1375
1483
  items: items.map(({ _id, ...rest }) => rest),
1376
1484
  installments: movInputs
1377
1485
  });
1486
+ saas.invalidateLimit("movements_month");
1378
1487
  onSaved?.(invoice.id);
1379
1488
  }
1380
1489
  } finally {
@@ -3516,7 +3625,7 @@ function CardsView() {
3516
3625
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: t2("financial.cards.dueOn", { day: String(card.dueDay) }) })
3517
3626
  ] })
3518
3627
  ] }),
3519
- /* @__PURE__ */ jsxRuntime.jsxs(
3628
+ /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "financial", action: "create", children: /* @__PURE__ */ jsxRuntime.jsxs(
3520
3629
  "button",
3521
3630
  {
3522
3631
  type: "button",
@@ -3527,7 +3636,7 @@ function CardsView() {
3527
3636
  t2("financial.cards.addExpense")
3528
3637
  ]
3529
3638
  }
3530
- )
3639
+ ) })
3531
3640
  ] })
3532
3641
  ] }, card.id);
3533
3642
  }) })
@@ -3688,28 +3797,31 @@ function ReconciliationView() {
3688
3797
  ] })
3689
3798
  ] });
3690
3799
  }
3800
+ var FINANCIAL_DEFAULTS = {
3801
+ autoInstallments: true,
3802
+ requireDoc: false,
3803
+ allowPartial: true,
3804
+ requireOpening: true,
3805
+ autoClose: false,
3806
+ overdueAlerts: true,
3807
+ dailySummary: false
3808
+ };
3691
3809
  function FinancialGeneralSettings() {
3692
3810
  const t2 = core.useTranslation();
3811
+ const s = saas.useTenantPluginSettings("financial", FINANCIAL_DEFAULTS);
3693
3812
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
3694
3813
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("financial.settings.invoices"), description: t2("financial.settings.invoicesDesc"), children: [
3695
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.autoInstallments"), description: t2("financial.settings.autoInstallmentsDesc"), checked: true, onChange: () => {
3696
- } }),
3697
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.requireDoc"), description: t2("financial.settings.requireDocDesc"), checked: false, onChange: () => {
3698
- } }),
3699
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.allowPartial"), description: t2("financial.settings.allowPartialDesc"), checked: true, onChange: () => {
3700
- } })
3814
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.autoInstallments"), description: t2("financial.settings.autoInstallmentsDesc"), checked: s.get("autoInstallments"), onChange: (v) => s.set("autoInstallments", v) }),
3815
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.requireDoc"), description: t2("financial.settings.requireDocDesc"), checked: s.get("requireDoc"), onChange: (v) => s.set("requireDoc", v) }),
3816
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.allowPartial"), description: t2("financial.settings.allowPartialDesc"), checked: s.get("allowPartial"), onChange: (v) => s.set("allowPartial", v) })
3701
3817
  ] }),
3702
3818
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("financial.settings.cashRegister"), description: t2("financial.settings.cashRegisterDesc"), children: [
3703
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.requireOpening"), description: t2("financial.settings.requireOpeningDesc"), checked: true, onChange: () => {
3704
- } }),
3705
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.autoClose"), description: t2("financial.settings.autoCloseDesc"), checked: false, onChange: () => {
3706
- } })
3819
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.requireOpening"), description: t2("financial.settings.requireOpeningDesc"), checked: s.get("requireOpening"), onChange: (v) => s.set("requireOpening", v) }),
3820
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.autoClose"), description: t2("financial.settings.autoCloseDesc"), checked: s.get("autoClose"), onChange: (v) => s.set("autoClose", v) })
3707
3821
  ] }),
3708
3822
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("financial.settings.notifications"), description: t2("financial.settings.notificationsDesc"), children: [
3709
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.overdueAlerts"), description: t2("financial.settings.overdueAlertsDesc"), checked: true, onChange: () => {
3710
- } }),
3711
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.dailySummary"), description: t2("financial.settings.dailySummaryDesc"), checked: false, onChange: () => {
3712
- } })
3823
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.overdueAlerts"), description: t2("financial.settings.overdueAlertsDesc"), checked: s.get("overdueAlerts"), onChange: (v) => s.set("overdueAlerts", v) }),
3824
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("financial.settings.dailySummary"), description: t2("financial.settings.dailySummaryDesc"), checked: s.get("dailySummary"), onChange: (v) => s.set("dailySummary", v) })
3713
3825
  ] })
3714
3826
  ] });
3715
3827
  }
@@ -3850,6 +3962,10 @@ function buildNav(config, activeView, navigate, t2) {
3850
3962
  ]
3851
3963
  });
3852
3964
  }
3965
+ for (const item of items) {
3966
+ const feature = config.navFeatureMap[item.id];
3967
+ if (feature) item.feature = feature;
3968
+ }
3853
3969
  return items;
3854
3970
  }
3855
3971
  function FinancialPage({ config, provider, store, registries }) {
@@ -3885,9 +4001,11 @@ function FinancialPage({ config, provider, store, registries }) {
3885
4001
  };
3886
4002
  }, [onboardingComplete, provider]);
3887
4003
  const t2 = core.useTranslation();
4004
+ const can = saas.usePermissionOptional();
4005
+ const canCreate = can("financial", "create");
3888
4006
  const intent = parseIntent(view);
3889
4007
  const isSettings = view === "settings";
3890
- const nav = buildNav(config, view, navigate, t2);
4008
+ const nav = saas.useModuleNavAccess(buildNav(config, view, navigate, t2));
3891
4009
  const [quickAddOpen, setQuickAddOpen] = React17.useState(false);
3892
4010
  const [quickAddType, setQuickAddType] = React17.useState("expense");
3893
4011
  const [quickAddReceipt, setQuickAddReceipt] = React17.useState(false);
@@ -3901,12 +4019,14 @@ function FinancialPage({ config, provider, store, registries }) {
3901
4019
  if (!pendingQuickAdd) return;
3902
4020
  const req = consumeQuickAdd();
3903
4021
  if (!req) return;
4022
+ if (!canCreate) return;
3904
4023
  setQuickAddType(req.type);
3905
4024
  setQuickAddReceipt(req.receipt);
3906
4025
  setQuickAddOpen(true);
3907
4026
  }, [pendingQuickAdd]);
3908
4027
  const quickActions = React17.useMemo(() => {
3909
4028
  if (config.quickAdd) return [];
4029
+ if (!canCreate) return [];
3910
4030
  const actions = [];
3911
4031
  if (config.modules.payables) {
3912
4032
  actions.push({
@@ -3936,7 +4056,7 @@ function FinancialPage({ config, provider, store, registries }) {
3936
4056
  });
3937
4057
  }
3938
4058
  return actions;
3939
- }, [config.modules, config.quickAdd]);
4059
+ }, [config.modules, config.quickAdd, canCreate, navigate, t2]);
3940
4060
  function handleOnboardingComplete() {
3941
4061
  setOnboardingComplete(true);
3942
4062
  try {
@@ -3980,6 +4100,11 @@ function FinancialPage({ config, provider, store, registries }) {
3980
4100
  return /* @__PURE__ */ jsxRuntime.jsx(SummaryView, {});
3981
4101
  }
3982
4102
  }
4103
+ function renderGatedView() {
4104
+ const feature = config.navFeatureMap[intent.view];
4105
+ if (!feature) return renderView();
4106
+ return /* @__PURE__ */ jsxRuntime.jsx(saas.EntitlementGate, { feature, fallback: /* @__PURE__ */ jsxRuntime.jsx(saas.UpgradePrompt, { feature }), children: renderView() });
4107
+ }
3983
4108
  return /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsxs(
3984
4109
  ui.ModulePage,
3985
4110
  {
@@ -3994,7 +4119,8 @@ function FinancialPage({ config, provider, store, registries }) {
3994
4119
  // add affordance in B2C mode. The receipt entry point lives inside the
3995
4120
  // sheet's "Fotografar ou anexar recibo" attachment step, so no separate
3996
4121
  // "Enviar recibo" button is needed here.
3997
- /* @__PURE__ */ jsxRuntime.jsxs(
4122
+ // Gated on the role's `create` action so read-only members don't see it.
4123
+ /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "financial", action: "create", children: /* @__PURE__ */ jsxRuntime.jsxs(
3998
4124
  ui.Button,
3999
4125
  {
4000
4126
  size: "sm",
@@ -4005,7 +4131,7 @@ function FinancialPage({ config, provider, store, registries }) {
4005
4131
  t2("financial.quickTx.newTransaction")
4006
4132
  ]
4007
4133
  }
4008
- ),
4134
+ ) }),
4009
4135
  /* @__PURE__ */ jsxRuntime.jsx(
4010
4136
  saas.ModuleActionBar,
4011
4137
  {
@@ -4016,7 +4142,7 @@ function FinancialPage({ config, provider, store, registries }) {
4016
4142
  )
4017
4143
  ] }),
4018
4144
  children: [
4019
- renderView(),
4145
+ renderGatedView(),
4020
4146
  /* @__PURE__ */ jsxRuntime.jsx(
4021
4147
  QuickTransactionForm,
4022
4148
  {
@@ -5175,7 +5301,10 @@ function createSupabaseFinancialProvider() {
5175
5301
  }
5176
5302
  if (query.contactId) qb = qb.eq("party_id", query.contactId);
5177
5303
  if (query.search) qb = qb.ilike("notes", `%${query.search}%`);
5178
- if (query.dateRange) qb = qb.gte("created_at", query.dateRange.from).lte("created_at", query.dateRange.to);
5304
+ if (query.dateRange) {
5305
+ const to = query.dateRange.to.length === 10 ? `${query.dateRange.to}T23:59:59.999` : query.dateRange.to;
5306
+ qb = qb.gte("created_at", query.dateRange.from).lte("created_at", to);
5307
+ }
5179
5308
  const page = query.page ?? 1;
5180
5309
  const pageSize = query.pageSize ?? 50;
5181
5310
  qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("created_at", { ascending: false });
@@ -6412,6 +6541,16 @@ var en = {
6412
6541
  "financial.invoice.filterAllAccounts": "All accounts",
6413
6542
  "financial.invoice.filterCostCenter": "Cost center",
6414
6543
  "financial.invoice.filterAllCostCenters": "All cost centers",
6544
+ "financial.invoice.filterPeriod": "Period",
6545
+ "financial.invoice.filterAllPeriods": "All time",
6546
+ "financial.invoice.filterThisMonth": "This month",
6547
+ "financial.invoice.filterLastMonth": "Last month",
6548
+ "financial.invoice.filterLast30": "Last 30 days",
6549
+ "financial.invoice.filterThisYear": "This year",
6550
+ "financial.invoice.filterCustom": "Custom",
6551
+ "financial.invoice.filterFrom": "From",
6552
+ "financial.invoice.filterTo": "To",
6553
+ "financial.invoice.filterClear": "Clear",
6415
6554
  "financial.reconciliation.title": "Reconciliation",
6416
6555
  "financial.reconciliation.subtitle": "Match imported bank lines to your movements",
6417
6556
  "financial.reconciliation.pending": "{{count}} to reconcile",
@@ -6895,6 +7034,16 @@ var ptBR = {
6895
7034
  "financial.invoice.filterAllAccounts": "Todos os planos",
6896
7035
  "financial.invoice.filterCostCenter": "Centro de custo",
6897
7036
  "financial.invoice.filterAllCostCenters": "Todos os centros",
7037
+ "financial.invoice.filterPeriod": "Per\xEDodo",
7038
+ "financial.invoice.filterAllPeriods": "Todo o per\xEDodo",
7039
+ "financial.invoice.filterThisMonth": "Este m\xEAs",
7040
+ "financial.invoice.filterLastMonth": "M\xEAs passado",
7041
+ "financial.invoice.filterLast30": "\xDAltimos 30 dias",
7042
+ "financial.invoice.filterThisYear": "Este ano",
7043
+ "financial.invoice.filterCustom": "Personalizado",
7044
+ "financial.invoice.filterFrom": "De",
7045
+ "financial.invoice.filterTo": "At\xE9",
7046
+ "financial.invoice.filterClear": "Limpar",
6898
7047
  "financial.reconciliation.title": "Concilia\xE7\xE3o",
6899
7048
  "financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lan\xE7amentos",
6900
7049
  "financial.reconciliation.pending": "{{count}} para conciliar",
@@ -7364,6 +7513,15 @@ var DEFAULT_LABELS = {
7364
7513
  cardsReconciliation: "Reconciliation"
7365
7514
  };
7366
7515
  var DEFAULT_CURRENCY = { code: "BRL", locale: "pt-BR", symbol: "R$" };
7516
+ var FINANCIAL_NAV_FEATURES = {
7517
+ reconciliation: "fin_reconciliation",
7518
+ cards: "fin_cards",
7519
+ commissions: "fin_commissions",
7520
+ statements: "fin_statements",
7521
+ receivables: "fin_receivables",
7522
+ payables: "fin_payables",
7523
+ "cash-registers": "fin_cash_registers"
7524
+ };
7367
7525
  var DEFAULT_ITEM_TYPES = [
7368
7526
  { value: "service", label: "Service", icon: "Briefcase" },
7369
7527
  { value: "product", label: "Product", icon: "Package" },
@@ -7394,7 +7552,8 @@ function resolveConfig(options) {
7394
7552
  locations: options?.locations ?? [],
7395
7553
  entityLookups: options?.entityLookups ?? {},
7396
7554
  contactLookup: options?.contactLookup,
7397
- onBookingClick: options?.onBookingClick
7555
+ onBookingClick: options?.onBookingClick,
7556
+ navFeatureMap: { ...options?.navFeatureMap }
7398
7557
  };
7399
7558
  }
7400
7559
  function createFinancialPlugin(options) {
@@ -7422,6 +7581,70 @@ function createFinancialPlugin(options) {
7422
7581
  ...config.modules.payables ? [{ id: "financial.payables", label: config.labels.payables, group: config.labels.pageTitle }] : [],
7423
7582
  ...config.modules.commissions ? [{ id: "financial.commissions", label: config.labels.commissions, group: config.labels.pageTitle }] : []
7424
7583
  ],
7584
+ // Recurring monthly quota — counts financial movement rows created this
7585
+ // month (invoices write one per installment; quick transactions one, or two
7586
+ // for a transfer).
7587
+ declaredLimits: [
7588
+ { key: "movements_month", label: "Financial movements this month", table: "plg_financial_movements", period: "month" }
7589
+ ],
7590
+ queryEntities: [
7591
+ {
7592
+ key: "financial:invoices",
7593
+ entity: {
7594
+ name: "Invoice",
7595
+ namePlural: "Invoices (receipts REC- / payables PAG-)",
7596
+ icon: "Receipt",
7597
+ permission: { feature: "financial", action: "read" },
7598
+ fields: [
7599
+ { key: "referenceNumber", label: "Reference (REC-/PAG-)", type: "text", searchable: true },
7600
+ { key: "flow", label: "Flow (receivable/payable)", type: "text" },
7601
+ { key: "amount", label: "Amount", type: "number" },
7602
+ { key: "paid", label: "Paid", type: "number" },
7603
+ { key: "balance", label: "Balance", type: "number" },
7604
+ { key: "status", label: "Status", type: "text" },
7605
+ { key: "createdAt", label: "Created at", type: "text" }
7606
+ ],
7607
+ data: {
7608
+ table: "v_invoice_balances",
7609
+ tenantScoped: true,
7610
+ searchColumns: ["reference_number"]
7611
+ }
7612
+ }
7613
+ },
7614
+ {
7615
+ key: "financial:movements",
7616
+ entity: {
7617
+ name: "Financial movement",
7618
+ namePlural: "Financial movements (installment ledger)",
7619
+ icon: "Banknote",
7620
+ permission: { feature: "financial", action: "read" },
7621
+ fields: [
7622
+ { key: "direction", label: "Direction", type: "text" },
7623
+ { key: "movementKind", label: "Kind (bill/payment)", type: "text" },
7624
+ { key: "amount", label: "Amount", type: "number" },
7625
+ { key: "paidAmount", label: "Paid amount", type: "number" },
7626
+ { key: "status", label: "Status", type: "text" },
7627
+ { key: "dueDate", label: "Due date", type: "text" },
7628
+ { key: "paymentDate", label: "Payment date", type: "text" },
7629
+ { key: "installmentNumber", label: "Installment", type: "number" },
7630
+ { key: "cardInstallments", label: "Card installments", type: "number" },
7631
+ { key: "createdAt", label: "Created at", type: "text" }
7632
+ ],
7633
+ data: {
7634
+ table: "plg_financial_movements",
7635
+ tenantScoped: true
7636
+ }
7637
+ }
7638
+ }
7639
+ ],
7640
+ declaredRpcs: [
7641
+ {
7642
+ name: "agent_financial_mark_payment_received",
7643
+ kind: "write",
7644
+ description: "Guarded settle: invoices the order via fn_invoice_from_order (installments for credit card) and marks the receivable paid, audited with the acting user.",
7645
+ audits: true
7646
+ }
7647
+ ],
7425
7648
  navigation: [
7426
7649
  {
7427
7650
  section: options?.navSection ?? "main",
@@ -7475,6 +7698,7 @@ function createFinancialPlugin(options) {
7475
7698
  description: "Creates a new invoice/payable for a contact.",
7476
7699
  icon: "FileText",
7477
7700
  mode: "persist",
7701
+ limitKey: "movements_month",
7478
7702
  category: "Finance",
7479
7703
  parameters: {
7480
7704
  type: "object",
@@ -7490,6 +7714,33 @@ function createFinancialPlugin(options) {
7490
7714
  ],
7491
7715
  permission: { feature: "financial", action: "create" }
7492
7716
  },
7717
+ {
7718
+ id: "financial.mark-payment-received",
7719
+ name: "markPaymentReceived",
7720
+ description: 'Marks an order/appointment as paid: invoices it (with installments for credit card, e.g. "cart\xE3o 2x") and settles the receivable. Resolve the booking or order id via search tools first.',
7721
+ icon: "BadgeDollarSign",
7722
+ mode: "persist",
7723
+ execution: { plane: "server", kind: "rpc", rpc: "agent_financial_mark_payment_received" },
7724
+ category: "Finance",
7725
+ parameters: {
7726
+ type: "object",
7727
+ properties: {
7728
+ booking_id: { type: "string", description: "Appointment id (alternative to order_id)" },
7729
+ order_id: { type: "string", description: "Order id" },
7730
+ method: {
7731
+ type: "string",
7732
+ description: "Payment method",
7733
+ enum: ["cash", "pix", "credit_card", "debit_card", "transfer"]
7734
+ },
7735
+ installments: { type: "number", description: 'Credit-card installments (1-12), e.g. 2 for "cart\xE3o 2x"' }
7736
+ },
7737
+ required: ["method"]
7738
+ },
7739
+ suggestions: [
7740
+ { label: "Marcar o atendimento como pago no cart\xE3o em 2x" }
7741
+ ],
7742
+ permission: { feature: "financial.receivables", action: "update" }
7743
+ },
7493
7744
  {
7494
7745
  id: "financial.list-payables",
7495
7746
  name: "listPayables",
@@ -7537,6 +7788,7 @@ function createFinancialPlugin(options) {
7537
7788
  };
7538
7789
  }
7539
7790
 
7791
+ exports.FINANCIAL_NAV_FEATURES = FINANCIAL_NAV_FEATURES;
7540
7792
  exports.createFinancialPlugin = createFinancialPlugin;
7541
7793
  exports.createMockFinancialProvider = createMockFinancialProvider;
7542
7794
  exports.createSafeFinancialProvider = createSafeFinancialProvider;