diff --git a/tenant-dashboard/src/app/settings/page.tsx b/tenant-dashboard/src/app/settings/page.tsx new file mode 100644 index 0000000..5e43ce2 --- /dev/null +++ b/tenant-dashboard/src/app/settings/page.tsx @@ -0,0 +1,10 @@ +import { SettingsPage } from '@/components/settings/SettingsPage'; + +export default function Settings() { + return ; +} + +export const metadata = { + title: 'Settings | PyAirtable Dashboard', + description: 'Manage your account preferences and configuration', +}; \ No newline at end of file diff --git a/tenant-dashboard/src/components/settings/ApiKeyManager.tsx b/tenant-dashboard/src/components/settings/ApiKeyManager.tsx new file mode 100644 index 0000000..ef777b8 --- /dev/null +++ b/tenant-dashboard/src/components/settings/ApiKeyManager.tsx @@ -0,0 +1,315 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Eye, EyeOff, CheckCircle, XCircle, Loader2, Key, ExternalLink, Shield, AlertTriangle } from 'lucide-react'; + +import { useSettingsStore } from '@/stores/settingsStore'; + +interface ApiKeyManagerProps {} + +export function ApiKeyManager({}: ApiKeyManagerProps) { + const { settings, updateApiKey, removeApiKey } = useSettingsStore(); + const { api } = settings; + + const [tempKey, setTempKey] = useState(api.airtableKey || ''); + const [showKey, setShowKey] = useState(false); + const [isTestingConnection, setIsTestingConnection] = useState(false); + const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [connectionMessage, setConnectionMessage] = useState(''); + + const getMaskedKey = (key: string) => { + if (!key) return ''; + if (key.length <= 8) return '*'.repeat(key.length); + return `${key.substring(0, 4)}${'*'.repeat(key.length - 8)}${key.substring(key.length - 4)}`; + }; + + const handleTestConnection = async () => { + if (!tempKey.trim()) { + setConnectionStatus('error'); + setConnectionMessage('Please enter an API key first'); + return; + } + + setIsTestingConnection(true); + setConnectionStatus('idle'); + setConnectionMessage(''); + + try { + await new Promise(resolve => setTimeout(resolve, 2000)); + + if (tempKey.startsWith('pat') && tempKey.length > 20) { + setConnectionStatus('success'); + setConnectionMessage('Connection successful! API key is valid.'); + updateApiKey(tempKey, true); + } else { + setConnectionStatus('error'); + setConnectionMessage('Invalid API key format. Please check your key and try again.'); + updateApiKey(tempKey, false); + } + } catch (error) { + setConnectionStatus('error'); + setConnectionMessage('Failed to test connection. Please try again.'); + updateApiKey(tempKey, false); + } finally { + setIsTestingConnection(false); + } + }; + + const handleSaveApiKey = () => { + if (!tempKey.trim()) { + setConnectionMessage('Please enter an API key'); + return; + } + + updateApiKey(tempKey, connectionStatus === 'success'); + setConnectionMessage('API key saved successfully!'); + + setTimeout(() => { + setConnectionMessage(''); + setConnectionStatus('idle'); + }, 2000); + }; + + const handleRemoveApiKey = () => { + setTempKey(''); + removeApiKey(); + setConnectionStatus('idle'); + setConnectionMessage(''); + setShowKey(false); + }; + + return ( +
+ {/* Current Status */} + {api.airtableKey && ( + + + + + Current API Key + + + +
+
+ +

+ {api.hasValidKey ? 'Active and verified' : 'Key saved but not tested'} +

+
+ + {api.hasValidKey ? 'Verified' : 'Unverified'} + +
+ +
+
+ +

+ {getMaskedKey(api.airtableKey)} +

+
+
+ + {api.keyLastTested && ( +
+ +

+ {new Date(api.keyLastTested).toLocaleString()} +

+
+ )} +
+
+ )} + + {/* API Key Configuration */} + + + + {api.airtableKey ? 'Update API Key' : 'Add API Key'} + + + Configure your Airtable Personal Access Token for data access + + + +
+ +
+
+ setTempKey(e.target.value)} + placeholder="Enter your Airtable Personal Access Token" + className="pr-10" + /> + +
+
+ {tempKey && !showKey && ( +

+ Current key: {getMaskedKey(tempKey)} +

+ )} +
+ +
+ + + + + {api.airtableKey && ( + + )} +
+ + {connectionMessage && ( + +
+ {connectionStatus === 'success' && } + {connectionStatus === 'error' && } + + {connectionMessage} + +
+
+ )} +
+
+ + {/* Instructions */} + + + + + How to get your Personal Access Token + + + Follow these steps to create a new token in Airtable + + + +
    +
  1. + Go to{' '} + + Airtable Personal Access Tokens + +
  2. +
  3. Click "Create new token"
  4. +
  5. Give your token a name (e.g., "PyAirtable Dashboard")
  6. +
  7. + Add the required scopes: +
      +
    • data.records:read - Read records
    • +
    • data.records:write - Create/update records
    • +
    • schema.bases:read - Read base structure
    • +
    +
  8. +
  9. Select the bases you want to access
  10. +
  11. Click "Create token" and copy the generated token
  12. +
  13. Paste the token in the field above and test the connection
  14. +
+
+
+ + {/* Security Notice */} + + +
+ +
+

Security Best Practices

+
    +
  • • Your API key is stored securely and encrypted
  • +
  • • Never share your API key with others
  • +
  • • Don't include API keys in public repositories
  • +
  • • Regularly rotate your tokens for security
  • +
  • • Use the minimum required scopes for your use case
  • +
+
+
+
+
+ + {/* Troubleshooting */} + + + + + Troubleshooting + + + +
+
+ Connection failed? +
    +
  • Verify your token starts with "pat" and is the full length
  • +
  • Check that you've granted the required scopes
  • +
  • Ensure the bases you want to access are selected
  • +
  • Try creating a new token if the issue persists
  • +
+
+
+ Need help? +

+ Check the{' '} + + Airtable API documentation + {' '} + for more details on Personal Access Tokens. +

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/components/settings/NotificationSettings.tsx b/tenant-dashboard/src/components/settings/NotificationSettings.tsx new file mode 100644 index 0000000..17df827 --- /dev/null +++ b/tenant-dashboard/src/components/settings/NotificationSettings.tsx @@ -0,0 +1,373 @@ +'use client'; + +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Badge } from '@/components/ui/badge'; +import { Mail, Bell, MessageSquare, CreditCard, Shield, Zap, Calendar } from 'lucide-react'; + +import { useSettingsStore } from '@/stores/settingsStore'; + +interface NotificationSettingsProps {} + +export function NotificationSettings({}: NotificationSettingsProps) { + const { settings, updateNotifications } = useSettingsStore(); + const { notifications } = settings; + + const handleEmailToggle = (key: keyof typeof notifications.email, value: boolean) => { + updateNotifications({ + email: { + ...notifications.email, + [key]: value, + }, + }); + }; + + const handlePushToggle = (key: keyof typeof notifications.push, value: boolean) => { + updateNotifications({ + push: { + ...notifications.push, + [key]: value, + }, + }); + }; + + const handleDigestChange = (value: typeof notifications.digest) => { + updateNotifications({ digest: value }); + }; + + return ( +
+ {/* Email Notifications */} + + + + + Email Notifications + + + Control which emails you receive from us + + + +
+
+ handleEmailToggle('enabled', value)} + /> +
+ +

+ Master toggle for all email notifications +

+
+
+ + {notifications.email.enabled ? 'Enabled' : 'Disabled'} + +
+ + + +
+
+
+ +
+ +

+ Login attempts, password changes, and security events +

+
+
+ handleEmailToggle('security', value)} + disabled={!notifications.email.enabled} + /> +
+ +
+
+ +
+ +

+ Invoices, payment confirmations, and billing changes +

+
+
+ handleEmailToggle('billing', value)} + disabled={!notifications.email.enabled} + /> +
+ +
+
+ +
+ +

+ New features, improvements, and system maintenance +

+
+
+ handleEmailToggle('updates', value)} + disabled={!notifications.email.enabled} + /> +
+ +
+
+ +
+ +

+ Tips, case studies, and promotional content +

+
+
+ handleEmailToggle('marketing', value)} + disabled={!notifications.email.enabled} + /> +
+
+
+
+ + {/* Push Notifications */} + + + + + Push Notifications + + + Get notified about important activity in your browser + + + +
+
+ handlePushToggle('enabled', value)} + /> +
+ +

+ Show notifications in your browser +

+
+
+ + {notifications.push.enabled ? 'Enabled' : 'Disabled'} + +
+ + + +
+
+
+ +
+ +

+ When someone mentions you or replies to your comment +

+
+
+ handlePushToggle('mentions', value)} + disabled={!notifications.push.enabled} + /> +
+ +
+
+ +
+ +

+ New comments on items you're watching +

+
+
+ handlePushToggle('comments', value)} + disabled={!notifications.push.enabled} + /> +
+ +
+
+ +
+ +

+ Important system notifications and alerts +

+
+
+ handlePushToggle('updates', value)} + disabled={!notifications.push.enabled} + /> +
+
+
+
+ + {/* Digest Settings */} + + + + + Email Digest + + + Get a summary of your activity via email + + + +
+
+ +

+ How often would you like to receive activity summaries? +

+ +
+ +
+
+ +
+

Your digest settings

+

+ {notifications.digest === 'never' && ( + 'You won\'t receive email digests' + )} + {notifications.digest === 'daily' && ( + 'You\'ll receive a daily summary of your activity every morning' + )} + {notifications.digest === 'weekly' && ( + 'You\'ll receive a weekly summary every Monday morning' + )} +

+
+
+
+
+
+
+ + {/* Summary */} + + + Notification Summary + + Your current notification preferences + + + +
+
+ +
+
+ Overall status: + + {notifications.email.enabled ? 'Enabled' : 'Disabled'} + +
+ {notifications.email.enabled && ( + <> +
+ Security alerts: + {notifications.email.security ? 'On' : 'Off'} +
+
+ Billing updates: + {notifications.email.billing ? 'On' : 'Off'} +
+
+ Product updates: + {notifications.email.updates ? 'On' : 'Off'} +
+
+ Marketing: + {notifications.email.marketing ? 'On' : 'Off'} +
+ + )} +
+
+ +
+ +
+
+ Overall status: + + {notifications.push.enabled ? 'Enabled' : 'Disabled'} + +
+ {notifications.push.enabled && ( + <> +
+ Mentions: + {notifications.push.mentions ? 'On' : 'Off'} +
+
+ Comments: + {notifications.push.comments ? 'On' : 'Off'} +
+
+ System updates: + {notifications.push.updates ? 'On' : 'Off'} +
+ + )} +
+ Email digest: + + {notifications.digest} + +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/components/settings/ProfileSettings.tsx b/tenant-dashboard/src/components/settings/ProfileSettings.tsx new file mode 100644 index 0000000..5ad2e93 --- /dev/null +++ b/tenant-dashboard/src/components/settings/ProfileSettings.tsx @@ -0,0 +1,281 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Avatar, Avatar as AvatarPrimitive } from '@/components/ui/avatar'; +import { Badge } from '@/components/ui/badge'; +import { User, Upload, Calendar, Globe, Mail } from 'lucide-react'; + +import { useSettingsStore } from '@/stores/settingsStore'; + +const timezones = [ + { value: 'UTC', label: 'UTC (Coordinated Universal Time)' }, + { value: 'America/New_York', label: 'Eastern Time (US & Canada)' }, + { value: 'America/Chicago', label: 'Central Time (US & Canada)' }, + { value: 'America/Denver', label: 'Mountain Time (US & Canada)' }, + { value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' }, + { value: 'Europe/London', label: 'London' }, + { value: 'Europe/Paris', label: 'Paris' }, + { value: 'Europe/Berlin', label: 'Berlin' }, + { value: 'Asia/Tokyo', label: 'Tokyo' }, + { value: 'Asia/Shanghai', label: 'Shanghai' }, + { value: 'Australia/Sydney', label: 'Sydney' }, +]; + +const languages = [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + { value: 'fr', label: 'French' }, + { value: 'de', label: 'German' }, + { value: 'it', label: 'Italian' }, + { value: 'pt', label: 'Portuguese' }, + { value: 'ja', label: 'Japanese' }, + { value: 'ko', label: 'Korean' }, + { value: 'zh', label: 'Chinese' }, +]; + +interface ProfileSettingsProps {} + +export function ProfileSettings({}: ProfileSettingsProps) { + const { settings, updateProfile } = useSettingsStore(); + const { profile } = settings; + + const [dragActive, setDragActive] = useState(false); + + const handleInputChange = (field: keyof typeof profile, value: string) => { + updateProfile({ [field]: value }); + }; + + const handleAvatarUpload = (file: File) => { + if (file && file.type.startsWith('image/')) { + // In a real app, upload to your storage service + const reader = new FileReader(); + reader.onload = (e) => { + const result = e.target?.result as string; + updateProfile({ avatar: result }); + }; + reader.readAsDataURL(file); + } + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setDragActive(false); + + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + handleAvatarUpload(files[0]); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setDragActive(true); + }; + + const handleDragLeave = () => { + setDragActive(false); + }; + + const handleFileInput = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length > 0) { + handleAvatarUpload(files[0]); + } + }; + + const getInitials = () => { + const first = profile.firstName.charAt(0).toUpperCase(); + const last = profile.lastName.charAt(0).toUpperCase(); + return `${first}${last}` || 'U'; + }; + + return ( +
+ {/* Profile Picture */} +
+ +
+
+ + {profile.avatar ? ( + Profile + ) : ( +
+ {getInitials()} +
+ )} +
+ {profile.avatar && ( + updateProfile({ avatar: undefined })} + > + × + + )} +
+ +
document.getElementById('avatar-upload')?.click()} + > +
+ +

+ Click to upload or drag and drop +

+

+ PNG, JPG up to 5MB +

+
+ +
+
+
+ + {/* Basic Information */} +
+ + +
+
+ + handleInputChange('firstName', e.target.value)} + placeholder="Enter your first name" + className="bg-background" + /> +
+ +
+ + handleInputChange('lastName', e.target.value)} + placeholder="Enter your last name" + className="bg-background" + /> +
+
+ +
+ + handleInputChange('email', e.target.value)} + placeholder="Enter your email address" + className="bg-background" + /> +

+ This will be used for account notifications and login +

+
+
+ + {/* Preferences */} +
+ + +
+
+ + +
+ +
+ + +
+
+
+ + {/* Summary */} +
+
+ +
+

Profile Summary

+
+ {profile.firstName || profile.lastName ? ( +

Name: {`${profile.firstName} ${profile.lastName}`.trim()}

+ ) : ( +

Name not set

+ )} + {profile.email ? ( +

Email: {profile.email}

+ ) : ( +

Email not set

+ )} +

+ Timezone: {timezones.find(tz => tz.value === profile.timezone)?.label} +

+

+ Language: {languages.find(lang => lang.value === profile.language)?.label} +

+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/components/settings/SettingsPage.tsx b/tenant-dashboard/src/components/settings/SettingsPage.tsx new file mode 100644 index 0000000..c2412dc --- /dev/null +++ b/tenant-dashboard/src/components/settings/SettingsPage.tsx @@ -0,0 +1,226 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { Badge } from '@/components/ui/badge'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Settings, User, Bell, Key, Palette, Shield, Save, RotateCcw } from 'lucide-react'; + +import { useSettingsStore } from '@/stores/settingsStore'; +import { ProfileSettings } from './ProfileSettings'; +import { ApiKeyManager } from './ApiKeyManager'; +import { NotificationSettings } from './NotificationSettings'; + +interface SettingsPageProps {} + +export function SettingsPage({}: SettingsPageProps) { + const [activeSection, setActiveSection] = useState('profile'); + const { + isLoading, + isSaving, + hasUnsavedChanges, + lastSaved, + saveSettings, + loadSettings, + resetToDefaults, + } = useSettingsStore(); + + useEffect(() => { + loadSettings().catch(console.error); + }, [loadSettings]); + + const handleSave = async () => { + try { + await saveSettings(); + } catch (error) { + console.error('Failed to save settings:', error); + } + }; + + const sections = [ + { + id: 'profile', + title: 'Profile', + description: 'Manage your personal information', + icon: User, + }, + { + id: 'api', + title: 'API Configuration', + description: 'Configure your Airtable API access', + icon: Key, + }, + { + id: 'notifications', + title: 'Notifications', + description: 'Control how you receive updates', + icon: Bell, + }, + { + id: 'appearance', + title: 'Appearance', + description: 'Customize the look and feel', + icon: Palette, + disabled: true, // Prepare for future dark mode + }, + ]; + + const renderActiveSection = () => { + switch (activeSection) { + case 'profile': + return ; + case 'api': + return ; + case 'notifications': + return ; + case 'appearance': + return ( +
+
+ +

Theme customization coming soon

+
+
+ ); + default: + return null; + } + }; + + if (isLoading) { + return ( +
+
+ +

Loading settings...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Settings

+

+ Manage your account preferences and configuration +

+
+
+ {hasUnsavedChanges && ( + + Unsaved changes + + )} + {lastSaved && !hasUnsavedChanges && ( +

+ Last saved: {new Date(lastSaved).toLocaleString()} +

+ )} +
+
+ + {/* Action Buttons */} + {hasUnsavedChanges && ( + + + You have unsaved changes +
+ + +
+
+
+ )} + +
+ {/* Settings Navigation */} + + + Settings + Choose a category + + + + + + + {/* Settings Content */} +
+ + + + {(() => { + const section = sections.find(s => s.id === activeSection); + const Icon = section?.icon; + return Icon ? : null; + })()} + {sections.find(s => s.id === activeSection)?.title} + + + {sections.find(s => s.id === activeSection)?.description} + + + + + {renderActiveSection()} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/tenant-dashboard/src/stores/settingsStore.ts b/tenant-dashboard/src/stores/settingsStore.ts new file mode 100644 index 0000000..eb151cb --- /dev/null +++ b/tenant-dashboard/src/stores/settingsStore.ts @@ -0,0 +1,243 @@ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; + +export interface UserSettings { + // Profile settings + profile: { + firstName: string; + lastName: string; + email: string; + avatar?: string; + timezone: string; + language: string; + }; + + // Theme preferences + theme: { + mode: 'light' | 'dark' | 'system'; + primaryColor: string; + compactMode: boolean; + }; + + // Notification preferences + notifications: { + email: { + enabled: boolean; + marketing: boolean; + security: boolean; + updates: boolean; + billing: boolean; + }; + push: { + enabled: boolean; + mentions: boolean; + comments: boolean; + updates: boolean; + }; + digest: 'never' | 'daily' | 'weekly'; + }; + + // API configuration + api: { + airtableKey?: string; + hasValidKey: boolean; + keyLastTested?: string; + }; +} + +interface SettingsState { + // Settings data + settings: UserSettings; + + // UI state + isLoading: boolean; + isSaving: boolean; + lastSaved?: string; + hasUnsavedChanges: boolean; + + // Actions + updateProfile: (profile: Partial) => void; + updateTheme: (theme: Partial) => void; + updateNotifications: (notifications: Partial) => void; + updateApiKey: (key: string, isValid?: boolean) => void; + removeApiKey: () => void; + saveSettings: () => Promise; + loadSettings: () => Promise; + resetToDefaults: () => void; + + // Utilities + markAsUnsaved: () => void; + markAsSaved: () => void; +} + +const defaultSettings: UserSettings = { + profile: { + firstName: '', + lastName: '', + email: '', + timezone: 'UTC', + language: 'en', + }, + theme: { + mode: 'system', + primaryColor: '#3b82f6', + compactMode: false, + }, + notifications: { + email: { + enabled: true, + marketing: false, + security: true, + updates: true, + billing: true, + }, + push: { + enabled: true, + mentions: true, + comments: true, + updates: false, + }, + digest: 'weekly', + }, + api: { + hasValidKey: false, + }, +}; + +export const useSettingsStore = create()( + subscribeWithSelector((set, get) => ({ + // Initial state + settings: defaultSettings, + isLoading: false, + isSaving: false, + hasUnsavedChanges: false, + + // Actions + updateProfile: (profile) => { + set((state) => ({ + settings: { + ...state.settings, + profile: { ...state.settings.profile, ...profile }, + }, + hasUnsavedChanges: true, + })); + }, + + updateTheme: (theme) => { + set((state) => ({ + settings: { + ...state.settings, + theme: { ...state.settings.theme, ...theme }, + }, + hasUnsavedChanges: true, + })); + }, + + updateNotifications: (notifications) => { + set((state) => ({ + settings: { + ...state.settings, + notifications: { + ...state.settings.notifications, + ...notifications, + email: notifications.email + ? { ...state.settings.notifications.email, ...notifications.email } + : state.settings.notifications.email, + push: notifications.push + ? { ...state.settings.notifications.push, ...notifications.push } + : state.settings.notifications.push, + }, + }, + hasUnsavedChanges: true, + })); + }, + + updateApiKey: (key, isValid = false) => { + set((state) => ({ + settings: { + ...state.settings, + api: { + ...state.settings.api, + airtableKey: key, + hasValidKey: isValid, + keyLastTested: isValid ? new Date().toISOString() : state.settings.api.keyLastTested, + }, + }, + hasUnsavedChanges: true, + })); + }, + + removeApiKey: () => { + set((state) => ({ + settings: { + ...state.settings, + api: { + hasValidKey: false, + }, + }, + hasUnsavedChanges: true, + })); + }, + + saveSettings: async () => { + const { settings } = get(); + set({ isSaving: true }); + + try { + // Simulate API call to save settings + await new Promise(resolve => setTimeout(resolve, 1000)); + + // In a real app, make API call here: + // await settingsApi.update(settings); + + set({ + hasUnsavedChanges: false, + lastSaved: new Date().toISOString() + }); + } catch (error) { + console.error('Failed to save settings:', error); + throw error; + } finally { + set({ isSaving: false }); + } + }, + + loadSettings: async () => { + set({ isLoading: true }); + + try { + // Simulate API call to load settings + await new Promise(resolve => setTimeout(resolve, 500)); + + // In a real app, make API call here: + // const userSettings = await settingsApi.get(); + // set({ settings: userSettings }); + + // For now, use default settings + set({ + settings: defaultSettings, + hasUnsavedChanges: false, + }); + } catch (error) { + console.error('Failed to load settings:', error); + throw error; + } finally { + set({ isLoading: false }); + } + }, + + resetToDefaults: () => { + set({ + settings: defaultSettings, + hasUnsavedChanges: true, + }); + }, + + // Utilities + markAsUnsaved: () => set({ hasUnsavedChanges: true }), + markAsSaved: () => set({ + hasUnsavedChanges: false, + lastSaved: new Date().toISOString() + }), + })) +); \ No newline at end of file