@fayz-ai/admin 0.47.0 → 0.49.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.
- package/dist/ImportWizard-SYP3OXZU.js +3 -0
- package/dist/{ImportWizard-7UFB2PPF.js.map → ImportWizard-SYP3OXZU.js.map} +1 -1
- package/dist/PersonTimeline-X7JTPUWJ.js +3 -0
- package/dist/PersonTimeline-X7JTPUWJ.js.map +1 -0
- package/dist/ScheduleEditor-BXWJ7CMI.js +6 -0
- package/dist/{ScheduleEditor-MNHZCQIG.js.map → ScheduleEditor-BXWJ7CMI.js.map} +1 -1
- package/dist/SubscriptionPage-VBDDRSVW.js +8 -0
- package/dist/{SubscriptionPage-JWWS44SE.js.map → SubscriptionPage-VBDDRSVW.js.map} +1 -1
- package/dist/access/ModuleUnavailable.d.ts +14 -0
- package/dist/access/ModuleUnavailable.d.ts.map +1 -0
- package/dist/access/context.d.ts.map +1 -1
- package/dist/access/useModuleAccess.d.ts +11 -0
- package/dist/access/useModuleAccess.d.ts.map +1 -0
- package/dist/app/AdminShell.d.ts +1 -1
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/OfflineBoot.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +1 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +3 -3
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/rail-preset.d.ts.map +1 -1
- package/dist/billing/TrialWidget.d.ts.map +1 -1
- package/dist/billing/ai-usage.d.ts +67 -0
- package/dist/billing/ai-usage.d.ts.map +1 -0
- package/dist/billing/index.d.ts +2 -0
- package/dist/billing/index.d.ts.map +1 -1
- package/dist/billing/index.js +3 -2
- package/dist/billing/module-families.d.ts +25 -0
- package/dist/billing/module-families.d.ts.map +1 -0
- package/dist/billing/module-pricing.d.ts +199 -0
- package/dist/billing/module-pricing.d.ts.map +1 -0
- package/dist/{chunk-IUCDHAAB.js → chunk-2Q6QHTZV.js} +13 -78
- package/dist/chunk-2Q6QHTZV.js.map +1 -0
- package/dist/{chunk-46UABOOC.js → chunk-5GCC7YCE.js} +40 -39
- package/dist/chunk-5GCC7YCE.js.map +1 -0
- package/dist/{chunk-WXNTUCX3.js → chunk-E4MESMNR.js} +10 -4
- package/dist/chunk-E4MESMNR.js.map +1 -0
- package/dist/chunk-F346TYKO.js +1448 -0
- package/dist/chunk-F346TYKO.js.map +1 -0
- package/dist/chunk-GZWX4ZIK.js +111 -0
- package/dist/chunk-GZWX4ZIK.js.map +1 -0
- package/dist/{chunk-OINP2UYL.js → chunk-KVIINVY6.js} +133 -169
- package/dist/chunk-KVIINVY6.js.map +1 -0
- package/dist/chunk-OEY3V6J3.js +581 -0
- package/dist/chunk-OEY3V6J3.js.map +1 -0
- package/dist/{chunk-V2IBAF4O.js → chunk-ZMP52ZHM.js} +23 -83
- package/dist/chunk-ZMP52ZHM.js.map +1 -0
- package/dist/components/RightRail.d.ts.map +1 -1
- package/dist/components/auth/CenteredLogin.d.ts +1 -1
- package/dist/components/auth/CenteredLogin.d.ts.map +1 -1
- package/dist/components/auth/LoginForm.d.ts +1 -1
- package/dist/components/auth/LoginForm.d.ts.map +1 -1
- package/dist/components/auth/LoginPage.d.ts +1 -1
- package/dist/components/auth/LoginPage.d.ts.map +1 -1
- package/dist/components/auth/OAuthButtons.d.ts +1 -1
- package/dist/components/auth/OAuthButtons.d.ts.map +1 -1
- package/dist/components/auth/ProtectedRoute.d.ts +1 -1
- package/dist/components/auth/ProtectedRoute.d.ts.map +1 -1
- package/dist/components/auth/RecoveryForm.d.ts.map +1 -1
- package/dist/components/auth/SignupForm.d.ts.map +1 -1
- package/dist/components/auth/SplitLogin.d.ts +1 -1
- package/dist/components/auth/SplitLogin.d.ts.map +1 -1
- package/dist/components/billing/AiUsageSection.d.ts +16 -0
- package/dist/components/billing/AiUsageSection.d.ts.map +1 -0
- package/dist/components/billing/InvoiceList.d.ts +1 -1
- package/dist/components/billing/InvoiceList.d.ts.map +1 -1
- package/dist/components/billing/PlanCheckoutSheet.d.ts +24 -0
- package/dist/components/billing/PlanCheckoutSheet.d.ts.map +1 -0
- package/dist/components/billing/PlanSelector.d.ts +1 -1
- package/dist/components/billing/PlanSelector.d.ts.map +1 -1
- package/dist/components/billing/PricingOverview.d.ts +20 -0
- package/dist/components/billing/PricingOverview.d.ts.map +1 -0
- package/dist/components/billing/SoftLimitWidget.d.ts.map +1 -1
- package/dist/components/billing/SubscriptionCard.d.ts +1 -1
- package/dist/components/billing/SubscriptionCard.d.ts.map +1 -1
- package/dist/components/billing/SubscriptionPage.d.ts.map +1 -1
- package/dist/components/billing/UpgradePrompt.d.ts.map +1 -1
- package/dist/components/billing/access-contract.d.ts +5 -3
- package/dist/components/billing/access-contract.d.ts.map +1 -1
- package/dist/components/billing/plan-feature.d.ts.map +1 -1
- package/dist/components/chat/AssistantActivityBar.d.ts.map +1 -1
- package/dist/components/chat/AssistantInvite.d.ts.map +1 -1
- package/dist/components/chat/AssistantRecordingView.d.ts.map +1 -1
- package/dist/components/chat/ChatAttachments.d.ts.map +1 -1
- package/dist/components/chat/ChatComposer.d.ts.map +1 -1
- package/dist/components/chat/ChatConversation.d.ts.map +1 -1
- package/dist/components/chat/ChatFab.d.ts.map +1 -1
- package/dist/components/chat/ChatHistory.d.ts.map +1 -1
- package/dist/components/chat/ChatMessages.d.ts.map +1 -1
- package/dist/components/chat/ChatSuggestions.d.ts.map +1 -1
- package/dist/components/chat/ChoiceCard.d.ts.map +1 -1
- package/dist/components/chat/ConfirmActionCard.d.ts.map +1 -1
- package/dist/components/chat/ConnectorSuggestionCard.d.ts.map +1 -1
- package/dist/components/chat/RecordChip.d.ts.map +1 -1
- package/dist/components/chat/TourCoachCard.d.ts.map +1 -1
- package/dist/components/connect/AddConnectorPanel.d.ts.map +1 -1
- package/dist/components/connect/ConnectConsentPage.d.ts.map +1 -1
- package/dist/components/connect/ConnectorDetail.d.ts.map +1 -1
- package/dist/components/connect/ConnectorRow.d.ts.map +1 -1
- package/dist/components/connect/ConnectorSyncActions.d.ts.map +1 -1
- package/dist/components/connect/ConnectorsPanel.d.ts.map +1 -1
- package/dist/components/layout/BottomNav.d.ts.map +1 -1
- package/dist/components/notifications/NotificationInbox.d.ts +1 -1
- package/dist/components/notifications/NotificationInbox.d.ts.map +1 -1
- package/dist/components/notifications/UpdateReady.d.ts.map +1 -1
- package/dist/components/organization/PermissionMatrixEditor.d.ts.map +1 -1
- package/dist/components/organization/PermissionProfilesTab.d.ts.map +1 -1
- package/dist/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/components/plugins/ConnectorsHub.d.ts.map +1 -1
- package/dist/components/plugins/PluginSettingsPanel.d.ts.map +1 -1
- package/dist/components/settings/AddModulesPanel.d.ts.map +1 -1
- package/dist/components/settings/BrandingSettings.d.ts.map +1 -1
- package/dist/components/settings/CompanySettings.d.ts +1 -1
- package/dist/components/settings/CompanySettings.d.ts.map +1 -1
- package/dist/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
- package/dist/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
- package/dist/components/settings/ConnectedModulesSettings.d.ts.map +1 -1
- package/dist/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
- package/dist/components/settings/FieldRulesEditor.d.ts +12 -0
- package/dist/components/settings/FieldRulesEditor.d.ts.map +1 -0
- package/dist/components/settings/FieldRulesSettings.d.ts.map +1 -1
- package/dist/components/settings/HolidaysSettings.d.ts.map +1 -1
- package/dist/components/settings/ModuleCard.d.ts +18 -3
- package/dist/components/settings/ModuleCard.d.ts.map +1 -1
- package/dist/components/settings/ModuleFamilyBits.d.ts +4 -0
- package/dist/components/settings/ModuleFamilyBits.d.ts.map +1 -0
- package/dist/components/settings/ModulesSettings.d.ts +3 -1
- package/dist/components/settings/ModulesSettings.d.ts.map +1 -1
- package/dist/components/settings/SecuritySettings.d.ts +1 -1
- package/dist/components/settings/SecuritySettings.d.ts.map +1 -1
- package/dist/components/settings/SettingsPage.d.ts +19 -0
- package/dist/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/components/settings/UserProfile.d.ts +1 -1
- package/dist/components/settings/UserProfile.d.ts.map +1 -1
- package/dist/components/settings/module-links.d.ts +10 -0
- package/dist/components/settings/module-links.d.ts.map +1 -0
- package/dist/components/settings/module-scope.d.ts +28 -0
- package/dist/components/settings/module-scope.d.ts.map +1 -0
- package/dist/components/settings/settings-focus.d.ts +17 -0
- package/dist/components/settings/settings-focus.d.ts.map +1 -0
- package/dist/components/settings/settings-parents.d.ts +11 -0
- package/dist/components/settings/settings-parents.d.ts.map +1 -0
- package/dist/components/settings/settings-sections.d.ts +56 -0
- package/dist/components/settings/settings-sections.d.ts.map +1 -0
- package/dist/components/settings/settings-shortcuts.d.ts +7 -0
- package/dist/components/settings/settings-shortcuts.d.ts.map +1 -0
- package/dist/components/settings/settings-subtabs.d.ts +14 -0
- package/dist/components/settings/settings-subtabs.d.ts.map +1 -0
- package/dist/components/shared/ContactPicker.d.ts.map +1 -1
- package/dist/components/shared/PersonLink.d.ts.map +1 -1
- package/dist/components/shared/RecordPickerModal.d.ts.map +1 -1
- package/dist/components/survey/PublicSurveyPage.d.ts.map +1 -1
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudFormPage.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/EntityOverview.d.ts.map +1 -1
- package/dist/crud/ImportWizard.d.ts.map +1 -1
- package/dist/crud/InlineField.d.ts +36 -0
- package/dist/crud/InlineField.d.ts.map +1 -0
- package/dist/crud/LookupCreateInline.d.ts.map +1 -1
- package/dist/crud/RelatedSection.d.ts.map +1 -1
- package/dist/crud/TagsField.d.ts.map +1 -1
- package/dist/crud/archetypes/AccessTab.d.ts.map +1 -1
- package/dist/crud/archetypes/ActiveToggle.d.ts.map +1 -1
- package/dist/crud/archetypes/AddressFormModal.d.ts +34 -0
- package/dist/crud/archetypes/AddressFormModal.d.ts.map +1 -0
- package/dist/crud/archetypes/AddressesTab.d.ts +2 -20
- package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -1
- package/dist/crud/archetypes/ArchetypeStatusBar.d.ts.map +1 -1
- package/dist/crud/archetypes/BlockSettingsPopover.d.ts +1 -1
- package/dist/crud/archetypes/BlockSettingsPopover.d.ts.map +1 -1
- package/dist/crud/archetypes/ClientOrdersTab.d.ts.map +1 -1
- package/dist/crud/archetypes/KanbanBoard.d.ts.map +1 -1
- package/dist/crud/archetypes/LocationFormLayout.d.ts.map +1 -1
- package/dist/crud/archetypes/OrderBoardView.d.ts.map +1 -1
- package/dist/crud/archetypes/OrderCreateScreen.d.ts +3 -1
- package/dist/crud/archetypes/OrderCreateScreen.d.ts.map +1 -1
- package/dist/crud/archetypes/OrderDetailPage.d.ts.map +1 -1
- package/dist/crud/archetypes/OrderDetailRail.d.ts.map +1 -1
- package/dist/crud/archetypes/OrderFormView.d.ts.map +1 -1
- package/dist/crud/archetypes/PayeeAccountsSection.d.ts.map +1 -1
- package/dist/crud/archetypes/PaymentStatusBadge.d.ts.map +1 -1
- package/dist/crud/archetypes/PersonPicker.d.ts.map +1 -1
- package/dist/crud/archetypes/ScheduleDateExceptions.d.ts.map +1 -1
- package/dist/crud/archetypes/ScheduleGridPanel.d.ts.map +1 -1
- package/dist/crud/fieldToColumn.d.ts.map +1 -1
- package/dist/db.cjs +831 -5
- package/dist/db.d.ts +2 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1 -1
- package/dist/directory/AppSwitcher.d.ts.map +1 -1
- package/dist/handbook/NotebookPanel.d.ts.map +1 -1
- package/dist/hooks/useAITools.d.ts.map +1 -1
- package/dist/hooks/useAuth.d.ts +3 -3
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useChat.d.ts.map +1 -1
- package/dist/hooks/useFieldRules.d.ts +4 -2
- package/dist/hooks/useFieldRules.d.ts.map +1 -1
- package/dist/hooks/useFormatters.d.ts.map +1 -1
- package/dist/hooks/useGlobalSearch.d.ts.map +1 -1
- package/dist/hooks/useLayout.d.ts.map +1 -1
- package/dist/hooks/useNotifications.d.ts +1 -1
- package/dist/hooks/useNotifications.d.ts.map +1 -1
- package/dist/hooks/useTenant.d.ts +2 -2
- package/dist/hooks/useTenant.d.ts.map +1 -1
- package/dist/hooks/useTenantPlugins.d.ts +1 -1
- package/dist/impersonation/ImpersonationBanner.d.ts.map +1 -1
- package/dist/index.d.ts +51 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4997 -4563
- package/dist/index.js.map +1 -1
- package/dist/lib/core-ai-tools.d.ts.map +1 -1
- package/dist/lib/currency.d.ts +6 -0
- package/dist/lib/currency.d.ts.map +1 -1
- package/dist/lib/field-rules-db.d.ts +36 -0
- package/dist/lib/field-rules-db.d.ts.map +1 -0
- package/dist/lib/report-attachments.d.ts +26 -0
- package/dist/lib/report-attachments.d.ts.map +1 -0
- package/dist/lib/tool-search.d.ts.map +1 -1
- package/dist/onboarding/ActivationProvider.d.ts.map +1 -1
- package/dist/onboarding/activation.store.d.ts +1 -1
- package/dist/onboarding/activation.store.d.ts.map +1 -1
- package/dist/onboarding/checklist.d.ts +1 -1
- package/dist/onboarding/checklist.d.ts.map +1 -1
- package/dist/onboarding/tasks.d.ts +1 -1
- package/dist/onboarding/tasks.d.ts.map +1 -1
- package/dist/org/WorkspaceChooser.d.ts.map +1 -1
- package/dist/org/WorkspaceUnitSheet.d.ts.map +1 -1
- package/dist/org/adapters/supabase.d.ts +1 -0
- package/dist/org/adapters/supabase.d.ts.map +1 -1
- package/dist/org/index.js +2 -2
- package/dist/permissions/v1-catalog.d.ts +16 -0
- package/dist/permissions/v1-catalog.d.ts.map +1 -0
- package/dist/plugins/PluginRegistryManager.d.ts.map +1 -1
- package/dist/plugins/QuickActionsButton.d.ts.map +1 -1
- package/dist/plugins/SettingsGroup.d.ts +12 -4
- package/dist/plugins/SettingsGroup.d.ts.map +1 -1
- package/dist/plugins/WidgetSlot.d.ts.map +1 -1
- package/dist/projects/ProjectsSettings.d.ts.map +1 -1
- package/dist/projects/SeuSiteWidget.d.ts.map +1 -1
- package/dist/settings/MessageTemplateRow.d.ts +16 -0
- package/dist/settings/MessageTemplateRow.d.ts.map +1 -0
- package/dist/settings/SettingsForm.d.ts +60 -0
- package/dist/settings/SettingsForm.d.ts.map +1 -0
- package/dist/settings/UnitScopePicker.d.ts +22 -0
- package/dist/settings/UnitScopePicker.d.ts.map +1 -0
- package/dist/settings/core-v1-tabs.d.ts +8 -0
- package/dist/settings/core-v1-tabs.d.ts.map +1 -0
- package/dist/settings/defineSettingsTab.d.ts +17 -0
- package/dist/settings/defineSettingsTab.d.ts.map +1 -0
- package/dist/settings/registry-parent.d.ts +8 -0
- package/dist/settings/registry-parent.d.ts.map +1 -0
- package/dist/settings/useSettingKeys.d.ts +28 -0
- package/dist/settings/useSettingKeys.d.ts.map +1 -0
- package/dist/stores/auth.store.d.ts +1 -1
- package/dist/stores/auth.store.d.ts.map +1 -1
- package/dist/stores/chat.store.d.ts.map +1 -1
- package/dist/stores/notifications.store.d.ts +1 -1
- package/dist/stores/notifications.store.d.ts.map +1 -1
- package/dist/stores/tenant.store.d.ts +1 -1
- package/dist/stores/tenant.store.d.ts.map +1 -1
- package/dist/tasks/TasksPanel.d.ts +4 -0
- package/dist/tasks/TasksPanel.d.ts.map +1 -0
- package/dist/tasks/TasksRailPanel.d.ts.map +1 -1
- package/dist/tasks/index.d.ts +15 -14
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/timeline/PersonTimeline.d.ts +1 -0
- package/dist/timeline/PersonTimeline.d.ts.map +1 -1
- package/dist/types/billing.d.ts +2 -0
- package/dist/types/billing.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/ui.d.ts +2 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +1 -1
- package/dist/units/MemberUnitsCell.d.ts.map +1 -1
- package/dist/units/UnitMembersTab.d.ts.map +1 -1
- package/dist/units/UnitSwitcher.d.ts.map +1 -1
- package/package.json +7 -7
- package/dist/ImportWizard-7UFB2PPF.js +0 -3
- package/dist/ScheduleEditor-MNHZCQIG.js +0 -6
- package/dist/SubscriptionPage-JWWS44SE.js +0 -9
- package/dist/access/index.d.ts +0 -12
- package/dist/access/index.d.ts.map +0 -1
- package/dist/access/resolver.d.ts +0 -2
- package/dist/access/resolver.d.ts.map +0 -1
- package/dist/app/LoginPage.d.ts +0 -3
- package/dist/app/LoginPage.d.ts.map +0 -1
- package/dist/chunk-46UABOOC.js.map +0 -1
- package/dist/chunk-IUCDHAAB.js.map +0 -1
- package/dist/chunk-OINP2UYL.js.map +0 -1
- package/dist/chunk-QOS6UAQ4.js +0 -54
- package/dist/chunk-QOS6UAQ4.js.map +0 -1
- package/dist/chunk-V2IBAF4O.js.map +0 -1
- package/dist/chunk-VH4LLIMH.js +0 -531
- package/dist/chunk-VH4LLIMH.js.map +0 -1
- package/dist/chunk-WXNTUCX3.js.map +0 -1
- package/dist/components/auth/index.d.ts +0 -7
- package/dist/components/auth/index.d.ts.map +0 -1
- package/dist/components/layout/index.d.ts +0 -6
- package/dist/components/layout/index.d.ts.map +0 -1
- package/dist/components/notifications/index.d.ts +0 -5
- package/dist/components/notifications/index.d.ts.map +0 -1
- package/dist/components/organization/index.d.ts +0 -5
- package/dist/components/organization/index.d.ts.map +0 -1
- package/dist/components/settings/index.d.ts +0 -11
- package/dist/components/settings/index.d.ts.map +0 -1
- package/dist/crud/index.d.ts +0 -18
- package/dist/crud/index.d.ts.map +0 -1
- package/dist/hooks/index.d.ts +0 -20
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/lib/format.d.ts +0 -7
- package/dist/lib/format.d.ts.map +0 -1
- package/dist/lib/plugins.d.ts +0 -2
- package/dist/lib/plugins.d.ts.map +0 -1
- package/dist/stores/billing.store.d.ts +0 -2
- package/dist/stores/billing.store.d.ts.map +0 -1
- package/dist/stores/index.d.ts +0 -13
- package/dist/stores/index.d.ts.map +0 -1
- package/dist/stores/layout.store.d.ts +0 -2
- package/dist/stores/layout.store.d.ts.map +0 -1
- package/dist/stores/permissions.store.d.ts +0 -2
- package/dist/stores/permissions.store.d.ts.map +0 -1
- package/dist/tasks/components/TaskCard.d.ts +0 -11
- package/dist/tasks/components/TaskCard.d.ts.map +0 -1
- package/dist/tasks/components/TaskDetail.d.ts +0 -5
- package/dist/tasks/components/TaskDetail.d.ts.map +0 -1
- package/dist/tasks/components/TaskLabelBadge.d.ts +0 -9
- package/dist/tasks/components/TaskLabelBadge.d.ts.map +0 -1
- package/dist/tasks/components/TaskPriorityBadge.d.ts +0 -7
- package/dist/tasks/components/TaskPriorityBadge.d.ts.map +0 -1
- package/dist/tasks/components/TaskQuickAdd.d.ts +0 -5
- package/dist/tasks/components/TaskQuickAdd.d.ts.map +0 -1
- package/dist/tasks/components/TaskSourceBadge.d.ts +0 -5
- package/dist/tasks/components/TaskSourceBadge.d.ts.map +0 -1
- package/dist/tasks/components/TaskStatusGroup.d.ts +0 -17
- package/dist/tasks/components/TaskStatusGroup.d.ts.map +0 -1
- package/dist/tasks/components/TasksFilterBar.d.ts +0 -3
- package/dist/tasks/components/TasksFilterBar.d.ts.map +0 -1
- package/dist/tasks/components/TasksPanel.d.ts +0 -14
- package/dist/tasks/components/TasksPanel.d.ts.map +0 -1
- package/dist/tasks/data/mock.d.ts +0 -3
- package/dist/tasks/data/mock.d.ts.map +0 -1
- package/dist/tasks/data/supabase.d.ts +0 -3
- package/dist/tasks/data/supabase.d.ts.map +0 -1
- package/dist/tasks/entity.d.ts +0 -11
- package/dist/tasks/entity.d.ts.map +0 -1
- package/dist/tasks/store.d.ts +0 -34
- package/dist/tasks/store.d.ts.map +0 -1
- package/dist/tasks/types.d.ts +0 -89
- package/dist/tasks/types.d.ts.map +0 -1
- package/dist/theme/index.d.ts +0 -8
- package/dist/theme/index.d.ts.map +0 -1
- package/dist/timeline/index.d.ts +0 -7
- package/dist/timeline/index.d.ts.map +0 -1
- package/dist/timeline-LECYQ4IA.js +0 -3
- package/dist/timeline-LECYQ4IA.js.map +0 -1
- package/dist/types/index.d.ts +0 -16
- package/dist/types/index.d.ts.map +0 -1
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
import { useTranslation } from './chunk-YUHCC7JY.js';
|
|
2
|
+
import { PRICING_PLANS, useBillingStore, useTrial, effectivePlan, pricingForPlans, quote, aiCreditsFor, planFit, EXTRA_SEAT_PRICE, PRICING_SCENARIOS, packageFor, perUnitModules, moduleFitsTrades, billableModules, useAiUsage, PRICE_BOOK, SCENARIO_VERTICAL_LABELS, TRADE_BY_APP, activeModuleKeys, EXTRA_AI_CREDITS, includedIn, UPSELL_KEY } from './chunk-OEY3V6J3.js';
|
|
3
|
+
import { useAppConfigStore, resolveAppId, activePluginIds, hasPlugin, useOrgAdapterOptional, useUnitStore } from './chunk-ZMP52ZHM.js';
|
|
4
|
+
import { usePermissionsStore } from './chunk-UQ2NTFQC.js';
|
|
5
|
+
import { cn, useOrganizationStore, getSupabaseClientOptional } from './chunk-GZWX4ZIK.js';
|
|
6
|
+
import * as React from 'react';
|
|
7
|
+
import { useMemo, useCallback, useLayoutEffect, useEffect } from 'react';
|
|
8
|
+
import { Sparkles, Check, Crown, Minus, Building2, Users, Blocks, TrendingDown, Calculator, RotateCcw, ChevronDown, AlertCircle, AlertTriangle, ArrowUpRight } from 'lucide-react';
|
|
9
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
10
|
+
import { DataTable, toast, Card, CardContent, Button, Skeleton, ConfirmDialog, Badge, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, getLucideIcon, Switch, SheetFooter, ChoiceSelect, Input } from '@fayz-ai/ui';
|
|
11
|
+
import { tl, invalidateCount, usePluginRuntimeOptional, getAllEntities, countByTenant, invalidateAppConfig, errorMessage, toTenantPluginBindings, fetchAppConfig, formatCurrency } from '@fayz-ai/core';
|
|
12
|
+
import { useAuthStore } from '@fayz-ai/auth';
|
|
13
|
+
import { mergeLimitDeclarations, isEntitledByPlan, resolveAccess, resolveLimit } from '@fayz-ai/core/access';
|
|
14
|
+
import { create } from 'zustand';
|
|
15
|
+
|
|
16
|
+
function parsePlanFeature(feature) {
|
|
17
|
+
return feature.startsWith("*") ? { label: feature.slice(1).trim(), highlighted: true } : { label: feature, highlighted: false };
|
|
18
|
+
}
|
|
19
|
+
function PlanFeatureItem({ feature, className, iconClassName }) {
|
|
20
|
+
const { label, highlighted } = parsePlanFeature(feature);
|
|
21
|
+
return /* @__PURE__ */ jsxs("li", { className: cn(
|
|
22
|
+
"flex items-start gap-2",
|
|
23
|
+
className,
|
|
24
|
+
highlighted && "-mx-1.5 rounded-md bg-violet-50 px-1.5 py-1 font-medium text-violet-700 dark:bg-violet-950/60 dark:text-violet-200"
|
|
25
|
+
), children: [
|
|
26
|
+
highlighted ? /* @__PURE__ */ jsx(Sparkles, { className: cn("mt-0.5 shrink-0 text-violet-600 dark:text-violet-300", iconClassName), "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Check, { className: cn("mt-0.5 shrink-0 text-primary", iconClassName), "aria-hidden": "true" }),
|
|
27
|
+
/* @__PURE__ */ jsx("span", { children: label })
|
|
28
|
+
] });
|
|
29
|
+
}
|
|
30
|
+
var CORE_LIMIT_DECLARATIONS = [
|
|
31
|
+
{ key: "users", label: "Users", table: "v_team_members" },
|
|
32
|
+
{ key: "locations", label: "Companies", table: "v_units" }
|
|
33
|
+
];
|
|
34
|
+
function entityDerivedLimitDeclarations(entities) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const e of entities) {
|
|
37
|
+
const def = e.entityDef;
|
|
38
|
+
if (def?.limitKey && def.data?.table) {
|
|
39
|
+
out.push({
|
|
40
|
+
key: def.limitKey,
|
|
41
|
+
label: e.labelPlural,
|
|
42
|
+
table: def.data.table,
|
|
43
|
+
kindFilter: def.data.archetypeKind
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
var registry = /* @__PURE__ */ new Map();
|
|
50
|
+
function setLimitRegistry(declarations) {
|
|
51
|
+
const next = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const decl of declarations) next.set(decl.key, decl);
|
|
53
|
+
registry = next;
|
|
54
|
+
}
|
|
55
|
+
function getLimitDeclaration(key) {
|
|
56
|
+
return registry.get(key);
|
|
57
|
+
}
|
|
58
|
+
var listeners = /* @__PURE__ */ new Map();
|
|
59
|
+
function subscribeLimit(key, cb) {
|
|
60
|
+
let set = listeners.get(key);
|
|
61
|
+
if (!set) {
|
|
62
|
+
set = /* @__PURE__ */ new Set();
|
|
63
|
+
listeners.set(key, set);
|
|
64
|
+
}
|
|
65
|
+
set.add(cb);
|
|
66
|
+
return () => {
|
|
67
|
+
set?.delete(cb);
|
|
68
|
+
if (set && set.size === 0) listeners.delete(key);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function invalidateLimit(key) {
|
|
72
|
+
const decl = registry.get(key);
|
|
73
|
+
if (decl) invalidateCount(decl.table);
|
|
74
|
+
listeners.get(key)?.forEach((cb) => cb());
|
|
75
|
+
}
|
|
76
|
+
var useUpgradeModalStore = create((set) => ({
|
|
77
|
+
current: null,
|
|
78
|
+
open: (payload) => set({ current: payload }),
|
|
79
|
+
close: () => set({ current: null })
|
|
80
|
+
}));
|
|
81
|
+
var AccessContext = React.createContext(null);
|
|
82
|
+
function AccessProvider({ children, limitDeclarations, trialPlanId }) {
|
|
83
|
+
const user = useAuthStore((s) => s.user);
|
|
84
|
+
const org = useOrganizationStore((s) => s.currentOrg);
|
|
85
|
+
const profile = usePermissionsStore((s) => s.currentProfile);
|
|
86
|
+
const plans = useBillingStore((s) => s.plans);
|
|
87
|
+
const runtime = usePluginRuntimeOptional();
|
|
88
|
+
const pluginLimits = runtime?.pluginLimits;
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
setLimitRegistry(
|
|
91
|
+
mergeLimitDeclarations(
|
|
92
|
+
CORE_LIMIT_DECLARATIONS,
|
|
93
|
+
entityDerivedLimitDeclarations(getAllEntities()),
|
|
94
|
+
pluginLimits,
|
|
95
|
+
limitDeclarations
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
}, [pluginLimits, limitDeclarations]);
|
|
99
|
+
const trial = useTrial();
|
|
100
|
+
const plan = React.useMemo(
|
|
101
|
+
() => effectivePlan(org, plans, trial, trialPlanId),
|
|
102
|
+
[plans, org?.plan, trial.status, trialPlanId]
|
|
103
|
+
);
|
|
104
|
+
const session = React.useMemo(
|
|
105
|
+
() => ({ user, org, profile, plan }),
|
|
106
|
+
[user, org, profile, plan]
|
|
107
|
+
);
|
|
108
|
+
const value = React.useMemo(
|
|
109
|
+
() => ({
|
|
110
|
+
session,
|
|
111
|
+
can: (feature, action) => resolveAccess(session, feature, action),
|
|
112
|
+
entitled: (feature) => isEntitledByPlan(session.plan, feature)
|
|
113
|
+
}),
|
|
114
|
+
[session]
|
|
115
|
+
);
|
|
116
|
+
return /* @__PURE__ */ jsx(AccessContext.Provider, { value, children });
|
|
117
|
+
}
|
|
118
|
+
AccessProvider.displayName = "AccessProvider";
|
|
119
|
+
var ALLOW_ALL = {
|
|
120
|
+
can: () => ({ allowed: true }),
|
|
121
|
+
entitled: () => true
|
|
122
|
+
};
|
|
123
|
+
function useAccess() {
|
|
124
|
+
const ctx = React.useContext(AccessContext);
|
|
125
|
+
if (!ctx) throw new Error("useAccess must be used inside <AccessProvider>");
|
|
126
|
+
return { can: ctx.can, entitled: ctx.entitled };
|
|
127
|
+
}
|
|
128
|
+
function useAccessOptional() {
|
|
129
|
+
const ctx = React.useContext(AccessContext);
|
|
130
|
+
return ctx ? { can: ctx.can, entitled: ctx.entitled } : ALLOW_ALL;
|
|
131
|
+
}
|
|
132
|
+
function useAccessSession() {
|
|
133
|
+
return React.useContext(AccessContext)?.session ?? null;
|
|
134
|
+
}
|
|
135
|
+
function resolveCap(plan, key) {
|
|
136
|
+
return plan?.entitlements?.limits?.[key];
|
|
137
|
+
}
|
|
138
|
+
function useLimit(key) {
|
|
139
|
+
const session = useAccessSession();
|
|
140
|
+
const tenantId = session?.org?.id;
|
|
141
|
+
const cap = resolveCap(session?.plan ?? null, key);
|
|
142
|
+
const declaration = getLimitDeclaration(key);
|
|
143
|
+
const unlimited = !declaration || cap === void 0 || cap === -1;
|
|
144
|
+
const [used, setUsed] = React.useState(0);
|
|
145
|
+
const [loading, setLoading] = React.useState(!unlimited);
|
|
146
|
+
const [version, setVersion] = React.useState(0);
|
|
147
|
+
const refresh = React.useCallback(() => setVersion((v) => v + 1), []);
|
|
148
|
+
React.useEffect(() => subscribeLimit(key, refresh), [key, refresh]);
|
|
149
|
+
React.useEffect(() => {
|
|
150
|
+
if (unlimited || !declaration || !tenantId) {
|
|
151
|
+
setUsed(0);
|
|
152
|
+
setLoading(false);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let active = true;
|
|
156
|
+
setLoading(true);
|
|
157
|
+
countByTenant(declaration.table, {
|
|
158
|
+
kind: declaration.kindFilter,
|
|
159
|
+
period: declaration.period,
|
|
160
|
+
tenantId,
|
|
161
|
+
fresh: version > 0
|
|
162
|
+
}).then((n) => {
|
|
163
|
+
if (active) {
|
|
164
|
+
setUsed(n);
|
|
165
|
+
setLoading(false);
|
|
166
|
+
}
|
|
167
|
+
}).catch(() => {
|
|
168
|
+
if (active) setLoading(false);
|
|
169
|
+
});
|
|
170
|
+
return () => {
|
|
171
|
+
active = false;
|
|
172
|
+
};
|
|
173
|
+
}, [unlimited, declaration, tenantId, version]);
|
|
174
|
+
const bounds = resolveLimit({ cap, hasDeclaration: Boolean(declaration), used });
|
|
175
|
+
return { key, ...bounds, loading, refresh };
|
|
176
|
+
}
|
|
177
|
+
function useLimitGuard(key) {
|
|
178
|
+
const session = useAccessSession();
|
|
179
|
+
const tenantId = session?.org?.id;
|
|
180
|
+
const plan = session?.plan ?? null;
|
|
181
|
+
const openModal = useUpgradeModalStore((s) => s.open);
|
|
182
|
+
return React.useCallback(
|
|
183
|
+
async (n = 1) => {
|
|
184
|
+
const cap = resolveCap(plan, key);
|
|
185
|
+
const declaration = getLimitDeclaration(key);
|
|
186
|
+
if (!declaration || cap === void 0 || cap === -1 || !tenantId) return "ok";
|
|
187
|
+
const used = await countByTenant(declaration.table, {
|
|
188
|
+
kind: declaration.kindFilter,
|
|
189
|
+
period: declaration.period,
|
|
190
|
+
tenantId,
|
|
191
|
+
fresh: true
|
|
192
|
+
});
|
|
193
|
+
if (used + n > cap) {
|
|
194
|
+
openModal({ limitKey: key });
|
|
195
|
+
return "blocked";
|
|
196
|
+
}
|
|
197
|
+
return "ok";
|
|
198
|
+
},
|
|
199
|
+
[key, plan, tenantId, openModal]
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/components/billing/access-contract.ts
|
|
204
|
+
function getPlanEntitlements(plan) {
|
|
205
|
+
return plan?.entitlements;
|
|
206
|
+
}
|
|
207
|
+
function featureDisplayName(label) {
|
|
208
|
+
if (!label) return "";
|
|
209
|
+
return label.replace(/\s*\([^)]*\)\s*$/, "").trim() || label.trim();
|
|
210
|
+
}
|
|
211
|
+
var CURRENCY_BY_REGION = {
|
|
212
|
+
BR: "BRL",
|
|
213
|
+
PT: "EUR",
|
|
214
|
+
ES: "EUR",
|
|
215
|
+
FR: "EUR",
|
|
216
|
+
DE: "EUR",
|
|
217
|
+
IT: "EUR",
|
|
218
|
+
NL: "EUR",
|
|
219
|
+
IE: "EUR",
|
|
220
|
+
GB: "GBP",
|
|
221
|
+
US: "USD",
|
|
222
|
+
CA: "CAD",
|
|
223
|
+
AU: "AUD",
|
|
224
|
+
MX: "MXN",
|
|
225
|
+
AR: "ARS",
|
|
226
|
+
CL: "CLP",
|
|
227
|
+
CO: "COP"
|
|
228
|
+
};
|
|
229
|
+
function resolvePlanCurrency(planCurrency, locale) {
|
|
230
|
+
if (planCurrency) return planCurrency;
|
|
231
|
+
const region = locale?.split(/[-_]/)[1]?.toUpperCase();
|
|
232
|
+
return region && CURRENCY_BY_REGION[region] || "USD";
|
|
233
|
+
}
|
|
234
|
+
function useAppPreset() {
|
|
235
|
+
const apps = useAppConfigStore((s) => s.config.apps);
|
|
236
|
+
return useMemo(() => {
|
|
237
|
+
const own = (apps ?? []).find((a) => !a.house);
|
|
238
|
+
return own?.preset ?? {};
|
|
239
|
+
}, [apps]);
|
|
240
|
+
}
|
|
241
|
+
function useOfferedPlugins() {
|
|
242
|
+
const apps = useAppConfigStore((s) => s.config.apps);
|
|
243
|
+
return useMemo(() => {
|
|
244
|
+
const ids = /* @__PURE__ */ new Set();
|
|
245
|
+
for (const app of apps ?? []) {
|
|
246
|
+
if (app.house) continue;
|
|
247
|
+
for (const id of app.preset?.modules ?? []) ids.add(id);
|
|
248
|
+
}
|
|
249
|
+
return [...ids];
|
|
250
|
+
}, [apps]);
|
|
251
|
+
}
|
|
252
|
+
function useTenantLedIdentity(hasTenantBrand) {
|
|
253
|
+
return hasTenantBrand || resolveAppId() === null;
|
|
254
|
+
}
|
|
255
|
+
function usePresetLabel(route, fallback) {
|
|
256
|
+
const preset = useAppPreset();
|
|
257
|
+
return (route ? preset.labels?.[route] : void 0) ?? fallback;
|
|
258
|
+
}
|
|
259
|
+
function useAppTheme() {
|
|
260
|
+
const apps = useAppConfigStore((s) => s.config.apps);
|
|
261
|
+
return useMemo(() => {
|
|
262
|
+
const own = (apps ?? []).find((a) => !a.house);
|
|
263
|
+
return own?.theme ?? null;
|
|
264
|
+
}, [apps]);
|
|
265
|
+
}
|
|
266
|
+
var num = (n) => n.toLocaleString("pt-BR");
|
|
267
|
+
var money = (n) => formatCurrency(n, "BRL");
|
|
268
|
+
var FEATURE_LABELS = {
|
|
269
|
+
assistant: ["Assistant", "Assistente"],
|
|
270
|
+
atendeai: ["AtendeAI (WhatsApp)", "AtendeAI (WhatsApp)"],
|
|
271
|
+
scribe: ["AI scribe", "Escriba (IA)"]
|
|
272
|
+
};
|
|
273
|
+
function AiUsageSection({ included, planName, upgrade }) {
|
|
274
|
+
const state = useAiUsage(included);
|
|
275
|
+
const checkout = useBillingStore((s) => s.checkout);
|
|
276
|
+
const [pack, setPack] = React.useState(null);
|
|
277
|
+
const buy = async (p) => {
|
|
278
|
+
if (checkout) {
|
|
279
|
+
try {
|
|
280
|
+
await checkout(`ai-credits-${p.credits}`);
|
|
281
|
+
} catch {
|
|
282
|
+
toast.error(tl("Could not start the purchase.", "N\xE3o foi poss\xEDvel iniciar a compra."));
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
setPack(p);
|
|
287
|
+
};
|
|
288
|
+
return /* @__PURE__ */ jsxs("section", { "aria-labelledby": "ai-usage-title", className: "space-y-3", children: [
|
|
289
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-2", children: [
|
|
290
|
+
/* @__PURE__ */ jsxs("h2", { id: "ai-usage-title", className: "flex items-center gap-2 text-lg font-semibold", children: [
|
|
291
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 text-violet-600 dark:text-violet-300", "aria-hidden": true }),
|
|
292
|
+
tl("AI usage this period", "Uso de IA neste per\xEDodo")
|
|
293
|
+
] }),
|
|
294
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
295
|
+
planName ? `${planName} \xB7 ` : "",
|
|
296
|
+
num(included),
|
|
297
|
+
" ",
|
|
298
|
+
tl("credits/mo included", "cr\xE9ditos/m\xEAs inclu\xEDdos")
|
|
299
|
+
] })
|
|
300
|
+
] }),
|
|
301
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4 p-5", children: [
|
|
302
|
+
state.status === "loading" && /* @__PURE__ */ jsx(Skeleton, { className: "h-16 w-full" }),
|
|
303
|
+
state.status === "not-metered" && /* @__PURE__ */ jsx(EmptyState, { text: tl(
|
|
304
|
+
"Usage starts counting once the assistant is used this period. Your plan includes these credits every month.",
|
|
305
|
+
"O consumo come\xE7a a contar quando o assistente for usado neste per\xEDodo. O seu plano inclui estes cr\xE9ditos todo m\xEAs."
|
|
306
|
+
) }),
|
|
307
|
+
state.status === "empty" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
308
|
+
/* @__PURE__ */ jsx(Meter, { s: state.summary }),
|
|
309
|
+
/* @__PURE__ */ jsx(EmptyState, { text: tl("No credits spent yet this period.", "Nenhum cr\xE9dito usado neste per\xEDodo ainda.") })
|
|
310
|
+
] }),
|
|
311
|
+
state.status === "ok" && /* @__PURE__ */ jsx(Usage, { s: state.summary, upgrade, onBuy: buy })
|
|
312
|
+
] }) }),
|
|
313
|
+
/* @__PURE__ */ jsx(
|
|
314
|
+
ConfirmDialog,
|
|
315
|
+
{
|
|
316
|
+
open: pack !== null,
|
|
317
|
+
onConfirm: () => setPack(null),
|
|
318
|
+
onCancel: () => setPack(null),
|
|
319
|
+
title: pack ? tl(`${num(pack.credits)} extra credits for ${money(pack.price)}`, `${num(pack.credits)} cr\xE9ditos extras por ${money(pack.price)}`) : "",
|
|
320
|
+
description: tl(
|
|
321
|
+
"Buying credits online is not available for this account yet. Ask support to add the pack \u2014 it is charged on the next invoice.",
|
|
322
|
+
"A compra de cr\xE9ditos online ainda n\xE3o est\xE1 dispon\xEDvel nesta conta. Pe\xE7a o pacote ao suporte \u2014 ele entra na pr\xF3xima fatura."
|
|
323
|
+
),
|
|
324
|
+
confirmLabel: tl("Got it", "Entendi"),
|
|
325
|
+
cancelLabel: tl("Cancel", "Cancelar")
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
] });
|
|
329
|
+
}
|
|
330
|
+
function EmptyState({ text }) {
|
|
331
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: text });
|
|
332
|
+
}
|
|
333
|
+
function Meter({ s }) {
|
|
334
|
+
const pct = Math.round(s.ratio * 100);
|
|
335
|
+
const scale = Math.max(s.included, s.used, s.projected, 1);
|
|
336
|
+
const at = (n) => `${Math.min(100, n / scale * 100)}%`;
|
|
337
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
338
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1", children: [
|
|
339
|
+
/* @__PURE__ */ jsxs("p", { className: "tabular-nums", children: [
|
|
340
|
+
/* @__PURE__ */ jsx("span", { className: "text-3xl font-bold", children: num(s.used) }),
|
|
341
|
+
/* @__PURE__ */ jsxs("span", { className: "text-base text-muted-foreground", children: [
|
|
342
|
+
" ",
|
|
343
|
+
tl("of", "de"),
|
|
344
|
+
" ",
|
|
345
|
+
num(s.included),
|
|
346
|
+
" ",
|
|
347
|
+
tl("credits", "cr\xE9ditos")
|
|
348
|
+
] }),
|
|
349
|
+
/* @__PURE__ */ jsxs("span", { className: cn("ml-2 text-base font-semibold", s.over ? "text-amber-700 dark:text-amber-300" : "text-violet-700 dark:text-violet-300"), children: [
|
|
350
|
+
"\xB7 ",
|
|
351
|
+
pct,
|
|
352
|
+
"%"
|
|
353
|
+
] })
|
|
354
|
+
] }),
|
|
355
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground tabular-nums", children: [
|
|
356
|
+
s.daysLeft,
|
|
357
|
+
" ",
|
|
358
|
+
tl(s.daysLeft === 1 ? "day left" : "days left", s.daysLeft === 1 ? "dia para fechar" : "dias para fechar")
|
|
359
|
+
] })
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ jsxs(
|
|
362
|
+
"div",
|
|
363
|
+
{
|
|
364
|
+
role: "progressbar",
|
|
365
|
+
"aria-valuemin": 0,
|
|
366
|
+
"aria-valuemax": s.included,
|
|
367
|
+
"aria-valuenow": s.used,
|
|
368
|
+
"aria-valuetext": tl(`${num(s.used)} of ${num(s.included)} credits`, `${num(s.used)} de ${num(s.included)} cr\xE9ditos`),
|
|
369
|
+
"aria-label": tl("AI credits used", "Cr\xE9ditos de IA usados"),
|
|
370
|
+
className: "relative h-4 w-full rounded-full bg-muted",
|
|
371
|
+
children: [
|
|
372
|
+
s.projected > s.used && /* @__PURE__ */ jsx("div", { className: cn("absolute inset-y-0 left-0 rounded-full", s.projectedOver ? "bg-amber-200 dark:bg-amber-900/70" : "bg-violet-200 dark:bg-violet-900/70"), style: { width: at(s.projected) } }),
|
|
373
|
+
/* @__PURE__ */ jsx("div", { className: cn("absolute inset-y-0 left-0 rounded-full", s.over ? "bg-amber-500" : "bg-violet-600 dark:bg-violet-400"), style: { width: at(s.used) } }),
|
|
374
|
+
scale > s.included && /* @__PURE__ */ jsx("div", { className: "absolute -inset-y-1 w-0.5 rounded bg-foreground/70", style: { left: at(s.included) }, title: tl("Plan allowance", "Inclu\xEDdo no plano") })
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
),
|
|
378
|
+
s.projected > 0 && /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [
|
|
379
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
380
|
+
/* @__PURE__ */ jsx("span", { className: cn("h-2.5 w-2.5 rounded-full", s.over ? "bg-amber-500" : "bg-violet-600 dark:bg-violet-400") }),
|
|
381
|
+
tl("Used", "Usado")
|
|
382
|
+
] }),
|
|
383
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
384
|
+
/* @__PURE__ */ jsx("span", { className: cn("h-2.5 w-2.5 rounded-full", s.projectedOver ? "bg-amber-200 dark:bg-amber-900" : "bg-violet-200 dark:bg-violet-900") }),
|
|
385
|
+
tl(`At this pace: ${num(s.projected)}`, `Neste ritmo: ${num(s.projected)}`)
|
|
386
|
+
] }),
|
|
387
|
+
scale > s.included && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
388
|
+
/* @__PURE__ */ jsx("span", { className: "h-3 w-0.5 bg-foreground/70" }),
|
|
389
|
+
tl("Included", "Inclu\xEDdo no plano")
|
|
390
|
+
] })
|
|
391
|
+
] })
|
|
392
|
+
] });
|
|
393
|
+
}
|
|
394
|
+
function Usage({ s, upgrade, onBuy }) {
|
|
395
|
+
const short = s.over || s.projectedOver;
|
|
396
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
397
|
+
/* @__PURE__ */ jsx(Meter, { s }),
|
|
398
|
+
s.over ? /* @__PURE__ */ jsxs("p", { role: "status", className: "flex items-start gap-2 rounded-input border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950/50 dark:text-amber-100", children: [
|
|
399
|
+
/* @__PURE__ */ jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": true }),
|
|
400
|
+
tl(`You went ${num(s.overBy)} credits over the plan's allowance.`, `Voc\xEA passou ${num(s.overBy)} cr\xE9ditos do que o plano inclui.`)
|
|
401
|
+
] }) : s.projectedOver ? /* @__PURE__ */ jsx("p", { className: "text-sm tabular-nums text-amber-800 dark:text-amber-200", children: tl(`At this pace you will reach ${num(s.projected)} credits \u2014 over the allowance before the period ends.`, `Neste ritmo voc\xEA chega a ${num(s.projected)} cr\xE9ditos \u2014 acima do que o plano inclui antes de o per\xEDodo fechar.`) }) : null,
|
|
402
|
+
(s.byUser.length > 0 || s.byFeature.length > 0) && /* @__PURE__ */ jsxs("details", { className: "group", children: [
|
|
403
|
+
/* @__PURE__ */ jsxs("summary", { className: "flex min-h-11 cursor-pointer list-none items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground", children: [
|
|
404
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 transition-transform group-open:rotate-180", "aria-hidden": true }),
|
|
405
|
+
tl("Who uses it and where", "Quem usa e onde")
|
|
406
|
+
] }),
|
|
407
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 pt-2 sm:grid-cols-2", children: [
|
|
408
|
+
s.byUser.length > 0 && /* @__PURE__ */ jsx(Breakdown, { title: tl("Who uses it most", "Quem mais usa"), rows: s.byUser.slice(0, 5).map((u) => [u.name, u.credits]), total: s.used }),
|
|
409
|
+
s.byFeature.length > 0 && /* @__PURE__ */ jsx(Breakdown, { title: tl("Where it goes", "Onde vai"), rows: s.byFeature.map((f) => [FEATURE_LABELS[f.feature] ? tl(...FEATURE_LABELS[f.feature]) : f.feature, f.credits]), total: s.used })
|
|
410
|
+
] })
|
|
411
|
+
] }),
|
|
412
|
+
short && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 border-t pt-4 sm:flex-row sm:flex-wrap sm:items-center", children: [
|
|
413
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: tl("Buy extra credits", "Comprar cr\xE9ditos extras") }),
|
|
414
|
+
EXTRA_AI_CREDITS.map((p) => /* @__PURE__ */ jsxs(Button, { variant: "outline", className: "h-11", onClick: () => onBuy(p), children: [
|
|
415
|
+
"+",
|
|
416
|
+
num(p.credits),
|
|
417
|
+
" \xB7 ",
|
|
418
|
+
money(p.price)
|
|
419
|
+
] }, p.credits)),
|
|
420
|
+
upgrade && /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "h-11", onClick: upgrade.onChoose, disabled: !upgrade.onChoose, children: [
|
|
421
|
+
/* @__PURE__ */ jsx(ArrowUpRight, { className: "h-4 w-4" }),
|
|
422
|
+
tl(`More with ${upgrade.name} (${num(upgrade.credits)}/mo)`, `Aumentar com o plano ${upgrade.name} (${num(upgrade.credits)}/m\xEAs)`)
|
|
423
|
+
] })
|
|
424
|
+
] })
|
|
425
|
+
] });
|
|
426
|
+
}
|
|
427
|
+
function Breakdown({ title, rows, total }) {
|
|
428
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
429
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: title }),
|
|
430
|
+
/* @__PURE__ */ jsx("ul", { className: "space-y-1.5 text-sm", children: rows.map(([label, credits]) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-3", children: [
|
|
431
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", title: label, children: label }),
|
|
432
|
+
/* @__PURE__ */ jsxs("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: [
|
|
433
|
+
num(credits),
|
|
434
|
+
" ",
|
|
435
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs", children: [
|
|
436
|
+
"(",
|
|
437
|
+
total ? Math.round(credits / total * 100) : 0,
|
|
438
|
+
"%)"
|
|
439
|
+
] })
|
|
440
|
+
] })
|
|
441
|
+
] }, label)) })
|
|
442
|
+
] });
|
|
443
|
+
}
|
|
444
|
+
var MODULE_FAMILIES = [
|
|
445
|
+
{
|
|
446
|
+
id: "sales",
|
|
447
|
+
label: ["Sales & floor", "Vendas e sal\xE3o"],
|
|
448
|
+
line: ["Selling at the counter, the table and online.", "Vender no balc\xE3o, na mesa e online."],
|
|
449
|
+
plugins: ["orders", "tables", "menu", "reservations", "shop", "payments", "sites", "marketplaces"]
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
id: "stock",
|
|
453
|
+
label: ["Stock & production", "Estoque e produ\xE7\xE3o"],
|
|
454
|
+
line: ["What comes in, what is made, what goes out.", "O que entra, o que se produz, o que sai."],
|
|
455
|
+
plugins: ["inventory", "production", "kitchen"]
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
id: "care",
|
|
459
|
+
label: ["Attendance & schedule", "Atendimento e agenda"],
|
|
460
|
+
line: ["Bookings, records and the service itself.", "Agendamento, prontu\xE1rio e o atendimento em si."],
|
|
461
|
+
plugins: ["agenda", "clinical", "forms", "custom_forms", "scribe", "courses", "tiss", "pet"]
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
id: "finance",
|
|
465
|
+
label: ["Finance & tax", "Financeiro e fiscal"],
|
|
466
|
+
line: ["Cash, bills, banks and invoices.", "Caixa, contas, bancos e nota fiscal."],
|
|
467
|
+
plugins: ["financial", "fiscal-br", "banking-br", "openbanking"]
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: "clients",
|
|
471
|
+
label: ["Clients & marketing", "Clientes e marketing"],
|
|
472
|
+
line: ["Who your clients are and how you talk to them.", "Quem s\xE3o seus clientes e como voc\xEA fala com eles."],
|
|
473
|
+
plugins: ["crm", "conversations", "marketing", "automations", "reputation"]
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: "team",
|
|
477
|
+
label: ["Team", "Equipe"],
|
|
478
|
+
line: ["Shifts, time clock and tasks.", "Escala, ponto e tarefas."],
|
|
479
|
+
plugins: ["workforce", "tasks"]
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "base",
|
|
483
|
+
label: ["Platform", "Base do sistema"],
|
|
484
|
+
line: ["Registries, reports and what every account has.", "Cadastros, relat\xF3rios e o que toda conta tem."],
|
|
485
|
+
plugins: ["registry", "reports", "dashboard", "notifications", "analytics"]
|
|
486
|
+
}
|
|
487
|
+
];
|
|
488
|
+
var FAMILY_OF = new Map(MODULE_FAMILIES.flatMap((f) => f.plugins.map((p) => [p, f.id])));
|
|
489
|
+
function moduleFamilyOf(key) {
|
|
490
|
+
return FAMILY_OF.get(key) ?? FAMILY_OF.get(key.split(".")[0]) ?? "base";
|
|
491
|
+
}
|
|
492
|
+
var UPCOMING_MODULE_INFO = {
|
|
493
|
+
tiss: { line: ["Bill health plans in the TISS standard, guide by guide.", "Fature conv\xEAnios no padr\xE3o TISS, guia por guia."] },
|
|
494
|
+
"pet.grooming": { line: ["Booked by pet, priced by size and coat, from drop-off to pick-up.", "Marcado pelo pet, pre\xE7o por porte e pelagem, do recebido ao entregue."], v1: "ThePet \xB7 /pet/servicos" },
|
|
495
|
+
"pet.vet": { line: ["Vet records, vaccines and returns per pet.", "Prontu\xE1rio, vacinas e retornos de cada pet."], v1: "ThePet" },
|
|
496
|
+
"pet.lodging": { line: ["Hotel and daycare stays with check-in and daily log.", "Hospedagem e creche com entrada, sa\xEDda e di\xE1rio."], v1: "ThePet" },
|
|
497
|
+
"pet.packages": { line: ["Weekly bath plans and pick-up & drop-off.", "Planos de banho semanais e leva-e-traz."], v1: "ThePet \xB7 /pet/planos, /pet/leva-e-traz" },
|
|
498
|
+
marketplaces: { line: ["Sell the same catalog on Mercado Livre, Shopee and Amazon.", "Venda o mesmo cat\xE1logo no Mercado Livre, Shopee e Amazon."] }
|
|
499
|
+
};
|
|
500
|
+
var money2 = (n) => formatCurrency(n, "BRL");
|
|
501
|
+
var split = (key) => {
|
|
502
|
+
const dot = key.indexOf(".");
|
|
503
|
+
return dot < 0 ? { plugin: key, facet: "" } : { plugin: key.slice(0, dot), facet: key.slice(dot + 1) };
|
|
504
|
+
};
|
|
505
|
+
function priceLabel(key, pkg) {
|
|
506
|
+
const e = PRICE_BOOK[key];
|
|
507
|
+
if (includedIn(pkg, key)) return tl(`Included in ${pkg.name}`, `Inclu\xEDdo no ${pkg.name}`);
|
|
508
|
+
return `${money2(e.price)} ${e.per === "unit" ? tl("per unit", "por unidade") : tl("/mo", "/m\xEAs")}`;
|
|
509
|
+
}
|
|
510
|
+
function PlanCheckoutSheet({ plan, pricing, size, trades, currentTotal, onCancel, onConfirm }) {
|
|
511
|
+
const runtime = usePluginRuntimeOptional();
|
|
512
|
+
const initial = React.useMemo(
|
|
513
|
+
() => new Set(billableModules([...size.modules, ...(size.unitModules ?? []).flatMap((u) => u.modules)]).concat([...size.modules, ...(size.unitModules ?? []).flatMap((u) => u.modules)].filter((k) => PRICE_BOOK[k]))),
|
|
514
|
+
[size]
|
|
515
|
+
);
|
|
516
|
+
const [selected, setSelected] = React.useState(initial);
|
|
517
|
+
const [busy, setBusy] = React.useState(false);
|
|
518
|
+
React.useEffect(() => {
|
|
519
|
+
if (plan) setSelected(new Set(initial));
|
|
520
|
+
}, [plan, initial]);
|
|
521
|
+
const offer = React.useMemo(() => {
|
|
522
|
+
const plugins = runtime?.plugins ?? [];
|
|
523
|
+
return Object.keys(PRICE_BOOK).filter((key) => {
|
|
524
|
+
if (PRICE_BOOK[key].comingSoon) return false;
|
|
525
|
+
if (trades.length && !moduleFitsTrades(key, trades) && !initial.has(key)) return false;
|
|
526
|
+
const { plugin, facet } = split(key);
|
|
527
|
+
const manifest = plugins.find((p) => p.id === plugin);
|
|
528
|
+
if (!manifest) return false;
|
|
529
|
+
return !facet || !!manifest.facets?.some((f) => f.id === facet && !f.includedWithPlugin);
|
|
530
|
+
});
|
|
531
|
+
}, [runtime?.plugins, trades, initial]);
|
|
532
|
+
const toggle = (key) => setSelected((cur) => {
|
|
533
|
+
const next = new Set(cur);
|
|
534
|
+
const { plugin, facet } = split(key);
|
|
535
|
+
if (next.has(key)) {
|
|
536
|
+
next.delete(key);
|
|
537
|
+
if (!facet) {
|
|
538
|
+
for (const k of [...next]) if (k.startsWith(`${plugin}.`)) next.delete(k);
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
next.add(key);
|
|
542
|
+
if (facet) next.add(plugin);
|
|
543
|
+
}
|
|
544
|
+
return next;
|
|
545
|
+
});
|
|
546
|
+
const pkg = React.useMemo(() => packageFor({ package: size.package, trades, modules: [...selected] }), [size.package, trades, selected]);
|
|
547
|
+
const nextSize = React.useMemo(() => {
|
|
548
|
+
const keys = [...selected];
|
|
549
|
+
const perUnit = perUnitModules(keys, pkg);
|
|
550
|
+
return {
|
|
551
|
+
...size,
|
|
552
|
+
trades,
|
|
553
|
+
modules: keys,
|
|
554
|
+
unitModules: (size.unitModules ?? []).map((u) => ({ ...u, modules: perUnit }))
|
|
555
|
+
};
|
|
556
|
+
}, [selected, size, pkg, trades]);
|
|
557
|
+
const q = pricing ? quote(nextSize, pricing) : null;
|
|
558
|
+
const total = q?.total ?? plan?.price ?? 0;
|
|
559
|
+
const fit = plan ? planFit(plan, nextSize) : { fits: true };
|
|
560
|
+
const units = nextSize.unitModules?.length || nextSize.units;
|
|
561
|
+
const delta = currentTotal !== void 0 ? total - currentTotal : void 0;
|
|
562
|
+
const changes = [
|
|
563
|
+
...[...selected].filter((k) => !initial.has(k)).map((k) => ({ ...split(k), status: "active" })),
|
|
564
|
+
...[...initial].filter((k) => !selected.has(k) && offer.includes(k)).map((k) => ({ ...split(k), status: "disabled" }))
|
|
565
|
+
];
|
|
566
|
+
const families = MODULE_FAMILIES.map((f) => ({ family: f, keys: offer.filter((k) => !k.includes(".") && moduleFamilyOf(k) === f.id) })).filter((g) => g.keys.length > 0);
|
|
567
|
+
const confirm = async () => {
|
|
568
|
+
if (!plan) return;
|
|
569
|
+
setBusy(true);
|
|
570
|
+
try {
|
|
571
|
+
await onConfirm(plan, changes);
|
|
572
|
+
} finally {
|
|
573
|
+
setBusy(false);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
return /* @__PURE__ */ jsx(Sheet, { open: !!plan, onOpenChange: (open) => {
|
|
577
|
+
if (!open && !busy) onCancel();
|
|
578
|
+
}, children: /* @__PURE__ */ jsxs(SheetContent, { width: "max-w-2xl", children: [
|
|
579
|
+
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
580
|
+
/* @__PURE__ */ jsx(SheetTitle, { children: plan ? tl(`Switch to ${plan.name}`, `Mudar para ${plan.name}`) : "" }),
|
|
581
|
+
/* @__PURE__ */ jsx(SheetDescription, { children: tl(
|
|
582
|
+
"Choose the modules this account will run on the plan. What is on today comes checked; the price follows each switch.",
|
|
583
|
+
"Escolha os m\xF3dulos que a conta vai usar no plano. O que j\xE1 est\xE1 ligado vem marcado; o pre\xE7o acompanha cada escolha."
|
|
584
|
+
) })
|
|
585
|
+
] }),
|
|
586
|
+
/* @__PURE__ */ jsx(SheetBody, { className: "space-y-6", children: families.map(({ family, keys }) => /* @__PURE__ */ jsxs("section", { className: "space-y-2", children: [
|
|
587
|
+
/* @__PURE__ */ jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: tl(...family.label) }),
|
|
588
|
+
/* @__PURE__ */ jsx("ul", { className: "divide-y rounded-card border", children: keys.map((key) => {
|
|
589
|
+
const manifest = runtime?.plugins.find((p) => p.id === key);
|
|
590
|
+
const Icon = manifest?.icon ? getLucideIcon(manifest.icon) : void 0;
|
|
591
|
+
const on = selected.has(key);
|
|
592
|
+
const parts = offer.filter((k) => k.startsWith(`${key}.`));
|
|
593
|
+
return /* @__PURE__ */ jsxs("li", { className: "space-y-2 px-4 py-3", children: [
|
|
594
|
+
/* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3", children: [
|
|
595
|
+
Icon ? /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }) : /* @__PURE__ */ jsx("span", { className: "w-4" }),
|
|
596
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
597
|
+
/* @__PURE__ */ jsx("span", { className: "block text-sm font-medium", children: manifest?.name ?? tl(...PRICE_BOOK[key].label) }),
|
|
598
|
+
/* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground tabular-nums", children: priceLabel(key, pkg) })
|
|
599
|
+
] }),
|
|
600
|
+
/* @__PURE__ */ jsx(Switch, { checked: on, onCheckedChange: () => toggle(key), "aria-label": tl(...PRICE_BOOK[key].label) })
|
|
601
|
+
] }),
|
|
602
|
+
parts.length > 0 && /* @__PURE__ */ jsx("ul", { className: "space-y-2 border-l pl-4 ml-2", children: parts.map((part) => {
|
|
603
|
+
const ai = part === UPSELL_KEY;
|
|
604
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-3", children: [
|
|
605
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
606
|
+
/* @__PURE__ */ jsx("span", { className: cn("block text-sm", ai && "font-medium text-violet-700 dark:text-violet-300"), children: tl(...PRICE_BOOK[part].label) }),
|
|
607
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground tabular-nums", children: [
|
|
608
|
+
"+ ",
|
|
609
|
+
priceLabel(part, pkg)
|
|
610
|
+
] })
|
|
611
|
+
] }),
|
|
612
|
+
/* @__PURE__ */ jsx(Switch, { checked: selected.has(part), onCheckedChange: () => toggle(part), "aria-label": tl(...PRICE_BOOK[part].label) })
|
|
613
|
+
] }) }, part);
|
|
614
|
+
}) })
|
|
615
|
+
] }, key);
|
|
616
|
+
}) })
|
|
617
|
+
] }, family.id)) }),
|
|
618
|
+
/* @__PURE__ */ jsxs(SheetFooter, { className: "flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between", children: [
|
|
619
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0", children: fit.fits ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
620
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xl font-bold tabular-nums", children: [
|
|
621
|
+
total > 0 ? money2(total) : tl("Free", "Gr\xE1tis"),
|
|
622
|
+
total > 0 && /* @__PURE__ */ jsx("span", { className: "text-sm font-normal text-muted-foreground", children: tl("/mo", "/m\xEAs") })
|
|
623
|
+
] }),
|
|
624
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground tabular-nums", children: [
|
|
625
|
+
units > 1 && total > 0 ? `\u2248 ${money2(total / units)} ${tl("per unit", "por unidade")} \xB7 ` : "",
|
|
626
|
+
delta !== void 0 && Math.abs(delta) >= 0.01 ? delta > 0 ? tl(`+${money2(delta)} vs today`, `+${money2(delta)} em rela\xE7\xE3o a hoje`) : tl(`${money2(-delta)} less than today`, `${money2(-delta)} a menos que hoje`) : tl("Same as today", "Igual a hoje")
|
|
627
|
+
] })
|
|
628
|
+
] }) : /* @__PURE__ */ jsxs("p", { className: "flex items-start gap-2 text-sm text-muted-foreground", children: [
|
|
629
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0", "aria-hidden": true }),
|
|
630
|
+
fit.limit === "locations" ? tl(`This plan takes up to ${fit.max} units; you have ${fit.have}.`, `Este plano vai at\xE9 ${fit.max} unidades; voc\xEA tem ${fit.have}.`) : tl(`This plan takes up to ${fit.max} users; you have ${fit.have}.`, `Este plano vai at\xE9 ${fit.max} usu\xE1rios; voc\xEA tem ${fit.have}.`)
|
|
631
|
+
] }) }),
|
|
632
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
633
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", className: "h-11", onClick: onCancel, disabled: busy, children: tl("Cancel", "Cancelar") }),
|
|
634
|
+
/* @__PURE__ */ jsxs(Button, { className: "h-11", onClick: () => void confirm(), disabled: busy || !fit.fits, children: [
|
|
635
|
+
/* @__PURE__ */ jsx(Check, { className: "h-4 w-4", "aria-hidden": true }),
|
|
636
|
+
busy ? tl("Saving\u2026", "Salvando\u2026") : tl("Confirm", "Confirmar"),
|
|
637
|
+
changes.length > 0 && !busy ? ` \xB7 ${changes.length} ${tl(changes.length === 1 ? "change" : "changes", changes.length === 1 ? "mudan\xE7a" : "mudan\xE7as")}` : ""
|
|
638
|
+
] })
|
|
639
|
+
] })
|
|
640
|
+
] })
|
|
641
|
+
] }) });
|
|
642
|
+
}
|
|
643
|
+
var money3 = (n) => formatCurrency(n, "BRL");
|
|
644
|
+
var MINE = "mine";
|
|
645
|
+
var FEATURES_SHOWN = 5;
|
|
646
|
+
function useRealOperation() {
|
|
647
|
+
const units = useUnitStore((s) => s.units);
|
|
648
|
+
const members = useOrganizationStore((s) => s.members);
|
|
649
|
+
const orgId = useOrganizationStore((s) => s.currentOrg?.id);
|
|
650
|
+
const adapter = useOrgAdapterOptional();
|
|
651
|
+
const runtime = usePluginRuntimeOptional();
|
|
652
|
+
const [fetchedMembers, setFetchedMembers] = React.useState(null);
|
|
653
|
+
React.useEffect(() => {
|
|
654
|
+
if (members.length > 0 || !adapter || !orgId) return;
|
|
655
|
+
let alive = true;
|
|
656
|
+
adapter.listMembers(orgId).then((list2) => {
|
|
657
|
+
if (alive) setFetchedMembers(list2.length);
|
|
658
|
+
}).catch(() => {
|
|
659
|
+
});
|
|
660
|
+
return () => {
|
|
661
|
+
alive = false;
|
|
662
|
+
};
|
|
663
|
+
}, [members.length, adapter, orgId]);
|
|
664
|
+
const apps = useAppConfigStore((s) => s.config.apps);
|
|
665
|
+
const offered = useOfferedPlugins();
|
|
666
|
+
const trades = React.useMemo(
|
|
667
|
+
() => [...new Set((apps ?? []).filter((a) => !a.house).map((a) => TRADE_BY_APP[a.id]).filter(Boolean))],
|
|
668
|
+
[apps]
|
|
669
|
+
);
|
|
670
|
+
const modules = React.useMemo(
|
|
671
|
+
() => activeModuleKeys(runtime?.plugins ?? [], { trades, offered }),
|
|
672
|
+
[runtime?.plugins, trades, offered]
|
|
673
|
+
);
|
|
674
|
+
const active = units.filter((u) => u.isActive !== false && u.kind !== "region");
|
|
675
|
+
const list = active.length ? active.map((u) => ({ id: u.id, name: u.name })) : [{ id: "main", name: tl("Main unit", "Unidade principal") }];
|
|
676
|
+
const perUnit = perUnitModules(modules, packageFor({ trades, modules }));
|
|
677
|
+
return {
|
|
678
|
+
units: list.length,
|
|
679
|
+
users: Math.max(1, members.length || fetchedMembers || 1),
|
|
680
|
+
modules,
|
|
681
|
+
unitModules: list.map((u) => ({ ...u, modules: perUnit })),
|
|
682
|
+
trades
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function useOperation() {
|
|
686
|
+
const real = useRealOperation();
|
|
687
|
+
const [scenario, setScenarioState] = React.useState(MINE);
|
|
688
|
+
const [users, setUsers] = React.useState(null);
|
|
689
|
+
const [footprints, setFootprints] = React.useState(null);
|
|
690
|
+
const example = PRICING_SCENARIOS.find((x) => x.id === scenario);
|
|
691
|
+
const base = example ? example.size : real;
|
|
692
|
+
const trades = example ? [example.vertical] : real.trades;
|
|
693
|
+
const allBase = [...base.modules, ...(base.unitModules ?? []).flatMap((u) => u.modules)];
|
|
694
|
+
const pkg = packageFor({ trades, modules: allBase });
|
|
695
|
+
const available = perUnitModules(allBase, pkg).filter((k, i, all) => all.indexOf(k) === i && (!trades.length || moduleFitsTrades(k, trades)));
|
|
696
|
+
const baseUnits = base.unitModules?.length ? [...base.unitModules] : Array.from({ length: base.units }, (_, i) => ({ id: `u${i + 1}`, name: `${tl("Unit", "Unidade")} ${i + 1}`, modules: available }));
|
|
697
|
+
const units = footprints ?? baseUnits;
|
|
698
|
+
const size = { units: units.length, users: users ?? base.users, modules: base.modules, unitModules: units, trades, package: pkg.id };
|
|
699
|
+
const reset = () => {
|
|
700
|
+
setUsers(null);
|
|
701
|
+
setFootprints(null);
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
size,
|
|
705
|
+
units,
|
|
706
|
+
available,
|
|
707
|
+
trades,
|
|
708
|
+
scenario,
|
|
709
|
+
simulated: scenario !== MINE || users !== null || footprints !== null,
|
|
710
|
+
pkg,
|
|
711
|
+
paidModules: billableModules([...size.modules, ...units.flatMap((u) => u.modules)], pkg).filter((k, i, all) => all.indexOf(k) === i).length,
|
|
712
|
+
setScenario: (id) => {
|
|
713
|
+
setScenarioState(id);
|
|
714
|
+
reset();
|
|
715
|
+
},
|
|
716
|
+
setUsers,
|
|
717
|
+
reset: () => {
|
|
718
|
+
setScenarioState(MINE);
|
|
719
|
+
reset();
|
|
720
|
+
},
|
|
721
|
+
setUnitCount: (n) => {
|
|
722
|
+
const next = units.slice(0, n);
|
|
723
|
+
while (next.length < n) next.push({ id: `extra-${next.length + 1}`, name: `${tl("Unit", "Unidade")} ${next.length + 1}`, modules: units[0]?.modules ?? available });
|
|
724
|
+
setFootprints(next);
|
|
725
|
+
},
|
|
726
|
+
toggle: (unitId, key) => setFootprints(units.map((u) => u.id !== unitId ? u : {
|
|
727
|
+
...u,
|
|
728
|
+
modules: u.modules.includes(key) ? u.modules.filter((m) => m !== key && !m.startsWith(`${key}.`)) : [...u.modules, key, ...key.includes(".") ? [key.slice(0, key.indexOf("."))] : []].filter((m, i, all) => all.indexOf(m) === i)
|
|
729
|
+
}))
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
var AI_FEATURE = /assistente de ia|ai assistant/i;
|
|
733
|
+
var MODEL_PLANS = PRICING_PLANS.filter((p) => p.id !== "a-la-carte").map((p) => ({
|
|
734
|
+
id: p.id,
|
|
735
|
+
name: tl(...p.name),
|
|
736
|
+
price: p.platformFee,
|
|
737
|
+
currency: "BRL",
|
|
738
|
+
interval: "month",
|
|
739
|
+
features: [],
|
|
740
|
+
pricing: { platformFee: p.platformFee, seatsPerUnit: p.seatsPerUnit, discount: p.discount }
|
|
741
|
+
}));
|
|
742
|
+
function Stat({ icon, children }) {
|
|
743
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 text-sm text-muted-foreground", children: [
|
|
744
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: icon }),
|
|
745
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums text-foreground", children })
|
|
746
|
+
] });
|
|
747
|
+
}
|
|
748
|
+
function fitReason(fit) {
|
|
749
|
+
if (fit.fits) return null;
|
|
750
|
+
const one = fit.max === 1;
|
|
751
|
+
return fit.limit === "locations" ? tl(`Up to ${fit.max} ${one ? "unit" : "units"} \u2014 you have ${fit.have}`, `At\xE9 ${fit.max} ${one ? "unidade" : "unidades"} \u2014 voc\xEA tem ${fit.have}`) : tl(`Up to ${fit.max} ${one ? "user" : "users"} \u2014 you have ${fit.have}`, `At\xE9 ${fit.max} ${one ? "usu\xE1rio" : "usu\xE1rios"} \u2014 voc\xEA tem ${fit.have}`);
|
|
752
|
+
}
|
|
753
|
+
function PlanCard({ p, units, recommended, current, onChoose, onDetails }) {
|
|
754
|
+
const [allFeatures, setAllFeatures] = React.useState(false);
|
|
755
|
+
const reason = fitReason(p.fit);
|
|
756
|
+
const all = [
|
|
757
|
+
`*${tl(`AI assistant \u2014 ${p.aiCredits.toLocaleString("pt-BR")} credits/mo`, `Assistente de IA \u2014 ${p.aiCredits.toLocaleString("pt-BR")} cr\xE9ditos/m\xEAs`)}`,
|
|
758
|
+
...p.plan.features.filter((f) => !AI_FEATURE.test(parsePlanFeature(f).label))
|
|
759
|
+
];
|
|
760
|
+
const features = allFeatures ? all : all.slice(0, FEATURES_SHOWN);
|
|
761
|
+
const hidden = all.length - features.length;
|
|
762
|
+
return (
|
|
763
|
+
// On a phone the recommended plan comes first; wide screens keep the price ladder.
|
|
764
|
+
/* @__PURE__ */ jsxs(Card, { className: cn("relative flex flex-col", recommended && "order-first border-primary shadow-md md:order-none", !p.fit.fits && "bg-muted/30"), children: [
|
|
765
|
+
(recommended || p.plan.highlighted && p.fit.fits) && /* @__PURE__ */ jsx("div", { className: "absolute -top-3 left-5", children: /* @__PURE__ */ jsx(Badge, { className: cn(recommended ? "border-transparent bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground"), children: recommended ? tl("Recommended for you", "Recomendado para voc\xEA") : tl("Most popular", "Mais popular") }) }),
|
|
766
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "flex flex-1 flex-col gap-4 p-5 pt-6", children: [
|
|
767
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
768
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
769
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: p.plan.name }),
|
|
770
|
+
current && /* @__PURE__ */ jsx(Badge, { variant: "outline", children: tl("Your plan", "Seu plano") })
|
|
771
|
+
] }),
|
|
772
|
+
p.plan.description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: p.plan.description })
|
|
773
|
+
] }),
|
|
774
|
+
reason ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 rounded-input border border-dashed px-3 py-2.5 text-sm text-muted-foreground", children: [
|
|
775
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "mt-0.5 h-4 w-4 shrink-0" }),
|
|
776
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
777
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: tl("Too small for you", "N\xE3o comporta a sua opera\xE7\xE3o") }),
|
|
778
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
779
|
+
reason
|
|
780
|
+
] })
|
|
781
|
+
] }) : /* @__PURE__ */ jsxs("div", { children: [
|
|
782
|
+
/* @__PURE__ */ jsxs("p", { className: "flex min-w-0 flex-wrap items-baseline gap-x-1", children: [
|
|
783
|
+
/* @__PURE__ */ jsx("span", { className: "text-2xl font-bold tabular-nums 2xl:text-3xl", children: p.total > 0 ? money3(p.total) : tl("Free", "Gr\xE1tis") }),
|
|
784
|
+
p.total > 0 && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: tl("/mo", "/m\xEAs") })
|
|
785
|
+
] }),
|
|
786
|
+
p.total > 0 && units > 1 && /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground tabular-nums", children: [
|
|
787
|
+
"\u2248 ",
|
|
788
|
+
money3(p.total / units),
|
|
789
|
+
" ",
|
|
790
|
+
tl("per unit", "por unidade")
|
|
791
|
+
] })
|
|
792
|
+
] }),
|
|
793
|
+
features.length > 0 && /* @__PURE__ */ jsxs("ul", { className: "flex-1 space-y-2 text-sm", children: [
|
|
794
|
+
features.map((f) => /* @__PURE__ */ jsx(PlanFeatureItem, { feature: f, iconClassName: "h-4 w-4" }, f)),
|
|
795
|
+
hidden > 0 && /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Button, { variant: "bare", size: "bare", type: "button", className: "font-normal whitespace-normal text-sm font-medium text-primary underline-offset-2 hover:underline", onClick: () => setAllFeatures(true), children: [
|
|
796
|
+
"+ ",
|
|
797
|
+
hidden,
|
|
798
|
+
" ",
|
|
799
|
+
tl("more", hidden === 1 ? "recurso" : "recursos")
|
|
800
|
+
] }) })
|
|
801
|
+
] }),
|
|
802
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-auto space-y-2", children: [
|
|
803
|
+
p.quote && p.fit.fits && /* @__PURE__ */ jsxs(
|
|
804
|
+
Button,
|
|
805
|
+
{
|
|
806
|
+
variant: "bare",
|
|
807
|
+
size: "bare",
|
|
808
|
+
type: "button",
|
|
809
|
+
onClick: onDetails,
|
|
810
|
+
className: "font-normal whitespace-normal flex w-full items-center justify-center gap-1.5 py-1 text-sm text-muted-foreground underline-offset-2 hover:text-foreground hover:underline",
|
|
811
|
+
children: [
|
|
812
|
+
/* @__PURE__ */ jsx(Calculator, { className: "h-3.5 w-3.5" }),
|
|
813
|
+
tl("How we got here", "Como chegamos neste valor")
|
|
814
|
+
]
|
|
815
|
+
}
|
|
816
|
+
),
|
|
817
|
+
current ? /* @__PURE__ */ jsx(Button, { variant: "outline", className: "w-full", disabled: true, children: tl("Current plan", "Plano atual") }) : onChoose ? /* @__PURE__ */ jsx(Button, { className: "w-full", variant: recommended ? "default" : "outline", disabled: !p.fit.fits, onClick: onChoose, children: tl(`Switch to ${p.plan.name}`, `Mudar para ${p.plan.name}`) }) : null
|
|
818
|
+
] })
|
|
819
|
+
] })
|
|
820
|
+
] })
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
function NumberField({ icon, label, value, onChange }) {
|
|
824
|
+
return /* @__PURE__ */ jsxs("label", { className: "flex h-11 items-center gap-2 rounded-input border bg-background px-3 text-sm", children: [
|
|
825
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", "aria-hidden": true, children: icon }),
|
|
826
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: label }),
|
|
827
|
+
/* @__PURE__ */ jsx(
|
|
828
|
+
Input,
|
|
829
|
+
{
|
|
830
|
+
type: "number",
|
|
831
|
+
min: 1,
|
|
832
|
+
inputMode: "numeric",
|
|
833
|
+
wrapperClassName: "w-14",
|
|
834
|
+
className: "h-auto border-0 bg-transparent p-0 text-right font-medium tabular-nums shadow-none focus-visible:ring-0 focus-visible:ring-offset-0",
|
|
835
|
+
value,
|
|
836
|
+
onChange: (e) => onChange(Math.max(1, Number(e.target.value) || 1))
|
|
837
|
+
}
|
|
838
|
+
)
|
|
839
|
+
] });
|
|
840
|
+
}
|
|
841
|
+
function UnitCards({ op, quoteFor }) {
|
|
842
|
+
if (op.available.length === 0) return null;
|
|
843
|
+
return /* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: op.units.map((u) => /* @__PURE__ */ jsx(UnitCard, { op, unit: u, amount: quoteFor?.lines.find((l) => l.key === `unit:${u.id}`)?.amount ?? 0 }, u.id)) });
|
|
844
|
+
}
|
|
845
|
+
function UnitCard({ op, unit, amount }) {
|
|
846
|
+
const [adding, setAdding] = React.useState(false);
|
|
847
|
+
const on = op.available.filter((k) => unit.modules.includes(k));
|
|
848
|
+
const off = op.available.filter((k) => !unit.modules.includes(k));
|
|
849
|
+
const chip = (key, active) => /* @__PURE__ */ jsxs(
|
|
850
|
+
Button,
|
|
851
|
+
{
|
|
852
|
+
variant: "bare",
|
|
853
|
+
size: "bare",
|
|
854
|
+
type: "button",
|
|
855
|
+
"aria-pressed": active,
|
|
856
|
+
onClick: () => op.toggle(unit.id, key),
|
|
857
|
+
title: active ? tl("Take out of this unit", "Tirar desta unidade") : tl("Add to this unit", "P\xF4r nesta unidade"),
|
|
858
|
+
className: cn(
|
|
859
|
+
"min-h-8 rounded-full border px-2.5 py-1 text-xs transition-colors",
|
|
860
|
+
active ? "border-primary/40 bg-primary/10 text-foreground hover:bg-primary/20" : "border-dashed text-muted-foreground hover:text-foreground"
|
|
861
|
+
),
|
|
862
|
+
children: [
|
|
863
|
+
active ? "" : "+ ",
|
|
864
|
+
tl(...PRICE_BOOK[key].label)
|
|
865
|
+
]
|
|
866
|
+
},
|
|
867
|
+
key
|
|
868
|
+
);
|
|
869
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-card border border-t-4 border-t-primary bg-card p-4", children: [
|
|
870
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
871
|
+
/* @__PURE__ */ jsx("p", { className: "min-w-0 font-semibold", children: unit.name }),
|
|
872
|
+
/* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: money3(amount) })
|
|
873
|
+
] }),
|
|
874
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5", children: [
|
|
875
|
+
on.map((k) => chip(k, true)),
|
|
876
|
+
off.length > 0 && !adding && /* @__PURE__ */ jsxs(
|
|
877
|
+
Button,
|
|
878
|
+
{
|
|
879
|
+
variant: "bare",
|
|
880
|
+
size: "bare",
|
|
881
|
+
type: "button",
|
|
882
|
+
onClick: () => setAdding(true),
|
|
883
|
+
className: "font-normal whitespace-normal min-h-8 rounded-full px-2.5 py-1 text-xs font-medium text-primary underline-offset-2 hover:underline",
|
|
884
|
+
children: [
|
|
885
|
+
"+ ",
|
|
886
|
+
off.length,
|
|
887
|
+
" ",
|
|
888
|
+
tl(off.length === 1 ? "module" : "modules", off.length === 1 ? "m\xF3dulo" : "m\xF3dulos")
|
|
889
|
+
]
|
|
890
|
+
}
|
|
891
|
+
),
|
|
892
|
+
adding && off.map((k) => chip(k, false))
|
|
893
|
+
] })
|
|
894
|
+
] });
|
|
895
|
+
}
|
|
896
|
+
function Breakdown2({ q }) {
|
|
897
|
+
return /* @__PURE__ */ jsxs("ul", { className: "divide-y rounded-card border text-sm", children: [
|
|
898
|
+
q.lines.map((line) => /* @__PURE__ */ jsxs("li", { className: "flex items-start justify-between gap-4 px-4 py-2.5", children: [
|
|
899
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
900
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium", children: line.label }),
|
|
901
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: line.detail })
|
|
902
|
+
] }),
|
|
903
|
+
line.kind === "credits" ? /* @__PURE__ */ jsx("span", { className: "shrink-0 font-medium text-violet-700 dark:text-violet-200", children: tl("Included", "Inclu\xEDdo") }) : /* @__PURE__ */ jsx("span", { className: cn("shrink-0 tabular-nums", line.amount < 0 && "text-emerald-700 dark:text-emerald-400"), children: money3(line.amount) })
|
|
904
|
+
] }, line.key)),
|
|
905
|
+
/* @__PURE__ */ jsxs("li", { className: "flex justify-between gap-4 bg-muted/40 px-4 py-3 font-semibold", children: [
|
|
906
|
+
/* @__PURE__ */ jsx("span", { children: tl("Total per month", "Total por m\xEAs") }),
|
|
907
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: money3(q.total) })
|
|
908
|
+
] })
|
|
909
|
+
] });
|
|
910
|
+
}
|
|
911
|
+
function DetailsSheet({ open, onOpenChange, op, priced, planId, setPlanId }) {
|
|
912
|
+
const withMath = priced.filter((p) => p.quote);
|
|
913
|
+
const scenarioInfo = PRICING_SCENARIOS.find((x) => x.id === op.scenario);
|
|
914
|
+
const shown = withMath.find((p) => p.plan.id === planId) ?? withMath[0];
|
|
915
|
+
return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { width: "max-w-2xl", children: [
|
|
916
|
+
/* @__PURE__ */ jsxs(SheetHeader, { children: [
|
|
917
|
+
/* @__PURE__ */ jsx(SheetTitle, { children: tl("How we price", "Como calculamos") }),
|
|
918
|
+
/* @__PURE__ */ jsx(SheetDescription, { children: tl(
|
|
919
|
+
"Each unit pays for what it runs; the one that runs most pays in full, the others 85% (65% from the 6th). Group-wide modules are paid once.",
|
|
920
|
+
"Cada unidade paga o que usa; a que mais usa paga cheio, as outras 85% (65% a partir da 6\xAA). O que serve o grupo todo \xE9 pago uma vez."
|
|
921
|
+
) })
|
|
922
|
+
] }),
|
|
923
|
+
/* @__PURE__ */ jsxs(SheetBody, { className: "space-y-6", children: [
|
|
924
|
+
/* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
|
|
925
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: tl("1. Your operation", "1. Sua opera\xE7\xE3o") }),
|
|
926
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
927
|
+
/* @__PURE__ */ jsx(
|
|
928
|
+
ChoiceSelect,
|
|
929
|
+
{
|
|
930
|
+
"aria-label": tl("Operation", "Opera\xE7\xE3o"),
|
|
931
|
+
className: "h-11 w-full sm:w-72",
|
|
932
|
+
value: op.scenario,
|
|
933
|
+
onChange: (v) => op.setScenario(v ?? MINE),
|
|
934
|
+
options: [
|
|
935
|
+
{ value: MINE, label: tl("My operation", "Minha opera\xE7\xE3o") },
|
|
936
|
+
// Grouped by trade, so a salon compares itself with salons.
|
|
937
|
+
...Object.keys(SCENARIO_VERTICAL_LABELS).flatMap((v) => PRICING_SCENARIOS.filter((x) => x.vertical === v).map((x) => ({
|
|
938
|
+
value: x.id,
|
|
939
|
+
label: `${tl(...SCENARIO_VERTICAL_LABELS[v])} \xB7 ${tl(...x.name)}`
|
|
940
|
+
})))
|
|
941
|
+
]
|
|
942
|
+
}
|
|
943
|
+
),
|
|
944
|
+
/* @__PURE__ */ jsx(NumberField, { icon: /* @__PURE__ */ jsx(Building2, { className: "h-4 w-4" }), label: tl("Units", "Unidades"), value: op.units.length, onChange: op.setUnitCount }),
|
|
945
|
+
/* @__PURE__ */ jsx(NumberField, { icon: /* @__PURE__ */ jsx(Users, { className: "h-4 w-4" }), label: tl("Users", "Usu\xE1rios"), value: op.size.users, onChange: op.setUsers })
|
|
946
|
+
] }),
|
|
947
|
+
scenarioInfo?.assumptions?.length ? /* @__PURE__ */ jsxs("div", { className: "rounded-input border border-dashed bg-muted/30 px-3 py-2 text-xs text-muted-foreground", children: [
|
|
948
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-foreground", children: tl("Assumptions", "Premissas") }),
|
|
949
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-1 list-disc space-y-0.5 pl-4", children: scenarioInfo.assumptions.map((a) => /* @__PURE__ */ jsx("li", { children: a }, a)) })
|
|
950
|
+
] }) : null,
|
|
951
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: tl("Tap a module to take it out of a unit or put it back. The plan cards follow.", "Toque num m\xF3dulo para tirar ou p\xF4r em uma unidade. Os cart\xF5es dos planos acompanham.") }),
|
|
952
|
+
/* @__PURE__ */ jsx(UnitCards, { op, quoteFor: shown?.quote ?? null })
|
|
953
|
+
] }),
|
|
954
|
+
shown?.quote && /* @__PURE__ */ jsxs("section", { className: "space-y-3", children: [
|
|
955
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: tl("2. The math, per plan", "2. A conta, por plano") }),
|
|
956
|
+
withMath.length > 1 && // Plan · value, one line each: a tab strip of names alone made the
|
|
957
|
+
// reader open each plan to learn what it costs.
|
|
958
|
+
/* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": tl("Plan", "Plano"), className: cn("grid gap-2", withMath.length >= 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"), children: withMath.map((p) => {
|
|
959
|
+
const on = p.plan.id === shown.plan.id;
|
|
960
|
+
const seats = p.pricing?.seatsPerUnit;
|
|
961
|
+
const units = op.units.length;
|
|
962
|
+
return /* @__PURE__ */ jsxs(
|
|
963
|
+
Button,
|
|
964
|
+
{
|
|
965
|
+
variant: "bare",
|
|
966
|
+
size: "bare",
|
|
967
|
+
type: "button",
|
|
968
|
+
role: "radio",
|
|
969
|
+
"aria-checked": on,
|
|
970
|
+
onClick: () => setPlanId(p.plan.id),
|
|
971
|
+
className: cn(
|
|
972
|
+
"min-h-11 rounded-card border px-3 py-2.5 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
973
|
+
on ? "border-primary bg-primary/5" : "hover:bg-muted/40"
|
|
974
|
+
),
|
|
975
|
+
children: [
|
|
976
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-baseline justify-between gap-2", children: [
|
|
977
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: p.plan.name }),
|
|
978
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold tabular-nums", children: money3(p.total) })
|
|
979
|
+
] }),
|
|
980
|
+
/* @__PURE__ */ jsxs("span", { className: "mt-0.5 block text-[11px] leading-snug text-muted-foreground", children: [
|
|
981
|
+
units > 1 ? `\u2248 ${money3(p.total / units)} ${tl("per unit", "por unidade")} \xB7 ` : "",
|
|
982
|
+
seats === null || seats === void 0 ? tl("Unlimited users", "Usu\xE1rios ilimitados") : tl(`${seats} users per unit`, `${seats} usu\xE1rios por unidade`),
|
|
983
|
+
` \xB7 ${p.aiCredits.toLocaleString("pt-BR")} ${tl("AI credits", "cr\xE9ditos de IA")}`
|
|
984
|
+
] })
|
|
985
|
+
]
|
|
986
|
+
},
|
|
987
|
+
p.plan.id
|
|
988
|
+
);
|
|
989
|
+
}) }),
|
|
990
|
+
/* @__PURE__ */ jsx(Breakdown2, { q: shown.quote })
|
|
991
|
+
] })
|
|
992
|
+
] })
|
|
993
|
+
] }) });
|
|
994
|
+
}
|
|
995
|
+
function PricingOverview({ plans, currentPlanId, onChoose, onConfirm, compare }) {
|
|
996
|
+
const op = useOperation();
|
|
997
|
+
const sources = plans?.length ? plans : MODEL_PLANS;
|
|
998
|
+
const [detailsOpen, setDetailsOpen] = React.useState(false);
|
|
999
|
+
const [detailsPlan, setDetailsPlan] = React.useState();
|
|
1000
|
+
const [compareOpen, setCompareOpen] = React.useState(false);
|
|
1001
|
+
const [checkout, setCheckout] = React.useState(null);
|
|
1002
|
+
const choose = onConfirm ? (plan) => setCheckout(plan) : onChoose;
|
|
1003
|
+
const priced = React.useMemo(() => {
|
|
1004
|
+
const pricings = pricingForPlans(sources);
|
|
1005
|
+
return sources.map((plan, i) => {
|
|
1006
|
+
const pricing = pricings[i] ?? null;
|
|
1007
|
+
const q = pricing ? quote(op.size, pricing) : null;
|
|
1008
|
+
return { plan, pricing, quote: q, total: q?.total ?? plan.price, fit: planFit(plan, op.size), aiCredits: aiCreditsFor(plan, pricing) };
|
|
1009
|
+
});
|
|
1010
|
+
}, [sources, JSON.stringify(op.size)]);
|
|
1011
|
+
const fitting = priced.filter((p) => p.fit.fits);
|
|
1012
|
+
const recommended = fitting.reduce((best, p) => !best || p.total < best.total ? p : best, void 0);
|
|
1013
|
+
const current = priced.find((p) => p.plan.id === currentPlanId);
|
|
1014
|
+
const saving = op.scenario === MINE && current && recommended && current.fit.fits && recommended.plan.id !== current.plan.id ? current.total - recommended.total : 0;
|
|
1015
|
+
const openDetails = (planId) => {
|
|
1016
|
+
setDetailsPlan(planId ?? current?.plan.id ?? recommended?.plan.id);
|
|
1017
|
+
setDetailsOpen(true);
|
|
1018
|
+
};
|
|
1019
|
+
const cols = priced.length >= 4 ? "lg:grid-cols-4" : priced.length === 3 ? "lg:grid-cols-3" : "lg:grid-cols-2";
|
|
1020
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-8", children: [
|
|
1021
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-5 p-5 md:flex-row md:items-center md:justify-between", children: [
|
|
1022
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
1023
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: op.simulated ? tl("Simulation", "Simula\xE7\xE3o") : current ? tl("Your plan", "Seu plano") : tl("Your operation", "Sua opera\xE7\xE3o") }),
|
|
1024
|
+
current && !op.simulated ? /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap items-baseline gap-x-2", children: [
|
|
1025
|
+
/* @__PURE__ */ jsx("span", { className: "text-lg font-semibold", children: current.plan.name }),
|
|
1026
|
+
/* @__PURE__ */ jsx("span", { className: "text-3xl font-bold tabular-nums", children: current.total > 0 ? money3(current.total) : tl("Free", "Gr\xE1tis") }),
|
|
1027
|
+
current.total > 0 && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: tl("/mo for your operation", "/m\xEAs para a sua opera\xE7\xE3o") })
|
|
1028
|
+
] }) : recommended ? /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap items-baseline gap-x-2", children: [
|
|
1029
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: tl("From", "A partir de") }),
|
|
1030
|
+
/* @__PURE__ */ jsx("span", { className: "text-3xl font-bold tabular-nums", children: money3(recommended.total) }),
|
|
1031
|
+
/* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
|
|
1032
|
+
tl("/mo on", "/m\xEAs no"),
|
|
1033
|
+
" ",
|
|
1034
|
+
recommended.plan.name
|
|
1035
|
+
] })
|
|
1036
|
+
] }) : null,
|
|
1037
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 pt-1", children: [
|
|
1038
|
+
/* @__PURE__ */ jsxs(Stat, { icon: /* @__PURE__ */ jsx(Building2, { className: "h-4 w-4" }), children: [
|
|
1039
|
+
op.units.length,
|
|
1040
|
+
" ",
|
|
1041
|
+
tl(op.units.length === 1 ? "unit" : "units", op.units.length === 1 ? "unidade" : "unidades")
|
|
1042
|
+
] }),
|
|
1043
|
+
/* @__PURE__ */ jsxs(Stat, { icon: /* @__PURE__ */ jsx(Users, { className: "h-4 w-4" }), children: [
|
|
1044
|
+
op.size.users,
|
|
1045
|
+
" ",
|
|
1046
|
+
tl(op.size.users === 1 ? "user" : "users", op.size.users === 1 ? "usu\xE1rio" : "usu\xE1rios")
|
|
1047
|
+
] }),
|
|
1048
|
+
/* @__PURE__ */ jsxs(Stat, { icon: /* @__PURE__ */ jsx(Blocks, { className: "h-4 w-4" }), children: [
|
|
1049
|
+
op.pkg.name,
|
|
1050
|
+
" + ",
|
|
1051
|
+
op.paidModules,
|
|
1052
|
+
" ",
|
|
1053
|
+
tl(op.paidModules === 1 ? "extra" : "extras", op.paidModules === 1 ? "extra" : "extras")
|
|
1054
|
+
] })
|
|
1055
|
+
] }),
|
|
1056
|
+
/* @__PURE__ */ jsx("p", { className: "pt-1 text-xs text-muted-foreground", children: tl(
|
|
1057
|
+
`What sets your price: the modules each unit runs (the next units pay less), what serves the whole group, and the plan fee. Logins beyond the plan: R$ ${EXTRA_SEAT_PRICE} each.`,
|
|
1058
|
+
`O que define o seu pre\xE7o: os m\xF3dulos de cada unidade (as seguintes pagam menos), o que serve o grupo todo e a plataforma do plano. Login al\xE9m do inclu\xEDdo: R$ ${EXTRA_SEAT_PRICE} cada.`
|
|
1059
|
+
) }),
|
|
1060
|
+
saving > 0.5 && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1.5 pt-1 text-sm font-medium text-emerald-700 dark:text-emerald-400", children: [
|
|
1061
|
+
/* @__PURE__ */ jsx(TrendingDown, { className: "h-4 w-4", "aria-hidden": true }),
|
|
1062
|
+
tl(
|
|
1063
|
+
`On ${recommended.plan.name} you would pay ${money3(saving)} less a month.`,
|
|
1064
|
+
`No ${recommended.plan.name} voc\xEA pagaria ${money3(saving)} a menos por m\xEAs.`
|
|
1065
|
+
)
|
|
1066
|
+
] })
|
|
1067
|
+
] }),
|
|
1068
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-col gap-2 sm:flex-row md:flex-col", children: [
|
|
1069
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", className: "h-11", onClick: () => openDetails(), children: [
|
|
1070
|
+
/* @__PURE__ */ jsx(Calculator, { className: "h-4 w-4" }),
|
|
1071
|
+
tl("See the details", "Ver detalhes do c\xE1lculo")
|
|
1072
|
+
] }),
|
|
1073
|
+
op.simulated && /* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "h-11", onClick: op.reset, children: [
|
|
1074
|
+
/* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4" }),
|
|
1075
|
+
tl("Back to my numbers", "Voltar aos meus n\xFAmeros")
|
|
1076
|
+
] })
|
|
1077
|
+
] })
|
|
1078
|
+
] }) }),
|
|
1079
|
+
current && op.scenario === MINE && (() => {
|
|
1080
|
+
const next = priced.find((p) => p.aiCredits > current.aiCredits && p.plan.id !== current.plan.id && p.fit.fits);
|
|
1081
|
+
return /* @__PURE__ */ jsx(
|
|
1082
|
+
AiUsageSection,
|
|
1083
|
+
{
|
|
1084
|
+
included: current.aiCredits,
|
|
1085
|
+
planName: current.plan.name,
|
|
1086
|
+
upgrade: next ? { name: next.plan.name, credits: next.aiCredits, onChoose: choose && plans?.length ? () => choose(next.plan) : void 0 } : void 0
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
})(),
|
|
1090
|
+
/* @__PURE__ */ jsx("div", { className: cn("grid items-stretch gap-5 md:grid-cols-2", cols), children: priced.map((p) => /* @__PURE__ */ jsx(
|
|
1091
|
+
PlanCard,
|
|
1092
|
+
{
|
|
1093
|
+
p,
|
|
1094
|
+
units: op.units.length,
|
|
1095
|
+
recommended: p.plan.id === recommended?.plan.id,
|
|
1096
|
+
current: op.scenario === MINE && p.plan.id === currentPlanId,
|
|
1097
|
+
onChoose: choose && plans?.length && op.scenario === MINE ? () => choose(p.plan) : void 0,
|
|
1098
|
+
onDetails: () => openDetails(p.plan.id)
|
|
1099
|
+
},
|
|
1100
|
+
p.plan.id
|
|
1101
|
+
)) }),
|
|
1102
|
+
compare && /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
1103
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", className: "h-11 px-2", "aria-expanded": compareOpen, onClick: () => setCompareOpen((o) => !o), children: [
|
|
1104
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: cn("h-4 w-4 transition-transform", compareOpen && "rotate-180") }),
|
|
1105
|
+
compareOpen ? tl("Hide the comparison", "Ocultar compara\xE7\xE3o") : tl("Compare what each plan includes", "Comparar o que cada plano inclui")
|
|
1106
|
+
] }),
|
|
1107
|
+
compareOpen && compare
|
|
1108
|
+
] }),
|
|
1109
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: tl(
|
|
1110
|
+
"Not included: equipment, digital certificates, internet and the providers' own fees (invoicing, iFood, acquirers). Setup and migration are free.",
|
|
1111
|
+
"N\xE3o inclu\xEDdo: equipamentos, certificado digital, internet e as taxas dos pr\xF3prios provedores (emiss\xE3o fiscal, iFood, adquirentes). Implanta\xE7\xE3o e migra\xE7\xE3o sem custo."
|
|
1112
|
+
) }),
|
|
1113
|
+
/* @__PURE__ */ jsx(
|
|
1114
|
+
PlanCheckoutSheet,
|
|
1115
|
+
{
|
|
1116
|
+
plan: checkout,
|
|
1117
|
+
pricing: checkout ? priced.find((p) => p.plan.id === checkout.id)?.pricing ?? null : null,
|
|
1118
|
+
size: op.size,
|
|
1119
|
+
trades: op.trades,
|
|
1120
|
+
currentTotal: op.scenario === MINE ? current?.total : void 0,
|
|
1121
|
+
onCancel: () => setCheckout(null),
|
|
1122
|
+
onConfirm: async (plan, changes) => {
|
|
1123
|
+
await onConfirm(plan, changes);
|
|
1124
|
+
setCheckout(null);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
),
|
|
1128
|
+
/* @__PURE__ */ jsx(DetailsSheet, { open: detailsOpen, onOpenChange: setDetailsOpen, op, priced, planId: detailsPlan, setPlanId: setDetailsPlan })
|
|
1129
|
+
] });
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/app/app-config-cache.ts
|
|
1133
|
+
var STORAGE_KEY = "fayz:app-config";
|
|
1134
|
+
var VERSION = 1;
|
|
1135
|
+
var APP_CONFIG_TTL_MS = 5 * 60 * 1e3;
|
|
1136
|
+
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1137
|
+
var MAX_BYTES = 256 * 1024;
|
|
1138
|
+
function storage() {
|
|
1139
|
+
try {
|
|
1140
|
+
return globalThis.localStorage ?? null;
|
|
1141
|
+
} catch {
|
|
1142
|
+
return null;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
var keyFor = (tenantId) => `${STORAGE_KEY}:${tenantId}`;
|
|
1146
|
+
function readAppConfigSnapshot(tenantId) {
|
|
1147
|
+
const store = storage();
|
|
1148
|
+
if (!store) return null;
|
|
1149
|
+
try {
|
|
1150
|
+
const raw = store.getItem(keyFor(tenantId));
|
|
1151
|
+
if (!raw) return null;
|
|
1152
|
+
const snap = JSON.parse(raw);
|
|
1153
|
+
if (snap.version !== VERSION || snap.tenantId !== tenantId) return null;
|
|
1154
|
+
const age = Date.now() - snap.savedAt;
|
|
1155
|
+
if (age > MAX_AGE_MS) {
|
|
1156
|
+
store.removeItem(keyFor(tenantId));
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
return { config: snap.config, stale: age > APP_CONFIG_TTL_MS };
|
|
1160
|
+
} catch {
|
|
1161
|
+
return null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function writeAppConfigSnapshot(tenantId, config) {
|
|
1165
|
+
const store = storage();
|
|
1166
|
+
if (!store) return;
|
|
1167
|
+
try {
|
|
1168
|
+
const payload = JSON.stringify({ version: VERSION, tenantId, savedAt: Date.now(), config });
|
|
1169
|
+
if (payload.length > MAX_BYTES) return;
|
|
1170
|
+
store.setItem(keyFor(tenantId), payload);
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
function clearAppConfigSnapshot(tenantId) {
|
|
1175
|
+
const store = storage();
|
|
1176
|
+
if (!store) return;
|
|
1177
|
+
try {
|
|
1178
|
+
if (tenantId) {
|
|
1179
|
+
store.removeItem(keyFor(tenantId));
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
for (let i = store.length - 1; i >= 0; i -= 1) {
|
|
1183
|
+
const key = store.key(i);
|
|
1184
|
+
if (key?.startsWith(`${STORAGE_KEY}:`)) store.removeItem(key);
|
|
1185
|
+
}
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
var CONFIG_POLL_MS = 6e4;
|
|
1190
|
+
var EMPTY = { tenant_id: null, plugins: [], apps: [], nav: [], permissions: [] };
|
|
1191
|
+
async function loadFor(tenantId, force = false) {
|
|
1192
|
+
const supabase = getSupabaseClientOptional();
|
|
1193
|
+
if (!supabase) return { ...EMPTY, tenant_id: tenantId };
|
|
1194
|
+
return fetchAppConfig(supabase, { tenantId: tenantId ?? void 0, force });
|
|
1195
|
+
}
|
|
1196
|
+
function useAppConfig(options) {
|
|
1197
|
+
const enabled = options?.enabled !== false;
|
|
1198
|
+
const currentOrg = useOrganizationStore((state) => state.currentOrg);
|
|
1199
|
+
const tenantId = currentOrg?.id ?? null;
|
|
1200
|
+
const config = useAppConfigStore((state) => state.config);
|
|
1201
|
+
const configTenantId = useAppConfigStore((state) => state.tenantId);
|
|
1202
|
+
const beginTenant = useAppConfigStore((state) => state.beginTenant);
|
|
1203
|
+
const initializing = useAppConfigStore((state) => state.initializing);
|
|
1204
|
+
const stale = useAppConfigStore((state) => state.stale);
|
|
1205
|
+
const error = useAppConfigStore((state) => state.error);
|
|
1206
|
+
const setConfig = useAppConfigStore((state) => state.setConfig);
|
|
1207
|
+
const setStale = useAppConfigStore((state) => state.setStale);
|
|
1208
|
+
const setError = useAppConfigStore((state) => state.setError);
|
|
1209
|
+
const reset = useAppConfigStore((state) => state.reset);
|
|
1210
|
+
const refresh = useCallback(async () => {
|
|
1211
|
+
setStale(true);
|
|
1212
|
+
try {
|
|
1213
|
+
if (tenantId) {
|
|
1214
|
+
invalidateAppConfig(tenantId);
|
|
1215
|
+
clearAppConfigSnapshot(tenantId);
|
|
1216
|
+
}
|
|
1217
|
+
const next = await loadFor(tenantId, true);
|
|
1218
|
+
if (tenantId) writeAppConfigSnapshot(tenantId, next);
|
|
1219
|
+
setConfig(next, tenantId);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
setError(errorMessage(err));
|
|
1222
|
+
} finally {
|
|
1223
|
+
setStale(false);
|
|
1224
|
+
}
|
|
1225
|
+
}, [tenantId, setConfig, setError, setStale]);
|
|
1226
|
+
useLayoutEffect(() => {
|
|
1227
|
+
if (!enabled) return;
|
|
1228
|
+
if (configTenantId !== tenantId) beginTenant(tenantId);
|
|
1229
|
+
}, [enabled, tenantId, configTenantId, beginTenant]);
|
|
1230
|
+
useLayoutEffect(() => {
|
|
1231
|
+
if (!enabled || !tenantId) return;
|
|
1232
|
+
const snap = readAppConfigSnapshot(tenantId);
|
|
1233
|
+
if (!snap) return;
|
|
1234
|
+
setConfig(snap.config, tenantId);
|
|
1235
|
+
if (snap.stale) setStale(true);
|
|
1236
|
+
}, [enabled, tenantId, setConfig, setStale]);
|
|
1237
|
+
useEffect(() => {
|
|
1238
|
+
if (!enabled) {
|
|
1239
|
+
reset();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
let cancelled = false;
|
|
1243
|
+
void (async () => {
|
|
1244
|
+
try {
|
|
1245
|
+
const next = await loadFor(tenantId);
|
|
1246
|
+
if (cancelled) return;
|
|
1247
|
+
if (tenantId) writeAppConfigSnapshot(tenantId, next);
|
|
1248
|
+
setConfig(next, tenantId);
|
|
1249
|
+
setStale(false);
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
if (!cancelled) setError(errorMessage(err));
|
|
1252
|
+
}
|
|
1253
|
+
})();
|
|
1254
|
+
return () => {
|
|
1255
|
+
cancelled = true;
|
|
1256
|
+
};
|
|
1257
|
+
}, [enabled, tenantId, setConfig, setError, setStale, reset]);
|
|
1258
|
+
useEffect(() => {
|
|
1259
|
+
if (!enabled || !tenantId) return;
|
|
1260
|
+
const supabase = getSupabaseClientOptional();
|
|
1261
|
+
if (!supabase) return;
|
|
1262
|
+
let cancelled = false;
|
|
1263
|
+
let seen = null;
|
|
1264
|
+
const poll = async () => {
|
|
1265
|
+
try {
|
|
1266
|
+
const { data, error: error2 } = await supabase.rpc("tenant_config_version");
|
|
1267
|
+
if (cancelled || error2 || !data) return;
|
|
1268
|
+
const stamp = String(data);
|
|
1269
|
+
if (seen === null) {
|
|
1270
|
+
seen = stamp;
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (stamp !== seen) {
|
|
1274
|
+
seen = stamp;
|
|
1275
|
+
invalidateAppConfig(tenantId);
|
|
1276
|
+
const next = await loadFor(tenantId, true);
|
|
1277
|
+
writeAppConfigSnapshot(tenantId, next);
|
|
1278
|
+
setConfig(next, tenantId);
|
|
1279
|
+
}
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
const timer = setInterval(() => void poll(), CONFIG_POLL_MS);
|
|
1284
|
+
void poll();
|
|
1285
|
+
return () => {
|
|
1286
|
+
cancelled = true;
|
|
1287
|
+
clearInterval(timer);
|
|
1288
|
+
};
|
|
1289
|
+
}, [enabled, tenantId, setConfig]);
|
|
1290
|
+
const activePlugins = useMemo(() => activePluginIds(config), [config]);
|
|
1291
|
+
const bindings = useMemo(() => toTenantPluginBindings(config), [config]);
|
|
1292
|
+
const has = useCallback(
|
|
1293
|
+
(plugin, facet) => hasPlugin(config, plugin, facet),
|
|
1294
|
+
[config]
|
|
1295
|
+
);
|
|
1296
|
+
const ready = !tenantId || configTenantId === tenantId && !initializing;
|
|
1297
|
+
return { config, initializing, ready, stale, error, activePlugins, has, bindings, refresh };
|
|
1298
|
+
}
|
|
1299
|
+
function SubscriptionPage({ className }) {
|
|
1300
|
+
const { t } = useTranslation();
|
|
1301
|
+
const tr = (key, fallback) => {
|
|
1302
|
+
const v = t(key);
|
|
1303
|
+
return !v || v === key ? fallback : v;
|
|
1304
|
+
};
|
|
1305
|
+
const plans = useBillingStore((s) => s.plans);
|
|
1306
|
+
const checkout = useBillingStore((s) => s.checkout);
|
|
1307
|
+
const adapter = useOrgAdapterOptional();
|
|
1308
|
+
const setCurrentOrg = useOrganizationStore((s) => s.setCurrentOrg);
|
|
1309
|
+
const currentOrg = useOrganizationStore((s) => s.currentOrg);
|
|
1310
|
+
const currentPlanId = currentOrg?.plan || "free";
|
|
1311
|
+
const { refresh: refreshAppConfig } = useAppConfig();
|
|
1312
|
+
const allFeatures = React.useMemo(() => {
|
|
1313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1314
|
+
const out = [];
|
|
1315
|
+
for (const plan of plans) {
|
|
1316
|
+
for (const f of plan.features) {
|
|
1317
|
+
if (!seen.has(f)) {
|
|
1318
|
+
seen.add(f);
|
|
1319
|
+
out.push(f);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return out;
|
|
1324
|
+
}, [plans]);
|
|
1325
|
+
const featureCatalog = usePermissionsStore((s) => s.features);
|
|
1326
|
+
const entitlementsMatrix = React.useMemo(() => {
|
|
1327
|
+
const hasAny = plans.some((p) => getPlanEntitlements(p));
|
|
1328
|
+
if (!hasAny) return null;
|
|
1329
|
+
const featureIds = [];
|
|
1330
|
+
const featureSeen = /* @__PURE__ */ new Set();
|
|
1331
|
+
const limitKeys = [];
|
|
1332
|
+
const limitSeen = /* @__PURE__ */ new Set();
|
|
1333
|
+
for (const plan of plans) {
|
|
1334
|
+
const ent = getPlanEntitlements(plan);
|
|
1335
|
+
for (const id of Object.keys(ent?.features ?? {})) {
|
|
1336
|
+
if (!featureSeen.has(id)) {
|
|
1337
|
+
featureSeen.add(id);
|
|
1338
|
+
featureIds.push(id);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
for (const key of Object.keys(ent?.limits ?? {})) {
|
|
1342
|
+
if (!limitSeen.has(key)) {
|
|
1343
|
+
limitSeen.add(key);
|
|
1344
|
+
limitKeys.push(key);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (featureIds.length === 0 && limitKeys.length === 0) return null;
|
|
1349
|
+
const labelForFeature = (id) => featureCatalog.find((f) => f.id === id)?.label ?? id;
|
|
1350
|
+
const labelForLimit = (key) => {
|
|
1351
|
+
const v = t(`limit.label.${key}`);
|
|
1352
|
+
return !v || v === `limit.label.${key}` ? key : v;
|
|
1353
|
+
};
|
|
1354
|
+
return { featureIds, limitKeys, labelForFeature, labelForLimit };
|
|
1355
|
+
}, [plans, featureCatalog, t]);
|
|
1356
|
+
async function applyChange(plan) {
|
|
1357
|
+
try {
|
|
1358
|
+
if (checkout) {
|
|
1359
|
+
await checkout(plan.id);
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
if (adapter && currentOrg) {
|
|
1363
|
+
const updated = await adapter.updateOrg(currentOrg.id, { plan: plan.id });
|
|
1364
|
+
setCurrentOrg(updated);
|
|
1365
|
+
} else if (currentOrg) {
|
|
1366
|
+
setCurrentOrg({ ...currentOrg, plan: plan.id });
|
|
1367
|
+
}
|
|
1368
|
+
toast.success(
|
|
1369
|
+
tr("billing.planChanged", "Plan updated").replace("{{plan}}", plan.name)
|
|
1370
|
+
);
|
|
1371
|
+
return true;
|
|
1372
|
+
} catch {
|
|
1373
|
+
toast.error(tr("billing.planChangeFailed", "Couldn't change your plan. Please try again."));
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const featureMark = (on) => on ? /* @__PURE__ */ jsx(Check, { className: "mx-auto h-4 w-4 text-primary" }) : /* @__PURE__ */ jsx(Minus, { className: "mx-auto h-4 w-4 text-muted-foreground/40" });
|
|
1378
|
+
const limitText = (cap) => cap === void 0 ? "\u2014" : cap < 0 ? tr("billing.unlimited", "Unlimited") : tr("billing.upTo", "Up to {count}").replace("{count}", String(cap)).replace("{{count}}", String(cap));
|
|
1379
|
+
const matrixRows = (entitlementsMatrix ? [
|
|
1380
|
+
...entitlementsMatrix.featureIds.map((id) => ({
|
|
1381
|
+
id: `f:${id}`,
|
|
1382
|
+
label: entitlementsMatrix.labelForFeature(id),
|
|
1383
|
+
cell: (plan) => featureMark(getPlanEntitlements(plan)?.features?.[id] !== false)
|
|
1384
|
+
})),
|
|
1385
|
+
...entitlementsMatrix.limitKeys.map((key) => ({
|
|
1386
|
+
id: `l:${key}`,
|
|
1387
|
+
label: entitlementsMatrix.labelForLimit(key),
|
|
1388
|
+
cell: (plan) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: limitText(getPlanEntitlements(plan)?.limits?.[key]) })
|
|
1389
|
+
}))
|
|
1390
|
+
] : allFeatures.map((feature) => ({
|
|
1391
|
+
id: feature,
|
|
1392
|
+
label: parsePlanFeature(feature).label,
|
|
1393
|
+
cell: (plan) => featureMark(plan.features.includes(feature))
|
|
1394
|
+
}))).map((row, i) => ({ ...row, odd: i % 2 === 1 }));
|
|
1395
|
+
const matrixColumns = [
|
|
1396
|
+
{ id: "feature", header: tr("billing.features", "Features"), cell: ({ row }) => row.original.label },
|
|
1397
|
+
...plans.map((plan) => ({
|
|
1398
|
+
id: plan.id,
|
|
1399
|
+
header: () => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
|
|
1400
|
+
plan.price > 0 && /* @__PURE__ */ jsx(Crown, { className: "h-3.5 w-3.5 text-amber-500" }),
|
|
1401
|
+
plan.name
|
|
1402
|
+
] }),
|
|
1403
|
+
meta: { className: "text-center" },
|
|
1404
|
+
cell: ({ row }) => row.original.cell(plan)
|
|
1405
|
+
}))
|
|
1406
|
+
];
|
|
1407
|
+
const compare = entitlementsMatrix || allFeatures.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: /* @__PURE__ */ jsx(DataTable, { variant: "flat", columns: matrixColumns, data: matrixRows, rowClassName: (r) => r.odd ? "bg-muted/20" : void 0 }) }) : null;
|
|
1408
|
+
if (plans.length === 0) {
|
|
1409
|
+
return /* @__PURE__ */ jsx("div", { className: className ?? "mx-auto max-w-5xl", children: /* @__PURE__ */ jsx(PricingOverview, {}) });
|
|
1410
|
+
}
|
|
1411
|
+
async function applyPlanWithModules(plan, changes) {
|
|
1412
|
+
const supabase = getSupabaseClientOptional();
|
|
1413
|
+
const failed = [];
|
|
1414
|
+
const setModules = async () => {
|
|
1415
|
+
if (!supabase) return;
|
|
1416
|
+
for (const c of changes) {
|
|
1417
|
+
const { error } = await supabase.rpc("tenant_plugin_set", { p_plugin: c.plugin, p_status: c.status, p_facet: c.facet });
|
|
1418
|
+
if (error) failed.push(c.facet ? `${c.plugin}.${c.facet}` : c.plugin);
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
if (checkout) await setModules();
|
|
1422
|
+
if (plan.id !== currentPlanId && !await applyChange(plan)) return;
|
|
1423
|
+
if (!checkout) await setModules();
|
|
1424
|
+
if (currentOrg?.id) {
|
|
1425
|
+
invalidateAppConfig(currentOrg.id);
|
|
1426
|
+
clearAppConfigSnapshot(currentOrg.id);
|
|
1427
|
+
}
|
|
1428
|
+
await refreshAppConfig();
|
|
1429
|
+
if (failed.length) {
|
|
1430
|
+
toast.error(tr("billing.modulesNotChanged", "Some modules were not changed"), { description: failed.join(", ") });
|
|
1431
|
+
} else if (changes.length) {
|
|
1432
|
+
toast.success(tr("billing.modulesChanged", "Modules updated"));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
return /* @__PURE__ */ jsx("div", { className: className ?? "mx-auto max-w-5xl space-y-8", children: /* @__PURE__ */ jsx(
|
|
1436
|
+
PricingOverview,
|
|
1437
|
+
{
|
|
1438
|
+
plans,
|
|
1439
|
+
currentPlanId,
|
|
1440
|
+
onConfirm: applyPlanWithModules,
|
|
1441
|
+
compare
|
|
1442
|
+
}
|
|
1443
|
+
) });
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
export { AccessProvider, CORE_LIMIT_DECLARATIONS, MODULE_FAMILIES, PlanFeatureItem, SubscriptionPage, UPCOMING_MODULE_INFO, clearAppConfigSnapshot, entityDerivedLimitDeclarations, featureDisplayName, getLimitDeclaration, getPlanEntitlements, invalidateLimit, moduleFamilyOf, resolvePlanCurrency, useAccess, useAccessOptional, useAppConfig, useAppPreset, useAppTheme, useLimit, useLimitGuard, useOfferedPlugins, usePresetLabel, useTenantLedIdentity, useUpgradeModalStore };
|
|
1447
|
+
//# sourceMappingURL=chunk-F346TYKO.js.map
|
|
1448
|
+
//# sourceMappingURL=chunk-F346TYKO.js.map
|