@licklist/design 0.78.5-dev.107 → 0.78.5-dev.108
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/bitbucket-pipelines.yml +4 -13
- package/dist/Maintenance/Maintenance.scss.js +1 -1
- package/dist/index.js +2 -0
- package/dist/product-set/form/ProductsControl.d.ts +1 -2
- package/dist/product-set/form/ProductsControl.d.ts.map +1 -1
- package/dist/product-set/form/ProductsControl.js +24 -0
- package/dist/v2/components/ActionMenu/ActionMenu.scss.js +1 -1
- package/dist/v2/components/Badge/Badge.scss.js +1 -1
- package/dist/v2/components/Button/Button.scss.js +1 -1
- package/dist/v2/components/Button/GhostButton.scss.js +1 -1
- package/dist/v2/components/Checkbox/Checkbox.scss.js +1 -1
- package/dist/v2/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/v2/components/DataTable/DataTable.js +2 -86
- package/dist/v2/components/IconButton/IconButton.scss.js +1 -1
- package/dist/v2/components/Modal/DeleteModal.d.ts.map +1 -1
- package/dist/v2/components/Modal/DeleteModal.js +11 -13
- package/dist/v2/components/Modal/DeleteModal.scss.js +1 -1
- package/dist/v2/components/NPSScore/NPSScore.scss.js +1 -1
- package/dist/v2/components/NewTabs/NewTabs.scss.js +1 -1
- package/dist/v2/components/PeriodCard/PeriodCard.d.ts +66 -0
- package/dist/v2/components/PeriodCard/PeriodCard.d.ts.map +1 -0
- package/dist/v2/components/PeriodCard/PeriodCard.js +351 -0
- package/dist/v2/components/PeriodCard/PeriodCard.scss.js +6 -0
- package/dist/v2/components/PeriodCard/index.d.ts +3 -0
- package/dist/v2/components/PeriodCard/index.d.ts.map +1 -0
- package/dist/v2/components/ReorderRow/ReorderRow.d.ts +24 -0
- package/dist/v2/components/ReorderRow/ReorderRow.d.ts.map +1 -0
- package/dist/v2/components/ReorderRow/ReorderRow.js +109 -0
- package/dist/v2/components/ReorderRow/ReorderRow.scss.js +6 -0
- package/dist/v2/components/ReorderRow/index.d.ts +3 -0
- package/dist/v2/components/ReorderRow/index.d.ts.map +1 -0
- package/dist/v2/components/Select/Select.scss.js +1 -1
- package/dist/v2/components/StatusBadge/StatusBadge.scss.js +1 -1
- package/dist/v2/components/StepIndicator/StepIndicator.scss.js +1 -1
- package/dist/v2/components/Tabs/Tabs.scss.js +1 -1
- package/dist/v2/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/v2/components/Toggle/Toggle.js +5 -8
- package/dist/v2/components/Tooltip/Tooltip.scss.js +1 -1
- package/dist/v2/components/UserAvatar/UserAvatar.scss.js +1 -1
- package/dist/v2/components/UserPanel/UserPanel.scss.js +1 -1
- package/dist/v2/components/WYSIWYGEditor/WYSIWYGEditor.scss.js +1 -1
- package/dist/v2/components/ZoneCard/ZoneCard.scss.js +1 -1
- package/dist/v2/components/index.d.ts +4 -0
- package/dist/v2/components/index.d.ts.map +1 -1
- package/dist/v2/dashboard-analytics/chart/Chart.scss.js +1 -1
- package/dist/v2/dashboard-analytics/metric-card/MetricCard.scss.js +1 -1
- package/dist/v2/dashboard-analytics/venue-card/VenueCard.scss.js +1 -1
- package/dist/v2/dashboard-analytics/venue-closed-card/VenueClosedCard.scss.js +1 -1
- package/dist/v2/icons/index.js +16 -1
- package/dist/v2/index.d.ts +8 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/navigation/DashboardLayout/AdminSidebar.scss.js +1 -1
- package/dist/v2/navigation/DashboardLayout/DashboardLayout.scss.js +1 -1
- package/dist/v2/navigation/DashboardLayout/ProviderSidebar.scss.js +1 -1
- package/dist/v2/navigation/DashboardLayout/TopNavigation.scss.js +1 -1
- package/dist/v2/pages/Settings/SettingsTabs.scss.js +1 -1
- package/dist/v2/pages/Settings/components/SidebarCustomisation.js +5 -0
- package/dist/v2/pages/Settings/components/SidebarCustomisation.scss.js +1 -1
- package/dist/v2/pages/Settings/components/SidebarNavItem.js +5 -0
- package/dist/v2/pages/auth/AuthLayout/AuthLayout.scss.js +1 -1
- package/dist/v2/shadcn/components/ui/accordion.d.ts +8 -0
- package/dist/v2/shadcn/components/ui/accordion.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/alert-dialog.d.ts +21 -0
- package/dist/v2/shadcn/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/alert.d.ts +9 -0
- package/dist/v2/shadcn/components/ui/alert.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/aspect-ratio.d.ts +4 -0
- package/dist/v2/shadcn/components/ui/aspect-ratio.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/avatar.d.ts +7 -0
- package/dist/v2/shadcn/components/ui/avatar.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/badge.d.ts +10 -0
- package/dist/v2/shadcn/components/ui/badge.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/breadcrumb.d.ts +20 -0
- package/dist/v2/shadcn/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/button.d.ts +14 -0
- package/dist/v2/shadcn/components/ui/button.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/calendar.d.ts +9 -0
- package/dist/v2/shadcn/components/ui/calendar.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/card.d.ts +9 -0
- package/dist/v2/shadcn/components/ui/card.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/carousel.d.ts +19 -0
- package/dist/v2/shadcn/components/ui/carousel.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/checkbox.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/checkbox.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/checkbox.js +115 -0
- package/dist/v2/shadcn/components/ui/checkbox.scss.js +6 -0
- package/dist/v2/shadcn/components/ui/collapsible.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/collapsible.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/command.d.ts +83 -0
- package/dist/v2/shadcn/components/ui/command.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/context-menu.d.ts +28 -0
- package/dist/v2/shadcn/components/ui/context-menu.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/dialog.d.ts +20 -0
- package/dist/v2/shadcn/components/ui/dialog.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/dialog.js +169 -0
- package/dist/v2/shadcn/components/ui/drawer.d.ts +23 -0
- package/dist/v2/shadcn/components/ui/drawer.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/dropdown-menu.d.ts +28 -0
- package/dist/v2/shadcn/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/form.d.ts +24 -0
- package/dist/v2/shadcn/components/ui/form.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/hover-card.d.ts +7 -0
- package/dist/v2/shadcn/components/ui/hover-card.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/input-otp.d.ts +35 -0
- package/dist/v2/shadcn/components/ui/input-otp.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/input.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/input.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/label.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/label.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/menubar.d.ts +34 -0
- package/dist/v2/shadcn/components/ui/menubar.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/navigation-menu.d.ts +13 -0
- package/dist/v2/shadcn/components/ui/navigation-menu.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/pagination.d.ts +29 -0
- package/dist/v2/shadcn/components/ui/pagination.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/popover.d.ts +7 -0
- package/dist/v2/shadcn/components/ui/popover.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/progress.d.ts +5 -0
- package/dist/v2/shadcn/components/ui/progress.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/radio-card.d.ts +12 -0
- package/dist/v2/shadcn/components/ui/radio-card.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/radio-group.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/radio-group.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/scroll-area.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/select.d.ts +14 -0
- package/dist/v2/shadcn/components/ui/select.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/separator.d.ts +5 -0
- package/dist/v2/shadcn/components/ui/separator.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/sheet.d.ts +26 -0
- package/dist/v2/shadcn/components/ui/sheet.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/sidebar.d.ts +67 -0
- package/dist/v2/shadcn/components/ui/sidebar.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/skeleton.d.ts +3 -0
- package/dist/v2/shadcn/components/ui/skeleton.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/slider.d.ts +5 -0
- package/dist/v2/shadcn/components/ui/slider.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/switch.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/switch.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/switch.js +115 -0
- package/dist/v2/shadcn/components/ui/switch.scss.js +6 -0
- package/dist/v2/shadcn/components/ui/table-pagination.d.ts +11 -0
- package/dist/v2/shadcn/components/ui/table-pagination.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/table.d.ts +11 -0
- package/dist/v2/shadcn/components/ui/table.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/tabs.d.ts +8 -0
- package/dist/v2/shadcn/components/ui/tabs.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/textarea.d.ts +6 -0
- package/dist/v2/shadcn/components/ui/textarea.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/toast.d.ts +16 -0
- package/dist/v2/shadcn/components/ui/toast.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/toaster.d.ts +2 -0
- package/dist/v2/shadcn/components/ui/toaster.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/toggle-group.d.ts +13 -0
- package/dist/v2/shadcn/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/toggle.d.ts +13 -0
- package/dist/v2/shadcn/components/ui/toggle.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/tooltip.d.ts +8 -0
- package/dist/v2/shadcn/components/ui/tooltip.d.ts.map +1 -0
- package/dist/v2/shadcn/components/ui/use-toast.d.ts +3 -0
- package/dist/v2/shadcn/components/ui/use-toast.d.ts.map +1 -0
- package/dist/v2/shadcn/hooks/use-mobile.d.ts +2 -0
- package/dist/v2/shadcn/hooks/use-mobile.d.ts.map +1 -0
- package/dist/v2/shadcn/hooks/use-toast.d.ts +45 -0
- package/dist/v2/shadcn/hooks/use-toast.d.ts.map +1 -0
- package/dist/v2/shadcn/index.d.ts +20 -0
- package/dist/v2/shadcn/index.d.ts.map +1 -0
- package/dist/v2/shadcn/lib/utils.d.ts +3 -0
- package/dist/v2/shadcn/lib/utils.d.ts.map +1 -0
- package/dist/v2/shadcn/lib/utils.js +11 -0
- package/dist/v2/shadcn/styles/globals.css +112 -0
- package/dist/v2/styles/form/NewInput.scss.js +1 -1
- package/package.json +6 -6
- package/rollup.config.js +2 -16
- package/src/iframe/payment/payment-status-page/PaymentStatusPage.tsx +1 -1
- package/src/product-set/form/ProductsControl.tsx +1 -2
- package/src/v2/components/DataTable/DataTable.tsx +1 -23
- package/src/v2/components/Modal/DeleteModal.tsx +20 -12
- package/src/v2/components/PeriodCard/PeriodCard.scss +157 -0
- package/src/v2/components/PeriodCard/PeriodCard.stories.tsx +245 -0
- package/src/v2/components/PeriodCard/PeriodCard.tsx +350 -0
- package/src/v2/components/PeriodCard/index.ts +8 -0
- package/src/v2/components/ReorderRow/ReorderRow.scss +68 -0
- package/src/v2/components/ReorderRow/ReorderRow.stories.tsx +124 -0
- package/src/v2/components/ReorderRow/ReorderRow.tsx +88 -0
- package/src/v2/components/ReorderRow/index.ts +2 -0
- package/src/v2/components/Toggle/Toggle.tsx +5 -6
- package/src/v2/components/index.ts +6 -0
- package/src/v2/index.ts +82 -0
- package/src/v2/shadcn/_reference/AccountManagerCard.tsx +45 -0
- package/src/v2/shadcn/_reference/AffiliatesTable.tsx +178 -0
- package/src/v2/shadcn/_reference/AuditArchive.tsx +165 -0
- package/src/v2/shadcn/_reference/AuditContent.tsx +270 -0
- package/src/v2/shadcn/_reference/AutomationsGeneralSettings.tsx +251 -0
- package/src/v2/shadcn/_reference/AvatarUpload.tsx +150 -0
- package/src/v2/shadcn/_reference/BookingsSummaryCard.tsx +268 -0
- package/src/v2/shadcn/_reference/CodeCleanUpAudit.tsx +274 -0
- package/src/v2/shadcn/_reference/CompaniesTable.tsx +387 -0
- package/src/v2/shadcn/_reference/ComponentAudit.tsx +239 -0
- package/src/v2/shadcn/_reference/ConfigureSettingsCard.tsx +95 -0
- package/src/v2/shadcn/_reference/CustomerCard.tsx +155 -0
- package/src/v2/shadcn/_reference/DashboardCards.tsx +50 -0
- package/src/v2/shadcn/_reference/DashboardFooter.tsx +18 -0
- package/src/v2/shadcn/_reference/DiarySettings.tsx +187 -0
- package/src/v2/shadcn/_reference/DiaryView.tsx +998 -0
- package/src/v2/shadcn/_reference/EmptyState.tsx +76 -0
- package/src/v2/shadcn/_reference/EntityInfoCard.tsx +48 -0
- package/src/v2/shadcn/_reference/ExistingUserAssignments.tsx +131 -0
- package/src/v2/shadcn/_reference/FeatureToggle.tsx +72 -0
- package/src/v2/shadcn/_reference/FlowCard.tsx +170 -0
- package/src/v2/shadcn/_reference/FlowsContent.tsx +688 -0
- package/src/v2/shadcn/_reference/FlowsGeneralSettings.tsx +27 -0
- package/src/v2/shadcn/_reference/GeneralSettings.tsx +33 -0
- package/src/v2/shadcn/_reference/InventoryGeneralSettings.tsx +82 -0
- package/src/v2/shadcn/_reference/LanguageSelector.tsx +97 -0
- package/src/v2/shadcn/_reference/LoadingScreen.tsx +25 -0
- package/src/v2/shadcn/_reference/LoadingSpinner.tsx +41 -0
- package/src/v2/shadcn/_reference/ManagedClientsList.tsx +121 -0
- package/src/v2/shadcn/_reference/NPSScore.tsx +379 -0
- package/src/v2/shadcn/_reference/NPSSummaryCard.tsx +181 -0
- package/src/v2/shadcn/_reference/NotificationBanner.tsx +129 -0
- package/src/v2/shadcn/_reference/NotificationPanel.tsx +208 -0
- package/src/v2/shadcn/_reference/OnlineUsersCard.tsx +73 -0
- package/src/v2/shadcn/_reference/ProtectedRoute.tsx +39 -0
- package/src/v2/shadcn/_reference/ProvidersTable.tsx +353 -0
- package/src/v2/shadcn/_reference/QuickAddPanel.tsx +1057 -0
- package/src/v2/shadcn/_reference/QuickFilters.tsx +112 -0
- package/src/v2/shadcn/_reference/ScheduleView.tsx +410 -0
- package/src/v2/shadcn/_reference/ScrollToTop.tsx +14 -0
- package/src/v2/shadcn/_reference/SecondaryNav.tsx +50 -0
- package/src/v2/shadcn/_reference/SecuritySettings.tsx +258 -0
- package/src/v2/shadcn/_reference/SessionDetailView.tsx +294 -0
- package/src/v2/shadcn/_reference/Sidebar.tsx +14 -0
- package/src/v2/shadcn/_reference/SidebarAwareLayout.tsx +30 -0
- package/src/v2/shadcn/_reference/SidebarLabelCustomization.tsx +285 -0
- package/src/v2/shadcn/_reference/SimulationBanner.tsx +57 -0
- package/src/v2/shadcn/_reference/SortControls.tsx +65 -0
- package/src/v2/shadcn/_reference/StatusBadge.tsx +49 -0
- package/src/v2/shadcn/_reference/StyleGuideContent.tsx +331 -0
- package/src/v2/shadcn/_reference/TableActionMenu.tsx +126 -0
- package/src/v2/shadcn/_reference/ThemeProvider.tsx +119 -0
- package/src/v2/shadcn/_reference/ThemeSettings.tsx +73 -0
- package/src/v2/shadcn/_reference/TopNavigation.tsx +332 -0
- package/src/v2/shadcn/_reference/UserActivityHistory.tsx +209 -0
- package/src/v2/shadcn/_reference/UserLanguageSettings.tsx +94 -0
- package/src/v2/shadcn/_reference/UserPanel.tsx +472 -0
- package/src/v2/shadcn/_reference/UsersTable.tsx +1023 -0
- package/src/v2/shadcn/_reference/WaiverForm.tsx +301 -0
- package/src/v2/shadcn/_reference/WaiversGeneralSettings.tsx +46 -0
- package/src/v2/shadcn/_reference/WaiversTable.tsx +290 -0
- package/src/v2/shadcn/_reference/WaiversTemplatesSettings.tsx +416 -0
- package/src/v2/shadcn/_reference/ai/AIChatPanel.tsx +313 -0
- package/src/v2/shadcn/_reference/ai/AIChatSearchBar.tsx +36 -0
- package/src/v2/shadcn/_reference/ai/ChatInteractiveBlock.tsx +298 -0
- package/src/v2/shadcn/_reference/ai/ChatMessageContent.tsx +40 -0
- package/src/v2/shadcn/_reference/ai/parseInteractiveBlocks.ts +142 -0
- package/src/v2/shadcn/_reference/auth/AuthLayout.tsx +55 -0
- package/src/v2/shadcn/_reference/auth/CreatePasswordForm.tsx +285 -0
- package/src/v2/shadcn/_reference/auth/CreatePasswordPanel.tsx +20 -0
- package/src/v2/shadcn/_reference/auth/LoginFooter.tsx +14 -0
- package/src/v2/shadcn/_reference/auth/LoginForm.tsx +205 -0
- package/src/v2/shadcn/_reference/auth/LoginPanel.tsx +41 -0
- package/src/v2/shadcn/_reference/auth/ResetPasswordForm.tsx +102 -0
- package/src/v2/shadcn/_reference/auth/ResetPasswordPanel.tsx +20 -0
- package/src/v2/shadcn/_reference/auth/VerifyEmailForm.tsx +95 -0
- package/src/v2/shadcn/_reference/auth/VerifyEmailPanel.tsx +20 -0
- package/src/v2/shadcn/_reference/email/EmailAttachment.tsx +119 -0
- package/src/v2/shadcn/_reference/email/EmailAutomation.tsx +92 -0
- package/src/v2/shadcn/_reference/email/EmailPlaceholders.tsx +64 -0
- package/src/v2/shadcn/_reference/email/UnlayerEmailEditor.tsx +41 -0
- package/src/v2/shadcn/_reference/email/emailTemplateData.ts +53 -0
- package/src/v2/shadcn/_reference/emptyStateIcons.tsx +103 -0
- package/src/v2/shadcn/_reference/games/MazeGame.tsx +394 -0
- package/src/v2/shadcn/_reference/games/RunnerGame.tsx +497 -0
- package/src/v2/shadcn/_reference/logos/BookedLogoFull.tsx +36 -0
- package/src/v2/shadcn/_reference/logos/BookedLogoMark.tsx +31 -0
- package/src/v2/shadcn/_reference/logos/BookedLogoNew.tsx +36 -0
- package/src/v2/shadcn/_reference/pricing/DynamicPricingRulesEditor.tsx +401 -0
- package/src/v2/shadcn/_reference/pricing/DynamicPricingTierCard.tsx +77 -0
- package/src/v2/shadcn/_reference/pricing/DynamicPricingTiersList.tsx +218 -0
- package/src/v2/shadcn/_reference/pricing/PricingCalendar.tsx +810 -0
- package/src/v2/shadcn/_reference/pricing/PricingPeriodCard.tsx +152 -0
- package/src/v2/shadcn/_reference/pricing/PricingPeriodForm.tsx +377 -0
- package/src/v2/shadcn/_reference/pricing/PricingPeriodsList.tsx +213 -0
- package/src/v2/shadcn/_reference/pricing/getRuleSummary.ts +39 -0
- package/src/v2/shadcn/_reference/products/AvailabilityRulesSection.tsx +184 -0
- package/src/v2/shadcn/_reference/products/AvailabilitySection.tsx +677 -0
- package/src/v2/shadcn/_reference/products/BookingTypeConfigOptions.tsx +40 -0
- package/src/v2/shadcn/_reference/products/CapacityPeriodsSection.tsx +238 -0
- package/src/v2/shadcn/_reference/products/DynamicPricingTiersSection.tsx +131 -0
- package/src/v2/shadcn/_reference/products/GiftCardOrdersTab.tsx +192 -0
- package/src/v2/shadcn/_reference/products/GiftCardSettings.tsx +342 -0
- package/src/v2/shadcn/_reference/products/PackageProductsSection.tsx +322 -0
- package/src/v2/shadcn/_reference/products/PricingSection.tsx +173 -0
- package/src/v2/shadcn/_reference/products/ProductTypeFields.tsx +353 -0
- package/src/v2/shadcn/_reference/products/ProductTypeIcon.tsx +95 -0
- package/src/v2/shadcn/_reference/products/VariablePricingSection.tsx +140 -0
- package/src/v2/shadcn/_reference/products/productTypeConfig.ts +182 -0
- package/src/v2/shadcn/_reference/shared/BackButton.tsx +50 -0
- package/src/v2/shadcn/_reference/shared/CancelConfirmationDialog.tsx +18 -0
- package/src/v2/shadcn/_reference/shared/ConfirmationDialog.tsx +136 -0
- package/src/v2/shadcn/_reference/shared/DeleteConfirmationDialog.tsx +18 -0
- package/src/v2/shadcn/_reference/shared/DeleteEntityPage.tsx +221 -0
- package/src/v2/shadcn/_reference/shared/SidebarIcons.tsx +108 -0
- package/src/v2/shadcn/_reference/shared/UnifiedSidebar.tsx +722 -0
- package/src/v2/shadcn/_reference/tables/BulkActionsBar.tsx +68 -0
- package/src/v2/shadcn/_reference/tables/DataTable.tsx +221 -0
- package/src/v2/shadcn/_reference/tables/TableControls.tsx +94 -0
- package/src/v2/shadcn/_reference/tables/index.ts +3 -0
- package/src/v2/shadcn/_reference/tables/types.ts +79 -0
- package/src/v2/shadcn/_reference/zones/LegacyZoneSettings.tsx +299 -0
- package/src/v2/shadcn/components/ui/accordion.stories.tsx +63 -0
- package/src/v2/shadcn/components/ui/accordion.tsx +52 -0
- package/src/v2/shadcn/components/ui/alert-dialog.stories.tsx +44 -0
- package/src/v2/shadcn/components/ui/alert-dialog.tsx +104 -0
- package/src/v2/shadcn/components/ui/alert.stories.tsx +44 -0
- package/src/v2/shadcn/components/ui/alert.tsx +43 -0
- package/src/v2/shadcn/components/ui/aspect-ratio.stories.tsx +46 -0
- package/src/v2/shadcn/components/ui/aspect-ratio.tsx +5 -0
- package/src/v2/shadcn/components/ui/avatar.stories.tsx +39 -0
- package/src/v2/shadcn/components/ui/avatar.tsx +38 -0
- package/src/v2/shadcn/components/ui/badge.stories.tsx +17 -0
- package/src/v2/shadcn/components/ui/badge.tsx +30 -0
- package/src/v2/shadcn/components/ui/breadcrumb.stories.tsx +91 -0
- package/src/v2/shadcn/components/ui/breadcrumb.tsx +90 -0
- package/src/v2/shadcn/components/ui/button.stories.tsx +20 -0
- package/src/v2/shadcn/components/ui/button.tsx +60 -0
- package/src/v2/shadcn/components/ui/calendar.stories.tsx +61 -0
- package/src/v2/shadcn/components/ui/calendar.tsx +54 -0
- package/src/v2/shadcn/components/ui/card.stories.tsx +37 -0
- package/src/v2/shadcn/components/ui/card.tsx +43 -0
- package/src/v2/shadcn/components/ui/carousel.stories.tsx +92 -0
- package/src/v2/shadcn/components/ui/carousel.tsx +224 -0
- package/src/v2/shadcn/components/ui/checkbox.scss +38 -0
- package/src/v2/shadcn/components/ui/checkbox.stories.tsx +23 -0
- package/src/v2/shadcn/components/ui/checkbox.tsx +24 -0
- package/src/v2/shadcn/components/ui/collapsible.stories.tsx +59 -0
- package/src/v2/shadcn/components/ui/collapsible.tsx +9 -0
- package/src/v2/shadcn/components/ui/command.stories.tsx +70 -0
- package/src/v2/shadcn/components/ui/command.tsx +132 -0
- package/src/v2/shadcn/components/ui/context-menu.stories.tsx +72 -0
- package/src/v2/shadcn/components/ui/context-menu.tsx +178 -0
- package/src/v2/shadcn/components/ui/dialog.stories.tsx +67 -0
- package/src/v2/shadcn/components/ui/dialog.tsx +95 -0
- package/src/v2/shadcn/components/ui/drawer.stories.tsx +50 -0
- package/src/v2/shadcn/components/ui/drawer.tsx +87 -0
- package/src/v2/shadcn/components/ui/dropdown-menu.stories.tsx +73 -0
- package/src/v2/shadcn/components/ui/dropdown-menu.tsx +179 -0
- package/src/v2/shadcn/components/ui/form.stories.tsx +105 -0
- package/src/v2/shadcn/components/ui/form.tsx +129 -0
- package/src/v2/shadcn/components/ui/hover-card.stories.tsx +35 -0
- package/src/v2/shadcn/components/ui/hover-card.tsx +27 -0
- package/src/v2/shadcn/components/ui/input-otp.stories.tsx +72 -0
- package/src/v2/shadcn/components/ui/input-otp.tsx +61 -0
- package/src/v2/shadcn/components/ui/input.stories.tsx +16 -0
- package/src/v2/shadcn/components/ui/input.tsx +25 -0
- package/src/v2/shadcn/components/ui/label.stories.tsx +13 -0
- package/src/v2/shadcn/components/ui/label.tsx +17 -0
- package/src/v2/shadcn/components/ui/menubar.stories.tsx +86 -0
- package/src/v2/shadcn/components/ui/menubar.tsx +207 -0
- package/src/v2/shadcn/components/ui/navigation-menu.stories.tsx +68 -0
- package/src/v2/shadcn/components/ui/navigation-menu.tsx +120 -0
- package/src/v2/shadcn/components/ui/pagination.stories.tsx +78 -0
- package/src/v2/shadcn/components/ui/pagination.tsx +81 -0
- package/src/v2/shadcn/components/ui/popover.stories.tsx +44 -0
- package/src/v2/shadcn/components/ui/popover.tsx +29 -0
- package/src/v2/shadcn/components/ui/progress.stories.tsx +17 -0
- package/src/v2/shadcn/components/ui/progress.tsx +23 -0
- package/src/v2/shadcn/components/ui/radio-card.stories.tsx +68 -0
- package/src/v2/shadcn/components/ui/radio-card.tsx +52 -0
- package/src/v2/shadcn/components/ui/radio-group.stories.tsx +77 -0
- package/src/v2/shadcn/components/ui/radio-group.tsx +35 -0
- package/src/v2/shadcn/components/ui/scroll-area.stories.tsx +56 -0
- package/src/v2/shadcn/components/ui/scroll-area.tsx +38 -0
- package/src/v2/shadcn/components/ui/select.stories.tsx +60 -0
- package/src/v2/shadcn/components/ui/select.tsx +148 -0
- package/src/v2/shadcn/components/ui/separator.stories.tsx +30 -0
- package/src/v2/shadcn/components/ui/separator.tsx +20 -0
- package/src/v2/shadcn/components/ui/sheet.stories.tsx +115 -0
- package/src/v2/shadcn/components/ui/sheet.tsx +107 -0
- package/src/v2/shadcn/components/ui/sidebar.stories.tsx +167 -0
- package/src/v2/shadcn/components/ui/sidebar.tsx +637 -0
- package/src/v2/shadcn/components/ui/skeleton.stories.tsx +36 -0
- package/src/v2/shadcn/components/ui/skeleton.tsx +7 -0
- package/src/v2/shadcn/components/ui/slider.stories.tsx +16 -0
- package/src/v2/shadcn/components/ui/slider.tsx +23 -0
- package/src/v2/shadcn/components/ui/switch.scss +63 -0
- package/src/v2/shadcn/components/ui/switch.stories.tsx +23 -0
- package/src/v2/shadcn/components/ui/switch.tsx +24 -0
- package/src/v2/shadcn/components/ui/table-pagination.stories.tsx +81 -0
- package/src/v2/shadcn/components/ui/table-pagination.tsx +61 -0
- package/src/v2/shadcn/components/ui/table.stories.tsx +40 -0
- package/src/v2/shadcn/components/ui/table.tsx +72 -0
- package/src/v2/shadcn/components/ui/tabs.stories.tsx +85 -0
- package/src/v2/shadcn/components/ui/tabs.tsx +53 -0
- package/src/v2/shadcn/components/ui/textarea.stories.tsx +15 -0
- package/src/v2/shadcn/components/ui/textarea.tsx +21 -0
- package/src/v2/shadcn/components/ui/toast.stories.tsx +77 -0
- package/src/v2/shadcn/components/ui/toast.tsx +111 -0
- package/src/v2/shadcn/components/ui/toaster.stories.tsx +46 -0
- package/src/v2/shadcn/components/ui/toaster.tsx +24 -0
- package/src/v2/shadcn/components/ui/toggle-group.stories.tsx +95 -0
- package/src/v2/shadcn/components/ui/toggle-group.tsx +49 -0
- package/src/v2/shadcn/components/ui/toggle.stories.tsx +18 -0
- package/src/v2/shadcn/components/ui/toggle.tsx +37 -0
- package/src/v2/shadcn/components/ui/tooltip.stories.tsx +57 -0
- package/src/v2/shadcn/components/ui/tooltip.tsx +28 -0
- package/src/v2/shadcn/components/ui/use-toast.ts +3 -0
- package/src/v2/shadcn/hooks/use-mobile.tsx +19 -0
- package/src/v2/shadcn/hooks/use-toast.ts +184 -0
- package/src/v2/shadcn/index.ts +76 -0
- package/src/v2/shadcn/lib/utils.ts +6 -0
- package/src/v2/shadcn/styles/globals.css +112 -0
- package/.vscode/settings.json +0 -3
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { DataTable } from './tables/DataTable';
|
|
4
|
+
import { TableControls } from './tables/TableControls';
|
|
5
|
+
import { ColumnConfig, RowAction, SearchConfig, HeaderAction } from './tables/types';
|
|
6
|
+
import { BulkActionsBar } from './tables/BulkActionsBar';
|
|
7
|
+
import StatusBadge from './StatusBadge';
|
|
8
|
+
import { supabase } from '@/integrations/supabase/client';
|
|
9
|
+
import LoadingScreen from './LoadingScreen';
|
|
10
|
+
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog';
|
|
11
|
+
import { Button } from './ui/button';
|
|
12
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
13
|
+
import DeleteConfirmationDialog from './shared/DeleteConfirmationDialog';
|
|
14
|
+
import { getShortUserId } from '@/lib/userUtils';
|
|
15
|
+
import { useAuth } from '@/contexts/AuthContext';
|
|
16
|
+
import { usePermissions } from '../hooks/usePermissions';
|
|
17
|
+
import { useNotify } from '../hooks/useNotify';
|
|
18
|
+
import NotificationBanner, { Notification } from './NotificationBanner';
|
|
19
|
+
import IconImportUser from '@/assets/Icon_ImportUser.svg';
|
|
20
|
+
import { IconAddUser } from '../../icons';
|
|
21
|
+
|
|
22
|
+
interface UserRole {
|
|
23
|
+
user_id: string;
|
|
24
|
+
role: string;
|
|
25
|
+
provider_id: string | null;
|
|
26
|
+
company_id: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface User {
|
|
30
|
+
id: string;
|
|
31
|
+
full_name: string | null;
|
|
32
|
+
email: string | null;
|
|
33
|
+
status: string | null;
|
|
34
|
+
created_at: string;
|
|
35
|
+
roles: string[];
|
|
36
|
+
currentLevelRole: string | null;
|
|
37
|
+
otherProviderRoleCount: number;
|
|
38
|
+
avatar_url: string | null;
|
|
39
|
+
last_seen_at: string | null;
|
|
40
|
+
user_number: number | null;
|
|
41
|
+
providers: string[];
|
|
42
|
+
companies: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface UsersTableProps {
|
|
46
|
+
/** If provided, scopes users to this provider */
|
|
47
|
+
providerId?: string;
|
|
48
|
+
/** If provided alongside providerId, also includes company users */
|
|
49
|
+
companyId?: string | null;
|
|
50
|
+
/** Navigation prefix for row clicks/actions (e.g. "/company/01/venue/01") */
|
|
51
|
+
navigatePrefix?: string;
|
|
52
|
+
/** Custom header actions (overrides default admin actions) */
|
|
53
|
+
headerActions?: HeaderAction[];
|
|
54
|
+
/** Whether to show bulk actions (default: true for admin) */
|
|
55
|
+
showBulkActions?: boolean;
|
|
56
|
+
/** Whether to show delete row action (default: true for admin) */
|
|
57
|
+
showDeleteAction?: boolean;
|
|
58
|
+
/** Whether to show the "Role & Provider" column or just "Role" (default: true) */
|
|
59
|
+
showProviderColumn?: boolean;
|
|
60
|
+
/** Filter out super_admin, system_admin, customer roles (default: false) */
|
|
61
|
+
filterAdminRoles?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const UsersTable: React.FC<UsersTableProps> = ({
|
|
65
|
+
providerId,
|
|
66
|
+
companyId,
|
|
67
|
+
navigatePrefix = '/admin',
|
|
68
|
+
headerActions: customHeaderActions,
|
|
69
|
+
showBulkActions = true,
|
|
70
|
+
showDeleteAction = true,
|
|
71
|
+
showProviderColumn = true,
|
|
72
|
+
filterAdminRoles = false,
|
|
73
|
+
}) => {
|
|
74
|
+
const navigate = useNavigate();
|
|
75
|
+
const { showSuccess, showError } = useNotify();
|
|
76
|
+
const { user: currentUser } = useAuth();
|
|
77
|
+
const { hasPermission } = usePermissions();
|
|
78
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
79
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
80
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
81
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
82
|
+
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
83
|
+
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
|
|
84
|
+
const [notification, setNotification] = useState<Notification | null>(null);
|
|
85
|
+
const [sortKey, setSortKey] = useState<string>('name');
|
|
86
|
+
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
fetchUsers();
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const fetchUsers = async () => {
|
|
93
|
+
try {
|
|
94
|
+
setIsLoading(true);
|
|
95
|
+
|
|
96
|
+
let assignedUserIds: string[] | null = null;
|
|
97
|
+
|
|
98
|
+
// If scoped to a provider, fetch only assigned user IDs
|
|
99
|
+
if (providerId) {
|
|
100
|
+
const { data: userProviderAssignments, error: assignmentsError } = await supabase
|
|
101
|
+
.from('user_providers')
|
|
102
|
+
.select('user_id')
|
|
103
|
+
.eq('provider_id', providerId);
|
|
104
|
+
|
|
105
|
+
if (assignmentsError) throw assignmentsError;
|
|
106
|
+
|
|
107
|
+
assignedUserIds = userProviderAssignments?.map(up => up.user_id) || [];
|
|
108
|
+
|
|
109
|
+
// Also include company users if provider belongs to a company
|
|
110
|
+
if (companyId) {
|
|
111
|
+
const { data: companyUserAssignments, error: companyError } = await supabase
|
|
112
|
+
.from('user_companies')
|
|
113
|
+
.select('user_id')
|
|
114
|
+
.eq('company_id', companyId);
|
|
115
|
+
|
|
116
|
+
if (companyError) throw companyError;
|
|
117
|
+
|
|
118
|
+
const companyUserIds = companyUserAssignments?.map(uc => uc.user_id) || [];
|
|
119
|
+
assignedUserIds = [...new Set([...assignedUserIds, ...companyUserIds])];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (assignedUserIds.length === 0) {
|
|
123
|
+
setUsers([]);
|
|
124
|
+
setIsLoading(false);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
} else if (companyId) {
|
|
128
|
+
// Company-only scope: get users from user_companies + all providers under this company
|
|
129
|
+
const { data: companyUserAssignments, error: companyError } = await supabase
|
|
130
|
+
.from('user_companies')
|
|
131
|
+
.select('user_id')
|
|
132
|
+
.eq('company_id', companyId);
|
|
133
|
+
|
|
134
|
+
if (companyError) throw companyError;
|
|
135
|
+
|
|
136
|
+
const companyUserIds = companyUserAssignments?.map(uc => uc.user_id) || [];
|
|
137
|
+
|
|
138
|
+
// Also get users assigned to any provider under this company
|
|
139
|
+
const { data: companyProviders, error: cpError } = await supabase
|
|
140
|
+
.from('providers')
|
|
141
|
+
.select('id')
|
|
142
|
+
.eq('company_id', companyId);
|
|
143
|
+
|
|
144
|
+
if (cpError) throw cpError;
|
|
145
|
+
|
|
146
|
+
const providerIds = companyProviders?.map(p => p.id) || [];
|
|
147
|
+
let providerUserIds: string[] = [];
|
|
148
|
+
|
|
149
|
+
if (providerIds.length > 0) {
|
|
150
|
+
const { data: providerUserAssignments, error: puError } = await supabase
|
|
151
|
+
.from('user_providers')
|
|
152
|
+
.select('user_id')
|
|
153
|
+
.in('provider_id', providerIds);
|
|
154
|
+
|
|
155
|
+
if (puError) throw puError;
|
|
156
|
+
providerUserIds = providerUserAssignments?.map(up => up.user_id) || [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
assignedUserIds = [...new Set([...companyUserIds, ...providerUserIds])];
|
|
160
|
+
|
|
161
|
+
if (assignedUserIds.length === 0) {
|
|
162
|
+
setUsers([]);
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Fetch profiles (scoped or all)
|
|
169
|
+
let profilesQuery = supabase.from('profiles').select('*');
|
|
170
|
+
if (assignedUserIds) {
|
|
171
|
+
profilesQuery = profilesQuery.in('id', assignedUserIds);
|
|
172
|
+
}
|
|
173
|
+
const { data: profiles, error: profilesError } = await profilesQuery;
|
|
174
|
+
|
|
175
|
+
if (profilesError) throw profilesError;
|
|
176
|
+
|
|
177
|
+
const userIds = profiles?.map(p => p.id) || [];
|
|
178
|
+
|
|
179
|
+
// Fetch user roles with scoping columns
|
|
180
|
+
let rolesQuery = supabase.from('user_roles').select('user_id, role, provider_id, company_id');
|
|
181
|
+
if (assignedUserIds) {
|
|
182
|
+
rolesQuery = rolesQuery.in('user_id', assignedUserIds);
|
|
183
|
+
}
|
|
184
|
+
const { data: userRoles, error: rolesError } = await rolesQuery;
|
|
185
|
+
if (rolesError) throw rolesError;
|
|
186
|
+
|
|
187
|
+
// Fetch user provider assignments
|
|
188
|
+
let providersQuery = supabase.from('user_providers').select('user_id, provider_id');
|
|
189
|
+
if (assignedUserIds) {
|
|
190
|
+
providersQuery = providersQuery.in('user_id', assignedUserIds);
|
|
191
|
+
}
|
|
192
|
+
const { data: userProviders, error: providersError } = await providersQuery;
|
|
193
|
+
if (providersError) throw providersError;
|
|
194
|
+
|
|
195
|
+
// Fetch all providers
|
|
196
|
+
const { data: providers, error: allProvidersError } = await supabase
|
|
197
|
+
.from('providers')
|
|
198
|
+
.select('id, name');
|
|
199
|
+
if (allProvidersError) throw allProvidersError;
|
|
200
|
+
|
|
201
|
+
// Fetch user company assignments
|
|
202
|
+
let companiesQuery = supabase.from('user_companies').select('user_id, company_id');
|
|
203
|
+
if (assignedUserIds) {
|
|
204
|
+
companiesQuery = companiesQuery.in('user_id', assignedUserIds);
|
|
205
|
+
}
|
|
206
|
+
const { data: userCompanies, error: companiesError } = await companiesQuery;
|
|
207
|
+
if (companiesError) throw companiesError;
|
|
208
|
+
|
|
209
|
+
// Fetch all companies
|
|
210
|
+
const { data: companies, error: allCompaniesError } = await supabase
|
|
211
|
+
.from('companies')
|
|
212
|
+
.select('id, name');
|
|
213
|
+
if (allCompaniesError) throw allCompaniesError;
|
|
214
|
+
|
|
215
|
+
// Combine profiles with roles, providers, and companies
|
|
216
|
+
let usersWithRoles = profiles?.map(profile => {
|
|
217
|
+
const allRoles = (userRoles as UserRole[] || [])
|
|
218
|
+
.filter(ur => ur.user_id === profile.id);
|
|
219
|
+
const roles = allRoles.map(ur => ur.role);
|
|
220
|
+
|
|
221
|
+
// Determine current level role based on context
|
|
222
|
+
let currentLevelRole: string | null = null;
|
|
223
|
+
let otherProviderRoleCount = 0;
|
|
224
|
+
|
|
225
|
+
if (providerId) {
|
|
226
|
+
// Provider level: show the role scoped to this provider
|
|
227
|
+
const providerRole = allRoles.find(r => r.provider_id === providerId);
|
|
228
|
+
currentLevelRole = providerRole?.role || null;
|
|
229
|
+
|
|
230
|
+
// If user has no provider-scoped role but is in user_providers, they have access but no explicit role
|
|
231
|
+
if (!currentLevelRole) {
|
|
232
|
+
// Check if they have a company role (company user viewing provider)
|
|
233
|
+
const companyRole = companyId ? allRoles.find(r => r.company_id === companyId && !r.provider_id) : null;
|
|
234
|
+
currentLevelRole = companyRole?.role || 'No Access';
|
|
235
|
+
}
|
|
236
|
+
} else if (companyId) {
|
|
237
|
+
// Company level: show the role scoped to this company
|
|
238
|
+
const companyRole = allRoles.find(r => r.company_id === companyId && !r.provider_id);
|
|
239
|
+
currentLevelRole = companyRole?.role || 'No Access';
|
|
240
|
+
|
|
241
|
+
// Count all provider-level roles
|
|
242
|
+
const providerRoles = allRoles.filter(r => r.provider_id !== null);
|
|
243
|
+
otherProviderRoleCount = providerRoles.length;
|
|
244
|
+
} else {
|
|
245
|
+
// Admin level: show super_admin/system_admin or "Provider User"
|
|
246
|
+
const hasAdminRole = allRoles.find(r => r.role === 'super_admin' || r.role === 'system_admin');
|
|
247
|
+
currentLevelRole = hasAdminRole?.role || 'provider_user';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const userProviderIds = userProviders
|
|
251
|
+
?.filter(up => up.user_id === profile.id)
|
|
252
|
+
.map(up => up.provider_id) || [];
|
|
253
|
+
const providerNames = providers
|
|
254
|
+
?.filter(p => userProviderIds.includes(p.id))
|
|
255
|
+
.map(p => p.name) || [];
|
|
256
|
+
|
|
257
|
+
const userCompanyIds = userCompanies
|
|
258
|
+
?.filter(uc => uc.user_id === profile.id)
|
|
259
|
+
.map(uc => uc.company_id) || [];
|
|
260
|
+
const companyNames = companies
|
|
261
|
+
?.filter(c => userCompanyIds.includes(c.id))
|
|
262
|
+
.map(c => c.name) || [];
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
id: profile.id,
|
|
266
|
+
full_name: profile.full_name,
|
|
267
|
+
email: profile.email,
|
|
268
|
+
status: profile.status,
|
|
269
|
+
created_at: profile.created_at,
|
|
270
|
+
roles: roles,
|
|
271
|
+
currentLevelRole,
|
|
272
|
+
otherProviderRoleCount,
|
|
273
|
+
avatar_url: profile.avatar_url,
|
|
274
|
+
last_seen_at: profile.last_seen_at,
|
|
275
|
+
user_number: profile.user_number,
|
|
276
|
+
providers: providerNames,
|
|
277
|
+
companies: companyNames
|
|
278
|
+
};
|
|
279
|
+
}) || [];
|
|
280
|
+
|
|
281
|
+
// Filter out admin/system roles if requested
|
|
282
|
+
if (filterAdminRoles) {
|
|
283
|
+
usersWithRoles = usersWithRoles.filter(user =>
|
|
284
|
+
!user.roles.some(role =>
|
|
285
|
+
role === 'super_admin' ||
|
|
286
|
+
role === 'system_admin' ||
|
|
287
|
+
role === 'customer'
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
setUsers(usersWithRoles);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Error fetching users:', error);
|
|
295
|
+
} finally {
|
|
296
|
+
setIsLoading(false);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const handleSort = (key: string) => {
|
|
301
|
+
if (sortKey === key) {
|
|
302
|
+
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
|
303
|
+
} else {
|
|
304
|
+
setSortKey(key);
|
|
305
|
+
setSortDirection('asc');
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const filteredUsers = users.filter(user => {
|
|
310
|
+
const searchLower = searchQuery.toLowerCase();
|
|
311
|
+
const userIdString = getShortUserId(user.user_number);
|
|
312
|
+
return (
|
|
313
|
+
user.full_name?.toLowerCase().includes(searchLower) ||
|
|
314
|
+
user.email?.toLowerCase().includes(searchLower) ||
|
|
315
|
+
userIdString.toLowerCase().includes(searchLower) ||
|
|
316
|
+
user.providers.some(provider => provider.toLowerCase().includes(searchLower)) ||
|
|
317
|
+
user.companies.some(company => company.toLowerCase().includes(searchLower))
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const sortedUsers = [...filteredUsers].sort((a, b) => {
|
|
322
|
+
let aValue: any;
|
|
323
|
+
let bValue: any;
|
|
324
|
+
|
|
325
|
+
switch (sortKey) {
|
|
326
|
+
case 'name':
|
|
327
|
+
aValue = a.full_name?.toLowerCase() || '';
|
|
328
|
+
bValue = b.full_name?.toLowerCase() || '';
|
|
329
|
+
break;
|
|
330
|
+
case 'created':
|
|
331
|
+
aValue = new Date(a.created_at).getTime();
|
|
332
|
+
bValue = new Date(b.created_at).getTime();
|
|
333
|
+
break;
|
|
334
|
+
case 'last_seen':
|
|
335
|
+
aValue = a.last_seen_at ? new Date(a.last_seen_at).getTime() : 0;
|
|
336
|
+
bValue = b.last_seen_at ? new Date(b.last_seen_at).getTime() : 0;
|
|
337
|
+
break;
|
|
338
|
+
default:
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1;
|
|
343
|
+
if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1;
|
|
344
|
+
return 0;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const searchConfig: SearchConfig = {
|
|
348
|
+
placeholder: 'Search by name, email, ID, company or provider',
|
|
349
|
+
value: searchQuery,
|
|
350
|
+
onChange: setSearchQuery,
|
|
351
|
+
label: 'Search Users'
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Auto-cleanup orphaned auth users on mount (one-time, admin only)
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
if (providerId) return; // Skip cleanup for provider-scoped views
|
|
357
|
+
const cleanupOrphanedUsers = async () => {
|
|
358
|
+
try {
|
|
359
|
+
const { data: { session } } = await supabase.auth.getSession();
|
|
360
|
+
if (!session) return; // Skip if not authenticated
|
|
361
|
+
await supabase.functions.invoke('cleanup-orphaned-users', { body: {} });
|
|
362
|
+
console.log('Orphaned users cleanup completed');
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Cleanup error:', error);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
cleanupOrphanedUsers();
|
|
368
|
+
}, [providerId]);
|
|
369
|
+
|
|
370
|
+
const defaultActions: HeaderAction[] = [
|
|
371
|
+
{
|
|
372
|
+
label: 'Add New User',
|
|
373
|
+
mobileLabel: 'Add',
|
|
374
|
+
variant: 'ghost',
|
|
375
|
+
withIcon: true,
|
|
376
|
+
icon: <IconAddUser className="fill-current" />,
|
|
377
|
+
onClick: () => navigate(`${navigatePrefix}/users/add`)
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
label: 'Import Users',
|
|
381
|
+
mobileLabel: 'Import',
|
|
382
|
+
icon: <img src={IconImportUser} alt="" className="w-5 h-5" />,
|
|
383
|
+
onClick: () => navigate(`${navigatePrefix}/users/import`),
|
|
384
|
+
variant: 'outline'
|
|
385
|
+
}
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
const actions = customHeaderActions || defaultActions;
|
|
389
|
+
|
|
390
|
+
const getInitials = (name: string | null) => {
|
|
391
|
+
if (!name) return 'U';
|
|
392
|
+
const parts = name.trim().split(' ');
|
|
393
|
+
if (parts.length === 1) return parts[0][0].toUpperCase();
|
|
394
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const getUserIcon = (user: User) => {
|
|
398
|
+
if (user.avatar_url) {
|
|
399
|
+
return (
|
|
400
|
+
<div className="flex w-10 h-10 justify-center items-center rounded-full overflow-hidden flex-shrink-0">
|
|
401
|
+
<img src={user.avatar_url} alt={user.full_name || 'User'} className="w-full h-full object-cover" />
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const initials = getInitials(user.full_name);
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div className="w-10 h-10 relative flex items-center justify-center flex-shrink-0 overflow-hidden rounded-full">
|
|
410
|
+
<svg width="40" height="40" viewBox="0 0 32 32" fill="none" className="absolute">
|
|
411
|
+
<circle cx="16" cy="16" r="15" className="fill-fill-secondary stroke-fill-secondary" fillOpacity="0.2" strokeWidth="2" />
|
|
412
|
+
</svg>
|
|
413
|
+
<div className="text-xs font-extrabold leading-3 text-label-secondary relative">
|
|
414
|
+
{initials}
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const formatDate = (dateString: string) => {
|
|
421
|
+
const date = new Date(dateString);
|
|
422
|
+
return date.toLocaleDateString('en-GB', {
|
|
423
|
+
day: '2-digit',
|
|
424
|
+
month: '2-digit',
|
|
425
|
+
year: 'numeric'
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const formatRole = (user: User) => {
|
|
430
|
+
if (!user.currentLevelRole || user.currentLevelRole === 'No Access') return 'No Access';
|
|
431
|
+
if (user.currentLevelRole === 'provider_user') return 'Provider User';
|
|
432
|
+
return user.currentLevelRole.replace(/_/g, ' ');
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const getCompanyProvider = (roles: string[], providers: string[], companies: string[]) => {
|
|
436
|
+
// Check if user has System Admin or Super Admin role
|
|
437
|
+
const hasAdminRole = roles.some(role =>
|
|
438
|
+
role === 'system_admin' || role === 'super_admin'
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
if (hasAdminRole) {
|
|
442
|
+
return 'Booked it';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Combine companies and providers
|
|
446
|
+
const allAssignments = [...companies, ...providers];
|
|
447
|
+
|
|
448
|
+
// Handle multiple assignments
|
|
449
|
+
if (allAssignments.length === 0) return 'Unassigned';
|
|
450
|
+
if (allAssignments.length === 1) return allAssignments[0];
|
|
451
|
+
return `${allAssignments[0]} +${allAssignments.length - 1}`;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const formatLastSeen = (lastSeenAt: string | null, status: string | null) => {
|
|
455
|
+
// If user is active and last seen is within 5 minutes, show "Active Now"
|
|
456
|
+
if (status === 'active' && lastSeenAt) {
|
|
457
|
+
const lastSeenDate = new Date(lastSeenAt);
|
|
458
|
+
const now = new Date();
|
|
459
|
+
const diffMinutes = (now.getTime() - lastSeenDate.getTime()) / (1000 * 60);
|
|
460
|
+
if (diffMinutes < 5) {
|
|
461
|
+
return 'Active Now';
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!lastSeenAt) return 'N/A';
|
|
466
|
+
try {
|
|
467
|
+
// Remove "about" from the output
|
|
468
|
+
return formatDistanceToNow(new Date(lastSeenAt), { addSuffix: true }).replace('about ', '');
|
|
469
|
+
} catch {
|
|
470
|
+
return 'N/A';
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const handleDeleteClick = (user: User) => {
|
|
475
|
+
setSelectedUser(user);
|
|
476
|
+
setShowDeleteDialog(true);
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const handleDeleteConfirm = async () => {
|
|
480
|
+
if (!selectedUser) return;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
// Call edge function to properly delete user from auth.users
|
|
484
|
+
const { data, error } = await supabase.functions.invoke('delete-user', {
|
|
485
|
+
body: { userId: selectedUser.id }
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (error) throw error;
|
|
489
|
+
if (data?.error) throw new Error(data.error);
|
|
490
|
+
|
|
491
|
+
showSuccess(`User ${selectedUser.full_name || selectedUser.email} deleted successfully`);
|
|
492
|
+
fetchUsers(); // Refresh the list
|
|
493
|
+
} catch (error) {
|
|
494
|
+
console.error('Error deleting user:', error);
|
|
495
|
+
showError('Failed to delete user', error instanceof Error ? error.message : 'Please try again.');
|
|
496
|
+
} finally {
|
|
497
|
+
setShowDeleteDialog(false);
|
|
498
|
+
setSelectedUser(null);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const handleSuspendToggle = async (user: User) => {
|
|
503
|
+
const newStatus = user.status === 'active' ? 'suspended' : 'active';
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const { error } = await supabase
|
|
507
|
+
.from('profiles')
|
|
508
|
+
.update({ status: newStatus })
|
|
509
|
+
.eq('id', user.id);
|
|
510
|
+
|
|
511
|
+
if (error) throw error;
|
|
512
|
+
|
|
513
|
+
fetchUsers(); // Refresh the list
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error('Error updating user status:', error);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
const handleResendInvitation = async (user: User) => {
|
|
521
|
+
try {
|
|
522
|
+
const { data, error } = await supabase.functions.invoke('resend-invitation', {
|
|
523
|
+
body: {
|
|
524
|
+
userId: user.id
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (error) {
|
|
529
|
+
showError(`Failed to resend invitation to ${user.email}`, 'Please try again or contact support if the issue persists.');
|
|
530
|
+
console.error('Error resending invitation:', error);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
showSuccess(`Invitation email sent to ${user.email}`, 'The user will receive an email with instructions to set up their account.');
|
|
535
|
+
fetchUsers(); // Refresh the table
|
|
536
|
+
} catch (error) {
|
|
537
|
+
showError('Unable to send invitation email', 'An unexpected error occurred. Please try again.');
|
|
538
|
+
console.error('Error resending invitation:', error);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const handleForcePasswordReset = async (user: User) => {
|
|
543
|
+
try {
|
|
544
|
+
const { data, error } = await supabase.functions.invoke('force-password-reset', {
|
|
545
|
+
body: {
|
|
546
|
+
userId: user.id
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (error) {
|
|
551
|
+
showError('Failed to force password reset', 'Please try again or contact support if the issue persists.');
|
|
552
|
+
console.error('Error forcing password reset:', error);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
showSuccess(`Password reset required for ${user.full_name || user.email}`, 'The user will be required to reset their password on next login.');
|
|
557
|
+
fetchUsers();
|
|
558
|
+
} catch (error) {
|
|
559
|
+
showError('Unable to force password reset', 'An unexpected error occurred. Please try again.');
|
|
560
|
+
console.error('Error forcing password reset:', error);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const handleResetToSurnamePassword = async (user: User) => {
|
|
565
|
+
try {
|
|
566
|
+
const { data, error } = await supabase.functions.invoke('reset-to-surname-password', {
|
|
567
|
+
body: {
|
|
568
|
+
userId: user.id
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (error) {
|
|
573
|
+
showError('Failed to reset password', 'Please try again or contact support if the issue persists.');
|
|
574
|
+
console.error('Error resetting password:', error);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
showSuccess(`Password reset to surname for ${user.full_name || user.email}`, `New password: ${data.password}`);
|
|
579
|
+
fetchUsers();
|
|
580
|
+
} catch (error) {
|
|
581
|
+
showError('Unable to reset password', 'An unexpected error occurred. Please try again.');
|
|
582
|
+
console.error('Error resetting password:', error);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Bulk action handlers
|
|
587
|
+
const handleClearSelection = () => {
|
|
588
|
+
setSelectedUsers([]);
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const handleBulkResendInvite = async () => {
|
|
592
|
+
const selectedUserData = users.filter(u =>
|
|
593
|
+
selectedUsers.includes(u.id) && u.status === 'invited'
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
if (selectedUserData.length === 0) {
|
|
597
|
+
setNotification({
|
|
598
|
+
type: 'error',
|
|
599
|
+
title: 'No Invited Users Selected',
|
|
600
|
+
message: 'Please select users with "invited" status to resend invitations.'
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let successCount = 0;
|
|
606
|
+
let failCount = 0;
|
|
607
|
+
|
|
608
|
+
for (const user of selectedUserData) {
|
|
609
|
+
try {
|
|
610
|
+
const { error } = await supabase.functions.invoke('resend-invitation', {
|
|
611
|
+
body: { userId: user.id }
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
if (error) {
|
|
615
|
+
failCount++;
|
|
616
|
+
} else {
|
|
617
|
+
successCount++;
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
failCount++;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (successCount > 0) {
|
|
625
|
+
setNotification({
|
|
626
|
+
type: 'success',
|
|
627
|
+
title: 'Invitations Sent',
|
|
628
|
+
message: `Successfully resent ${successCount} invitation${successCount > 1 ? 's' : ''}${failCount > 0 ? `. ${failCount} failed.` : '.'}`
|
|
629
|
+
});
|
|
630
|
+
} else {
|
|
631
|
+
setNotification({
|
|
632
|
+
type: 'error',
|
|
633
|
+
title: 'Failed to Resend Invitations',
|
|
634
|
+
message: 'All invitation attempts failed. Please try again.'
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
setSelectedUsers([]);
|
|
639
|
+
fetchUsers();
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const handleBulkSuspend = async () => {
|
|
643
|
+
// Filter out current user to prevent self-suspension
|
|
644
|
+
const selectedUserData = users.filter(u =>
|
|
645
|
+
selectedUsers.includes(u.id) && u.status === 'active' && u.id !== currentUser?.id
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
if (selectedUserData.length === 0) {
|
|
649
|
+
setNotification({
|
|
650
|
+
type: 'error',
|
|
651
|
+
title: 'No Active Users Selected',
|
|
652
|
+
message: selectedUsers.includes(currentUser?.id || '')
|
|
653
|
+
? 'You cannot suspend your own account. Please select other users with "active" status to suspend.'
|
|
654
|
+
: 'Please select users with "active" status to suspend.'
|
|
655
|
+
});
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
const { error } = await supabase
|
|
661
|
+
.from('profiles')
|
|
662
|
+
.update({ status: 'suspended' })
|
|
663
|
+
.in('id', selectedUserData.map(u => u.id));
|
|
664
|
+
|
|
665
|
+
if (error) throw error;
|
|
666
|
+
|
|
667
|
+
setNotification({
|
|
668
|
+
type: 'success',
|
|
669
|
+
title: 'Users Suspended',
|
|
670
|
+
message: `Successfully suspended ${selectedUserData.length} user${selectedUserData.length > 1 ? 's' : ''}.`
|
|
671
|
+
});
|
|
672
|
+
setSelectedUsers([]);
|
|
673
|
+
fetchUsers();
|
|
674
|
+
} catch (error) {
|
|
675
|
+
console.error('Error suspending users:', error);
|
|
676
|
+
setNotification({
|
|
677
|
+
type: 'error',
|
|
678
|
+
title: 'Failed to Suspend Users',
|
|
679
|
+
message: 'An error occurred. Please try again.'
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const handleBulkPasswordReset = async () => {
|
|
685
|
+
if (selectedUsers.length === 0) return;
|
|
686
|
+
|
|
687
|
+
let successCount = 0;
|
|
688
|
+
let failCount = 0;
|
|
689
|
+
|
|
690
|
+
for (const userId of selectedUsers) {
|
|
691
|
+
try {
|
|
692
|
+
const { error } = await supabase.functions.invoke('force-password-reset', {
|
|
693
|
+
body: { userId }
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
if (error) {
|
|
697
|
+
failCount++;
|
|
698
|
+
} else {
|
|
699
|
+
successCount++;
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
failCount++;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (successCount > 0) {
|
|
707
|
+
setNotification({
|
|
708
|
+
type: 'success',
|
|
709
|
+
title: 'Password Resets Triggered',
|
|
710
|
+
message: `Successfully triggered password reset for ${successCount} user${successCount > 1 ? 's' : ''}${failCount > 0 ? `. ${failCount} failed.` : '.'}`
|
|
711
|
+
});
|
|
712
|
+
} else {
|
|
713
|
+
setNotification({
|
|
714
|
+
type: 'error',
|
|
715
|
+
title: 'Failed to Trigger Password Resets',
|
|
716
|
+
message: 'All attempts failed. Please try again.'
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
setSelectedUsers([]);
|
|
721
|
+
fetchUsers();
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const handleBulkDelete = () => {
|
|
725
|
+
// Filter out current user to prevent self-deletion
|
|
726
|
+
const filteredUserIds = selectedUsers.filter(id => id !== currentUser?.id);
|
|
727
|
+
|
|
728
|
+
if (filteredUserIds.length === 0) {
|
|
729
|
+
setNotification({
|
|
730
|
+
type: 'error',
|
|
731
|
+
title: 'Cannot Delete Your Own Account',
|
|
732
|
+
message: 'You cannot delete your own account through bulk actions.'
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Navigate to bulk delete page with selected user IDs
|
|
738
|
+
navigate(`${navigatePrefix}/users/bulk-delete`, {
|
|
739
|
+
state: { userIds: filteredUserIds }
|
|
740
|
+
});
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const columns: ColumnConfig<User>[] = [
|
|
744
|
+
{
|
|
745
|
+
label: 'User',
|
|
746
|
+
sortKey: 'name',
|
|
747
|
+
sortable: true,
|
|
748
|
+
render: (user) => (
|
|
749
|
+
<div className="flex items-center gap-3 flex-1">
|
|
750
|
+
{getUserIcon(user)}
|
|
751
|
+
<div className="flex flex-col justify-center items-start gap-0.5 flex-1 min-w-0 relative">
|
|
752
|
+
<h3 className="text-label-primary text-sm font-semibold leading-4 truncate w-full">
|
|
753
|
+
{user.full_name || 'Unnamed User'}
|
|
754
|
+
</h3>
|
|
755
|
+
<div className="relative w-full h-4">
|
|
756
|
+
<p className="text-label-secondary text-xs font-normal leading-4 truncate w-full absolute inset-0 group-hover:opacity-0 transition-opacity">
|
|
757
|
+
{user.email || 'No email'}
|
|
758
|
+
</p>
|
|
759
|
+
<p className="text-label-secondary text-xs font-normal leading-4 truncate w-full absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
760
|
+
ID: {getShortUserId(user.user_number)}
|
|
761
|
+
</p>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
)
|
|
766
|
+
},
|
|
767
|
+
...(showProviderColumn ? [{
|
|
768
|
+
label: 'Role',
|
|
769
|
+
render: (user: User) => (
|
|
770
|
+
<div className="flex flex-col justify-center items-start gap-0.5">
|
|
771
|
+
<span className="text-label-primary text-sm font-normal capitalize">
|
|
772
|
+
{formatRole(user)}
|
|
773
|
+
</span>
|
|
774
|
+
{user.otherProviderRoleCount > 0 && (
|
|
775
|
+
<span className="text-label-secondary text-xs font-normal">
|
|
776
|
+
{user.otherProviderRoleCount} provider role{user.otherProviderRoleCount > 1 ? 's' : ''}
|
|
777
|
+
</span>
|
|
778
|
+
)}
|
|
779
|
+
</div>
|
|
780
|
+
),
|
|
781
|
+
width: 'w-[180px]'
|
|
782
|
+
}] : [{
|
|
783
|
+
label: 'Role',
|
|
784
|
+
render: (user: User) => (
|
|
785
|
+
<div className="flex flex-col justify-center items-start gap-0.5">
|
|
786
|
+
<span className="text-label-primary text-sm font-normal capitalize">
|
|
787
|
+
{formatRole(user)}
|
|
788
|
+
</span>
|
|
789
|
+
{user.otherProviderRoleCount > 0 && (
|
|
790
|
+
<span className="text-label-secondary text-xs font-normal">
|
|
791
|
+
{user.otherProviderRoleCount} provider role{user.otherProviderRoleCount > 1 ? 's' : ''}
|
|
792
|
+
</span>
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
),
|
|
796
|
+
width: 'w-[180px]'
|
|
797
|
+
}]),
|
|
798
|
+
{
|
|
799
|
+
label: 'Status',
|
|
800
|
+
render: (user) => (
|
|
801
|
+
<StatusBadge status={(user.status as 'active' | 'suspended' | 'invited' | 'disabled') || 'invited'} />
|
|
802
|
+
),
|
|
803
|
+
width: 'w-[120px]'
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
label: 'Last Seen',
|
|
807
|
+
sortKey: 'last_seen',
|
|
808
|
+
sortable: true,
|
|
809
|
+
render: (user) => (
|
|
810
|
+
<span className="text-text-secondary">
|
|
811
|
+
{formatLastSeen(user.last_seen_at, user.status)}
|
|
812
|
+
</span>
|
|
813
|
+
),
|
|
814
|
+
width: 'w-[140px]'
|
|
815
|
+
}
|
|
816
|
+
];
|
|
817
|
+
|
|
818
|
+
const rowActions = (user: User): RowAction<User>[] => {
|
|
819
|
+
const isCurrentUser = currentUser?.id === user.id;
|
|
820
|
+
|
|
821
|
+
const actions: RowAction<User>[] = [
|
|
822
|
+
{
|
|
823
|
+
label: 'View User',
|
|
824
|
+
onClick: (user) => navigate(`${navigatePrefix}/users/${user.id}`),
|
|
825
|
+
variant: 'default'
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
label: 'Edit User',
|
|
829
|
+
onClick: (user) => navigate(`${navigatePrefix}/users/${user.id}/edit`),
|
|
830
|
+
variant: 'default'
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
label: 'Configure Permissions',
|
|
834
|
+
onClick: (user) => navigate(`${navigatePrefix}/users/${user.id}/permissions`),
|
|
835
|
+
variant: 'default'
|
|
836
|
+
}
|
|
837
|
+
];
|
|
838
|
+
|
|
839
|
+
// Don't allow users to suspend or delete their own account
|
|
840
|
+
if (!isCurrentUser) {
|
|
841
|
+
// Add Resend Invitation action for invited users with proper permission check
|
|
842
|
+
if (user.status === 'invited' && hasPermission('user.resend_invitation')) {
|
|
843
|
+
actions.push({
|
|
844
|
+
label: 'Resend User Invitation',
|
|
845
|
+
onClick: handleResendInvitation,
|
|
846
|
+
variant: 'default'
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Add Force Password Reset action for active users
|
|
851
|
+
if (user.status === 'active') {
|
|
852
|
+
actions.push({
|
|
853
|
+
label: 'Trigger Password Reset',
|
|
854
|
+
onClick: handleForcePasswordReset,
|
|
855
|
+
variant: 'default'
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Add Reset to Surname Password action
|
|
860
|
+
actions.push({
|
|
861
|
+
label: 'Reset to Surname Password',
|
|
862
|
+
onClick: handleResetToSurnamePassword,
|
|
863
|
+
variant: 'default'
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Add Suspend/Activate action only for active or suspended users (not invited)
|
|
867
|
+
if (user.status === 'active' || user.status === 'suspended') {
|
|
868
|
+
actions.push({
|
|
869
|
+
label: user.status === 'active' ? 'Suspend User' : 'Activate User',
|
|
870
|
+
onClick: handleSuspendToggle,
|
|
871
|
+
variant: 'default'
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return actions;
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
if (isLoading) {
|
|
880
|
+
return <LoadingScreen message="Loading users..." />;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Get bulk action configuration based on selection
|
|
884
|
+
const getBulkActions = () => {
|
|
885
|
+
const selectedUserData = users.filter(u => selectedUsers.includes(u.id));
|
|
886
|
+
const hasInvited = selectedUserData.some(u => u.status === 'invited');
|
|
887
|
+
const hasActive = selectedUserData.some(u => u.status === 'active');
|
|
888
|
+
const hasSuspended = selectedUserData.some(u => u.status === 'suspended');
|
|
889
|
+
|
|
890
|
+
const actions = [];
|
|
891
|
+
|
|
892
|
+
if (hasInvited && hasPermission('user.resend_invitation')) {
|
|
893
|
+
actions.push({
|
|
894
|
+
label: 'Resend Invite',
|
|
895
|
+
onClick: handleBulkResendInvite,
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (hasActive) {
|
|
900
|
+
actions.push({
|
|
901
|
+
label: 'Suspend',
|
|
902
|
+
onClick: handleBulkSuspend,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (hasSuspended) {
|
|
907
|
+
actions.push({
|
|
908
|
+
label: 'Activate',
|
|
909
|
+
onClick: async () => {
|
|
910
|
+
// Filter out current user to prevent self-activation (shouldn't happen but for safety)
|
|
911
|
+
const suspendedUsers = selectedUserData.filter(u => u.status === 'suspended' && u.id !== currentUser?.id);
|
|
912
|
+
|
|
913
|
+
if (suspendedUsers.length === 0) {
|
|
914
|
+
setNotification({
|
|
915
|
+
type: 'error',
|
|
916
|
+
title: 'No Suspended Users Selected',
|
|
917
|
+
message: 'Please select users with "suspended" status to activate.'
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
const { error } = await supabase
|
|
924
|
+
.from('profiles')
|
|
925
|
+
.update({ status: 'active' })
|
|
926
|
+
.in('id', suspendedUsers.map(u => u.id));
|
|
927
|
+
|
|
928
|
+
if (error) throw error;
|
|
929
|
+
|
|
930
|
+
setNotification({
|
|
931
|
+
type: 'success',
|
|
932
|
+
title: 'Users Activated',
|
|
933
|
+
message: `Successfully activated ${suspendedUsers.length} user${suspendedUsers.length > 1 ? 's' : ''}.`
|
|
934
|
+
});
|
|
935
|
+
setSelectedUsers([]);
|
|
936
|
+
fetchUsers();
|
|
937
|
+
} catch (error) {
|
|
938
|
+
console.error('Error activating users:', error);
|
|
939
|
+
setNotification({
|
|
940
|
+
type: 'error',
|
|
941
|
+
title: 'Failed to Activate Users',
|
|
942
|
+
message: 'An error occurred. Please try again.'
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
actions.push({
|
|
950
|
+
label: 'Trigger Password Reset',
|
|
951
|
+
onClick: handleBulkPasswordReset,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
actions.push({
|
|
955
|
+
label: 'Delete',
|
|
956
|
+
icon: 'delete' as const,
|
|
957
|
+
onClick: handleBulkDelete,
|
|
958
|
+
variant: 'destructive' as const,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
return actions;
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
return (
|
|
965
|
+
<>
|
|
966
|
+
{notification && (
|
|
967
|
+
<div className="w-full mb-4">
|
|
968
|
+
<NotificationBanner
|
|
969
|
+
notification={notification}
|
|
970
|
+
onClose={() => setNotification(null)}
|
|
971
|
+
showNotificationsLink={false}
|
|
972
|
+
/>
|
|
973
|
+
</div>
|
|
974
|
+
)}
|
|
975
|
+
|
|
976
|
+
<div className="flex flex-col gap-6 w-full">
|
|
977
|
+
<TableControls
|
|
978
|
+
searchConfig={searchConfig}
|
|
979
|
+
actions={actions}
|
|
980
|
+
/>
|
|
981
|
+
{showBulkActions && selectedUsers.length > 0 && (
|
|
982
|
+
<BulkActionsBar
|
|
983
|
+
selectedCount={selectedUsers.length}
|
|
984
|
+
onClearSelection={handleClearSelection}
|
|
985
|
+
actions={getBulkActions()}
|
|
986
|
+
/>
|
|
987
|
+
)}
|
|
988
|
+
<DataTable
|
|
989
|
+
data={sortedUsers}
|
|
990
|
+
columns={columns}
|
|
991
|
+
keyExtractor={(user) => user.id}
|
|
992
|
+
rowActions={rowActions}
|
|
993
|
+
onRowClick={(user) => navigate(`${navigatePrefix}/users/${user.id}`)}
|
|
994
|
+
selectable={showBulkActions}
|
|
995
|
+
selectedItems={selectedUsers}
|
|
996
|
+
onSelectionChange={setSelectedUsers}
|
|
997
|
+
sortKey={sortKey}
|
|
998
|
+
sortDirection={sortDirection}
|
|
999
|
+
onSort={handleSort}
|
|
1000
|
+
pagination={{
|
|
1001
|
+
entityName: 'users'
|
|
1002
|
+
}}
|
|
1003
|
+
emptyState={
|
|
1004
|
+
<div className="text-center py-12">
|
|
1005
|
+
<p className="text-label-secondary">No users found</p>
|
|
1006
|
+
</div>
|
|
1007
|
+
}
|
|
1008
|
+
/>
|
|
1009
|
+
</div>
|
|
1010
|
+
|
|
1011
|
+
{/* Delete Confirmation Dialog */}
|
|
1012
|
+
<DeleteConfirmationDialog
|
|
1013
|
+
open={showDeleteDialog}
|
|
1014
|
+
onOpenChange={setShowDeleteDialog}
|
|
1015
|
+
onConfirm={handleDeleteConfirm}
|
|
1016
|
+
title={`Delete ${selectedUser?.full_name || 'User'}?`}
|
|
1017
|
+
itemName={selectedUser?.full_name || 'this user'}
|
|
1018
|
+
/>
|
|
1019
|
+
</>
|
|
1020
|
+
);
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
export default UsersTable;
|