@fayz-ai/admin 0.62.0 → 0.65.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CommandPalette-B5PMQFDG.js → CommandPalette-XDJJ5E3O.js} +3 -3
- package/dist/{CommandPalette-B5PMQFDG.js.map → CommandPalette-XDJJ5E3O.js.map} +1 -1
- package/dist/ConnectedBrandingSettings-43ZM3HCI.js +680 -0
- package/dist/ConnectedBrandingSettings-43ZM3HCI.js.map +1 -0
- package/dist/ConnectedCompanySettings-I4MALI2Q.js +197 -0
- package/dist/ConnectedCompanySettings-I4MALI2Q.js.map +1 -0
- package/dist/ConnectedModulesSettings-A2VJ5BMJ.js +563 -0
- package/dist/ConnectedModulesSettings-A2VJ5BMJ.js.map +1 -0
- package/dist/ConnectedSecuritySettings-25AHA4HJ.js +189 -0
- package/dist/ConnectedSecuritySettings-25AHA4HJ.js.map +1 -0
- package/dist/ConnectedUserProfile-E7USJRUE.js +424 -0
- package/dist/ConnectedUserProfile-E7USJRUE.js.map +1 -0
- package/dist/FieldRulesEditor-YGIPKI5T.js +129 -0
- package/dist/FieldRulesEditor-YGIPKI5T.js.map +1 -0
- package/dist/GettingStartedWidget-HMIJ3Y24.js +12 -0
- package/dist/GettingStartedWidget-HMIJ3Y24.js.map +1 -0
- package/dist/PermissionProfilesTab-3ZPRCHMZ.js +548 -0
- package/dist/PermissionProfilesTab-3ZPRCHMZ.js.map +1 -0
- package/dist/PersonTimeline-HQZT527S.js +3 -0
- package/dist/{PersonTimeline-X7JTPUWJ.js.map → PersonTimeline-HQZT527S.js.map} +1 -1
- package/dist/ProjectsSettings-7O2F27MD.js +5 -0
- package/dist/ProjectsSettings-7O2F27MD.js.map +1 -0
- package/dist/ScheduleEditor-54ELWR45.js +8 -0
- package/dist/{ScheduleEditor-L2UJXKMY.js.map → ScheduleEditor-54ELWR45.js.map} +1 -1
- package/dist/SeuSiteWidget-QLSUVC63.js +6 -0
- package/dist/SeuSiteWidget-QLSUVC63.js.map +1 -0
- package/dist/SubscriptionPage-5GE5HFJK.js +17 -0
- package/dist/{SubscriptionPage-IDVEFLZ5.js.map → SubscriptionPage-5GE5HFJK.js.map} +1 -1
- package/dist/TeamTab-6RAULFLW.js +502 -0
- package/dist/TeamTab-6RAULFLW.js.map +1 -0
- package/dist/TrialWidget-Z6CLUCU3.js +7 -0
- package/dist/TrialWidget-Z6CLUCU3.js.map +1 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +14 -0
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/routing.d.ts +6 -0
- package/dist/app/routing.d.ts.map +1 -1
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/billing/index.js +9 -4
- package/dist/chunk-2H46WDG5.js +53 -0
- package/dist/chunk-2H46WDG5.js.map +1 -0
- package/dist/chunk-3HLX7YB2.js +94 -0
- package/dist/chunk-3HLX7YB2.js.map +1 -0
- package/dist/chunk-3OUD4XBK.js +3 -0
- package/dist/chunk-3OUD4XBK.js.map +1 -0
- package/dist/{chunk-IXSY4GPK.js → chunk-5D5QLMOQ.js} +5 -3
- package/dist/chunk-5D5QLMOQ.js.map +1 -0
- package/dist/chunk-5WR62X2R.js +1081 -0
- package/dist/chunk-5WR62X2R.js.map +1 -0
- package/dist/{chunk-KPYRTOCK.js → chunk-6CQFGMEO.js} +4 -140
- package/dist/chunk-6CQFGMEO.js.map +1 -0
- package/dist/chunk-6MMSYC2I.js +116 -0
- package/dist/chunk-6MMSYC2I.js.map +1 -0
- package/dist/chunk-6QFOE3UD.js +1466 -0
- package/dist/chunk-6QFOE3UD.js.map +1 -0
- package/dist/chunk-6UUFVQ2E.js +283 -0
- package/dist/chunk-6UUFVQ2E.js.map +1 -0
- package/dist/chunk-7WNLL3F6.js +7889 -0
- package/dist/chunk-7WNLL3F6.js.map +1 -0
- package/dist/chunk-A74IHAQE.js +439 -0
- package/dist/chunk-A74IHAQE.js.map +1 -0
- package/dist/{chunk-JOBRLBIQ.js → chunk-AFA6IO23.js} +4 -3
- package/dist/chunk-AFA6IO23.js.map +1 -0
- package/dist/{chunk-P5UIGQFY.js → chunk-ALCSIN3Z.js} +5 -5
- package/dist/chunk-ALCSIN3Z.js.map +1 -0
- package/dist/chunk-AWM4AMBS.js +39 -0
- package/dist/chunk-AWM4AMBS.js.map +1 -0
- package/dist/{chunk-OQARLKZN.js → chunk-BG5Z6KLN.js} +6 -18
- package/dist/chunk-BG5Z6KLN.js.map +1 -0
- package/dist/chunk-DPNFZONC.js +19 -0
- package/dist/chunk-DPNFZONC.js.map +1 -0
- package/dist/chunk-DRCV643L.js +193 -0
- package/dist/chunk-DRCV643L.js.map +1 -0
- package/dist/chunk-E6DF3VR6.js +206 -0
- package/dist/chunk-E6DF3VR6.js.map +1 -0
- package/dist/chunk-ENHCVGXZ.js +120 -0
- package/dist/chunk-ENHCVGXZ.js.map +1 -0
- package/dist/chunk-FK6RHGI7.js +56 -0
- package/dist/chunk-FK6RHGI7.js.map +1 -0
- package/dist/chunk-G5HNA5QK.js +233 -0
- package/dist/chunk-G5HNA5QK.js.map +1 -0
- package/dist/chunk-HCFO4Z6A.js +53 -0
- package/dist/chunk-HCFO4Z6A.js.map +1 -0
- package/dist/chunk-JFIU6NQP.js +51 -0
- package/dist/chunk-JFIU6NQP.js.map +1 -0
- package/dist/chunk-JRZJBMOY.js +3 -0
- package/dist/chunk-JRZJBMOY.js.map +1 -0
- package/dist/{chunk-3YP7KW47.js → chunk-JVVOL4BB.js} +41 -512
- package/dist/chunk-JVVOL4BB.js.map +1 -0
- package/dist/chunk-KAQ3UIAA.js +415 -0
- package/dist/chunk-KAQ3UIAA.js.map +1 -0
- package/dist/{chunk-P6XUDT2P.js → chunk-KJELMULA.js} +4 -51
- package/dist/chunk-KJELMULA.js.map +1 -0
- package/dist/chunk-KLO7DKXX.js +1651 -0
- package/dist/chunk-KLO7DKXX.js.map +1 -0
- package/dist/chunk-M2CMDWUI.js +75 -0
- package/dist/chunk-M2CMDWUI.js.map +1 -0
- package/dist/chunk-PQ47MLTH.js +255 -0
- package/dist/chunk-PQ47MLTH.js.map +1 -0
- package/dist/{chunk-VX3WJLKK.js → chunk-QD7BMUDF.js} +17 -8
- package/dist/chunk-QD7BMUDF.js.map +1 -0
- package/dist/{chunk-E4MESMNR.js → chunk-QTSKCPSW.js} +7 -10
- package/dist/chunk-QTSKCPSW.js.map +1 -0
- package/dist/chunk-S56FQKPL.js +186 -0
- package/dist/chunk-S56FQKPL.js.map +1 -0
- package/dist/chunk-SGAFPNMA.js +75 -0
- package/dist/chunk-SGAFPNMA.js.map +1 -0
- package/dist/{chunk-OD7P4L3D.js → chunk-SOKCLVM4.js} +3 -3
- package/dist/{chunk-OD7P4L3D.js.map → chunk-SOKCLVM4.js.map} +1 -1
- package/dist/chunk-TL42NKTH.js +3 -0
- package/dist/chunk-TL42NKTH.js.map +1 -0
- package/dist/chunk-U6ZCO2PS.js +2964 -0
- package/dist/chunk-U6ZCO2PS.js.map +1 -0
- package/dist/chunk-VSXNROYH.js +16 -0
- package/dist/chunk-VSXNROYH.js.map +1 -0
- package/dist/chunk-WPFZCYZL.js +619 -0
- package/dist/chunk-WPFZCYZL.js.map +1 -0
- package/dist/chunk-XFTNIBDW.js +23 -0
- package/dist/chunk-XFTNIBDW.js.map +1 -0
- package/dist/chunk-Y66JDL6I.js +1020 -0
- package/dist/chunk-Y66JDL6I.js.map +1 -0
- package/dist/chunk-YEHIBPIX.js +137 -0
- package/dist/chunk-YEHIBPIX.js.map +1 -0
- package/dist/chunk-YKPPCV2Z.js +53 -0
- package/dist/chunk-YKPPCV2Z.js.map +1 -0
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
- package/dist/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/connect-XKQJOWR7.js +18 -0
- package/dist/connect-XKQJOWR7.js.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/RelatedListTab.d.ts.map +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts +1 -1
- package/dist/crud/archetypes/EntityLink.d.ts.map +1 -1
- package/dist/crud/record-chrome.d.ts.map +1 -1
- package/dist/entity-routes-PGU7EHAD.js +4 -0
- package/dist/{entity-routes-LXOBW5IB.js.map → entity-routes-PGU7EHAD.js.map} +1 -1
- package/dist/hooks/useModuleNavigation.d.ts.map +1 -1
- package/dist/impersonation/ImpersonateDialog.d.ts.map +1 -1
- package/dist/impersonation/session.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11951 -33576
- package/dist/index.js.map +1 -1
- package/dist/lib/attention.d.ts +6 -0
- package/dist/lib/attention.d.ts.map +1 -1
- package/dist/lib/entity-routes.d.ts.map +1 -1
- package/dist/org/index.js +4 -2
- package/dist/plugins/PluginRegistryManager.d.ts.map +1 -1
- package/dist/settings/MessageTemplateRow.d.ts.map +1 -1
- package/dist/timeline/PersonTimeline.d.ts.map +1 -1
- package/dist/timeline/TimelineList.d.ts.map +1 -1
- package/dist/units/UnitRailPage.d.ts.map +1 -1
- package/dist/units/useUnitFilter.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/PersonTimeline-X7JTPUWJ.js +0 -3
- package/dist/ScheduleEditor-L2UJXKMY.js +0 -6
- package/dist/SubscriptionPage-IDVEFLZ5.js +0 -8
- package/dist/chunk-3YP7KW47.js.map +0 -1
- package/dist/chunk-E4MESMNR.js.map +0 -1
- package/dist/chunk-IXSY4GPK.js.map +0 -1
- package/dist/chunk-JOBRLBIQ.js.map +0 -1
- package/dist/chunk-KPYRTOCK.js.map +0 -1
- package/dist/chunk-OQARLKZN.js.map +0 -1
- package/dist/chunk-P5UIGQFY.js.map +0 -1
- package/dist/chunk-P6XUDT2P.js.map +0 -1
- package/dist/chunk-VX3WJLKK.js.map +0 -1
- package/dist/entity-routes-LXOBW5IB.js +0 -4
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import { useTrial } from './chunk-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { useTrial } from './chunk-SGAFPNMA.js';
|
|
2
|
+
import { cn } from './chunk-JRZJBMOY.js';
|
|
3
|
+
import { navigateTo } from './chunk-QD7BMUDF.js';
|
|
4
4
|
import { Crown, Clock } from 'lucide-react';
|
|
5
5
|
import { tl } from '@fayz-ai/core';
|
|
6
6
|
import { useLayoutStore, Button } from '@fayz-ai/ui';
|
|
7
7
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
8
8
|
|
|
9
|
-
// src/billing/plan-badge.ts
|
|
10
|
-
function capitalize(s) {
|
|
11
|
-
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
12
|
-
}
|
|
13
|
-
function resolvePlanBadge(planId, plans) {
|
|
14
|
-
const id = planId || "free";
|
|
15
|
-
const match = plans.find((p) => p.id === id);
|
|
16
|
-
if (match) {
|
|
17
|
-
return { label: match.name, planId: match.id, paid: match.price > 0 };
|
|
18
|
-
}
|
|
19
|
-
return { label: capitalize(id), planId: id, paid: false };
|
|
20
|
-
}
|
|
21
9
|
function TrialWidget({ totalDays = 30, route = "/settings/subscription", className }) {
|
|
22
10
|
const trial = useTrial();
|
|
23
11
|
const collapsed = useLayoutStore((s) => s.sidebarCollapsed);
|
|
@@ -87,6 +75,6 @@ function TrialWidget({ totalDays = 30, route = "/settings/subscription", classNa
|
|
|
87
75
|
);
|
|
88
76
|
}
|
|
89
77
|
|
|
90
|
-
export { TrialWidget
|
|
91
|
-
//# sourceMappingURL=chunk-
|
|
92
|
-
//# sourceMappingURL=chunk-
|
|
78
|
+
export { TrialWidget };
|
|
79
|
+
//# sourceMappingURL=chunk-BG5Z6KLN.js.map
|
|
80
|
+
//# sourceMappingURL=chunk-BG5Z6KLN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/billing/TrialWidget.tsx"],"names":[],"mappings":"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","file":"chunk-BG5Z6KLN.js","sourcesContent":["import * as React from 'react'\nimport { Clock, Crown } from 'lucide-react'\nimport { tl } from '@fayz-ai/core'\nimport { cn } from '../lib/cn'\nimport { navigateTo } from '../app/routing'\nimport { Button, useLayoutStore } from '@fayz-ai/ui'\nimport { useTrial } from './trial'\n\nexport interface TrialWidgetProps {\n /** Length of the window, for the progress bar. Matches the DB column default (migration 044). */\n totalDays?: number\n /** Where \"see the plans\" goes. */\n route?: string\n className?: string\n}\n\n/**\n * The trial, in the rail.\n *\n * A countdown is a promise about the future, so it belongs where somebody looks\n * at their own progress — one line under the getting-started checklist, in the\n * menu's own colours, not a banner across the app. Two states only:\n *\n * • RUNNING — informational, and quiet until the last two days.\n * • ENDED — the paywall's face. It stops being a note and becomes a button,\n * because at that point every gated feature in the product is already\n * answering \"not on your plan\" and this is the one place that says why.\n *\n * There is no third state: a paying tenant, or one that predates trials, gets\n * nothing at all.\n */\nexport function TrialWidget({ totalDays = 30, route = '/settings/subscription', className }: TrialWidgetProps) {\n const trial = useTrial()\n const collapsed = useLayoutStore((s) => s.sidebarCollapsed)\n\n if (trial.status === 'none') return null\n\n const expired = trial.status === 'expired'\n const pct = Math.max(0, Math.min(100, (trial.daysLeft / totalDays) * 100))\n\n const label = expired\n ? tl('Trial ended', 'Seu teste terminou')\n : trial.daysLeft <= 1\n ? tl('Last day of your trial', 'Último dia do teste')\n : tl(`${trial.daysLeft} days of trial left`, `${trial.daysLeft} dias de teste`)\n\n if (collapsed) {\n return (\n <Button\n variant=\"bare\"\n size=\"bare\"\n type=\"button\"\n onClick={() => navigateTo(route)}\n title={label}\n aria-label={label}\n data-tour=\"billing.trial\"\n className={cn(\n 'flex w-full items-center justify-center rounded-button p-1.5 transition-colors',\n expired\n ? 'bg-primary/15 text-primary hover:bg-primary/25'\n : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/60',\n className,\n )}\n >\n {expired ? <Crown className=\"h-4 w-4\" /> : <Clock className=\"h-4 w-4\" />}\n </Button>\n )\n }\n\n return (\n <Button\n variant=\"bare\"\n size=\"bare\"\n type=\"button\"\n onClick={() => navigateTo(route)}\n data-tour=\"billing.trial\"\n className={cn(\n 'w-full rounded-button px-1.5 py-1.5 text-left transition-colors inline-block whitespace-normal text-[length:inherit] font-normal',\n expired ? 'bg-primary/15 hover:bg-primary/25' : 'hover:bg-sidebar-accent/50',\n className,\n )}\n >\n <span className=\"flex items-center gap-2\">\n {expired\n ? <Crown className=\"h-3.5 w-3.5 shrink-0 text-primary\" />\n : <Clock className={cn('h-3.5 w-3.5 shrink-0', trial.endingSoon ? 'text-warning' : 'text-sidebar-muted')} />}\n <span\n className={cn(\n 'min-w-0 flex-1 truncate text-[12px] font-medium',\n expired ? 'text-sidebar-foreground' : 'text-sidebar-foreground/90',\n )}\n >\n {label}\n </span>\n </span>\n {expired ? (\n <span className=\"mt-1 block pl-[22px] text-[11px] leading-snug text-sidebar-muted\">\n {tl('Choose a plan to keep the assistant', 'Escolha um plano para manter o assistente')}\n </span>\n ) : (\n <span className=\"mt-1.5 block h-1 overflow-hidden rounded-full bg-sidebar-accent\">\n <span\n className={cn(\n 'block h-full rounded-full transition-all',\n trial.endingSoon ? 'bg-warning' : 'bg-sidebar-foreground/70',\n )}\n style={{ width: `${pct}%` }}\n />\n </span>\n )}\n </Button>\n )\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useTenantOptional, useUnitStore } from './chunk-5D5QLMOQ.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useAuthStore } from '@fayz-ai/auth';
|
|
4
|
+
|
|
5
|
+
function useUnitBootstrap() {
|
|
6
|
+
const tenant = useTenantOptional();
|
|
7
|
+
const userId = useAuthStore((s) => s.user?.id);
|
|
8
|
+
const orgId = tenant?.org?.id;
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
if (!orgId) return;
|
|
11
|
+
const { loadedFor, load, reset } = useUnitStore.getState();
|
|
12
|
+
if (loadedFor && loadedFor !== orgId) reset();
|
|
13
|
+
void load(orgId, userId);
|
|
14
|
+
}, [orgId, userId]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { useUnitBootstrap };
|
|
18
|
+
//# sourceMappingURL=chunk-DPNFZONC.js.map
|
|
19
|
+
//# sourceMappingURL=chunk-DPNFZONC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/units/useUnitBootstrap.ts"],"names":[],"mappings":";;;;AAaO,SAAS,gBAAA,GAAyB;AACvC,EAAA,MAAM,SAAS,iBAAA,EAAkB;AACjC,EAAA,MAAM,SAAS,YAAA,CAAa,CAAC,CAAA,KAAM,CAAA,CAAE,MAAM,EAAE,CAAA;AAC7C,EAAA,MAAM,KAAA,GAAQ,QAAQ,GAAA,EAAK,EAAA;AAQ3B,EAAM,gBAAU,MAAM;AACpB,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,EAAE,SAAA,EAAW,IAAA,EAAM,KAAA,EAAM,GAAI,aAAa,QAAA,EAAS;AACzD,IAAA,IAAI,SAAA,IAAa,SAAA,KAAc,KAAA,EAAO,KAAA,EAAM;AAC5C,IAAA,KAAK,IAAA,CAAK,OAAO,MAAM,CAAA;AAAA,EACzB,CAAA,EAAG,CAAC,KAAA,EAAO,MAAM,CAAC,CAAA;AAEpB","file":"chunk-DPNFZONC.js","sourcesContent":["import * as React from 'react'\nimport { useTenantOptional } from '../org'\nimport { useAuthStore } from '@fayz-ai/auth'\nimport { useUnitStore } from './store'\n\n/**\n * Loads the unit tree, the person's bindings and the tenant's switch.\n *\n * Called from the surfaces that need it rather than from one initializer in the\n * app root, because the store dedupes by tenant (`loadedFor`) and a screen that\n * needs units should be the thing that asks for them. Resets on tenant change,\n * so a unit can never survive into another workspace.\n */\nexport function useUnitBootstrap(): void {\n const tenant = useTenantOptional()\n const userId = useAuthStore((s) => s.user?.id)\n const orgId = tenant?.org?.id\n\n // `loadedFor` é lido do estado em vez de assinado. Assinando, este efeito\n // dependia de um valor que o próprio `load` escreve: a primeira chamada\n // marcava a conta, todo componente montado via a mudança, todo efeito\n // reexecutava e chamava `load` de novo. Com dez telas pedindo unidades, é uma\n // requisição por tela — foi assim que uma chamada única virou cinco na aba de\n // rede. A dedupe da store fecha a porta; isto tira o dedo da campainha.\n React.useEffect(() => {\n if (!orgId) return\n const { loadedFor, load, reset } = useUnitStore.getState()\n if (loadedFor && loadedFor !== orgId) reset()\n void load(orgId, userId)\n }, [orgId, userId])\n\n}\n"]}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { getSupabaseClientOptional } from './chunk-KJELMULA.js';
|
|
2
|
+
import { useOrganizationStore } from './chunk-YKPPCV2Z.js';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { isCreateRoute } from '@fayz-ai/ui';
|
|
5
|
+
|
|
6
|
+
var VERTICAL_LABELS = {
|
|
7
|
+
food: "Restaurante",
|
|
8
|
+
beauty: "Sal\xE3o & Est\xE9tica",
|
|
9
|
+
commerce: "E-commerce",
|
|
10
|
+
education: "Educa\xE7\xE3o",
|
|
11
|
+
events: "Eventos",
|
|
12
|
+
accounting: "Contabilidade",
|
|
13
|
+
generic: "Gest\xE3o geral"
|
|
14
|
+
};
|
|
15
|
+
var CROSS_CUTTING = "all";
|
|
16
|
+
function verticalLabel(verticalId) {
|
|
17
|
+
return VERTICAL_LABELS[verticalId] ?? verticalId;
|
|
18
|
+
}
|
|
19
|
+
function pluginVerticalIndex(catalog) {
|
|
20
|
+
const byVertical = /* @__PURE__ */ new Map();
|
|
21
|
+
const mine = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const app of catalog) {
|
|
23
|
+
const vertical = app.vertical_id;
|
|
24
|
+
if (!vertical) continue;
|
|
25
|
+
const offer = byVertical.get(vertical) ?? /* @__PURE__ */ new Set();
|
|
26
|
+
for (const id of app.modules) offer.add(id);
|
|
27
|
+
byVertical.set(vertical, offer);
|
|
28
|
+
if (app.mine) mine.add(vertical);
|
|
29
|
+
}
|
|
30
|
+
const total = byVertical.size;
|
|
31
|
+
const index = /* @__PURE__ */ new Map();
|
|
32
|
+
for (const [vertical, offer] of byVertical) {
|
|
33
|
+
for (const moduleId of offer) {
|
|
34
|
+
if (index.has(moduleId)) continue;
|
|
35
|
+
const offering = [...byVertical].filter(([, o]) => o.has(moduleId)).map(([v]) => v);
|
|
36
|
+
const crossCutting = offering.length >= 2 && offering.length * 2 > total;
|
|
37
|
+
if (crossCutting) {
|
|
38
|
+
index.set(moduleId, null);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const own = offering.find((v) => mine.has(v));
|
|
42
|
+
index.set(moduleId, own ?? [...offering].sort()[0] ?? vertical);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return index;
|
|
46
|
+
}
|
|
47
|
+
function pluginAppNames(catalog, pluginId) {
|
|
48
|
+
return [...new Set(catalog.filter((app) => app.modules.includes(pluginId)).map((app) => app.name))].sort();
|
|
49
|
+
}
|
|
50
|
+
function isCrossCutting(catalog, pluginId) {
|
|
51
|
+
const byVertical = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const app of catalog) {
|
|
53
|
+
if (!app.vertical_id) continue;
|
|
54
|
+
const offer = byVertical.get(app.vertical_id) ?? /* @__PURE__ */ new Set();
|
|
55
|
+
for (const id of app.modules) offer.add(id);
|
|
56
|
+
byVertical.set(app.vertical_id, offer);
|
|
57
|
+
}
|
|
58
|
+
const total = byVertical.size;
|
|
59
|
+
const offering = [...byVertical.values()].filter((offer) => offer.has(pluginId)).length;
|
|
60
|
+
return offering >= 2 && offering * 2 > total;
|
|
61
|
+
}
|
|
62
|
+
function appLabelFor(catalog, pluginId) {
|
|
63
|
+
if (catalog.length === 0) return void 0;
|
|
64
|
+
if (isCrossCutting(catalog, pluginId)) return "Todos os neg\xF3cios";
|
|
65
|
+
const names = pluginAppNames(catalog, pluginId);
|
|
66
|
+
return names.length ? `Estende: ${names.join(", ")}` : void 0;
|
|
67
|
+
}
|
|
68
|
+
function buildCategories(catalog, rows) {
|
|
69
|
+
if (catalog.length === 0) {
|
|
70
|
+
return rows.length ? [{ id: CROSS_CUTTING, label: "Dispon\xEDveis", mine: true, rows }] : [];
|
|
71
|
+
}
|
|
72
|
+
const byVertical = /* @__PURE__ */ new Map();
|
|
73
|
+
const mine = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const app of catalog) {
|
|
75
|
+
const vertical = app.vertical_id;
|
|
76
|
+
if (!vertical) continue;
|
|
77
|
+
const offer = byVertical.get(vertical) ?? /* @__PURE__ */ new Set();
|
|
78
|
+
for (const id of app.modules) offer.add(id);
|
|
79
|
+
byVertical.set(vertical, offer);
|
|
80
|
+
if (app.mine) mine.add(vertical);
|
|
81
|
+
}
|
|
82
|
+
const total = byVertical.size;
|
|
83
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
84
|
+
const push = (key, row) => buckets.set(key, [...buckets.get(key) ?? [], row]);
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const offering = [...byVertical].filter(([, offer]) => offer.has(row.plugin.id)).map(([vertical]) => vertical);
|
|
87
|
+
if (offering.length === 0) continue;
|
|
88
|
+
if (offering.length >= 2 && offering.length * 2 > total) push(CROSS_CUTTING, row);
|
|
89
|
+
else for (const vertical of offering) push(vertical, row);
|
|
90
|
+
}
|
|
91
|
+
const category = (id) => ({
|
|
92
|
+
id,
|
|
93
|
+
label: id === CROSS_CUTTING ? "Todos os neg\xF3cios" : verticalLabel(id),
|
|
94
|
+
mine: id === CROSS_CUTTING || mine.has(id),
|
|
95
|
+
rows: buckets.get(id) ?? []
|
|
96
|
+
});
|
|
97
|
+
const ordered = [
|
|
98
|
+
...[...mine].sort((a, b) => verticalLabel(a).localeCompare(verticalLabel(b))),
|
|
99
|
+
CROSS_CUTTING,
|
|
100
|
+
...[...byVertical.keys()].filter((v) => !mine.has(v)).sort((a, b) => verticalLabel(a).localeCompare(verticalLabel(b)))
|
|
101
|
+
];
|
|
102
|
+
return ordered.map(category).filter((c) => c.rows.length > 0);
|
|
103
|
+
}
|
|
104
|
+
function useModuleCatalog() {
|
|
105
|
+
const tenantId = useOrganizationStore((s) => s.currentOrg?.id ?? null);
|
|
106
|
+
const [catalog, setCatalog] = React.useState([]);
|
|
107
|
+
React.useEffect(() => {
|
|
108
|
+
const supabase = getSupabaseClientOptional();
|
|
109
|
+
if (!supabase) return;
|
|
110
|
+
let alive = true;
|
|
111
|
+
void supabase.rpc("app_catalog").then((res) => {
|
|
112
|
+
if (!alive || res.error || !Array.isArray(res.data)) return;
|
|
113
|
+
setCatalog(res.data);
|
|
114
|
+
});
|
|
115
|
+
return () => {
|
|
116
|
+
alive = false;
|
|
117
|
+
};
|
|
118
|
+
}, [tenantId]);
|
|
119
|
+
return catalog;
|
|
120
|
+
}
|
|
121
|
+
var isGroup = (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.under);
|
|
122
|
+
function matchesPrefix(route, prefix) {
|
|
123
|
+
return route === prefix || route.startsWith(`${prefix}/`);
|
|
124
|
+
}
|
|
125
|
+
function applyRailPreset(rows, rail, hide, whole) {
|
|
126
|
+
const hidden = (route) => !!hide?.some((p) => matchesPrefix(route, p));
|
|
127
|
+
const opensWhole = (route) => !!whole?.some((p) => matchesPrefix(route, p));
|
|
128
|
+
const prune = (list) => list.filter((r) => !hidden(r.route)).map((r) => {
|
|
129
|
+
if (!r.children?.length) return r;
|
|
130
|
+
const children = opensWhole(r.route) ? [] : prune(r.children);
|
|
131
|
+
const real = children.some((c) => c.route !== r.route);
|
|
132
|
+
return { ...r, children: real ? children : void 0 };
|
|
133
|
+
});
|
|
134
|
+
const visible = hide?.length ? prune(rows) : rows;
|
|
135
|
+
if (!rail?.length) return visible;
|
|
136
|
+
const pool = [...visible];
|
|
137
|
+
const take = (pred) => {
|
|
138
|
+
const taken = [];
|
|
139
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
140
|
+
if (pred(pool[i])) taken.unshift(...pool.splice(i, 1));
|
|
141
|
+
}
|
|
142
|
+
return taken;
|
|
143
|
+
};
|
|
144
|
+
const groupClaims = /* @__PURE__ */ new Map();
|
|
145
|
+
rail.forEach((entry, i) => {
|
|
146
|
+
if (!isGroup(entry)) return;
|
|
147
|
+
groupClaims.set(i, take((row) => entry.under.some((p) => matchesPrefix(row.route, p))));
|
|
148
|
+
});
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const [i, entry] of rail.entries()) {
|
|
151
|
+
if (isGroup(entry)) {
|
|
152
|
+
const claimed = groupClaims.get(i) ?? [];
|
|
153
|
+
const rank = (row) => {
|
|
154
|
+
const i2 = entry.under.findIndex((p) => matchesPrefix(row.route, p));
|
|
155
|
+
return i2 === -1 ? entry.under.length : i2;
|
|
156
|
+
};
|
|
157
|
+
claimed.sort((a, b) => rank(a) - rank(b));
|
|
158
|
+
const children = claimed.flatMap((row) => row.children?.length ? row.children : [row]);
|
|
159
|
+
if (!children.length) continue;
|
|
160
|
+
const places = children.filter((row) => !isCreateRoute(row.route));
|
|
161
|
+
if (places.length <= 1) {
|
|
162
|
+
const only = places[0] ?? children[0];
|
|
163
|
+
out.push({ ...only, label: entry.label, icon: entry.icon ?? only.icon, children: void 0 });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
out.push({
|
|
167
|
+
// A rubrica leva à primeira coisa que ela contém: um cabeçalho que não
|
|
168
|
+
// vai a lugar nenhum é um clique que não faz nada.
|
|
169
|
+
...children[0],
|
|
170
|
+
route: children[0].route,
|
|
171
|
+
label: entry.label,
|
|
172
|
+
icon: entry.icon ?? children[0].icon,
|
|
173
|
+
children
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
out.push(...take((row) => row.pluginId === entry));
|
|
178
|
+
}
|
|
179
|
+
for (const row of pool) {
|
|
180
|
+
const before = row.before;
|
|
181
|
+
const at = before?.length ? out.findIndex((r) => before.some((b) => matchesPrefix(r.route, b) || r.children?.some((c) => matchesPrefix(c.route, b)))) : -1;
|
|
182
|
+
if (at >= 0) out.splice(at, 0, row);
|
|
183
|
+
else out.push(row);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function labelFor(labels, route, fallback) {
|
|
188
|
+
return labels?.[route] ?? fallback;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export { appLabelFor, applyRailPreset, buildCategories, labelFor, pluginVerticalIndex, useModuleCatalog, verticalLabel };
|
|
192
|
+
//# sourceMappingURL=chunk-DRCV643L.js.map
|
|
193
|
+
//# sourceMappingURL=chunk-DRCV643L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/settings/module-catalog.ts","../src/app/rail-preset.ts"],"names":["i"],"mappings":"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","file":"chunk-DRCV643L.js","sourcesContent":["import * as React from 'react'\nimport { getSupabaseClientOptional } from '../../lib/supabase'\nimport { useOrganizationStore } from '../../stores/organization.store'\nimport type { ModuleRow } from './ModuleCard'\n\n/**\n * O catálogo por ofício — a matéria-prima do marketplace de módulos (090).\n *\n * `app_catalog()` devolve os aplicativos que existem no produto, cada um com o\n * ofício dele e a lista que ele OFERECE. É a única fonte das categorias: uma\n * lista de rubricas escrita à mão aqui seria a terceira grafia do que a 079 já\n * guardou em `app.apps.preset.modules` — e a primeira a discordar.\n */\nexport interface CatalogApp {\n id: string\n name: string\n vertical_id: string | null\n modules: string[]\n /** Esta conta roda este aplicativo. Vem do servidor (090), não do cliente. */\n mine: boolean\n}\n\n/**\n * O ofício em palavra de gente.\n *\n * O vocabulário é fechado pela 079 e cabe num mapa. Um ofício novo que chegue\n * ao banco antes de chegar aqui aparece pelo id — ilegível é melhor que\n * invisível, e é o mesmo caminho que `moduleLabel` já faz nos conectores.\n */\nconst VERTICAL_LABELS: Record<string, string> = {\n food: 'Restaurante',\n beauty: 'Salão & Estética',\n commerce: 'E-commerce',\n education: 'Educação',\n events: 'Eventos',\n accounting: 'Contabilidade',\n generic: 'Gestão geral',\n}\n\n/** A rubrica dos módulos que não são de ofício nenhum porque são de todos. */\nexport const CROSS_CUTTING = 'all'\n\nexport interface ModuleCategory {\n /** Um `vertical_id`, ou `CROSS_CUTTING`. */\n id: string\n label: string\n /** A conta roda um aplicativo deste ofício — a rubrica abre expandida. */\n mine: boolean\n rows: ModuleRow[]\n}\n\nexport function verticalLabel(verticalId: string): string {\n return VERTICAL_LABELS[verticalId] ?? verticalId\n}\n\n/**\n * De que categoria é um módulo? Da de quem o oferece — e de ninguém, quando\n * todos oferecem.\n *\n * ── Por que \"transversal\" e não repetir em cada rubrica ────────────────────\n * `financial` está na oferta de seis dos sete ofícios, `crm` idem. Repetido,\n * ele apareceria seis vezes e ensinaria a ler a tela errado: que Financeiro é\n * uma escolha de restaurante, e outra de salão, e outra de escola. Ele não é\n * escolha de ofício nenhum — é o chão.\n *\n * ── O corte: MAIORIA dos ofícios ───────────────────────────────────────────\n * \"Todos\" literal não serviria — bastaria um ofício de retaguarda não oferecer\n * (o FiscalControl não oferece CRM) para o módulo mais transversal que existe\n * cair em seis rubricas. Maioria estrita é o corte que separa \"isto é de um\n * ramo\" de \"isto é de qualquer negócio\", e deixa a repetição existir onde ela\n * INFORMA: a agenda em Restaurante, Salão e Educação é três respostas certas.\n *\n * Dois ofícios no mínimo, para que a rubrica signifique alguma coisa: num pool\n * com um ofício só, \"todos os negócios\" e o ofício seriam a mesma lista.\n */\n/**\n * De que ofício é cada módulo, UMA resposta por módulo.\n *\n * O mesmo corte de `buildCategories` — maioria estrita dos ofícios quer dizer\n * \"de todos\", e aí a resposta é `null` —, com uma diferença: lá um módulo\n * oferecido por dois ofícios aparece nas duas rubricas, porque a tela é um\n * catálogo e repetir INFORMA. Aqui é um cabeçalho de lista, e uma linha não\n * pode estar em duas: ganha o ofício da própria conta quando ele está entre os\n * que oferecem, senão o primeiro em ordem alfabética — estável entre sessões,\n * que é o que um menu precisa ser.\n */\nexport function pluginVerticalIndex(catalog: CatalogApp[]): Map<string, string | null> {\n const byVertical = new Map<string, Set<string>>()\n const mine = new Set<string>()\n for (const app of catalog) {\n const vertical = app.vertical_id\n if (!vertical) continue\n const offer = byVertical.get(vertical) ?? new Set<string>()\n for (const id of app.modules) offer.add(id)\n byVertical.set(vertical, offer)\n if (app.mine) mine.add(vertical)\n }\n\n const total = byVertical.size\n const index = new Map<string, string | null>()\n for (const [vertical, offer] of byVertical) {\n for (const moduleId of offer) {\n if (index.has(moduleId)) continue\n const offering = [...byVertical].filter(([, o]) => o.has(moduleId)).map(([v]) => v)\n const crossCutting = offering.length >= 2 && offering.length * 2 > total\n if (crossCutting) { index.set(moduleId, null); continue }\n const own = offering.find((v) => mine.has(v))\n index.set(moduleId, own ?? [...offering].sort()[0] ?? vertical)\n }\n }\n return index\n}\n\n/**\n * Em quais APPS (não ofícios) este módulo aparece — \"Estende: ChefControl,\n * FullControl\" no card, em vez do rótulo de ofício que `pluginVerticalIndex`\n * já usa para agrupar o catálogo. Duas perguntas diferentes: aquela agrupa a\n * MARKETPLACE por ramo; esta responde, para UM módulo já ligado, quem o\n * hospeda de verdade — e um módulo pode estar em mais de um app.\n */\nexport function pluginAppNames(catalog: CatalogApp[], pluginId: string): string[] {\n return [...new Set(catalog.filter((app) => app.modules.includes(pluginId)).map((app) => app.name))].sort()\n}\n\n/**\n * Mesmo corte de `pluginVerticalIndex` — maioria estrita dos ofícios —,\n * isolado para responder uma pergunta binária por módulo: este é módulo de\n * TODO negócio (Financeiro, CRM) ou de um ramo em particular (Cozinha)?\n */\nexport function isCrossCutting(catalog: CatalogApp[], pluginId: string): boolean {\n const byVertical = new Map<string, Set<string>>()\n for (const app of catalog) {\n if (!app.vertical_id) continue\n const offer = byVertical.get(app.vertical_id) ?? new Set<string>()\n for (const id of app.modules) offer.add(id)\n byVertical.set(app.vertical_id, offer)\n }\n const total = byVertical.size\n const offering = [...byVertical.values()].filter((offer) => offer.has(pluginId)).length\n return offering >= 2 && offering * 2 > total\n}\n\n/**\n * O texto do badge \"Estende: …\" no card — ou nada, quando o catálogo não\n * chegou (pool sem `app_catalog()`) ou nenhum app declara o módulo ainda.\n */\nexport function appLabelFor(catalog: CatalogApp[], pluginId: string): string | undefined {\n if (catalog.length === 0) return undefined\n if (isCrossCutting(catalog, pluginId)) return 'Todos os negócios'\n const names = pluginAppNames(catalog, pluginId)\n return names.length ? `Estende: ${names.join(', ')}` : undefined\n}\n\nexport function buildCategories(catalog: CatalogApp[], rows: ModuleRow[]): ModuleCategory[] {\n // Sem catálogo (pool anterior à 090) a tela não inventa rubricas: uma lista\n // só, que é exatamente o \"Disponíveis\" que ela mostrava antes.\n if (catalog.length === 0) {\n return rows.length ? [{ id: CROSS_CUTTING, label: 'Disponíveis', mine: true, rows }] : []\n }\n\n // Um ofício pode ter mais de um aplicativo; a oferta dele é a união.\n const byVertical = new Map<string, Set<string>>()\n const mine = new Set<string>()\n for (const app of catalog) {\n const vertical = app.vertical_id\n if (!vertical) continue\n const offer = byVertical.get(vertical) ?? new Set<string>()\n for (const id of app.modules) offer.add(id)\n byVertical.set(vertical, offer)\n if (app.mine) mine.add(vertical)\n }\n\n const total = byVertical.size\n const buckets = new Map<string, ModuleRow[]>()\n const push = (key: string, row: ModuleRow) => buckets.set(key, [...(buckets.get(key) ?? []), row])\n\n for (const row of rows) {\n const offering = [...byVertical].filter(([, offer]) => offer.has(row.plugin.id)).map(([vertical]) => vertical)\n // Oferecido por nenhum ofício: o pacote o compilou, o produto não o vende.\n // É a mesma poda da 079 — o catálogo inteiro é o erro do IMG do SAP.\n if (offering.length === 0) continue\n if (offering.length >= 2 && offering.length * 2 > total) push(CROSS_CUTTING, row)\n else for (const vertical of offering) push(vertical, row)\n }\n\n const category = (id: string): ModuleCategory => ({\n id,\n label: id === CROSS_CUTTING ? 'Todos os negócios' : verticalLabel(id),\n mine: id === CROSS_CUTTING || mine.has(id),\n rows: buckets.get(id) ?? [],\n })\n\n // O ofício da conta primeiro, o chão comum em seguida, os vizinhos depois:\n // a ordem em que alguém procura — \"o meu\", \"o que serve a qualquer um\", \"o\n // que existe além\".\n const ordered = [\n ...[...mine].sort((a, b) => verticalLabel(a).localeCompare(verticalLabel(b))),\n CROSS_CUTTING,\n ...[...byVertical.keys()].filter((v) => !mine.has(v)).sort((a, b) => verticalLabel(a).localeCompare(verticalLabel(b))),\n ]\n\n return ordered.map(category).filter((c) => c.rows.length > 0)\n}\n\n/**\n * Lê `app_catalog()` uma vez por conta.\n *\n * Não entra no `app_config()`: aquela é a chamada do boot, e o catálogo é lido\n * por uma aba de Configurações (ver 090). Sem Supabase — storybook, app mock —\n * a lista fica vazia e a tela cai no comportamento anterior.\n */\nexport function useModuleCatalog(): CatalogApp[] {\n const tenantId = useOrganizationStore((s) => s.currentOrg?.id ?? null)\n const [catalog, setCatalog] = React.useState<CatalogApp[]>([])\n\n React.useEffect(() => {\n const supabase = getSupabaseClientOptional()\n if (!supabase) return\n let alive = true\n void supabase.rpc('app_catalog').then((res) => {\n if (!alive || res.error || !Array.isArray(res.data)) return\n setCatalog(res.data as CatalogApp[])\n })\n return () => {\n alive = false\n }\n }, [tenantId])\n\n return catalog\n}\n","import { isCreateRoute, type NavigationItem } from '@fayz-ai/ui'\n\n// ---------------------------------------------------------------------------\n// rail-preset — o ofício AGRUPA; o plugin é quem NOMEIA.\n//\n// ── O que isto substitui ────────────────────────────────────────────────────\n// A primeira forma do preset (077) redesenhava o rail inteiro em SQL: cada\n// linha com rota, rótulo, ícone e posição. Só que o plugin já diz tudo isso, e\n// duas fontes para o mesmo fato brigam. Brigaram quatro vezes: o Painel sumiu,\n// a Agenda apareceu em duplicata, \"Pedidos\" saiu duas vezes, e o Cardápio ficou\n// solto fora de Produtos. Todos os quatro eram a árvore de SQL discordando dos\n// plugins — nunca um defeito de renderização.\n//\n// Aqui o preset não sabe como uma linha se chama. Ele sabe só o que fica junto\n// de quê, e em que ordem.\n//\n// ── A forma ─────────────────────────────────────────────────────────────────\n// \"rail\": [\n// \"dashboard\", ← as linhas deste plugin\n// { \"label\": \"Produtos\", \"icon\": \"Package\",\n// \"under\": [\"/menu\", \"/inventory\"] }, ← uma rubrica\n// \"financial\"\n// ]\n//\n// Uma string é o plugin, com os rótulos dele. Um objeto é a rubrica, e `under`\n// são prefixos de ROTA — o mesmo casamento por prefixo que o rail já usa para\n// aninhar (`/financial` serve `/financial/payables`).\n//\n// ── Quem não foi nomeado não some ───────────────────────────────────────────\n// Vai para o fim, na ordem do próprio plugin. É a regra do ByDesign: uma\n// pergunta que ninguém respondeu cai no padrão, não em \"não\". O contrário\n// significaria que o dia em que um aplicativo publica um plugin novo é o dia em\n// que ele desaparece de toda conta, em silêncio.\n// ---------------------------------------------------------------------------\n\nexport interface RailGroup {\n label: string\n icon?: string\n /** Prefixos de rota que caem nesta rubrica. */\n under: string[]\n}\n\n/**\n * O contrário de `under`: a rota que este ofício NÃO põe no rail.\n *\n * É o `hideNav()` que os aplicativos de ofício escrevem em código, em forma de\n * dado. O resto-saas esconde Agenda, CRM e Marketing não porque a conta não os\n * tenha, mas porque no vocabulário dele a Agenda É \"Reservas\" e o CRM É\n * \"Clientes\" — mostrar os dois nomes é mostrar o mesmo módulo duas vezes.\n *\n * Sem isto, a regra \"quem não foi nomeado vai para o fim\" — que existe para um\n * plugin novo não sumir — devolvia cinco linhas soltas embaixo de um rail de\n * sete rubricas curadas.\n */\nexport interface RailPreset {\n rail?: RailEntry[]\n hide?: string[]\n /** Ver `whole` em applyRailPreset. */\n whole?: string[]\n labels?: Record<string, string>\n}\n\nexport type RailEntry = string | RailGroup\n\nexport interface RailRow extends Omit<NavigationItem, 'children'> {\n pluginId?: string\n children?: RailRow[]\n}\n\nconst isGroup = (entry: RailEntry): entry is RailGroup =>\n typeof entry === 'object' && entry !== null && Array.isArray((entry as RailGroup).under)\n\n/** `/financial` serve `/financial/payables`; `/fin` não serve `/financial`. */\nexport function matchesPrefix(route: string, prefix: string): boolean {\n return route === prefix || route.startsWith(`${prefix}/`)\n}\n\n/**\n * Aplica o `rail` do preset às linhas que os plugins contribuíram.\n *\n * Sem preset (lista vazia), devolve as linhas como vieram: é o aplicativo de\n * ofício, que cura pelo `import`, e nada aqui deve mudá-lo.\n *\n * ── `whole`: o módulo que ABRE INTEIRO ──────────────────────────────────────\n * CRM, Financeiro, Marketing e Conversas não são rubricas: são módulos, e cada\n * um já desenha a própria faixa de abas com as telas irmãs dele. Listado aqui\n * por prefixo de rota, o módulo vira UMA linha no rail e as sub-telas somem da\n * sanfona — o mesmo assunto deixa de ter dois níveis, um no menu e outro dentro\n * da página.\n *\n * É prefixo e não lista de rotas de propósito: `\"/sales\"` vale para as sete\n * telas do CRM hoje e para a oitava que o plugin publicar amanhã. Enumerar as\n * filhas no `hide` seria pedir que cada ofício reaprendesse o conteúdo de cada\n * módulo toda vez que ele crescesse.\n */\nexport function applyRailPreset(\n rows: RailRow[],\n rail: RailEntry[] | undefined,\n hide?: string[],\n whole?: string[],\n): RailRow[] {\n // Escondido antes de tudo: uma rota calada não pode ser reivindicada por\n // rubrica nenhuma, nem sobrar para o fim.\n //\n // E CALA OS FILHOS TAMBÉM. O rail já aninhou por prefixo antes de chegar\n // aqui, então `/conversations/campaigns` não é uma linha da lista — é filha\n // de `/conversations`. Esconder só no primeiro nível fazia o `hide` não ter\n // efeito nenhum sobre ela, em silêncio: a rota continuava desenhada dentro\n // da rubrica que recolheu o pai.\n const hidden = (route: string) => !!hide?.some((p) => matchesPrefix(route, p))\n const opensWhole = (route: string) => !!whole?.some((p) => matchesPrefix(route, p))\n // A LINHA SEM FILHO DE VERDADE VOLTA A SER FOLHA.\n //\n // `seedChildren` semeia um filho com a rota do próprio pai, para o módulo não\n // perder a porta para a raiz dele. Podados os filhos de verdade — pelo `hide`\n // ou pelo `whole` —, sobrava esse: o rail desenhava \"Financeiro\" com uma\n // sanfona e um item dentro repetindo \"Financeiro\", dois cliques e nenhum\n // destino novo.\n const prune = (list: RailRow[]): RailRow[] =>\n list\n .filter((r) => !hidden(r.route))\n .map((r) => {\n if (!r.children?.length) return r\n const children = opensWhole(r.route) ? [] : prune(r.children)\n const real = children.some((c) => c.route !== r.route)\n return { ...r, children: real ? children : undefined }\n })\n const visible = hide?.length ? prune(rows) : rows\n\n // SEM RAIL, MAS COM `hide`: esconder ainda vale.\n //\n // A saída antecipada ficava na primeira linha, e devolvia `rows` cru — então\n // um preset que só dizia \"não mostre esta tela\", sem reordenar nada, não\n // escondia coisa alguma. Era uma promessa silenciosamente ignorada em três\n // dos nove presets, e o `hide` é justamente o campo que se usa para tirar do\n // ar uma tela em toda conta sem esperar um release.\n if (!rail?.length) return visible\n\n // Cada linha sai do bolso quando alguém a reivindica: uma rota só pertence a\n // uma rubrica, e a PRIMEIRA que a nomeia ganha. Sem isso, `/orders` listado\n // em duas rubricas desenharia a mesma tela em dois lugares do menu.\n const pool = [...visible]\n const take = (pred: (row: RailRow) => boolean): RailRow[] => {\n const taken: RailRow[] = []\n for (let i = pool.length - 1; i >= 0; i--) {\n if (pred(pool[i]!)) taken.unshift(...pool.splice(i, 1))\n }\n return taken\n }\n\n // RUBRICA NOMEIA ROTA; `\"plugin\"` é pega-tudo — e o explícito ganha.\n //\n // A varredura era uma só, na ordem do rail, e `out.push(...take(row.pluginId === entry))`\n // levava TODAS as linhas de um plugin. O Financeiro passa antes de Cadastros\n // em todo preset, então a linha de Contas (`/registry/accounts`, contribuída\n // pelo plugin financeiro) era arrebatada por `\"financial\"` e saía solta no\n // topo, entre Financeiro e RH — a rubrica que a nomeava por prefixo nunca\n // chegava a vê-la.\n //\n // Duas passadas resolvem sem mexer na ORDEM do menu: as rubricas escolhem o\n // que nomearam, os pega-tudo ficam com o resto, e a emissão continua sendo na\n // ordem que o rail declara.\n const groupClaims = new Map<number, RailRow[]>()\n rail.forEach((entry, i) => {\n if (!isGroup(entry)) return\n groupClaims.set(i, take((row) => entry.under.some((p) => matchesPrefix(row.route, p))))\n })\n\n const out: RailRow[] = []\n for (const [i, entry] of rail.entries()) {\n if (isGroup(entry)) {\n const claimed = groupClaims.get(i) ?? []\n // `under` é uma ORDEM, não um conjunto. Listar as rotas uma a uma é como\n // o ofício diz em que sequência os filhos se leem — o V1 do salão abre\n // Cadastros em Contatos e fecha em Serviços, e a ordem de POSIÇÃO dos\n // plugins não tem como saber disso. Um prefixo largo no fim (`/registry`)\n // continua sendo o pega-tudo, na ordem que os plugins deram.\n const rank = (row: RailRow): number => {\n const i = entry.under.findIndex((p) => matchesPrefix(row.route, p))\n return i === -1 ? entry.under.length : i\n }\n claimed.sort((a, b) => rank(a) - rank(b))\n // UMA RUBRICA QUE RECOLHE UM GRUPO RECOLHE OS FILHOS DELE, não o grupo.\n //\n // O rail já aninha por prefixo antes de chegar aqui: `/agenda` chega como\n // uma linha com `/agenda` e `/agenda/confirmations` dentro. Empacotar\n // essa linha como filha da rubrica cria um neto — e o rail desenha um\n // nível só, então Confirmações simplesmente sumia do menu, sem erro\n // nenhum, com a rota ainda funcionando por deep link.\n //\n // Achatar é o que o ofício quis dizer: `under: [\"/agenda\", \"/checkin\"]`\n // é \"ponha estas telas sob Agenda\", e as telas são as folhas.\n const children = claimed.flatMap((row) => row.children?.length ? row.children : [row])\n // Uma rubrica sem filho ativo não desenha. \"Operação\" some sozinha numa\n // casa sem cozinha, sem mesa e sem reserva — e é assim que o ofício\n // propõe sem impor.\n if (!children.length) continue\n\n // UMA RUBRICA COM UM DESTINO SÓ NÃO É UMA RUBRICA.\n //\n // \"Clientes\" é `under: [\"/clients\", \"/customers\"]` — um cabeçalho sobre\n // DUAS listas. Numa conta que só tem uma delas sobra um destino, e o que\n // o rail desenhava era \"Clientes\" aberto sobre \"Listar\" e \"Inserir\":\n // dois cliques para chegar onde um bastava, e um nome (\"Listar\") que não\n // diz o que a tela mostra. Com um destino só, a rubrica VIRA o destino e\n // fica com o próprio nome.\n //\n // Criar não conta como destino: `/customers/new` é um verbo, e a tela da\n // lista já oferece o \"+\". Ele fica como filho quando há mesmo um grupo, e\n // some quando a conta do grupo é ele mais uma tela.\n const places = children.filter((row) => !isCreateRoute(row.route))\n if (places.length <= 1) {\n const only = places[0] ?? children[0]!\n out.push({ ...only, label: entry.label, icon: entry.icon ?? only.icon, children: undefined })\n continue\n }\n\n out.push({\n // A rubrica leva à primeira coisa que ela contém: um cabeçalho que não\n // vai a lugar nenhum é um clique que não faz nada.\n ...children[0]!,\n route: children[0]!.route,\n label: entry.label,\n icon: entry.icon ?? children[0]!.icon,\n children,\n })\n continue\n }\n out.push(...take((row) => row.pluginId === entry))\n }\n\n // A line no rubric claimed lands at the end — unless it said where it\n // belongs (\"above Análises\"), which a new module needs to look native.\n for (const row of pool) {\n const before = (row as RailRow & { before?: string[] }).before\n const at = before?.length\n ? out.findIndex((r) => before.some((b) => matchesPrefix(r.route, b) || r.children?.some((c) => matchesPrefix(c.route, b))))\n : -1\n if (at >= 0) out.splice(at, 0, row)\n else out.push(row)\n }\n return out\n}\n\n/**\n * O VOCABULÁRIO do ofício: a rota continua sendo do plugin, o nome passa a ser\n * desta conta.\n *\n * Isto não reabre a briga que a 083 fechou. Lá o preset declarava uma ÁRVORE\n * paralela — rota, rótulo, ícone e posição — e as duas fontes derivavam até\n * uma delas apontar para uma tela que a outra já tinha movido. Aqui não há\n * árvore: só um dicionário de rota → nome, aplicado sobre o que os plugins\n * contribuíram. Se a rota deixar de existir, a entrada fica inerte — ela não\n * tem como desenhar uma linha fantasma, que era exatamente o defeito de lá.\n *\n * Por que é preciso: o Espaço Facial vem do BeautySoft, onde a tela de\n * `/registry/staff` se chama \"Funcionários\" e a de `/registry/locations`,\n * \"Locais Operacionais\". Trocar esses nomes no plugin renomearia a mesma tela\n * no restaurante e na loja; deixá-los como estão faz a migração custar um\n * reaprendizado por linha de menu. O nome é do OFÍCIO, e o ofício mora no\n * banco.\n *\n * E ele NOMEIA o que não tinha nome: uma rota que o aplicativo registrou com\n * rótulo vazio (o alias de Origens é uma) nunca desenhava, por não ter como se\n * chamar. Com um nome, ela volta ao menu sem que o aplicativo mude.\n */\nexport function labelFor<T extends string | undefined>(\n labels: Record<string, string> | undefined,\n route: string,\n fallback: T,\n): string | T {\n return labels?.[route] ?? fallback\n}\n"]}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { featureDisplayName, PlanFeatureItem, getPlanEntitlements, resolvePlanCurrency } from './chunk-HCFO4Z6A.js';
|
|
2
|
+
import { useTranslation } from './chunk-YUHCC7JY.js';
|
|
3
|
+
import { useBillingStore } from './chunk-SGAFPNMA.js';
|
|
4
|
+
import { usePermissionsStore } from './chunk-UQ2NTFQC.js';
|
|
5
|
+
import { cn } from './chunk-JRZJBMOY.js';
|
|
6
|
+
import { useOrganizationStore } from './chunk-YKPPCV2Z.js';
|
|
7
|
+
import { navigateTo } from './chunk-QD7BMUDF.js';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { formatCurrency as formatCurrency$1, subscribeHouseCurrency, getHouseCurrency, resolveCurrency } from '@fayz-ai/core';
|
|
10
|
+
import { Crown, ArrowRight, Sparkles } from 'lucide-react';
|
|
11
|
+
import { Button, Card } from '@fayz-ai/ui';
|
|
12
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
13
|
+
|
|
14
|
+
function formatCurrency(value, currency) {
|
|
15
|
+
if (!currency) return formatCurrency$1(value);
|
|
16
|
+
return formatMoney(value, currency.code, currency.locale, { minimumFractionDigits: 2 });
|
|
17
|
+
}
|
|
18
|
+
function formatMoney(value, code, locale, options) {
|
|
19
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency: code, ...options }).format(value);
|
|
20
|
+
}
|
|
21
|
+
function formatPlanPrice(value, code, locale) {
|
|
22
|
+
try {
|
|
23
|
+
return formatMoney(value, code, locale, { minimumFractionDigits: Number.isInteger(value) ? 0 : 2 });
|
|
24
|
+
} catch {
|
|
25
|
+
return `${code} ${value}`.trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function currencySymbol(code, locale) {
|
|
29
|
+
try {
|
|
30
|
+
const parts = new Intl.NumberFormat(locale, { style: "currency", currency: code }).formatToParts(0);
|
|
31
|
+
return parts.find((p) => p.type === "currency")?.value ?? code;
|
|
32
|
+
} catch {
|
|
33
|
+
return code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function useHouseCurrency() {
|
|
37
|
+
return React.useSyncExternalStore(subscribeHouseCurrency, getHouseCurrency, getHouseCurrency);
|
|
38
|
+
}
|
|
39
|
+
var CurrencyScopeContext = React.createContext(null);
|
|
40
|
+
function CurrencyScope({ code, children }) {
|
|
41
|
+
const outer = React.useContext(CurrencyScopeContext);
|
|
42
|
+
return React.createElement(CurrencyScopeContext.Provider, { value: code || outer }, children);
|
|
43
|
+
}
|
|
44
|
+
function useCurrencyScope() {
|
|
45
|
+
return React.useContext(CurrencyScopeContext);
|
|
46
|
+
}
|
|
47
|
+
function useCurrency(declared, documentCode) {
|
|
48
|
+
const house = useHouseCurrency();
|
|
49
|
+
const scope = useCurrencyScope();
|
|
50
|
+
const code = documentCode || scope;
|
|
51
|
+
return React.useMemo(
|
|
52
|
+
() => resolveCurrency(declared, code),
|
|
53
|
+
// `house` is read inside resolveCurrency; listed so a switch re-resolves.
|
|
54
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
+
[declared, code, house]
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function planUnlocks(plan, feature) {
|
|
59
|
+
if (!feature) return plan.price > 0;
|
|
60
|
+
const ent = getPlanEntitlements(plan);
|
|
61
|
+
return ent?.features?.[feature] !== false;
|
|
62
|
+
}
|
|
63
|
+
function formatPrice(plan, locale) {
|
|
64
|
+
return formatPlanPrice(plan.price, resolvePlanCurrency(plan.currency, locale), locale);
|
|
65
|
+
}
|
|
66
|
+
function UpgradePrompt({ feature, inline, className }) {
|
|
67
|
+
const { t, locale } = useTranslation();
|
|
68
|
+
const tr = (key, fallback, params) => {
|
|
69
|
+
let v = t(key);
|
|
70
|
+
if (!v || v === key) v = fallback;
|
|
71
|
+
if (params) for (const [k, val] of Object.entries(params)) v = v.replace(`{${k}}`, val).replace(`{{${k}}}`, val);
|
|
72
|
+
return v;
|
|
73
|
+
};
|
|
74
|
+
const plans = useBillingStore((s) => s.plans);
|
|
75
|
+
const features = usePermissionsStore((s) => s.features);
|
|
76
|
+
const currentOrg = useOrganizationStore((s) => s.currentOrg);
|
|
77
|
+
const currentPlanId = currentOrg?.plan || "free";
|
|
78
|
+
const currentPlan = plans.find((p) => p.id === currentPlanId);
|
|
79
|
+
const currentPrice = currentPlan?.price ?? 0;
|
|
80
|
+
const currentPlanName = currentPlan?.name ?? tr("billing.free", "Free");
|
|
81
|
+
const featureName = React.useMemo(() => {
|
|
82
|
+
if (!feature) return null;
|
|
83
|
+
const label = features.find((f) => f.id === feature)?.label;
|
|
84
|
+
if (label) return featureDisplayName(label);
|
|
85
|
+
const key = `upgrade.feature.${feature}`;
|
|
86
|
+
const named = t(key);
|
|
87
|
+
return named && named !== key ? named : featureDisplayName(feature);
|
|
88
|
+
}, [features, feature, t]);
|
|
89
|
+
const unlockingPlans = React.useMemo(
|
|
90
|
+
() => plans.filter((p) => p.id !== currentPlanId && p.price > currentPrice && planUnlocks(p, feature)).sort((a, b) => a.price - b.price),
|
|
91
|
+
[plans, currentPlanId, currentPrice, feature]
|
|
92
|
+
);
|
|
93
|
+
const recommendedId = unlockingPlans[0]?.id;
|
|
94
|
+
const goToPlans = () => navigateTo("/settings/subscription");
|
|
95
|
+
const heading = featureName ? tr("upgrade.unlock", "Unlock {feature}", { feature: featureName }) : tr("upgrade.title", "Premium feature");
|
|
96
|
+
const valueKey = feature ? `upgrade.value.${feature}` : "";
|
|
97
|
+
const valueCopy = feature ? t(valueKey) : "";
|
|
98
|
+
const subcopy = valueCopy && valueCopy !== valueKey ? valueCopy : tr("upgrade.valueGeneric", "Get everything you need to grow \u2014 included in the plans below.");
|
|
99
|
+
if (inline) {
|
|
100
|
+
return /* @__PURE__ */ jsxs(
|
|
101
|
+
"div",
|
|
102
|
+
{
|
|
103
|
+
className: cn(
|
|
104
|
+
"flex flex-wrap items-center gap-3 rounded-xl border border-amber-400/40 bg-amber-50/60 px-4 py-3 dark:border-amber-400/25 dark:bg-amber-950/20",
|
|
105
|
+
className
|
|
106
|
+
),
|
|
107
|
+
children: [
|
|
108
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-400/20 text-amber-600 dark:text-amber-400", children: /* @__PURE__ */ jsx(Crown, { className: "h-4 w-4" }) }),
|
|
109
|
+
/* @__PURE__ */ jsxs("p", { className: "min-w-0 flex-1 text-sm", children: [
|
|
110
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: heading }),
|
|
111
|
+
/* @__PURE__ */ jsx("span", { className: "ml-1.5 text-muted-foreground", children: subcopy })
|
|
112
|
+
] }),
|
|
113
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", onClick: goToPlans, className: "shrink-0", children: [
|
|
114
|
+
tr("upgrade.ctaUpgrade", "Upgrade"),
|
|
115
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "ml-1.5 h-3.5 w-3.5" })
|
|
116
|
+
] })
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("mx-auto w-full max-w-4xl px-4 py-10 sm:py-14", className), children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center text-center", children: [
|
|
123
|
+
currentPlan && /* @__PURE__ */ jsxs("span", { className: "mb-5 inline-flex items-center gap-1.5 rounded-full border bg-muted/40 px-3 py-1 text-xs font-medium text-muted-foreground", children: [
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground/50", "aria-hidden": "true" }),
|
|
125
|
+
tr("upgrade.currentPlan", "Your plan: {plan}", { plan: currentPlanName })
|
|
126
|
+
] }),
|
|
127
|
+
/* @__PURE__ */ jsxs("h1", { className: "flex items-center gap-2.5 text-2xl font-bold tracking-tight text-foreground sm:text-3xl", children: [
|
|
128
|
+
/* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 items-center justify-center rounded-xl bg-amber-400/15 text-amber-500 sm:h-10 sm:w-10", children: /* @__PURE__ */ jsx(Crown, { className: "h-5 w-5 sm:h-[22px] sm:w-[22px]" }) }),
|
|
129
|
+
heading
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 max-w-md text-sm text-muted-foreground sm:text-base", children: subcopy })
|
|
132
|
+
] }),
|
|
133
|
+
unlockingPlans.length > 0 && /* @__PURE__ */ jsx(
|
|
134
|
+
"div",
|
|
135
|
+
{
|
|
136
|
+
className: cn(
|
|
137
|
+
"mt-9 grid gap-4 sm:gap-5",
|
|
138
|
+
unlockingPlans.length === 1 ? "max-w-sm mx-auto" : "sm:grid-cols-2",
|
|
139
|
+
unlockingPlans.length >= 3 && "lg:grid-cols-3"
|
|
140
|
+
),
|
|
141
|
+
children: unlockingPlans.map((plan) => {
|
|
142
|
+
const recommended = plan.id === recommendedId;
|
|
143
|
+
const paid = plan.price > 0;
|
|
144
|
+
const highlights = plan.features.slice(0, 3);
|
|
145
|
+
return /* @__PURE__ */ jsxs(
|
|
146
|
+
Card,
|
|
147
|
+
{
|
|
148
|
+
className: cn(
|
|
149
|
+
"relative flex flex-col p-5 transition-shadow",
|
|
150
|
+
recommended ? "border-primary shadow-lg ring-1 ring-primary/20 sm:-translate-y-1" : "hover:shadow-md"
|
|
151
|
+
),
|
|
152
|
+
children: [
|
|
153
|
+
recommended && /* @__PURE__ */ jsxs("span", { className: "absolute -top-2.5 left-1/2 -translate-x-1/2 whitespace-nowrap inline-flex items-center gap-1 rounded-full bg-primary px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-primary-foreground shadow-sm", children: [
|
|
154
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "h-3 w-3", "aria-hidden": "true" }),
|
|
155
|
+
tr("upgrade.recommended", "Recommended")
|
|
156
|
+
] }),
|
|
157
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
158
|
+
paid && /* @__PURE__ */ jsx(Crown, { className: "h-4 w-4 text-amber-500", "aria-hidden": "true" }),
|
|
159
|
+
/* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold text-foreground", children: plan.name })
|
|
160
|
+
] }),
|
|
161
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-baseline gap-1", children: [
|
|
162
|
+
/* @__PURE__ */ jsx("span", { className: "text-2xl font-bold tracking-tight text-foreground tabular-nums", children: formatPrice(plan, locale) }),
|
|
163
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: plan.interval === "year" ? tr("billing.perYear", "/yr") : tr("billing.perMonth", "/mo") })
|
|
164
|
+
] }),
|
|
165
|
+
plan.description && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: plan.description }),
|
|
166
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-4 flex-1 space-y-2", children: highlights.map((f) => /* @__PURE__ */ jsx(PlanFeatureItem, { feature: f, className: "text-xs text-foreground/80", iconClassName: "h-3.5 w-3.5" }, f)) }),
|
|
167
|
+
/* @__PURE__ */ jsxs(
|
|
168
|
+
Button,
|
|
169
|
+
{
|
|
170
|
+
className: "mt-5 w-full",
|
|
171
|
+
variant: recommended ? "default" : "outline",
|
|
172
|
+
onClick: goToPlans,
|
|
173
|
+
"aria-label": tr("upgrade.ctaUpgradeTo", "Upgrade to {plan}", { plan: plan.name }),
|
|
174
|
+
children: [
|
|
175
|
+
paid && /* @__PURE__ */ jsx(Crown, { className: "mr-1.5 h-4 w-4", "aria-hidden": "true" }),
|
|
176
|
+
tr("upgrade.ctaUpgrade", "Upgrade")
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
plan.id
|
|
183
|
+
);
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/* @__PURE__ */ jsx("div", { className: "mt-7 text-center", children: /* @__PURE__ */ jsxs(
|
|
188
|
+
Button,
|
|
189
|
+
{
|
|
190
|
+
variant: "bare",
|
|
191
|
+
size: "bare",
|
|
192
|
+
type: "button",
|
|
193
|
+
onClick: goToPlans,
|
|
194
|
+
className: "inline-flex items-center gap-1 rounded text-sm font-medium text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
195
|
+
children: [
|
|
196
|
+
tr("upgrade.compareAll", "Compare all plans"),
|
|
197
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
) })
|
|
201
|
+
] });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { CurrencyScope, UpgradePrompt, currencySymbol, formatCurrency, formatMoney, formatPlanPrice, useCurrency, useCurrencyScope, useHouseCurrency };
|
|
205
|
+
//# sourceMappingURL=chunk-E6DF3VR6.js.map
|
|
206
|
+
//# sourceMappingURL=chunk-E6DF3VR6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/currency.ts","../src/components/billing/UpgradePrompt.tsx"],"names":["formatCurrencyFromContext","React2"],"mappings":"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","file":"chunk-E6DF3VR6.js","sourcesContent":["/**\n * Shared currency config + formatter. Every vertical plugin (crm, financial,\n * inventory, orders, menu, marketing) copied this same Intl.NumberFormat wrapper;\n * they now import it from here. Plugins may still re-export it under a local alias\n * (e.g. `export { formatCurrency } from '@fayz-ai/admin'`) to keep call sites short.\n *\n * `currency` is optional so this name means ONE thing on both doors: called with\n * a config it formats exactly that, called without it delegates to the core\n * formatter, which reads the active locale + the currency the app declared.\n * (They used to be two different functions sharing a name — code that moved from\n * `@fayz-ai/core` to the `@fayz-ai/admin` front door silently changed behaviour.)\n */\nimport * as React from 'react'\nimport {\n formatCurrency as formatCurrencyFromContext,\n getHouseCurrency, subscribeHouseCurrency, resolveCurrency,\n type CurrencyConfig,\n} from '@fayz-ai/core'\n\nexport type { CurrencyConfig }\n\nexport function formatCurrency(value: number, currency?: CurrencyConfig): string {\n if (!currency) return formatCurrencyFromContext(value)\n return formatMoney(value, currency.code, currency.locale, { minimumFractionDigits: 2 })\n}\n\n/** One amount in one ISO currency; `locale` defaults to the runtime's. */\nexport function formatMoney(value: number, code: string, locale?: string, options?: Intl.NumberFormatOptions): string {\n return new Intl.NumberFormat(locale, { style: 'currency', currency: code, ...options }).format(value)\n}\n\n/** A plan price: whole amounts drop the cents (\"R$ 79\"), and an unknown code falls back to \"XYZ 79\". */\nexport function formatPlanPrice(value: number, code: string, locale?: string): string {\n try {\n return formatMoney(value, code, locale, { minimumFractionDigits: Number.isInteger(value) ? 0 : 2 })\n } catch {\n return `${code} ${value}`.trim()\n }\n}\n\n/** The symbol a locale prints for an ISO code (USD → \"$\"), or the code itself when Intl does not know it. */\nexport function currencySymbol(code: string, locale: string): string {\n try {\n const parts = new Intl.NumberFormat(locale, { style: 'currency', currency: code }).formatToParts(0)\n return parts.find((p) => p.type === 'currency')?.value ?? code\n } catch {\n return code\n }\n}\n\n/** The workspace's currency code, live: changes when the person switches unit or tenant. */\nexport function useHouseCurrency(): string | null {\n return React.useSyncExternalStore(subscribeHouseCurrency, getHouseCurrency, getHouseCurrency)\n}\n\nconst CurrencyScopeContext = React.createContext<string | null>(null)\n\n/**\n * Everything rendered inside prints in THIS currency: the screen of one\n * document wraps itself, and its dialogs (pay, cancel, charge) follow without\n * each being told. A bill written in reais stays in reais inside a tenant that\n * now works in euros.\n */\nexport function CurrencyScope({ code, children }: { code?: string | null; children?: React.ReactNode }) {\n const outer = React.useContext(CurrencyScopeContext)\n return React.createElement(CurrencyScopeContext.Provider, { value: code || outer }, children)\n}\n\nexport function useCurrencyScope(): string | null {\n return React.useContext(CurrencyScopeContext)\n}\n\n/**\n * The currency a screen prints in: the document's when it has one, else the\n * house's, else what the plugin was configured with.\n */\nexport function useCurrency(declared?: CurrencyConfig | null, documentCode?: string | null): CurrencyConfig {\n const house = useHouseCurrency()\n const scope = useCurrencyScope()\n const code = documentCode || scope\n return React.useMemo(\n () => resolveCurrency(declared, code),\n // `house` is read inside resolveCurrency; listed so a switch re-resolves.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [declared, code, house],\n )\n}\n","import * as React from 'react'\nimport { Crown, Sparkles, ArrowRight } from 'lucide-react'\nimport { Button, Card } from '@fayz-ai/ui'\nimport type { Plan } from '@fayz-ai/core'\nimport { cn } from '../../lib/cn'\nimport { useTranslation } from '../../hooks/useTranslation'\nimport { useBillingStore } from '../../billing'\nimport { usePermissionsStore } from '../../permissions'\nimport { useOrganizationStore } from '../../org/store'\nimport { navigateTo } from '../../app/routing'\nimport { formatPlanPrice } from '../../lib/currency'\nimport { getPlanEntitlements, featureDisplayName, resolvePlanCurrency } from './access-contract'\nimport { PlanFeatureItem } from './plan-feature'\n\n// ---------------------------------------------------------------------------\n// UpgradePrompt — the \"this is a higher-plan feature\" surface. Two variants:\n// • full (default): a benefit-led hero (compact icon + \"Unlock {Feature}\"),\n// a \"your plan\" context chip, and priced plan cards — the cheapest one that\n// unlocks the feature flagged \"Recommended\" and slightly elevated — each with\n// its own \"Upgrade\" CTA into /settings/subscription, plus a discreet\n// \"Compare all plans\" link.\n// • inline: a single-line banner for use as a gate fallback next to content.\n// Rendered by the AdminShell route guard (reason: 'plan'), the EntitlementGate\n// fallback, and anywhere a premium feature needs a discovery nudge.\n// ---------------------------------------------------------------------------\n\nexport interface UpgradePromptProps {\n /** Feature id (same id space as RBAC/nav) this prompt is about. */\n feature?: string\n /** Compact banner instead of the full hero block. */\n inline?: boolean\n className?: string\n}\n\nfunction planUnlocks(plan: Plan, feature: string | undefined): boolean {\n if (!feature) return plan.price > 0\n const ent = getPlanEntitlements(plan)\n // Entitled when the feature isn't explicitly disabled for the plan.\n return ent?.features?.[feature] !== false\n}\n\nfunction formatPrice(plan: Plan, locale: string | undefined): string {\n return formatPlanPrice(plan.price, resolvePlanCurrency(plan.currency, locale), locale)\n}\n\nexport function UpgradePrompt({ feature, inline, className }: UpgradePromptProps) {\n const { t, locale } = useTranslation()\n const tr = (key: string, fallback: string, params?: Record<string, string>) => {\n let v = t(key)\n if (!v || v === key) v = fallback\n if (params) for (const [k, val] of Object.entries(params)) v = v.replace(`{${k}}`, val).replace(`{{${k}}}`, val)\n return v\n }\n\n const plans = useBillingStore((s) => s.plans)\n const features = usePermissionsStore((s) => s.features)\n const currentOrg = useOrganizationStore((s) => s.currentOrg)\n const currentPlanId = currentOrg?.plan || 'free'\n const currentPlan = plans.find((p) => p.id === currentPlanId)\n const currentPrice = currentPlan?.price ?? 0\n const currentPlanName = currentPlan?.name ?? tr('billing.free', 'Free')\n\n // Clean display name (strips \"(acesso)\"/\"(access)\" suffixes from the raw label).\n // Features gated only by plan (no RBAC entry, e.g. the assistant) fall back to\n // the shell's upgrade.feature.<id> translation before showing the raw id.\n const featureName = React.useMemo(() => {\n if (!feature) return null\n const label = features.find((f) => f.id === feature)?.label\n if (label) return featureDisplayName(label)\n const key = `upgrade.feature.${feature}`\n const named = t(key)\n return named && named !== key ? named : featureDisplayName(feature)\n }, [features, feature, t])\n\n // Plans that unlock the feature AND are an upgrade over the current plan,\n // cheapest first — so the first is the natural \"recommended\" entry point.\n const unlockingPlans = React.useMemo(\n () =>\n plans\n .filter((p) => p.id !== currentPlanId && p.price > currentPrice && planUnlocks(p, feature))\n .sort((a, b) => a.price - b.price),\n [plans, currentPlanId, currentPrice, feature],\n )\n const recommendedId = unlockingPlans[0]?.id\n\n const goToPlans = () => navigateTo('/settings/subscription')\n\n const heading = featureName\n ? tr('upgrade.unlock', 'Unlock {feature}', { feature: featureName })\n : tr('upgrade.title', 'Premium feature')\n\n // Benefit subcopy: per-feature key (upgrade.value.<feature>) with a dignified\n // generic fallback so every feature reads intentionally.\n const valueKey = feature ? `upgrade.value.${feature}` : ''\n const valueCopy = feature ? t(valueKey) : ''\n const subcopy =\n valueCopy && valueCopy !== valueKey\n ? valueCopy\n : tr('upgrade.valueGeneric', 'Get everything you need to grow — included in the plans below.')\n\n if (inline) {\n return (\n <div\n className={cn(\n 'flex flex-wrap items-center gap-3 rounded-xl border border-amber-400/40 bg-amber-50/60 px-4 py-3 dark:border-amber-400/25 dark:bg-amber-950/20',\n className,\n )}\n >\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-400/20 text-amber-600 dark:text-amber-400\">\n <Crown className=\"h-4 w-4\" />\n </span>\n <p className=\"min-w-0 flex-1 text-sm\">\n <span className=\"font-semibold text-foreground\">{heading}</span>\n <span className=\"ml-1.5 text-muted-foreground\">{subcopy}</span>\n </p>\n <Button size=\"sm\" onClick={goToPlans} className=\"shrink-0\">\n {tr('upgrade.ctaUpgrade', 'Upgrade')}\n <ArrowRight className=\"ml-1.5 h-3.5 w-3.5\" />\n </Button>\n </div>\n )\n }\n\n return (\n <div className={cn('mx-auto w-full max-w-4xl px-4 py-10 sm:py-14', className)}>\n {/* Hero — compact, benefit-led. Icon integrated with the title, not floating. */}\n <div className=\"flex flex-col items-center text-center\">\n {currentPlan && (\n <span className=\"mb-5 inline-flex items-center gap-1.5 rounded-full border bg-muted/40 px-3 py-1 text-xs font-medium text-muted-foreground\">\n <span className=\"h-1.5 w-1.5 rounded-full bg-muted-foreground/50\" aria-hidden=\"true\" />\n {tr('upgrade.currentPlan', 'Your plan: {plan}', { plan: currentPlanName })}\n </span>\n )}\n <h1 className=\"flex items-center gap-2.5 text-2xl font-bold tracking-tight text-foreground sm:text-3xl\">\n <span className=\"flex h-9 w-9 items-center justify-center rounded-xl bg-amber-400/15 text-amber-500 sm:h-10 sm:w-10\">\n <Crown className=\"h-5 w-5 sm:h-[22px] sm:w-[22px]\" />\n </span>\n {heading}\n </h1>\n <p className=\"mt-3 max-w-md text-sm text-muted-foreground sm:text-base\">{subcopy}</p>\n </div>\n\n {unlockingPlans.length > 0 && (\n <div\n className={cn(\n 'mt-9 grid gap-4 sm:gap-5',\n unlockingPlans.length === 1 ? 'max-w-sm mx-auto' : 'sm:grid-cols-2',\n unlockingPlans.length >= 3 && 'lg:grid-cols-3',\n )}\n >\n {unlockingPlans.map((plan) => {\n const recommended = plan.id === recommendedId\n const paid = plan.price > 0\n const highlights = plan.features.slice(0, 3)\n return (\n <Card\n key={plan.id}\n className={cn(\n 'relative flex flex-col p-5 transition-shadow',\n recommended\n ? 'border-primary shadow-lg ring-1 ring-primary/20 sm:-translate-y-1'\n : 'hover:shadow-md',\n )}\n >\n {recommended && (\n <span className=\"absolute -top-2.5 left-1/2 -translate-x-1/2 whitespace-nowrap inline-flex items-center gap-1 rounded-full bg-primary px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-primary-foreground shadow-sm\">\n <Sparkles className=\"h-3 w-3\" aria-hidden=\"true\" />\n {tr('upgrade.recommended', 'Recommended')}\n </span>\n )}\n\n <div className=\"flex items-center gap-1.5\">\n {paid && <Crown className=\"h-4 w-4 text-amber-500\" aria-hidden=\"true\" />}\n <h2 className=\"text-sm font-semibold text-foreground\">{plan.name}</h2>\n </div>\n\n <div className=\"mt-2 flex items-baseline gap-1\">\n <span className=\"text-2xl font-bold tracking-tight text-foreground tabular-nums\">\n {formatPrice(plan, locale)}\n </span>\n <span className=\"text-sm text-muted-foreground\">\n {plan.interval === 'year' ? tr('billing.perYear', '/yr') : tr('billing.perMonth', '/mo')}\n </span>\n </div>\n\n {plan.description && (\n <p className=\"mt-1 text-xs text-muted-foreground\">{plan.description}</p>\n )}\n\n <ul className=\"mt-4 flex-1 space-y-2\">\n {highlights.map((f) => (\n <PlanFeatureItem key={f} feature={f} className=\"text-xs text-foreground/80\" iconClassName=\"h-3.5 w-3.5\" />\n ))}\n </ul>\n\n <Button\n className=\"mt-5 w-full\"\n variant={recommended ? 'default' : 'outline'}\n onClick={goToPlans}\n aria-label={tr('upgrade.ctaUpgradeTo', 'Upgrade to {plan}', { plan: plan.name })}\n >\n {paid && <Crown className=\"mr-1.5 h-4 w-4\" aria-hidden=\"true\" />}\n {tr('upgrade.ctaUpgrade', 'Upgrade')}\n </Button>\n </Card>\n )\n })}\n </div>\n )}\n\n <div className=\"mt-7 text-center\">\n <Button\n variant=\"bare\"\n size=\"bare\"\n type=\"button\"\n onClick={goToPlans}\n className=\"inline-flex items-center gap-1 rounded text-sm font-medium text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {tr('upgrade.compareAll', 'Compare all plans')}\n <ArrowRight className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n )\n}\n"]}
|