Files
smoothschedule/frontend/src/layouts/SettingsLayout.tsx

210 lines
7.7 KiB
TypeScript

/**
* Settings Layout
*
* Provides a sidebar navigation for settings pages with grouped sections.
* Used as a wrapper for all /settings/* routes.
*/
import React from 'react';
import { Outlet, Link, useLocation, useNavigate, useOutletContext } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
ArrowLeft,
Building2,
Palette,
Layers,
Globe,
Key,
Lock,
Mail,
Phone,
CreditCard,
AlertTriangle,
Calendar,
} from 'lucide-react';
import {
SettingsSidebarSection,
SettingsSidebarItem,
} from '../components/navigation/SidebarComponents';
import UnfinishedBadge from '../components/ui/UnfinishedBadge';
import { Business, User, PlanPermissions } from '../types';
import { usePlanFeatures, FeatureKey } from '../hooks/usePlanFeatures';
interface ParentContext {
user: User;
business: Business;
updateBusiness: (updates: Partial<Business>) => void;
}
// Map settings pages to their required plan features
const SETTINGS_PAGE_FEATURES: Record<string, FeatureKey> = {
'/settings/branding': 'white_label',
'/settings/custom-domains': 'custom_domain',
'/settings/api': 'api_access',
'/settings/authentication': 'custom_oauth',
'/settings/sms-calling': 'sms_reminders',
};
const SettingsLayout: React.FC = () => {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const { canUse } = usePlanFeatures();
// Get context from parent route (BusinessLayout)
const parentContext = useOutletContext<ParentContext>();
// Check if a feature is locked (returns true if locked)
const isLocked = (feature: FeatureKey | undefined): boolean => {
if (!feature) return false;
return !canUse(feature);
};
// Get the current page's feature requirement (if any)
const currentPageFeature = SETTINGS_PAGE_FEATURES[location.pathname];
const currentPageLocked = isLocked(currentPageFeature);
return (
<div className="flex h-full bg-gray-50 dark:bg-gray-900">
{/* Settings Sidebar */}
<aside className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col shrink-0">
{/* Back Button */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => navigate('/')}
className="flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
>
<ArrowLeft size={16} />
<span>{t('settings.backToApp', 'Back to App')}</span>
</button>
</div>
{/* Settings Title */}
<div className="px-4 py-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{t('settings.title', 'Settings')}
</h2>
</div>
{/* Navigation */}
<nav className="flex-1 px-2 pb-4 space-y-3 overflow-y-auto">
{/* Business Section */}
<SettingsSidebarSection title={t('settings.sections.business', 'Business')}>
<SettingsSidebarItem
to="/settings/general"
icon={Building2}
label={t('settings.general.title', 'General')}
description={t('settings.general.description', 'Name, timezone, contact')}
/>
<SettingsSidebarItem
to="/settings/resource-types"
icon={Layers}
label={t('settings.resourceTypes.title', 'Resource Types')}
description={t('settings.resourceTypes.description', 'Staff, rooms, equipment')}
badgeElement={<UnfinishedBadge />}
/>
<SettingsSidebarItem
to="/settings/booking"
icon={Calendar}
label={t('settings.booking.title', 'Booking')}
description={t('settings.booking.description', 'Booking URL, redirects')}
/>
</SettingsSidebarSection>
{/* Branding Section */}
<SettingsSidebarSection title={t('settings.sections.branding', 'Branding')}>
<SettingsSidebarItem
to="/settings/branding"
icon={Palette}
label={t('settings.appearance.title', 'Appearance')}
description={t('settings.appearance.description', 'Logo, colors, theme')}
locked={isLocked('white_label')}
/>
<SettingsSidebarItem
to="/settings/email-templates"
icon={Mail}
label={t('settings.emailTemplates.title', 'Email Templates')}
description={t('settings.emailTemplates.description', 'Customize email designs')}
/>
<SettingsSidebarItem
to="/settings/custom-domains"
icon={Globe}
label={t('settings.customDomains.title', 'Custom Domains')}
description={t('settings.customDomains.description', 'Use your own domain')}
locked={isLocked('custom_domain')}
/>
</SettingsSidebarSection>
{/* Integrations Section */}
<SettingsSidebarSection title={t('settings.sections.integrations', 'Integrations')}>
<SettingsSidebarItem
to="/settings/api"
icon={Key}
label={t('settings.api.title', 'API & Webhooks')}
description={t('settings.api.description', 'API tokens, webhooks')}
locked={isLocked('api_access')}
/>
</SettingsSidebarSection>
{/* Access Section */}
<SettingsSidebarSection title={t('settings.sections.access', 'Access')}>
<SettingsSidebarItem
to="/settings/authentication"
icon={Lock}
label={t('settings.authentication.title', 'Authentication')}
description={t('settings.authentication.description', 'OAuth, social login')}
locked={isLocked('custom_oauth')}
/>
</SettingsSidebarSection>
{/* Communication Section */}
<SettingsSidebarSection title={t('settings.sections.communication', 'Communication')}>
<SettingsSidebarItem
to="/settings/email"
icon={Mail}
label={t('settings.email.title', 'Email Setup')}
description={t('settings.email.description', 'Email addresses for tickets')}
/>
<SettingsSidebarItem
to="/settings/sms-calling"
icon={Phone}
label={t('settings.smsCalling.title', 'SMS & Calling')}
description={t('settings.smsCalling.description', 'Credits, phone numbers')}
locked={isLocked('sms_reminders')}
/>
</SettingsSidebarSection>
{/* Billing Section */}
<SettingsSidebarSection title={t('settings.sections.billing', 'Billing')}>
<SettingsSidebarItem
to="/settings/billing"
icon={CreditCard}
label={t('settings.billing.title', 'Plan & Billing')}
description={t('settings.billing.description', 'Subscription, invoices')}
/>
<SettingsSidebarItem
to="/settings/quota"
icon={AlertTriangle}
label={t('settings.quota.title', 'Quota Management')}
description={t('settings.quota.description', 'Usage limits, archiving')}
/>
</SettingsSidebarSection>
</nav>
</aside>
{/* Content Area */}
<main className="flex-1 overflow-y-auto">
<div className="max-w-4xl mx-auto p-8">
<Outlet context={{
...parentContext,
isFeatureLocked: currentPageLocked,
lockedFeature: currentPageFeature,
}} />
</div>
</main>
</div>
);
};
export default SettingsLayout;