@fayz-ai/admin 0.61.0 → 0.63.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 (172) hide show
  1. package/dist/{CommandPalette-B5PMQFDG.js → CommandPalette-XDJJ5E3O.js} +3 -3
  2. package/dist/{CommandPalette-B5PMQFDG.js.map → CommandPalette-XDJJ5E3O.js.map} +1 -1
  3. package/dist/ConnectedBrandingSettings-43ZM3HCI.js +680 -0
  4. package/dist/ConnectedBrandingSettings-43ZM3HCI.js.map +1 -0
  5. package/dist/ConnectedCompanySettings-I4MALI2Q.js +197 -0
  6. package/dist/ConnectedCompanySettings-I4MALI2Q.js.map +1 -0
  7. package/dist/ConnectedModulesSettings-A2VJ5BMJ.js +563 -0
  8. package/dist/ConnectedModulesSettings-A2VJ5BMJ.js.map +1 -0
  9. package/dist/ConnectedSecuritySettings-25AHA4HJ.js +189 -0
  10. package/dist/ConnectedSecuritySettings-25AHA4HJ.js.map +1 -0
  11. package/dist/ConnectedUserProfile-E7USJRUE.js +424 -0
  12. package/dist/ConnectedUserProfile-E7USJRUE.js.map +1 -0
  13. package/dist/FieldRulesEditor-YGIPKI5T.js +129 -0
  14. package/dist/FieldRulesEditor-YGIPKI5T.js.map +1 -0
  15. package/dist/GettingStartedWidget-HMIJ3Y24.js +12 -0
  16. package/dist/GettingStartedWidget-HMIJ3Y24.js.map +1 -0
  17. package/dist/PermissionProfilesTab-3ZPRCHMZ.js +548 -0
  18. package/dist/PermissionProfilesTab-3ZPRCHMZ.js.map +1 -0
  19. package/dist/PersonTimeline-HQZT527S.js +3 -0
  20. package/dist/{PersonTimeline-X7JTPUWJ.js.map → PersonTimeline-HQZT527S.js.map} +1 -1
  21. package/dist/ProjectsSettings-7O2F27MD.js +5 -0
  22. package/dist/ProjectsSettings-7O2F27MD.js.map +1 -0
  23. package/dist/ScheduleEditor-54ELWR45.js +8 -0
  24. package/dist/{ScheduleEditor-L2UJXKMY.js.map → ScheduleEditor-54ELWR45.js.map} +1 -1
  25. package/dist/SeuSiteWidget-QLSUVC63.js +6 -0
  26. package/dist/SeuSiteWidget-QLSUVC63.js.map +1 -0
  27. package/dist/SubscriptionPage-5GE5HFJK.js +17 -0
  28. package/dist/{SubscriptionPage-IDVEFLZ5.js.map → SubscriptionPage-5GE5HFJK.js.map} +1 -1
  29. package/dist/TeamTab-6RAULFLW.js +502 -0
  30. package/dist/TeamTab-6RAULFLW.js.map +1 -0
  31. package/dist/TrialWidget-Z6CLUCU3.js +7 -0
  32. package/dist/TrialWidget-Z6CLUCU3.js.map +1 -0
  33. package/dist/app/AdminShell.d.ts.map +1 -1
  34. package/dist/app/admin-app.d.ts.map +1 -1
  35. package/dist/app/config.d.ts +14 -0
  36. package/dist/app/config.d.ts.map +1 -1
  37. package/dist/app/routing.d.ts +6 -0
  38. package/dist/app/routing.d.ts.map +1 -1
  39. package/dist/app/scaffold.d.ts.map +1 -1
  40. package/dist/billing/index.js +9 -4
  41. package/dist/chunk-2H46WDG5.js +53 -0
  42. package/dist/chunk-2H46WDG5.js.map +1 -0
  43. package/dist/chunk-3HLX7YB2.js +94 -0
  44. package/dist/chunk-3HLX7YB2.js.map +1 -0
  45. package/dist/chunk-3OUD4XBK.js +3 -0
  46. package/dist/chunk-3OUD4XBK.js.map +1 -0
  47. package/dist/{chunk-IXSY4GPK.js → chunk-5D5QLMOQ.js} +5 -3
  48. package/dist/chunk-5D5QLMOQ.js.map +1 -0
  49. package/dist/chunk-5WR62X2R.js +1081 -0
  50. package/dist/chunk-5WR62X2R.js.map +1 -0
  51. package/dist/{chunk-KPYRTOCK.js → chunk-6CQFGMEO.js} +4 -140
  52. package/dist/chunk-6CQFGMEO.js.map +1 -0
  53. package/dist/chunk-6MMSYC2I.js +116 -0
  54. package/dist/chunk-6MMSYC2I.js.map +1 -0
  55. package/dist/chunk-6QFOE3UD.js +1466 -0
  56. package/dist/chunk-6QFOE3UD.js.map +1 -0
  57. package/dist/chunk-6UUFVQ2E.js +283 -0
  58. package/dist/chunk-6UUFVQ2E.js.map +1 -0
  59. package/dist/chunk-7WNLL3F6.js +7889 -0
  60. package/dist/chunk-7WNLL3F6.js.map +1 -0
  61. package/dist/chunk-A74IHAQE.js +439 -0
  62. package/dist/chunk-A74IHAQE.js.map +1 -0
  63. package/dist/{chunk-JOBRLBIQ.js → chunk-AFA6IO23.js} +4 -3
  64. package/dist/chunk-AFA6IO23.js.map +1 -0
  65. package/dist/{chunk-P5UIGQFY.js → chunk-ALCSIN3Z.js} +5 -5
  66. package/dist/chunk-ALCSIN3Z.js.map +1 -0
  67. package/dist/chunk-AWM4AMBS.js +39 -0
  68. package/dist/chunk-AWM4AMBS.js.map +1 -0
  69. package/dist/{chunk-OQARLKZN.js → chunk-BG5Z6KLN.js} +6 -18
  70. package/dist/chunk-BG5Z6KLN.js.map +1 -0
  71. package/dist/chunk-DPNFZONC.js +19 -0
  72. package/dist/chunk-DPNFZONC.js.map +1 -0
  73. package/dist/chunk-DRCV643L.js +193 -0
  74. package/dist/chunk-DRCV643L.js.map +1 -0
  75. package/dist/chunk-E6DF3VR6.js +206 -0
  76. package/dist/chunk-E6DF3VR6.js.map +1 -0
  77. package/dist/chunk-ENHCVGXZ.js +120 -0
  78. package/dist/chunk-ENHCVGXZ.js.map +1 -0
  79. package/dist/chunk-FK6RHGI7.js +56 -0
  80. package/dist/chunk-FK6RHGI7.js.map +1 -0
  81. package/dist/chunk-G5HNA5QK.js +233 -0
  82. package/dist/chunk-G5HNA5QK.js.map +1 -0
  83. package/dist/chunk-HCFO4Z6A.js +53 -0
  84. package/dist/chunk-HCFO4Z6A.js.map +1 -0
  85. package/dist/chunk-JFIU6NQP.js +51 -0
  86. package/dist/chunk-JFIU6NQP.js.map +1 -0
  87. package/dist/chunk-JRZJBMOY.js +3 -0
  88. package/dist/chunk-JRZJBMOY.js.map +1 -0
  89. package/dist/{chunk-3YP7KW47.js → chunk-JVVOL4BB.js} +41 -512
  90. package/dist/chunk-JVVOL4BB.js.map +1 -0
  91. package/dist/chunk-KAQ3UIAA.js +415 -0
  92. package/dist/chunk-KAQ3UIAA.js.map +1 -0
  93. package/dist/{chunk-P6XUDT2P.js → chunk-KJELMULA.js} +4 -51
  94. package/dist/chunk-KJELMULA.js.map +1 -0
  95. package/dist/chunk-KLO7DKXX.js +1651 -0
  96. package/dist/chunk-KLO7DKXX.js.map +1 -0
  97. package/dist/chunk-M2CMDWUI.js +75 -0
  98. package/dist/chunk-M2CMDWUI.js.map +1 -0
  99. package/dist/chunk-PQ47MLTH.js +255 -0
  100. package/dist/chunk-PQ47MLTH.js.map +1 -0
  101. package/dist/{chunk-VX3WJLKK.js → chunk-QD7BMUDF.js} +17 -8
  102. package/dist/chunk-QD7BMUDF.js.map +1 -0
  103. package/dist/{chunk-E4MESMNR.js → chunk-QTSKCPSW.js} +7 -10
  104. package/dist/chunk-QTSKCPSW.js.map +1 -0
  105. package/dist/chunk-S56FQKPL.js +186 -0
  106. package/dist/chunk-S56FQKPL.js.map +1 -0
  107. package/dist/chunk-SGAFPNMA.js +75 -0
  108. package/dist/chunk-SGAFPNMA.js.map +1 -0
  109. package/dist/{chunk-OD7P4L3D.js → chunk-SOKCLVM4.js} +3 -3
  110. package/dist/{chunk-OD7P4L3D.js.map → chunk-SOKCLVM4.js.map} +1 -1
  111. package/dist/chunk-TL42NKTH.js +3 -0
  112. package/dist/chunk-TL42NKTH.js.map +1 -0
  113. package/dist/chunk-U6ZCO2PS.js +2964 -0
  114. package/dist/chunk-U6ZCO2PS.js.map +1 -0
  115. package/dist/chunk-VSXNROYH.js +16 -0
  116. package/dist/chunk-VSXNROYH.js.map +1 -0
  117. package/dist/chunk-WPFZCYZL.js +619 -0
  118. package/dist/chunk-WPFZCYZL.js.map +1 -0
  119. package/dist/chunk-XFTNIBDW.js +23 -0
  120. package/dist/chunk-XFTNIBDW.js.map +1 -0
  121. package/dist/chunk-Y66JDL6I.js +1020 -0
  122. package/dist/chunk-Y66JDL6I.js.map +1 -0
  123. package/dist/chunk-YEHIBPIX.js +137 -0
  124. package/dist/chunk-YEHIBPIX.js.map +1 -0
  125. package/dist/chunk-YKPPCV2Z.js +53 -0
  126. package/dist/chunk-YKPPCV2Z.js.map +1 -0
  127. package/dist/components/chat/markdown.d.ts.map +1 -1
  128. package/dist/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
  129. package/dist/components/settings/SettingsPage.d.ts.map +1 -1
  130. package/dist/components/shared/ConnectionIndicator.d.ts.map +1 -1
  131. package/dist/connect-XKQJOWR7.js +18 -0
  132. package/dist/connect-XKQJOWR7.js.map +1 -0
  133. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  134. package/dist/crud/CrudListView.d.ts.map +1 -1
  135. package/dist/crud/CrudPage.d.ts.map +1 -1
  136. package/dist/crud/RelatedListTab.d.ts.map +1 -1
  137. package/dist/crud/archetypes/EntityLink.d.ts +1 -1
  138. package/dist/crud/archetypes/EntityLink.d.ts.map +1 -1
  139. package/dist/crud/record-chrome.d.ts.map +1 -1
  140. package/dist/entity-routes-PGU7EHAD.js +4 -0
  141. package/dist/{entity-routes-LXOBW5IB.js.map → entity-routes-PGU7EHAD.js.map} +1 -1
  142. package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
  143. package/dist/impersonation/ImpersonateDialog.d.ts.map +1 -1
  144. package/dist/impersonation/session.d.ts.map +1 -1
  145. package/dist/index.d.ts +2 -2
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +11922 -33564
  148. package/dist/index.js.map +1 -1
  149. package/dist/lib/connection.d.ts +14 -0
  150. package/dist/lib/connection.d.ts.map +1 -1
  151. package/dist/lib/entity-routes.d.ts.map +1 -1
  152. package/dist/org/index.js +4 -2
  153. package/dist/plugins/PluginRegistryManager.d.ts.map +1 -1
  154. package/dist/settings/MessageTemplateRow.d.ts.map +1 -1
  155. package/dist/timeline/PersonTimeline.d.ts.map +1 -1
  156. package/dist/timeline/TimelineList.d.ts.map +1 -1
  157. package/dist/units/UnitRailPage.d.ts.map +1 -1
  158. package/dist/units/useUnitFilter.d.ts.map +1 -1
  159. package/package.json +5 -5
  160. package/dist/PersonTimeline-X7JTPUWJ.js +0 -3
  161. package/dist/ScheduleEditor-L2UJXKMY.js +0 -6
  162. package/dist/SubscriptionPage-IDVEFLZ5.js +0 -8
  163. package/dist/chunk-3YP7KW47.js.map +0 -1
  164. package/dist/chunk-E4MESMNR.js.map +0 -1
  165. package/dist/chunk-IXSY4GPK.js.map +0 -1
  166. package/dist/chunk-JOBRLBIQ.js.map +0 -1
  167. package/dist/chunk-KPYRTOCK.js.map +0 -1
  168. package/dist/chunk-OQARLKZN.js.map +0 -1
  169. package/dist/chunk-P5UIGQFY.js.map +0 -1
  170. package/dist/chunk-P6XUDT2P.js.map +0 -1
  171. package/dist/chunk-VX3WJLKK.js.map +0 -1
  172. package/dist/entity-routes-LXOBW5IB.js +0 -4
@@ -0,0 +1,186 @@
1
+ import { useBillingStore, useTrial, effectivePlan } from './chunk-SGAFPNMA.js';
2
+ import { usePermissionsStore } from './chunk-UQ2NTFQC.js';
3
+ import { useOrganizationStore } from './chunk-YKPPCV2Z.js';
4
+ import { invalidateCount, usePluginRuntimeOptional, getAllEntities, countByTenant } from '@fayz-ai/core';
5
+ import { create } from 'zustand';
6
+ import * as React from 'react';
7
+ import { useAuthStore } from '@fayz-ai/auth';
8
+ import { mergeLimitDeclarations, isEntitledByPlan, resolveAccess, resolveLimit } from '@fayz-ai/core/access';
9
+ import { jsx } from 'react/jsx-runtime';
10
+
11
+ var CORE_LIMIT_DECLARATIONS = [
12
+ { key: "users", label: "Users", table: "v_team_members" },
13
+ { key: "locations", label: "Companies", table: "v_units" }
14
+ ];
15
+ function entityDerivedLimitDeclarations(entities) {
16
+ const out = [];
17
+ for (const e of entities) {
18
+ const def = e.entityDef;
19
+ if (def?.limitKey && def.data?.table) {
20
+ out.push({
21
+ key: def.limitKey,
22
+ label: e.labelPlural,
23
+ table: def.data.table,
24
+ kindFilter: def.data.archetypeKind
25
+ });
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+ var registry = /* @__PURE__ */ new Map();
31
+ function setLimitRegistry(declarations) {
32
+ const next = /* @__PURE__ */ new Map();
33
+ for (const decl of declarations) next.set(decl.key, decl);
34
+ registry = next;
35
+ }
36
+ function getLimitDeclaration(key) {
37
+ return registry.get(key);
38
+ }
39
+ var listeners = /* @__PURE__ */ new Map();
40
+ function subscribeLimit(key, cb) {
41
+ let set = listeners.get(key);
42
+ if (!set) {
43
+ set = /* @__PURE__ */ new Set();
44
+ listeners.set(key, set);
45
+ }
46
+ set.add(cb);
47
+ return () => {
48
+ set?.delete(cb);
49
+ if (set && set.size === 0) listeners.delete(key);
50
+ };
51
+ }
52
+ function invalidateLimit(key) {
53
+ const decl = registry.get(key);
54
+ if (decl) invalidateCount(decl.table);
55
+ listeners.get(key)?.forEach((cb) => cb());
56
+ }
57
+ var useUpgradeModalStore = create((set) => ({
58
+ current: null,
59
+ open: (payload) => set({ current: payload }),
60
+ close: () => set({ current: null })
61
+ }));
62
+ var AccessContext = React.createContext(null);
63
+ function AccessProvider({ children, limitDeclarations, trialPlanId }) {
64
+ const user = useAuthStore((s) => s.user);
65
+ const org = useOrganizationStore((s) => s.currentOrg);
66
+ const profile = usePermissionsStore((s) => s.currentProfile);
67
+ const plans = useBillingStore((s) => s.plans);
68
+ const runtime = usePluginRuntimeOptional();
69
+ const pluginLimits = runtime?.pluginLimits;
70
+ React.useEffect(() => {
71
+ setLimitRegistry(
72
+ mergeLimitDeclarations(
73
+ CORE_LIMIT_DECLARATIONS,
74
+ entityDerivedLimitDeclarations(getAllEntities()),
75
+ pluginLimits,
76
+ limitDeclarations
77
+ )
78
+ );
79
+ }, [pluginLimits, limitDeclarations]);
80
+ const trial = useTrial();
81
+ const plan = React.useMemo(
82
+ () => effectivePlan(org, plans, trial, trialPlanId),
83
+ [plans, org?.plan, trial.status, trialPlanId]
84
+ );
85
+ const session = React.useMemo(
86
+ () => ({ user, org, profile, plan }),
87
+ [user, org, profile, plan]
88
+ );
89
+ const value = React.useMemo(
90
+ () => ({
91
+ session,
92
+ can: (feature, action) => resolveAccess(session, feature, action),
93
+ entitled: (feature) => isEntitledByPlan(session.plan, feature)
94
+ }),
95
+ [session]
96
+ );
97
+ return /* @__PURE__ */ jsx(AccessContext.Provider, { value, children });
98
+ }
99
+ AccessProvider.displayName = "AccessProvider";
100
+ var ALLOW_ALL = {
101
+ can: () => ({ allowed: true }),
102
+ entitled: () => true
103
+ };
104
+ function useAccess() {
105
+ const ctx = React.useContext(AccessContext);
106
+ if (!ctx) throw new Error("useAccess must be used inside <AccessProvider>");
107
+ return { can: ctx.can, entitled: ctx.entitled };
108
+ }
109
+ function useAccessOptional() {
110
+ const ctx = React.useContext(AccessContext);
111
+ return ctx ? { can: ctx.can, entitled: ctx.entitled } : ALLOW_ALL;
112
+ }
113
+ function useAccessSession() {
114
+ return React.useContext(AccessContext)?.session ?? null;
115
+ }
116
+ function resolveCap(plan, key) {
117
+ return plan?.entitlements?.limits?.[key];
118
+ }
119
+ function useLimit(key) {
120
+ const session = useAccessSession();
121
+ const tenantId = session?.org?.id;
122
+ const cap = resolveCap(session?.plan ?? null, key);
123
+ const declaration = getLimitDeclaration(key);
124
+ const unlimited = !declaration || cap === void 0 || cap === -1;
125
+ const [used, setUsed] = React.useState(0);
126
+ const [loading, setLoading] = React.useState(!unlimited);
127
+ const [version, setVersion] = React.useState(0);
128
+ const refresh = React.useCallback(() => setVersion((v) => v + 1), []);
129
+ React.useEffect(() => subscribeLimit(key, refresh), [key, refresh]);
130
+ React.useEffect(() => {
131
+ if (unlimited || !declaration || !tenantId) {
132
+ setUsed(0);
133
+ setLoading(false);
134
+ return;
135
+ }
136
+ let active = true;
137
+ setLoading(true);
138
+ countByTenant(declaration.table, {
139
+ kind: declaration.kindFilter,
140
+ period: declaration.period,
141
+ tenantId,
142
+ fresh: version > 0
143
+ }).then((n) => {
144
+ if (active) {
145
+ setUsed(n);
146
+ setLoading(false);
147
+ }
148
+ }).catch(() => {
149
+ if (active) setLoading(false);
150
+ });
151
+ return () => {
152
+ active = false;
153
+ };
154
+ }, [unlimited, declaration, tenantId, version]);
155
+ const bounds = resolveLimit({ cap, hasDeclaration: Boolean(declaration), used });
156
+ return { key, ...bounds, loading, refresh };
157
+ }
158
+ function useLimitGuard(key) {
159
+ const session = useAccessSession();
160
+ const tenantId = session?.org?.id;
161
+ const plan = session?.plan ?? null;
162
+ const openModal = useUpgradeModalStore((s) => s.open);
163
+ return React.useCallback(
164
+ async (n = 1) => {
165
+ const cap = resolveCap(plan, key);
166
+ const declaration = getLimitDeclaration(key);
167
+ if (!declaration || cap === void 0 || cap === -1 || !tenantId) return "ok";
168
+ const used = await countByTenant(declaration.table, {
169
+ kind: declaration.kindFilter,
170
+ period: declaration.period,
171
+ tenantId,
172
+ fresh: true
173
+ });
174
+ if (used + n > cap) {
175
+ openModal({ limitKey: key });
176
+ return "blocked";
177
+ }
178
+ return "ok";
179
+ },
180
+ [key, plan, tenantId, openModal]
181
+ );
182
+ }
183
+
184
+ export { AccessProvider, CORE_LIMIT_DECLARATIONS, entityDerivedLimitDeclarations, getLimitDeclaration, invalidateLimit, useAccess, useAccessOptional, useLimit, useLimitGuard, useUpgradeModalStore };
185
+ //# sourceMappingURL=chunk-S56FQKPL.js.map
186
+ //# sourceMappingURL=chunk-S56FQKPL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/access/limits-registry.ts","../src/access/upgrade-modal-store.ts","../src/access/context.tsx"],"names":[],"mappings":"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","file":"chunk-S56FQKPL.js","sourcesContent":["import type { LimitDeclaration, RegisteredEntity } from '@fayz-ai/core'\nimport { invalidateCount } from '@fayz-ai/core'\n\n/**\n * Module-level registry bridging React (the provider that knows the declarations)\n * and the plain `invalidateLimit(key)` function that create handlers call outside\n * any component. The AccessProvider publishes the merged declarations here on\n * mount; `useLimit`/`useLimitGuard` and `invalidateLimit` read from it.\n */\n\n/**\n * Shell built-in limit declarations. Unlike plugin/app limits (contributed via\n * `PluginManifest.declaredLimits` / `billing.limitDeclarations`), these count\n * core tenant tables that have no owning plugin: seats and units. Merged as the\n * LOWEST-priority layer by the AccessProvider, so a plugin or app may still\n * override a key (e.g. add a `kindFilter`).\n *\n * Counted through the public `v_*` bridges, not the base tables: the spine\n * moved memberships and units into schema `app`, which PostgREST cannot reach.\n */\nexport const CORE_LIMIT_DECLARATIONS: LimitDeclaration[] = [\n { key: 'users', label: 'Users', table: 'v_team_members' },\n { key: 'locations', label: 'Companies', table: 'v_units' },\n]\n\n/**\n * Entity-derived layer of the limit merge: any registered CRUD entity carrying\n * `limitKey` yields a declaration for free from its own data mapping (table +\n * archetype kind) — a cap without a countable binding is a dead cap. Shared by\n * the AccessProvider (live registry) and the manifest derivation (agent\n * contract), so both resolve the identical layer.\n */\nexport function entityDerivedLimitDeclarations(entities: RegisteredEntity[]): LimitDeclaration[] {\n const out: LimitDeclaration[] = []\n for (const e of entities) {\n const def = e.entityDef\n if (def?.limitKey && def.data?.table) {\n out.push({\n key: def.limitKey,\n label: e.labelPlural,\n table: def.data.table,\n kindFilter: def.data.archetypeKind,\n })\n }\n }\n return out\n}\n\nlet registry = new Map<string, LimitDeclaration>()\n\n/** Called by AccessProvider whenever the merged limit declarations change. */\nexport function setLimitRegistry(declarations: LimitDeclaration[]): void {\n const next = new Map<string, LimitDeclaration>()\n for (const decl of declarations) next.set(decl.key, decl)\n registry = next\n}\n\nexport function getLimitDeclaration(key: string): LimitDeclaration | undefined {\n return registry.get(key)\n}\n\n// --- Invalidation pub/sub -------------------------------------------------\n// useLimit subscribes per key; invalidateLimit(key) drops the cached count for\n// the declaration's table and notifies subscribers to re-fetch.\n\nconst listeners = new Map<string, Set<() => void>>()\n\nexport function subscribeLimit(key: string, cb: () => void): () => void {\n let set = listeners.get(key)\n if (!set) {\n set = new Set()\n listeners.set(key, set)\n }\n set.add(cb)\n return () => {\n set?.delete(cb)\n if (set && set.size === 0) listeners.delete(key)\n }\n}\n\n/**\n * Invalidate a limit after a create/delete so its next read is fresh, then wake\n * any mounted `useLimit(key)`. Safe to call from plain handlers (no React).\n */\nexport function invalidateLimit(key: string): void {\n const decl = registry.get(key)\n if (decl) invalidateCount(decl.table)\n listeners.get(key)?.forEach((cb) => cb())\n}\n","import { create } from 'zustand'\n\n/**\n * Payload describing WHY the upgrade modal opened, so the visual component (owned\n * by the shell/gates layer) can tailor its copy — a hit quantity cap\n * (`limitKey`) or a plan-gated feature (`feature`).\n */\nexport interface UpgradeModalPayload {\n limitKey?: string\n feature?: string\n}\n\nexport interface UpgradeModalStore {\n /** The active payload, or null when closed. */\n current: UpgradeModalPayload | null\n open(payload: UpgradeModalPayload): void\n close(): void\n}\n\n/**\n * Global, lightweight store for the single app-wide UpgradeModal. `useLimitGuard`\n * and imperative create handlers call `open(...)` when a limit blocks; the visual\n * modal (rendered once near the shell root by the gates layer) subscribes to\n * `current`. Kept as a zustand singleton so store + component share one instance.\n */\nexport const useUpgradeModalStore = create<UpgradeModalStore>((set) => ({\n current: null,\n open: (payload) => set({ current: payload }),\n close: () => set({ current: null }),\n}))\n","import * as React from 'react'\nimport type { LimitDeclaration, PermissionAction, Plan } from '@fayz-ai/core'\nimport { countByTenant, usePluginRuntimeOptional, getAllEntities } from '@fayz-ai/core'\nimport { useAuthStore } from '@fayz-ai/auth'\nimport { useOrganizationStore } from '../org/store'\nimport { usePermissionsStore } from '../permissions/store'\nimport { useBillingStore } from '../billing/store'\nimport { effectivePlan, useTrial } from '../billing/trial'\nimport { mergeLimitDeclarations, resolveAccess, isEntitledByPlan, resolveLimit } from '@fayz-ai/core/access'\nimport { entityDerivedLimitDeclarations } from './limits-registry'\nimport { setLimitRegistry, getLimitDeclaration, subscribeLimit, CORE_LIMIT_DECLARATIONS } from './limits-registry'\nimport { useUpgradeModalStore } from './upgrade-modal-store'\nimport type { AccessApi, AccessDecision, AccessSession, LimitState } from './types'\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ninterface AccessContextValue extends AccessApi {\n session: AccessSession\n}\n\nconst AccessContext = React.createContext<AccessContextValue | null>(null)\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport interface AccessProviderProps {\n children: React.ReactNode\n /**\n * App-level limit declarations layered ON TOP of the plugins' `declaredLimits`\n * (app wins on key collision). Sourced from `config.billing.limitDeclarations`.\n */\n limitDeclarations?: LimitDeclaration[]\n /** Plan the tenant is entitled to while its trial runs. See billing/trial.ts. */\n trialPlanId?: string\n}\n\n/**\n * Assembles the reactive {@link AccessSession} from the singleton stores and\n * exposes the composed access API (role × plan). Thin by design — mounts just\n * inside PermissionsProvider so it can reuse the live permission profile and the\n * plugin runtime. Also publishes the merged limit declarations to the module\n * registry so `invalidateLimit` works from non-React create handlers.\n */\nexport function AccessProvider({ children, limitDeclarations, trialPlanId }: AccessProviderProps) {\n const user = useAuthStore((s) => s.user)\n const org = useOrganizationStore((s) => s.currentOrg)\n const profile = usePermissionsStore((s) => s.currentProfile)\n const plans = useBillingStore((s) => s.plans)\n\n const runtime = usePluginRuntimeOptional()\n const pluginLimits = runtime?.pluginLimits\n\n // Merge limit declarations by priority (later wins by key): shell built-ins\n // (users/locations) < entity-derived < plugin `declaredLimits` < app\n // overrides. Then publish to the module registry consumed by\n // invalidateLimit / the limit hooks.\n //\n // Entity-derived: any registered CRUD entity carrying `limitKey` yields a\n // declaration for free from its own data mapping (table + archetype kind) —\n // this is what makes an app's `EntityDef.limitKey: 'clients'` enforceable\n // with ZERO extra wiring (a cap without a countable binding is a dead cap).\n React.useEffect(() => {\n setLimitRegistry(\n mergeLimitDeclarations(\n CORE_LIMIT_DECLARATIONS,\n entityDerivedLimitDeclarations(getAllEntities()),\n pluginLimits,\n limitDeclarations,\n ),\n )\n }, [pluginLimits, limitDeclarations])\n\n // The trial is resolved HERE and nowhere else: it changes which plan's\n // entitlements apply, and every gate in the product already reads that. A\n // per-feature \"…or on trial\" check would have to be repeated in the nav, the\n // gates, the caps and the assistant, and one of them would be forgotten.\n const trial = useTrial()\n const plan: Plan | null = React.useMemo(\n () => effectivePlan(org, plans, trial, trialPlanId),\n [plans, org?.plan, trial.status, trialPlanId],\n )\n\n const session: AccessSession = React.useMemo(\n () => ({ user, org, profile, plan }),\n [user, org, profile, plan],\n )\n\n const value = React.useMemo<AccessContextValue>(\n () => ({\n session,\n can: (feature, action) => resolveAccess(session, feature, action),\n entitled: (feature) => isEntitledByPlan(session.plan, feature),\n }),\n [session],\n )\n\n return <AccessContext.Provider value={value}>{children}</AccessContext.Provider>\n}\nAccessProvider.displayName = 'AccessProvider'\n\n// ---------------------------------------------------------------------------\n// Hooks\n// ---------------------------------------------------------------------------\n\nconst ALLOW_ALL: AccessApi = {\n can: () => ({ allowed: true }),\n entitled: () => true,\n}\n\n/**\n * The composed access API. Throws outside <AccessProvider> — use\n * {@link useAccessOptional} for SDK components that must degrade gracefully.\n */\nexport function useAccess(): AccessApi {\n const ctx = React.useContext(AccessContext)\n if (!ctx) throw new Error('useAccess must be used inside <AccessProvider>')\n return { can: ctx.can, entitled: ctx.entitled }\n}\n\n/**\n * Like {@link useAccess} but degrades to allow-all when no provider is mounted,\n * so components stay usable under a bare host shell (gating is an enhancement).\n */\nexport function useAccessOptional(): AccessApi {\n const ctx = React.useContext(AccessContext)\n return ctx ? { can: ctx.can, entitled: ctx.entitled } : ALLOW_ALL\n}\n\n/** Internal: the live session (for hooks that need tenant/plan directly). */\nfunction useAccessSession(): AccessSession | null {\n return React.useContext(AccessContext)?.session ?? null\n}\n\n// ---------------------------------------------------------------------------\n// Limits\n// ---------------------------------------------------------------------------\n\nfunction resolveCap(plan: Plan | null, key: string): number | undefined {\n return plan?.entitlements?.limits?.[key]\n}\n\n/**\n * Live usage of a quantity limit for the active tenant. Resolves the limit's\n * declaration (plugin manifest or app override) → a tenant-scoped `count(*)`,\n * capped by the current plan. No declaration or no cap (or `-1`) ⇒ unlimited.\n * Re-fetches when the tenant, plan cap, or an `invalidateLimit(key)` fires.\n */\nexport function useLimit(key: string): LimitState {\n const session = useAccessSession()\n const tenantId = session?.org?.id\n const cap = resolveCap(session?.plan ?? null, key)\n const declaration = getLimitDeclaration(key)\n\n const unlimited = !declaration || cap === undefined || cap === -1\n\n const [used, setUsed] = React.useState(0)\n const [loading, setLoading] = React.useState(!unlimited)\n const [version, setVersion] = React.useState(0)\n\n const refresh = React.useCallback(() => setVersion((v) => v + 1), [])\n\n // Wake on external invalidation (a create/delete elsewhere).\n React.useEffect(() => subscribeLimit(key, refresh), [key, refresh])\n\n React.useEffect(() => {\n if (unlimited || !declaration || !tenantId) {\n setUsed(0)\n setLoading(false)\n return\n }\n let active = true\n setLoading(true)\n countByTenant(declaration.table, {\n kind: declaration.kindFilter,\n period: declaration.period,\n tenantId,\n fresh: version > 0,\n })\n .then((n) => {\n if (active) {\n setUsed(n)\n setLoading(false)\n }\n })\n .catch(() => {\n if (active) setLoading(false)\n })\n return () => {\n active = false\n }\n }, [unlimited, declaration, tenantId, version])\n\n const bounds = resolveLimit({ cap, hasDeclaration: Boolean(declaration), used })\n\n return { key, ...bounds, loading, refresh }\n}\n\n/**\n * Imperative guard for create handlers: `await guard(n)` returns `'ok'` when\n * adding `n` more rows stays within the cap, or `'blocked'` (and opens the global\n * UpgradeModal) when it would exceed it. Always checks a FRESH count so it never\n * relies on stale hook state.\n */\nexport function useLimitGuard(key: string): (n?: number) => Promise<'ok' | 'blocked'> {\n const session = useAccessSession()\n const tenantId = session?.org?.id\n const plan = session?.plan ?? null\n const openModal = useUpgradeModalStore((s) => s.open)\n\n return React.useCallback(\n async (n = 1) => {\n const cap = resolveCap(plan, key)\n const declaration = getLimitDeclaration(key)\n // Unlimited / no declaration / no tenant ⇒ never block.\n if (!declaration || cap === undefined || cap === -1 || !tenantId) return 'ok'\n\n const used = await countByTenant(declaration.table, {\n kind: declaration.kindFilter,\n period: declaration.period,\n tenantId,\n fresh: true,\n })\n\n if (used + n > cap) {\n openModal({ limitKey: key })\n return 'blocked'\n }\n return 'ok'\n },\n [key, plan, tenantId, openModal],\n )\n}\n"]}
@@ -0,0 +1,75 @@
1
+ import { useOrganizationStore } from './chunk-YKPPCV2Z.js';
2
+ import { create } from 'zustand';
3
+ import * as React from 'react';
4
+
5
+ var initialState = {
6
+ subscription: null,
7
+ invoices: [],
8
+ plans: [],
9
+ loading: false,
10
+ checkout: null
11
+ };
12
+ var useBillingStore = create((set) => ({
13
+ ...initialState,
14
+ setSubscription: (subscription) => set({ subscription }),
15
+ setInvoices: (invoices) => set({ invoices }),
16
+ setPlans: (plans) => set({ plans }),
17
+ setLoading: (loading) => set({ loading }),
18
+ setCheckout: (checkout) => set({ checkout }),
19
+ reset: () => set(initialState)
20
+ }));
21
+ var NO_TRIAL = { status: "none", daysLeft: 0, endingSoon: false };
22
+ var DAY_MS = 864e5;
23
+ function resolveTrial(org, paidPlanIds2, now = Date.now()) {
24
+ if (!org?.trialEndsAt) return NO_TRIAL;
25
+ if (org.plan && paidPlanIds2.has(org.plan)) return NO_TRIAL;
26
+ const ends = Date.parse(org.trialEndsAt);
27
+ if (Number.isNaN(ends)) return NO_TRIAL;
28
+ const remaining = ends - now;
29
+ if (remaining <= 0) return { status: "expired", endsAt: org.trialEndsAt, daysLeft: 0, endingSoon: false };
30
+ const daysLeft = Math.ceil(remaining / DAY_MS);
31
+ return {
32
+ status: "active",
33
+ endsAt: org.trialEndsAt,
34
+ daysLeft,
35
+ endingSoon: remaining <= 2 * DAY_MS
36
+ };
37
+ }
38
+ function effectivePlan(org, plans, trial, trialPlanId) {
39
+ if (trial.status === "active") {
40
+ const named2 = trialPlanId ? plans.find((p) => p.id === trialPlanId) : void 0;
41
+ if (named2) return named2;
42
+ const paid = plans.filter((p) => p.price > 0);
43
+ if (paid.length) return paid.reduce((best, p) => p.price > best.price ? p : best);
44
+ }
45
+ if (plans.length === 0) return null;
46
+ const named = org?.plan ? plans.find((p) => p.id === org.plan) : void 0;
47
+ if (named) return named;
48
+ const floor = plans.reduce((cheapest, p) => p.price < cheapest.price ? p : cheapest);
49
+ if (org?.plan) {
50
+ console.warn(
51
+ `[billing] tenant plan "${org.plan}" n\xE3o existe neste cat\xE1logo (${plans.map((p) => p.id).join(", ")}) \u2014 aplicando o piso "${floor.id}"`
52
+ );
53
+ }
54
+ return floor;
55
+ }
56
+ function paidPlanIds(plans) {
57
+ return new Set(plans.filter((p) => p.price > 0).map((p) => p.id));
58
+ }
59
+ function useTrial() {
60
+ const org = useOrganizationStore((s) => s.currentOrg);
61
+ const plans = useBillingStore((s) => s.plans);
62
+ const [now, setNow] = React.useState(() => Date.now());
63
+ React.useEffect(() => {
64
+ const id = window.setInterval(() => setNow(Date.now()), 36e5);
65
+ return () => window.clearInterval(id);
66
+ }, []);
67
+ return React.useMemo(
68
+ () => resolveTrial(org, paidPlanIds(plans), now),
69
+ [org?.plan, org?.trialEndsAt, plans, now]
70
+ );
71
+ }
72
+
73
+ export { NO_TRIAL, effectivePlan, paidPlanIds, resolveTrial, useBillingStore, useTrial };
74
+ //# sourceMappingURL=chunk-SGAFPNMA.js.map
75
+ //# sourceMappingURL=chunk-SGAFPNMA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/billing/store.ts","../src/billing/trial.ts"],"names":["paidPlanIds","named"],"mappings":"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","file":"chunk-SGAFPNMA.js","sourcesContent":["import { create } from 'zustand'\nimport type { Plan, BillingCheckoutFn } from '@fayz-ai/core'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface Subscription {\n id: string\n orgId: string\n planId: string\n status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'incomplete'\n currentPeriodStart: string\n currentPeriodEnd: string\n cancelAtPeriodEnd?: boolean\n stripeSubscriptionId?: string\n}\n\nexport interface Invoice {\n id: string\n orgId: string\n amount: number\n currency: string\n status: 'paid' | 'open' | 'void' | 'uncollectible'\n pdfUrl?: string\n createdAt: string\n}\n\nexport interface BillingState {\n subscription: Subscription | null\n invoices: Invoice[]\n plans: Plan[]\n loading: boolean\n /** Payment-gateway seam seeded from `config.billing.onCheckout` (see core\n * BillingConfig). When set, the Subscription page routes plan changes through\n * it instead of writing `plan` straight onto the org. */\n checkout: BillingCheckoutFn | null\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\nexport interface BillingStore extends BillingState {\n setSubscription: (subscription: Subscription | null) => void\n setInvoices: (invoices: Invoice[]) => void\n setPlans: (plans: Plan[]) => void\n setLoading: (loading: boolean) => void\n setCheckout: (checkout: BillingCheckoutFn | null) => void\n reset: () => void\n}\n\nconst initialState: BillingState = {\n subscription: null,\n invoices: [],\n plans: [],\n loading: false,\n checkout: null,\n}\n\nexport const useBillingStore = create<BillingStore>((set) => ({\n ...initialState,\n setSubscription: (subscription) => set({ subscription }),\n setInvoices: (invoices) => set({ invoices }),\n setPlans: (plans) => set({ plans }),\n setLoading: (loading) => set({ loading }),\n setCheckout: (checkout) => set({ checkout }),\n reset: () => set(initialState),\n}))\n","import * as React from 'react'\nimport type { Organization, Plan } from '@fayz-ai/core'\nimport { useOrganizationStore } from '../org/store'\nimport { useBillingStore } from './store'\n\n/**\n * The free trial, as one derived fact.\n *\n * A trial is not a fourth plan or a state machine — it is a WINDOW during which\n * a tenant on the free plan is entitled to a paid one. Modelling it that way is\n * what keeps it out of every gate in the product: the access provider resolves\n * the effective plan once, and the nav crowns, the entitlement gates, the limit\n * caps and the assistant's lock all follow without knowing a trial exists.\n *\n * `trialEndsAt` is written by the database when the tenant is created (see\n * migration 147) and is never written by the client — a trial the browser could\n * extend is not a trial. A tenant with no `trialEndsAt` predates the column and\n * is left alone: status `none`, plan exactly as stored.\n */\n\nexport type TrialStatus = 'none' | 'active' | 'expired'\n\nexport interface TrialState {\n status: TrialStatus\n /** ISO date the window closes. */\n endsAt?: string\n /** Whole days remaining, floored at 0. `0` on the last day, not a negative. */\n daysLeft: number\n /** The last 48 hours, when the banner stops being informational. */\n endingSoon: boolean\n}\n\nexport const NO_TRIAL: TrialState = { status: 'none', daysLeft: 0, endingSoon: false }\n\nconst DAY_MS = 86_400_000\n\n/** Pure — the part worth testing, and it needs neither React nor a clock. */\nexport function resolveTrial(\n org: Pick<Organization, 'plan' | 'trialEndsAt'> | null | undefined,\n paidPlanIds: ReadonlySet<string>,\n now: number = Date.now(),\n): TrialState {\n if (!org?.trialEndsAt) return NO_TRIAL\n // Somebody who is paying is not on a trial, whatever the column still says.\n if (org.plan && paidPlanIds.has(org.plan)) return NO_TRIAL\n\n const ends = Date.parse(org.trialEndsAt)\n if (Number.isNaN(ends)) return NO_TRIAL\n\n const remaining = ends - now\n if (remaining <= 0) return { status: 'expired', endsAt: org.trialEndsAt, daysLeft: 0, endingSoon: false }\n\n const daysLeft = Math.ceil(remaining / DAY_MS)\n return {\n status: 'active',\n endsAt: org.trialEndsAt,\n daysLeft,\n endingSoon: remaining <= 2 * DAY_MS,\n }\n}\n\n/**\n * The plan whose entitlements apply RIGHT NOW.\n *\n * During the trial that is the trial plan — by default the most expensive one\n * configured, because the point of a trial is to be worth losing. After it, the\n * tenant's own plan, which for a free tenant is the free one: that fall is the\n * paywall, and it needs no separate enforcement path.\n *\n * ── Por que um plano desconhecido cai no piso, e não em `null` ──────────────\n * A frase acima (\"essa queda é o paywall\") só era verdade quando o id gravado\n * em `tenants.plan` existia no catálogo do app. Quando não existia — o\n * ChefControl vendia `starter/professional/enterprise` enquanto o pool grava\n * `free/pro/business` — isto devolvia `null`, e `null` não é \"sem plano\": é SEM\n * TETO. `resolveLimit` lê teto ausente como ilimitado e `isEntitledByPlan` só\n * nega com um `false` explícito. O resultado é que vencer o trial LIBERAVA em\n * vez de travar, que é o oposto do paywall.\n *\n * Então um id que o catálogo não conhece cai no plano mais barato — o piso é a\n * resposta segura para \"não sei em que plano esta conta está\". `null` fica\n * reservado para o único caso em que ilimitado é a leitura certa: o app que não\n * vende plano nenhum e portanto não tem o que travar.\n */\nexport function effectivePlan(\n org: Pick<Organization, 'plan'> | null | undefined,\n plans: Plan[],\n trial: TrialState,\n trialPlanId?: string,\n): Plan | null {\n if (trial.status === 'active') {\n const named = trialPlanId ? plans.find((p) => p.id === trialPlanId) : undefined\n if (named) return named\n const paid = plans.filter((p) => p.price > 0)\n if (paid.length) return paid.reduce((best, p) => (p.price > best.price ? p : best))\n }\n if (plans.length === 0) return null\n\n const named = org?.plan ? plans.find((p) => p.id === org.plan) : undefined\n if (named) return named\n\n const floor = plans.reduce((cheapest, p) => (p.price < cheapest.price ? p : cheapest))\n if (org?.plan) {\n // Avisa alto: é um defeito de dados (catálogo e pool discordando), e quem\n // vê primeiro é quem estiver com o console aberto.\n console.warn(\n `[billing] tenant plan \"${org.plan}\" não existe neste catálogo (${plans.map((p) => p.id).join(', ')}) — aplicando o piso \"${floor.id}\"`,\n )\n }\n return floor\n}\n\n/** Ids of the plans somebody actually pays for. */\nexport function paidPlanIds(plans: Plan[]): Set<string> {\n return new Set(plans.filter((p) => p.price > 0).map((p) => p.id))\n}\n\n/**\n * Live trial state for the current workspace.\n *\n * Ticks once an hour rather than on a timer per second: nothing on screen\n * counts down in real time, and a banner that says \"2 days left\" only has to be\n * right the next time somebody looks at it.\n */\nexport function useTrial(): TrialState {\n const org = useOrganizationStore((s) => s.currentOrg)\n const plans = useBillingStore((s) => s.plans)\n const [now, setNow] = React.useState(() => Date.now())\n\n React.useEffect(() => {\n const id = window.setInterval(() => setNow(Date.now()), 3_600_000)\n return () => window.clearInterval(id)\n }, [])\n\n return React.useMemo(\n () => resolveTrial(org, paidPlanIds(plans), now),\n [org?.plan, org?.trialEndsAt, plans, now],\n )\n}\n"]}
@@ -63,12 +63,12 @@ function useRecordVisit(entrada) {
63
63
  }
64
64
  function useOpenRecent() {
65
65
  return useCallback((r) => {
66
- void import('./entity-routes-LXOBW5IB.js').then(({ openEntity }) => {
66
+ void import('./entity-routes-PGU7EHAD.js').then(({ openEntity }) => {
67
67
  openEntity(r.id, r.archetype, r.archetypeKind, void 0, r.entityKey);
68
68
  });
69
69
  }, []);
70
70
  }
71
71
 
72
72
  export { clearRecentRecords, listRecentRecords, rememberVisit, subscribeRecentRecords, useOpenRecent, useRecentRecords, useRecordVisit };
73
- //# sourceMappingURL=chunk-OD7P4L3D.js.map
74
- //# sourceMappingURL=chunk-OD7P4L3D.js.map
73
+ //# sourceMappingURL=chunk-SOKCLVM4.js.map
74
+ //# sourceMappingURL=chunk-SOKCLVM4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/recent-records.ts","../src/hooks/useRecentRecords.ts"],"names":[],"mappings":"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","file":"chunk-OD7P4L3D.js","sourcesContent":["/**\n * O que a pessoa abriu por último.\n *\n * ── por que no navegador, e não no banco ────────────────────────────────\n *\n * Isto é conveniência de quem está olhando, não informação da empresa. Uma\n * tabela custaria migração, RLS, escrita a cada navegação e uma pergunta que\n * ninguém faz (\"o que o Julio andou abrindo?\" é vigilância, não produto).\n *\n * O preço é que a lista não atravessa máquinas: quem abre no notebook não vê\n * no celular. É o preço certo — a alternativa é gravar a pegada de navegação\n * de cada vendedor num servidor para resolver um atalho.\n *\n * ── por que a TELA registra, e não o clique ─────────────────────────────\n *\n * `openEntity` parece o lugar óbvio (é a porta de todo registro), e não é: o\n * CRM navega por rota interna própria e nunca passa por lá, então metade das\n * visitas sumiria. Pior, um clique que cai numa rota quebrada contaria como\n * visita — e a lista de \"recentes\" passaria a guardar justamente os lugares\n * onde a pessoa NÃO conseguiu chegar.\n *\n * Visita é a tela de detalhe ter renderizado com o registro carregado. É lá\n * também que o NOME existe: `openEntity` só recebe o id, e uma lista de\n * recentes mostrando UUID não serve para nada.\n */\n\n/** Um registro aberto. `href` é resolvido na gravação, não na leitura — a rota\n * de hoje é a que funcionava quando a pessoa esteve lá. */\nexport interface RecentRecord {\n /** `${kind}:${id}` — um lead e uma empresa podem repetir id entre tabelas. */\n uid: string\n id: string\n /** `lead`, `company`, `quote`… o que a tela chama de si mesma. */\n kind: string\n /** Rótulo do grupo, já traduzido pela tela que gravou. */\n kindLabel: string\n label: string\n subtitle?: string\n /** Quando foi aberto (epoch ms). */\n at: number\n /** Para reabrir: os mesmos argumentos que a paleta passa a `openEntity`. */\n archetype?: string\n archetypeKind?: string\n entityKey?: string\n}\n\nconst CHAVE = 'fayz.recent-records.v1'\n\n/** Vinte. O suficiente para \"aquele que eu vi agora há pouco\" e pouco o\n * bastante para a lista continuar sendo uma lista, e não um histórico que\n * ninguém lê. */\nconst MAXIMO = 20\n\nconst ouvintes = new Set<() => void>()\nlet cache: RecentRecord[] | null = null\n\n/** Toda leitura e escrita passa por aqui.\n *\n * `localStorage` não é garantido: janela anônima, site data bloqueado,\n * captura de miniatura. Ele lança na hora do acesso, não na hora do uso — e\n * uma exceção aqui derrubaria a tela inteira por causa de um atalho. */\nfunction ler(): RecentRecord[] {\n if (cache) return cache\n try {\n const cru = localStorage.getItem(CHAVE)\n const dados = cru ? JSON.parse(cru) : []\n cache = Array.isArray(dados) ? dados.filter((x) => x && typeof x.id === 'string' && typeof x.label === 'string') : []\n } catch {\n cache = []\n }\n return cache\n}\n\nfunction gravar(lista: RecentRecord[]): void {\n cache = lista\n try {\n localStorage.setItem(CHAVE, JSON.stringify(lista))\n } catch {\n // Cota estourada ou armazenamento bloqueado. A lista em memória continua\n // valendo para esta sessão; perder o atalho é melhor que perder a tela.\n }\n for (const f of ouvintes) f()\n}\n\n/** Marca um registro como visto agora. Idempotente: reabrir sobe a entrada em\n * vez de duplicá-la, porque \"os últimos 20 que abri\" são vinte REGISTROS, não\n * vinte aberturas do mesmo. */\nexport function rememberVisit(entrada: Omit<RecentRecord, 'uid' | 'at'>): void {\n if (!entrada.id || !entrada.label?.trim()) return\n const uid = `${entrada.kind}:${entrada.id}`\n const nova: RecentRecord = { ...entrada, label: entrada.label.trim(), uid, at: Date.now() }\n const resto = ler().filter((x) => x.uid !== uid)\n gravar([nova, ...resto].slice(0, MAXIMO))\n}\n\n/** A lista, mais recente primeiro. */\nexport function listRecentRecords(): RecentRecord[] {\n return ler()\n}\n\nexport function clearRecentRecords(): void {\n gravar([])\n}\n\nexport function subscribeRecentRecords(f: () => void): () => void {\n ouvintes.add(f)\n return () => { ouvintes.delete(f) }\n}\n","import { useCallback, useEffect, useSyncExternalStore } from 'react'\nimport {\n listRecentRecords, rememberVisit, subscribeRecentRecords,\n type RecentRecord,\n} from '../lib/recent-records'\n\n/** A lista de visitados, que se atualiza sozinha quando outra tela grava. */\nexport function useRecentRecords(): RecentRecord[] {\n return useSyncExternalStore(\n subscribeRecentRecords,\n listRecentRecords,\n // No servidor não há `localStorage`. Devolver a mesma referência vazia\n // importa: um array novo a cada chamada faz o React re-renderizar para\n // sempre.\n () => VAZIO,\n )\n}\n\nconst VAZIO: RecentRecord[] = []\n\n/**\n * Registra a visita quando a tela de detalhe terminou de carregar.\n *\n * Recebe `null` enquanto o registro não chegou — e é justamente esse o\n * contrato: gravar antes do nome existir poria um UUID na lista, e gravar uma\n * tela que falhou em carregar poria na lista de \"recentes\" um lugar onde a\n * pessoa não conseguiu entrar.\n */\nexport function useRecordVisit(entrada: Omit<RecentRecord, 'uid' | 'at'> | null): void {\n const { id, kind, label } = entrada ?? {}\n // As dependências são os CAMPOS, não o objeto: a tela monta um literal novo\n // a cada render, e um objeto como dependência regravaria a visita em todo\n // ciclo — subindo a entrada, notificando os ouvintes e re-renderizando quem\n // a escuta, para sempre.\n useEffect(() => {\n if (!entrada || !id || !label) return\n rememberVisit(entrada)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [id, kind, label])\n}\n\n/** Reabrir um item da lista. */\nexport function useOpenRecent(): (r: RecentRecord) => void {\n return useCallback((r: RecentRecord) => {\n // Importado aqui para não puxar o roteador em quem só quer LER a lista.\n void import('../lib/entity-routes').then(({ openEntity }) => {\n openEntity(r.id, r.archetype, r.archetypeKind, undefined, r.entityKey)\n })\n }, [])\n}\n\nexport type { RecentRecord }\n"]}
1
+ {"version":3,"sources":["../src/lib/recent-records.ts","../src/hooks/useRecentRecords.ts"],"names":[],"mappings":"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","file":"chunk-SOKCLVM4.js","sourcesContent":["/**\n * O que a pessoa abriu por último.\n *\n * ── por que no navegador, e não no banco ────────────────────────────────\n *\n * Isto é conveniência de quem está olhando, não informação da empresa. Uma\n * tabela custaria migração, RLS, escrita a cada navegação e uma pergunta que\n * ninguém faz (\"o que o Julio andou abrindo?\" é vigilância, não produto).\n *\n * O preço é que a lista não atravessa máquinas: quem abre no notebook não vê\n * no celular. É o preço certo — a alternativa é gravar a pegada de navegação\n * de cada vendedor num servidor para resolver um atalho.\n *\n * ── por que a TELA registra, e não o clique ─────────────────────────────\n *\n * `openEntity` parece o lugar óbvio (é a porta de todo registro), e não é: o\n * CRM navega por rota interna própria e nunca passa por lá, então metade das\n * visitas sumiria. Pior, um clique que cai numa rota quebrada contaria como\n * visita — e a lista de \"recentes\" passaria a guardar justamente os lugares\n * onde a pessoa NÃO conseguiu chegar.\n *\n * Visita é a tela de detalhe ter renderizado com o registro carregado. É lá\n * também que o NOME existe: `openEntity` só recebe o id, e uma lista de\n * recentes mostrando UUID não serve para nada.\n */\n\n/** Um registro aberto. `href` é resolvido na gravação, não na leitura — a rota\n * de hoje é a que funcionava quando a pessoa esteve lá. */\nexport interface RecentRecord {\n /** `${kind}:${id}` — um lead e uma empresa podem repetir id entre tabelas. */\n uid: string\n id: string\n /** `lead`, `company`, `quote`… o que a tela chama de si mesma. */\n kind: string\n /** Rótulo do grupo, já traduzido pela tela que gravou. */\n kindLabel: string\n label: string\n subtitle?: string\n /** Quando foi aberto (epoch ms). */\n at: number\n /** Para reabrir: os mesmos argumentos que a paleta passa a `openEntity`. */\n archetype?: string\n archetypeKind?: string\n entityKey?: string\n}\n\nconst CHAVE = 'fayz.recent-records.v1'\n\n/** Vinte. O suficiente para \"aquele que eu vi agora há pouco\" e pouco o\n * bastante para a lista continuar sendo uma lista, e não um histórico que\n * ninguém lê. */\nconst MAXIMO = 20\n\nconst ouvintes = new Set<() => void>()\nlet cache: RecentRecord[] | null = null\n\n/** Toda leitura e escrita passa por aqui.\n *\n * `localStorage` não é garantido: janela anônima, site data bloqueado,\n * captura de miniatura. Ele lança na hora do acesso, não na hora do uso — e\n * uma exceção aqui derrubaria a tela inteira por causa de um atalho. */\nfunction ler(): RecentRecord[] {\n if (cache) return cache\n try {\n const cru = localStorage.getItem(CHAVE)\n const dados = cru ? JSON.parse(cru) : []\n cache = Array.isArray(dados) ? dados.filter((x) => x && typeof x.id === 'string' && typeof x.label === 'string') : []\n } catch {\n cache = []\n }\n return cache\n}\n\nfunction gravar(lista: RecentRecord[]): void {\n cache = lista\n try {\n localStorage.setItem(CHAVE, JSON.stringify(lista))\n } catch {\n // Cota estourada ou armazenamento bloqueado. A lista em memória continua\n // valendo para esta sessão; perder o atalho é melhor que perder a tela.\n }\n for (const f of ouvintes) f()\n}\n\n/** Marca um registro como visto agora. Idempotente: reabrir sobe a entrada em\n * vez de duplicá-la, porque \"os últimos 20 que abri\" são vinte REGISTROS, não\n * vinte aberturas do mesmo. */\nexport function rememberVisit(entrada: Omit<RecentRecord, 'uid' | 'at'>): void {\n if (!entrada.id || !entrada.label?.trim()) return\n const uid = `${entrada.kind}:${entrada.id}`\n const nova: RecentRecord = { ...entrada, label: entrada.label.trim(), uid, at: Date.now() }\n const resto = ler().filter((x) => x.uid !== uid)\n gravar([nova, ...resto].slice(0, MAXIMO))\n}\n\n/** A lista, mais recente primeiro. */\nexport function listRecentRecords(): RecentRecord[] {\n return ler()\n}\n\nexport function clearRecentRecords(): void {\n gravar([])\n}\n\nexport function subscribeRecentRecords(f: () => void): () => void {\n ouvintes.add(f)\n return () => { ouvintes.delete(f) }\n}\n","import { useCallback, useEffect, useSyncExternalStore } from 'react'\nimport {\n listRecentRecords, rememberVisit, subscribeRecentRecords,\n type RecentRecord,\n} from '../lib/recent-records'\n\n/** A lista de visitados, que se atualiza sozinha quando outra tela grava. */\nexport function useRecentRecords(): RecentRecord[] {\n return useSyncExternalStore(\n subscribeRecentRecords,\n listRecentRecords,\n // No servidor não há `localStorage`. Devolver a mesma referência vazia\n // importa: um array novo a cada chamada faz o React re-renderizar para\n // sempre.\n () => VAZIO,\n )\n}\n\nconst VAZIO: RecentRecord[] = []\n\n/**\n * Registra a visita quando a tela de detalhe terminou de carregar.\n *\n * Recebe `null` enquanto o registro não chegou — e é justamente esse o\n * contrato: gravar antes do nome existir poria um UUID na lista, e gravar uma\n * tela que falhou em carregar poria na lista de \"recentes\" um lugar onde a\n * pessoa não conseguiu entrar.\n */\nexport function useRecordVisit(entrada: Omit<RecentRecord, 'uid' | 'at'> | null): void {\n const { id, kind, label } = entrada ?? {}\n // As dependências são os CAMPOS, não o objeto: a tela monta um literal novo\n // a cada render, e um objeto como dependência regravaria a visita em todo\n // ciclo — subindo a entrada, notificando os ouvintes e re-renderizando quem\n // a escuta, para sempre.\n useEffect(() => {\n if (!entrada || !id || !label) return\n rememberVisit(entrada)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [id, kind, label])\n}\n\n/** Reabrir um item da lista. */\nexport function useOpenRecent(): (r: RecentRecord) => void {\n return useCallback((r: RecentRecord) => {\n // Importado aqui para não puxar o roteador em quem só quer LER a lista.\n void import('../lib/entity-routes').then(({ openEntity }) => {\n openEntity(r.id, r.archetype, r.archetypeKind, undefined, r.entityKey)\n })\n }, [])\n}\n\nexport type { RecentRecord }\n"]}
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=chunk-TL42NKTH.js.map
3
+ //# sourceMappingURL=chunk-TL42NKTH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-TL42NKTH.js"}