feat(contracts): Add contracts permission to subscription tiers
- Add contracts_enabled field to SubscriptionPlan model
- Add contracts toggle to plan create/edit modal in platform settings
- Hide contracts menu item for tenants without contracts permission
- Protect /contracts routes with canUse('contracts') check
- Add HasContractsPermission to contracts API ViewSets
- Add contracts to PlanPermissions interface and feature definitions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useCurrentUser, useMasquerade, useLogout } from './hooks/useAuth';
|
||||
import { useCurrentBusiness } from './hooks/useBusiness';
|
||||
import { useUpdateBusiness } from './hooks/useBusiness';
|
||||
import { usePlanFeatures } from './hooks/usePlanFeatures';
|
||||
import { setCookie } from './utils/cookies';
|
||||
|
||||
// Import Login Page
|
||||
@@ -192,6 +193,7 @@ const AppContent: React.FC = () => {
|
||||
const updateBusinessMutation = useUpdateBusiness();
|
||||
const masqueradeMutation = useMasquerade();
|
||||
const logoutMutation = useLogout();
|
||||
const { canUse } = usePlanFeatures();
|
||||
|
||||
// Apply dark mode class and persist to localStorage
|
||||
React.useEffect(() => {
|
||||
@@ -810,7 +812,7 @@ const AppContent: React.FC = () => {
|
||||
<Route
|
||||
path="/contracts"
|
||||
element={
|
||||
hasAccess(['owner', 'manager']) ? (
|
||||
hasAccess(['owner', 'manager']) && canUse('contracts') ? (
|
||||
<Contracts />
|
||||
) : (
|
||||
<Navigate to="/" />
|
||||
@@ -820,7 +822,7 @@ const AppContent: React.FC = () => {
|
||||
<Route
|
||||
path="/contracts/templates"
|
||||
element={
|
||||
hasAccess(['owner', 'manager']) ? (
|
||||
hasAccess(['owner', 'manager']) && canUse('contracts') ? (
|
||||
<ContractTemplates />
|
||||
) : (
|
||||
<Navigate to="/" />
|
||||
|
||||
@@ -162,12 +162,14 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
|
||||
label={t('nav.staff')}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
<SidebarItem
|
||||
to="/contracts"
|
||||
icon={FileSignature}
|
||||
label={t('nav.contracts', 'Contracts')}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
{canUse('contracts') && (
|
||||
<SidebarItem
|
||||
to="/contracts"
|
||||
icon={FileSignature}
|
||||
label={t('nav.contracts', 'Contracts')}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
)}
|
||||
<SidebarItem
|
||||
to="/time-blocks"
|
||||
icon={CalendarOff}
|
||||
|
||||
@@ -60,12 +60,14 @@ export const useCurrentBusiness = () => {
|
||||
white_label: false,
|
||||
custom_oauth: false,
|
||||
plugins: false,
|
||||
tasks: false,
|
||||
export_data: false,
|
||||
video_conferencing: false,
|
||||
two_factor_auth: false,
|
||||
masked_calling: false,
|
||||
pos_system: false,
|
||||
mobile_app: false,
|
||||
contracts: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -91,6 +91,7 @@ export const FEATURE_NAMES: Record<FeatureKey, string> = {
|
||||
masked_calling: 'Masked Calling',
|
||||
pos_system: 'POS System',
|
||||
mobile_app: 'Mobile App',
|
||||
contracts: 'Contracts',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,4 +112,5 @@ export const FEATURE_DESCRIPTIONS: Record<FeatureKey, string> = {
|
||||
masked_calling: 'Use masked phone numbers to protect privacy',
|
||||
pos_system: 'Process in-person payments with Point of Sale',
|
||||
mobile_app: 'Access SmoothSchedule on mobile devices',
|
||||
contracts: 'Create and manage contracts with customers',
|
||||
};
|
||||
|
||||
@@ -51,6 +51,8 @@ export interface SubscriptionPlan {
|
||||
masked_calling_price_per_minute_cents: number;
|
||||
proxy_number_enabled: boolean;
|
||||
proxy_number_monthly_fee_cents: number;
|
||||
// Contracts feature
|
||||
contracts_enabled: boolean;
|
||||
// Default credit settings
|
||||
default_auto_reload_enabled: boolean;
|
||||
default_auto_reload_threshold_cents: number;
|
||||
@@ -82,6 +84,8 @@ export interface SubscriptionPlanCreate {
|
||||
masked_calling_price_per_minute_cents?: number;
|
||||
proxy_number_enabled?: boolean;
|
||||
proxy_number_monthly_fee_cents?: number;
|
||||
// Contracts feature
|
||||
contracts_enabled?: boolean;
|
||||
// Default credit settings
|
||||
default_auto_reload_enabled?: boolean;
|
||||
default_auto_reload_threshold_cents?: number;
|
||||
|
||||
@@ -523,6 +523,7 @@ const StripeSettingsTab: React.FC = () => {
|
||||
};
|
||||
|
||||
const TiersSettingsTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: plans, isLoading, error } = useSubscriptionPlans();
|
||||
const createPlanMutation = useCreateSubscriptionPlan();
|
||||
const updatePlanMutation = useUpdateSubscriptionPlan();
|
||||
@@ -864,6 +865,8 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
|
||||
masked_calling_price_per_minute_cents: plan?.masked_calling_price_per_minute_cents ?? 5,
|
||||
proxy_number_enabled: plan?.proxy_number_enabled ?? false,
|
||||
proxy_number_monthly_fee_cents: plan?.proxy_number_monthly_fee_cents ?? 200,
|
||||
// Contracts feature
|
||||
contracts_enabled: plan?.contracts_enabled ?? false,
|
||||
// Default credit settings
|
||||
default_auto_reload_enabled: plan?.default_auto_reload_enabled ?? false,
|
||||
default_auto_reload_threshold_cents: plan?.default_auto_reload_threshold_cents ?? 1000,
|
||||
@@ -1251,6 +1254,25 @@ const PlanModal: React.FC<PlanModalProps> = ({ plan, onSave, onClose, isLoading
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contracts Feature */}
|
||||
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">Contracts</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Allow tenants to create and manage contracts with customers</p>
|
||||
</div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.contracts_enabled || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, contracts_enabled: e.target.checked }))}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default Credit Settings */}
|
||||
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">Default Credit Settings</h4>
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface PlanPermissions {
|
||||
masked_calling: boolean;
|
||||
pos_system: boolean;
|
||||
mobile_app: boolean;
|
||||
contracts: boolean;
|
||||
}
|
||||
|
||||
export interface Business {
|
||||
|
||||
Reference in New Issue
Block a user