diff --git a/DetailedReports_Analysis.md b/DetailedReports_Analysis.md new file mode 100644 index 0000000..2282c24 --- /dev/null +++ b/DetailedReports_Analysis.md @@ -0,0 +1,258 @@ +# Detailed Reports Page - Data Availability Analysis + +## Overview +This document analyzes what data is currently available in the backend and what information is missing for implementing the DetailedReports page. + +--- + +## 1. Request Lifecycle Report + +### ✅ **Available Data:** +- **Request Basic Info:** + - `requestNumber` (RE-REQ-2024-XXX) + - `title` + - `priority` (STANDARD/EXPRESS) + - `status` (DRAFT, PENDING, IN_PROGRESS, APPROVED, REJECTED, CLOSED) + - `initiatorId` → Can get initiator name via User model + - `submissionDate` + - `closureDate` + - `createdAt` + +- **Current Stage Info:** + - `currentLevel` (1-N) + - `totalLevels` + - Can get current approver from `approval_levels` table + +- **TAT Information:** + - `totalTatHours` (cumulative TAT) + - Can calculate overall TAT from `submissionDate` to `closureDate` or `updatedAt` + - Can get level-wise TAT from `approval_levels.tat_hours` + - Can get TAT compliance from `tat_alerts` table + +- **From Existing Services:** + - `getCriticalRequests()` - Returns requests with breach info + - `getUpcomingDeadlines()` - Returns active level info + - `getRecentActivity()` - Returns activity feed + +### ❌ **Missing Data:** +1. **Current Stage Name/Description:** + - Need to join with `approval_levels` to get `level_name` for current level + - Currently only have `currentLevel` number + +2. **Overall TAT Calculation:** + - Need API endpoint that calculates total time from submission to current/closure + - Currently have `totalTatHours` but need actual elapsed time + +3. **TAT Compliance Status:** + - Need to determine if "On Time" or "Delayed" based on TAT vs actual time + - Can calculate from `tat_alerts.is_breached` but need endpoint + +4. **Timeline/History:** + - Need endpoint to get all approval levels with their start/end times + - Need to show progression through levels + +### 🔧 **What Needs to be Built:** +- **New API Endpoint:** `/dashboard/reports/lifecycle` + - Returns requests with: + - Full lifecycle timeline (all levels with dates) + - Overall TAT calculation + - TAT compliance status (On Time/Delayed) + - Current stage name + - All approvers in sequence + +--- + +## 2. User Activity Log Report + +### ✅ **Available Data:** +- **Activity Model Fields:** + - `activityId` + - `requestId` + - `userId` → Can get user name from User model + - `userName` (stored directly) + - `activityType` (created, assignment, approval, rejection, etc.) + - `activityDescription` (details of action) + - `ipAddress` (available in model, but may not be logged) + - `createdAt` (timestamp) + - `metadata` (JSONB - can store additional info) + +- **From Existing Services:** + - `getRecentActivity()` - Already returns activity feed with pagination + - Returns: `activityId`, `requestId`, `requestNumber`, `requestTitle`, `type`, `action`, `details`, `userId`, `userName`, `timestamp`, `priority` + +### ❌ **Missing Data:** +1. **IP Address:** + - Field exists in model but may not be populated + - Need to ensure IP is captured when logging activities + +2. **User Agent/Device Info:** + - Field exists (`userAgent`) but may not be populated + - Need to capture browser/device info + +3. **Login Activities:** + - Current activity model is request-focused + - Need separate user session/login tracking + - Can check `users.last_login` but need detailed login history + +4. **Action Categorization:** + - Need to map `activityType` to display labels: + - "created" → "Created Request" + - "approval" → "Approved Request" + - "rejection" → "Rejected Request" + - "comment" → "Added Comment" + - etc. + +5. **Request ID Display:** + - Need to show request number when available + - Currently `getRecentActivity()` returns `requestNumber` ✅ + +### 🔧 **What Needs to be Built:** +- **Enhance Activity Logging:** + - Capture IP address in activity service + - Capture user agent in activity service + - Add login activity tracking (separate from request activities) + +- **New/Enhanced API Endpoint:** `/dashboard/reports/activity-log` + - Filter by date range + - Filter by user + - Filter by action type + - Include IP address and user agent + - Better categorization of actions + +--- + +## 3. Workflow Aging Report + +### ✅ **Available Data:** +- **Request Basic Info:** + - `requestNumber` + - `title` + - `initiatorId` → Can get initiator name + - `priority` + - `status` + - `createdAt` (can calculate days open) + - `submissionDate` + +- **Current Stage Info:** + - `currentLevel` + - `totalLevels` + - Can get current approver from `approval_levels` + +- **From Existing Services:** + - `getUpcomingDeadlines()` - Returns active requests with TAT info + - Can filter by days open using `createdAt` or `submissionDate` + +### ❌ **Missing Data:** +1. **Days Open Calculation:** + - Need to calculate from `submissionDate` (not `createdAt`) + - Need to exclude weekends/holidays for accurate business days + +2. **Start Date:** + - Should use `submissionDate` (when request was submitted, not created) + - Currently have this field ✅ + +3. **Assigned To:** + - Need current approver from `approval_levels` where `level_number = current_level` + - Can get from `approval_levels.approver_name` ✅ + +4. **Current Stage Name:** + - Need `approval_levels.level_name` for current level + - Currently only have level number + +5. **Aging Threshold Filtering:** + - Need to filter requests where days open > threshold + - Need to calculate business days (excluding weekends/holidays) + +### 🔧 **What Needs to be Built:** +- **New API Endpoint:** `/dashboard/reports/workflow-aging` + - Parameters: + - `threshold` (days) + - `dateRange` (optional) + - `page`, `limit` (pagination) + - Returns: + - Requests with days open > threshold + - Business days calculation + - Current stage name + - Current approver + - Days open (business days) + +--- + +## Summary + +### ✅ **Can Show Immediately:** +1. **Request Lifecycle Report (Partial):** + - Request ID, Title, Priority, Status + - Initiator name + - Submission date + - Current level number + - Basic TAT info + +2. **User Activity Log (Partial):** + - Timestamp, User, Action, Details + - Request ID (when applicable) + - Using existing `getRecentActivity()` service + +3. **Workflow Aging (Partial):** + - Request ID, Title, Initiator + - Days open (calendar days) + - Priority, Status + - Current approver (with join) + +### ❌ **Missing/Incomplete:** +1. **Request Lifecycle:** + - Full timeline/history of all levels + - Current stage name (not just number) + - Overall TAT calculation + - TAT compliance status (On Time/Delayed) + +2. **User Activity Log:** + - IP Address (field exists but may not be populated) + - User Agent (field exists but may not be populated) + - Login activities (separate tracking needed) + - Better action categorization + +3. **Workflow Aging:** + - Business days calculation (excluding weekends/holidays) + - Current stage name + - Proper threshold filtering + +### 🔧 **Required Backend Work:** +1. **New Endpoints:** + - `/dashboard/reports/lifecycle` - Full lifecycle with timeline + - `/dashboard/reports/activity-log` - Enhanced activity log with filters + - `/dashboard/reports/workflow-aging` - Aging report with business days + +2. **Enhancements:** + - Capture IP address in activity logging + - Capture user agent in activity logging + - Add login activity tracking + - Add business days calculation utility + - Add level name to approval levels response + +3. **Data Joins:** + - Join `approval_levels` to get current stage name + - Join `users` to get approver names + - Join `tat_alerts` to get breach/compliance info + +--- + +## Recommendations + +### Phase 1 (Quick Win - Use Existing Data): +- Implement basic reports using existing services +- Show available data (request info, basic activity, calendar days) +- Add placeholders for missing data + +### Phase 2 (Backend Development): +- Build new report endpoints +- Enhance activity logging to capture IP/user agent +- Add business days calculation +- Add level name to responses + +### Phase 3 (Full Implementation): +- Complete all three reports with full data +- Add filtering, sorting, export functionality +- Add date range filters +- Add user/role-based filtering + diff --git a/src/App.tsx b/src/App.tsx index 1e2452e..fba7209 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,8 @@ import { MyRequests } from '@/pages/MyRequests'; import { Profile } from '@/pages/Profile'; import { Settings } from '@/pages/Settings'; import { Notifications } from '@/pages/Notifications'; +import { DetailedReports } from '@/pages/DetailedReports'; +import { Admin } from '@/pages/Admin'; import { ApprovalActionModal } from '@/components/modals/ApprovalActionModal'; import { Toaster } from '@/components/ui/sonner'; import { toast } from 'sonner'; @@ -624,6 +626,26 @@ function AppRoutes({ onLogout }: AppProps) { } /> + + {/* Detailed Reports */} + + + + } + /> + + {/* Admin Control Panel */} + + + + } + /> >({ + claude: false, + openai: false, + gemini: false + }); + const [config, setConfig] = useState({ + aiEnabled: true, + aiProvider: 'claude', + claudeApiKey: '', + openaiApiKey: '', + geminiApiKey: '', + aiRemarkGeneration: true, + maxRemarkChars: 500 + }); + + useEffect(() => { + loadConfigurations(); + }, []); + + const loadConfigurations = async () => { + try { + setLoading(true); + const configs = await getAllConfigurations('AI_CONFIGURATION'); + + // Map configuration values to state + const configMap: Record = {}; + configs.forEach((c: AdminConfiguration) => { + configMap[c.configKey] = c.configValue; + }); + + setConfig({ + aiEnabled: configMap['AI_ENABLED'] === 'true', + aiProvider: (configMap['AI_PROVIDER'] || 'claude') as 'claude' | 'openai' | 'gemini', + claudeApiKey: configMap['CLAUDE_API_KEY'] || '', + openaiApiKey: configMap['OPENAI_API_KEY'] || '', + geminiApiKey: configMap['GEMINI_API_KEY'] || '', + aiRemarkGeneration: configMap['AI_REMARK_GENERATION_ENABLED'] === 'true', + maxRemarkChars: parseInt(configMap['AI_REMARK_MAX_CHARACTERS'] || '500') + }); + } catch (error: any) { + console.error('Failed to load AI configurations:', error); + toast.error('Failed to load AI configurations'); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + try { + setSaving(true); + + // Save all configurations + await Promise.all([ + updateConfiguration('AI_ENABLED', config.aiEnabled.toString()), + updateConfiguration('AI_PROVIDER', config.aiProvider), + updateConfiguration('CLAUDE_API_KEY', config.claudeApiKey), + updateConfiguration('OPENAI_API_KEY', config.openaiApiKey), + updateConfiguration('GEMINI_API_KEY', config.geminiApiKey), + updateConfiguration('AI_REMARK_GENERATION_ENABLED', config.aiRemarkGeneration.toString()), + updateConfiguration('AI_REMARK_MAX_CHARACTERS', config.maxRemarkChars.toString()) + ]); + + toast.success('AI configuration saved successfully'); + + // Reload to get updated values + await loadConfigurations(); + } catch (error: any) { + console.error('Failed to save AI configuration:', error); + toast.error(error.response?.data?.error || 'Failed to save AI configuration'); + } finally { + setSaving(false); + } + }; + + const updateConfig = (updates: Partial) => { + setConfig(prev => ({ ...prev, ...updates })); + }; + + const toggleApiKeyVisibility = (provider: 'claude' | 'openai' | 'gemini') => { + setShowApiKeys(prev => ({ + ...prev, + [provider]: !prev[provider] + })); + }; + + const maskApiKey = (key: string): string => { + if (!key || key.length === 0) return ''; + if (key.length <= 8) return '••••••••'; + return key.substring(0, 4) + '••••••••' + key.substring(key.length - 4); + }; + + if (loading) { + return ( + + + +

Loading AI configuration...

+
+
+ ); + } + + return ( + + +
+
+ +
+
+ AI Features Configuration + + Configure AI provider, API keys, and enable/disable AI-powered features + +
+
+
+ + updateConfig({ aiEnabled: enabled })} + onProviderChange={(provider) => updateConfig({ aiProvider: provider })} + onClaudeApiKeyChange={(key) => updateConfig({ claudeApiKey: key })} + onOpenaiApiKeyChange={(key) => updateConfig({ openaiApiKey: key })} + onGeminiApiKeyChange={(key) => updateConfig({ geminiApiKey: key })} + onToggleApiKeyVisibility={toggleApiKeyVisibility} + maskApiKey={maskApiKey} + /> + + + + updateConfig({ aiRemarkGeneration: enabled })} + /> + + + + updateConfig({ maxRemarkChars: chars })} + /> + +
+ +
+
+
+ ); +} diff --git a/src/components/admin/AIConfig/AIFeatures.tsx b/src/components/admin/AIConfig/AIFeatures.tsx new file mode 100644 index 0000000..177f3e8 --- /dev/null +++ b/src/components/admin/AIConfig/AIFeatures.tsx @@ -0,0 +1,43 @@ +import { Switch } from '@/components/ui/switch'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Sparkles } from 'lucide-react'; + +interface AIFeaturesProps { + aiRemarkGeneration: boolean; + onRemarkGenerationChange: (enabled: boolean) => void; +} + +export function AIFeatures({ + aiRemarkGeneration, + onRemarkGenerationChange +}: AIFeaturesProps) { + return ( + + +
+ + AI Features +
+ + Enable/disable specific AI-powered features + +
+ +
+
+
+

AI Remark Generation

+

+ Automatically generate conclusion remarks for workflow closures +

+
+ +
+
+
+
+ ); +} diff --git a/src/components/admin/AIConfig/AIParameters.tsx b/src/components/admin/AIConfig/AIParameters.tsx new file mode 100644 index 0000000..16ef489 --- /dev/null +++ b/src/components/admin/AIConfig/AIParameters.tsx @@ -0,0 +1,47 @@ +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Sliders } from 'lucide-react'; + +interface AIParametersProps { + maxRemarkChars: number; + onMaxRemarkCharsChange: (chars: number) => void; +} + +export function AIParameters({ + maxRemarkChars, + onMaxRemarkCharsChange +}: AIParametersProps) { + return ( + + +
+ + AI Parameters +
+ + Configure AI generation parameters + +
+ +
+ + onMaxRemarkCharsChange(parseInt(e.target.value) || 500)} + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" + /> +

+ Maximum character limit for AI-generated conclusion remarks (100-2000 characters) +

+
+
+
+ ); +} diff --git a/src/components/admin/AIConfig/AIProviderSettings.tsx b/src/components/admin/AIConfig/AIProviderSettings.tsx new file mode 100644 index 0000000..81f71ea --- /dev/null +++ b/src/components/admin/AIConfig/AIProviderSettings.tsx @@ -0,0 +1,240 @@ +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Brain, Eye, EyeOff, Key } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface AIProviderSettingsProps { + aiEnabled: boolean; + aiProvider: 'claude' | 'openai' | 'gemini'; + claudeApiKey: string; + openaiApiKey: string; + geminiApiKey: string; + showApiKeys: Record; + onAiEnabledChange: (enabled: boolean) => void; + onProviderChange: (provider: 'claude' | 'openai' | 'gemini') => void; + onClaudeApiKeyChange: (key: string) => void; + onOpenaiApiKeyChange: (key: string) => void; + onGeminiApiKeyChange: (key: string) => void; + onToggleApiKeyVisibility: (provider: 'claude' | 'openai' | 'gemini') => void; + maskApiKey: (key: string) => string; +} + +const PROVIDERS = [ + { value: 'claude', label: 'Claude (Anthropic)', description: 'Advanced AI by Anthropic' }, + { value: 'openai', label: 'OpenAI (GPT-4)', description: 'GPT-4 by OpenAI' }, + { value: 'gemini', label: 'Gemini (Google)', description: 'Gemini by Google' } +]; + +export function AIProviderSettings({ + aiEnabled, + aiProvider, + claudeApiKey, + openaiApiKey, + geminiApiKey, + showApiKeys, + onAiEnabledChange, + onProviderChange, + onClaudeApiKeyChange, + onOpenaiApiKeyChange, + onGeminiApiKeyChange, + onToggleApiKeyVisibility, + maskApiKey +}: AIProviderSettingsProps) { + const getCurrentApiKey = (provider: 'claude' | 'openai' | 'gemini'): string => { + switch (provider) { + case 'claude': + return claudeApiKey; + case 'openai': + return openaiApiKey; + case 'gemini': + return geminiApiKey; + } + }; + + const getApiKeyChangeHandler = (provider: 'claude' | 'openai' | 'gemini') => { + switch (provider) { + case 'claude': + return onClaudeApiKeyChange; + case 'openai': + return onOpenaiApiKeyChange; + case 'gemini': + return onGeminiApiKeyChange; + } + }; + + return ( + + +
+ + AI Provider & API Keys +
+ + Select your AI provider and configure API keys + +
+ + {/* Master Toggle */} +
+
+

Enable AI Features

+

+ Master toggle to enable/disable all AI-powered features +

+
+ +
+ + {aiEnabled && ( + <> + {/* Provider Selection */} +
+ + +
+ + {/* API Keys for each provider */} +
+ + + {/* Claude API Key */} +
+ +
+ onClaudeApiKeyChange(e.target.value)} + placeholder={showApiKeys.claude ? "sk-ant-..." : maskApiKey(claudeApiKey) || "sk-ant-..."} + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm" + /> + +
+

+ Get your API key from console.anthropic.com +

+
+ + {/* OpenAI API Key */} +
+ +
+ onOpenaiApiKeyChange(e.target.value)} + placeholder={showApiKeys.openai ? "sk-..." : maskApiKey(openaiApiKey) || "sk-..."} + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm" + /> + +
+

+ Get your API key from platform.openai.com +

+
+ + {/* Gemini API Key */} +
+ +
+ onGeminiApiKeyChange(e.target.value)} + placeholder={showApiKeys.gemini ? "AIza..." : maskApiKey(geminiApiKey) || "AIza..."} + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm" + /> + +
+

+ Get your API key from ai.google.dev +

+
+
+ + )} +
+
+ ); +} + diff --git a/src/components/admin/AIConfig/index.ts b/src/components/admin/AIConfig/index.ts new file mode 100644 index 0000000..8aca26f --- /dev/null +++ b/src/components/admin/AIConfig/index.ts @@ -0,0 +1,5 @@ +export { AIConfig } from './AIConfig'; +export { AIProviderSettings } from './AIProviderSettings'; +export { AIFeatures } from './AIFeatures'; +export { AIParameters } from './AIParameters'; + diff --git a/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx b/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx new file mode 100644 index 0000000..87bfcd4 --- /dev/null +++ b/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { Save } from 'lucide-react'; +import { AnalyticsSettingsForm } from './AnalyticsSettingsForm'; +import { DataFeaturesSection } from './DataFeaturesSection'; +import { ExportFormatsSection } from './ExportFormatsSection'; +import { DataRetentionSection } from './DataRetentionSection'; +import { toast } from 'sonner'; + +interface AnalyticsConfigData { + defaultPeriod: string; + refreshInterval: number; + autoRefresh: boolean; + realTimeUpdates: boolean; + dataExport: boolean; + exportFormats: string[]; + dataRetention: number; +} + +export function AnalyticsConfig() { + const [config, setConfig] = useState({ + defaultPeriod: 'This Month', + refreshInterval: 5, + autoRefresh: true, + realTimeUpdates: true, + dataExport: true, + exportFormats: ['CSV', 'Excel', 'PDF'], + dataRetention: 24 + }); + + const handleSave = () => { + // TODO: Implement API call to save configuration + console.log('Saving analytics configuration:', config); + toast.success('Analytics configuration saved successfully'); + }; + + const updateConfig = (updates: Partial) => { + setConfig(prev => ({ ...prev, ...updates })); + }; + + return ( + + + Analytics & Reporting Configuration + + Configure default reporting periods, auto-refresh, export settings, and data retention + + + + updateConfig({ defaultPeriod: period })} + onRefreshIntervalChange={(interval) => updateConfig({ refreshInterval: interval })} + /> + + + + updateConfig({ autoRefresh: enabled })} + onRealTimeUpdatesChange={(enabled) => updateConfig({ realTimeUpdates: enabled })} + onDataExportChange={(enabled) => updateConfig({ dataExport: enabled })} + /> + + + + updateConfig({ exportFormats: formats })} + /> + + updateConfig({ dataRetention: months })} + /> + + + + + ); +} + diff --git a/src/components/admin/AnalyticsConfig/AnalyticsSettingsForm.tsx b/src/components/admin/AnalyticsConfig/AnalyticsSettingsForm.tsx new file mode 100644 index 0000000..877819e --- /dev/null +++ b/src/components/admin/AnalyticsConfig/AnalyticsSettingsForm.tsx @@ -0,0 +1,60 @@ +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; + +interface AnalyticsSettingsFormProps { + defaultPeriod: string; + refreshInterval: number; + onDefaultPeriodChange: (period: string) => void; + onRefreshIntervalChange: (interval: number) => void; +} + +export function AnalyticsSettingsForm({ + defaultPeriod, + refreshInterval, + onDefaultPeriodChange, + onRefreshIntervalChange +}: AnalyticsSettingsFormProps) { + const periodOptions = [ + 'Today', + 'This Week', + 'This Month', + 'Last Month', + 'This Quarter', + 'This Year', + 'Custom Range' + ]; + + return ( +
+
+ + +
+ +
+ + onRefreshIntervalChange(parseInt(e.target.value) || 5)} + /> +
+
+ ); +} + diff --git a/src/components/admin/AnalyticsConfig/DataFeaturesSection.tsx b/src/components/admin/AnalyticsConfig/DataFeaturesSection.tsx new file mode 100644 index 0000000..d30468e --- /dev/null +++ b/src/components/admin/AnalyticsConfig/DataFeaturesSection.tsx @@ -0,0 +1,65 @@ +import { Switch } from '@/components/ui/switch'; + +interface DataFeaturesSectionProps { + autoRefresh: boolean; + realTimeUpdates: boolean; + dataExport: boolean; + onAutoRefreshChange: (enabled: boolean) => void; + onRealTimeUpdatesChange: (enabled: boolean) => void; + onDataExportChange: (enabled: boolean) => void; +} + +export function DataFeaturesSection({ + autoRefresh, + realTimeUpdates, + dataExport, + onAutoRefreshChange, + onRealTimeUpdatesChange, + onDataExportChange +}: DataFeaturesSectionProps) { + return ( +
+

Data Features

+ +
+
+

Enable Auto-Refresh

+

+ Automatically refresh dashboard data at set intervals +

+
+ +
+ +
+
+

Enable Real-time Updates

+

+ Show live updates when data changes occur +

+
+ +
+ +
+
+

Enable Data Export

+

+ Allow users to export analytics data and reports +

+
+ +
+
+ ); +} + diff --git a/src/components/admin/AnalyticsConfig/DataRetentionSection.tsx b/src/components/admin/AnalyticsConfig/DataRetentionSection.tsx new file mode 100644 index 0000000..684ed9d --- /dev/null +++ b/src/components/admin/AnalyticsConfig/DataRetentionSection.tsx @@ -0,0 +1,30 @@ +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; + +interface DataRetentionSectionProps { + dataRetention: number; + onDataRetentionChange: (months: number) => void; +} + +export function DataRetentionSection({ + dataRetention, + onDataRetentionChange +}: DataRetentionSectionProps) { + return ( +
+ + onDataRetentionChange(parseInt(e.target.value) || 24)} + /> +

+ Analytics data older than this will be archived or deleted +

+
+ ); +} + diff --git a/src/components/admin/AnalyticsConfig/ExportFormatsSection.tsx b/src/components/admin/AnalyticsConfig/ExportFormatsSection.tsx new file mode 100644 index 0000000..09b6684 --- /dev/null +++ b/src/components/admin/AnalyticsConfig/ExportFormatsSection.tsx @@ -0,0 +1,51 @@ +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; + +interface ExportFormatsSectionProps { + exportFormats: string[]; + onExportFormatsChange: (formats: string[]) => void; +} + +const availableFormats = ['CSV', 'Excel', 'PDF', 'JSON']; + +export function ExportFormatsSection({ + exportFormats, + onExportFormatsChange +}: ExportFormatsSectionProps) { + const handleFormatToggle = (format: string, checked: boolean) => { + if (checked) { + onExportFormatsChange([...exportFormats, format]); + } else { + onExportFormatsChange(exportFormats.filter(f => f !== format)); + } + }; + + return ( +
+ +
+ {availableFormats.map((format) => ( +
+ + handleFormatToggle(format, checked as boolean) + } + /> + +
+ ))} +
+
+ ); +} + diff --git a/src/components/admin/AnalyticsConfig/index.ts b/src/components/admin/AnalyticsConfig/index.ts new file mode 100644 index 0000000..b9e729b --- /dev/null +++ b/src/components/admin/AnalyticsConfig/index.ts @@ -0,0 +1,6 @@ +export { AnalyticsConfig } from './AnalyticsConfig'; +export { AnalyticsSettingsForm } from './AnalyticsSettingsForm'; +export { DataFeaturesSection } from './DataFeaturesSection'; +export { ExportFormatsSection } from './ExportFormatsSection'; +export { DataRetentionSection } from './DataRetentionSection'; + diff --git a/src/components/admin/DashboardConfig/DashboardConfig.tsx b/src/components/admin/DashboardConfig/DashboardConfig.tsx new file mode 100644 index 0000000..ad671e4 --- /dev/null +++ b/src/components/admin/DashboardConfig/DashboardConfig.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Save } from 'lucide-react'; +import { DashboardNote } from './DashboardNote'; +import { RoleDashboardSection } from './RoleDashboardSection'; +import { toast } from 'sonner'; + +export type Role = 'Initiator' | 'Approver' | 'Spectator'; + +export type KPICard = + | 'Total Requests' + | 'Open Requests' + | 'Approved Requests' + | 'Rejected Requests' + | 'My Pending Actions' + | 'TAT Compliance' + | 'Delayed Workflows' + | 'Average Cycle Time'; + +interface DashboardConfigData { + [key: string]: { + [kpi: string]: boolean; + }; +} + +export function DashboardConfig() { + const [config, setConfig] = useState({ + Initiator: { + 'Total Requests': true, + 'Open Requests': true, + 'Approved Requests': true, + 'Rejected Requests': true, + 'My Pending Actions': true, + 'TAT Compliance': true, + 'Delayed Workflows': true, + 'Average Cycle Time': true + }, + Approver: { + 'Total Requests': true, + 'Open Requests': true, + 'Approved Requests': true, + 'Rejected Requests': true, + 'My Pending Actions': true, + 'TAT Compliance': true, + 'Delayed Workflows': true, + 'Average Cycle Time': true + }, + Spectator: { + 'Total Requests': true, + 'Open Requests': true, + 'Approved Requests': true, + 'Rejected Requests': true, + 'My Pending Actions': true, + 'TAT Compliance': true, + 'Delayed Workflows': true, + 'Average Cycle Time': true + } + }); + + const handleSave = () => { + // TODO: Implement API call to save dashboard configuration + console.log('Saving dashboard configuration:', config); + toast.success('Dashboard layout saved successfully'); + }; + + const handleKPIToggle = (role: Role, kpi: KPICard, checked: boolean) => { + setConfig(prev => ({ + ...prev, + [role]: { + ...prev[role], + [kpi]: checked + } + })); + }; + + const roles: Role[] = ['Initiator', 'Approver', 'Spectator']; + + return ( + + + Dashboard Layout Configuration + + Control which KPI cards are visible for each user role + + + + + +
+ {roles.map((role) => ( + handleKPIToggle(role, kpi, checked)} + /> + ))} +
+ + +
+
+ ); +} + diff --git a/src/components/admin/DashboardConfig/DashboardNote.tsx b/src/components/admin/DashboardConfig/DashboardNote.tsx new file mode 100644 index 0000000..f7433bb --- /dev/null +++ b/src/components/admin/DashboardConfig/DashboardNote.tsx @@ -0,0 +1,10 @@ +export function DashboardNote() { + return ( +
+

+ Note: These settings control what information is visible to each role on their dashboard. Admins always have access to all metrics. +

+
+ ); +} + diff --git a/src/components/admin/DashboardConfig/RoleDashboardSection.tsx b/src/components/admin/DashboardConfig/RoleDashboardSection.tsx new file mode 100644 index 0000000..4011117 --- /dev/null +++ b/src/components/admin/DashboardConfig/RoleDashboardSection.tsx @@ -0,0 +1,63 @@ +import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Role, KPICard } from './DashboardConfig'; + +interface RoleDashboardSectionProps { + role: Role; + kpis: Record; + onKPIToggle: (kpi: KPICard, checked: boolean) => void; +} + +const kpiCards: KPICard[] = [ + 'Total Requests', + 'Open Requests', + 'Approved Requests', + 'Rejected Requests', + 'My Pending Actions', + 'TAT Compliance', + 'Delayed Workflows', + 'Average Cycle Time' +]; + +const getRoleBadgeColor = (role: Role) => { + switch (role) { + case 'Initiator': + return 'bg-blue-100 text-blue-800'; + case 'Approver': + return 'bg-green-100 text-green-800'; + case 'Spectator': + return 'bg-gray-100 text-gray-800'; + default: + return 'bg-gray-100 text-gray-800'; + } +}; + +export function RoleDashboardSection({ role, kpis, onKPIToggle }: RoleDashboardSectionProps) { + return ( +
+

+ + {role} Dashboard + +

+
+ {kpiCards.map((kpi) => ( +
+ onKPIToggle(kpi, checked === true)} + /> + +
+ ))} +
+
+ ); +} + diff --git a/src/components/admin/DashboardConfig/index.ts b/src/components/admin/DashboardConfig/index.ts new file mode 100644 index 0000000..20aebfb --- /dev/null +++ b/src/components/admin/DashboardConfig/index.ts @@ -0,0 +1,5 @@ +export { DashboardConfig } from './DashboardConfig'; +export type { Role, KPICard } from './DashboardConfig'; +export { DashboardNote } from './DashboardNote'; +export { RoleDashboardSection } from './RoleDashboardSection'; + diff --git a/src/components/admin/DocumentConfig/AllowedFileTypes.tsx b/src/components/admin/DocumentConfig/AllowedFileTypes.tsx new file mode 100644 index 0000000..1339c75 --- /dev/null +++ b/src/components/admin/DocumentConfig/AllowedFileTypes.tsx @@ -0,0 +1,119 @@ +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { FileCheck } from 'lucide-react'; + +interface AllowedFileTypesProps { + allowedFileTypes: string; + onAllowedFileTypesChange: (types: string) => void; +} + +// Common file type mappings +const FILE_TYPE_GROUPS = [ + { label: 'PDF Documents', extensions: ['pdf'] }, + { label: 'Microsoft Word', extensions: ['doc', 'docx'] }, + { label: 'Microsoft Excel', extensions: ['xls', 'xlsx'] }, + { label: 'Microsoft PowerPoint', extensions: ['ppt', 'pptx'] }, + { label: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'] }, + { label: 'CSV Files', extensions: ['csv'] }, + { label: 'Text Files', extensions: ['txt', 'rtf'] } +]; + +export function AllowedFileTypes({ allowedFileTypes, onAllowedFileTypesChange }: AllowedFileTypesProps) { + const allowedExtensions = allowedFileTypes.split(',').map(ext => ext.trim().toLowerCase()); + + const toggleFileType = (extensions: string[], enabled: boolean) => { + const currentExts = new Set(allowedExtensions); + + if (enabled) { + extensions.forEach(ext => currentExts.add(ext)); + } else { + extensions.forEach(ext => currentExts.delete(ext)); + } + + onAllowedFileTypesChange(Array.from(currentExts).join(',')); + }; + + const isGroupEnabled = (extensions: string[]) => { + return extensions.some(ext => allowedExtensions.includes(ext)); + }; + + const handleManualEdit = (value: string) => { + onAllowedFileTypesChange(value); + }; + + return ( + + +
+ + Allowed File Types +
+ + Select which file types are allowed for upload + +
+ +
+ {FILE_TYPE_GROUPS.map((group) => { + const isEnabled = isGroupEnabled(group.extensions); + return ( +
toggleFileType(group.extensions, !isEnabled)} + className={`flex items-center justify-between p-3 rounded-md border-2 cursor-pointer transition-all ${ + isEnabled + ? 'bg-re-green/5 border-re-green/30 hover:border-re-green/50' + : 'bg-gray-50 border-gray-200 hover:border-gray-300' + }`} + > +
+
+ {isEnabled && ( + + + + )} +
+ + {group.label} + +
+ + {group.extensions.join(', ')} + +
+ ); + })} +
+ +
+ + handleManualEdit(e.target.value)} + placeholder="pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif" + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20 font-mono text-sm" + /> +

+ Edit directly or use the checkboxes above. Separate extensions with commas. +

+
+
+
+ ); +} diff --git a/src/components/admin/DocumentConfig/DocumentConfig.tsx b/src/components/admin/DocumentConfig/DocumentConfig.tsx new file mode 100644 index 0000000..e5325f5 --- /dev/null +++ b/src/components/admin/DocumentConfig/DocumentConfig.tsx @@ -0,0 +1,144 @@ +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { Save, Loader2, FileText } from 'lucide-react'; +import { DocumentUploadSettings } from './DocumentUploadSettings'; +import { AllowedFileTypes } from './AllowedFileTypes'; +import { getAllConfigurations, updateConfiguration, AdminConfiguration } from '@/services/adminApi'; +import { toast } from 'sonner'; + +interface DocumentConfigData { + maxFileSizeMB: number; + retentionDays: number; + allowedFileTypes: string; +} + +export function DocumentConfig() { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [config, setConfig] = useState({ + maxFileSizeMB: 10, + retentionDays: 365, + allowedFileTypes: 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif' + }); + + useEffect(() => { + loadConfigurations(); + }, []); + + const loadConfigurations = async () => { + try { + setLoading(true); + const configs = await getAllConfigurations('DOCUMENT_POLICY'); + + // Map configuration values to state + const configMap: Record = {}; + configs.forEach((c: AdminConfiguration) => { + configMap[c.configKey] = c.configValue; + }); + + setConfig({ + maxFileSizeMB: parseInt(configMap['MAX_FILE_SIZE_MB'] || '10'), + retentionDays: parseInt(configMap['DOCUMENT_RETENTION_DAYS'] || '365'), + allowedFileTypes: configMap['ALLOWED_FILE_TYPES'] || 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif' + }); + } catch (error: any) { + console.error('Failed to load document configurations:', error); + toast.error('Failed to load document configurations'); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + try { + setSaving(true); + + // Save all configurations + await Promise.all([ + updateConfiguration('MAX_FILE_SIZE_MB', config.maxFileSizeMB.toString()), + updateConfiguration('DOCUMENT_RETENTION_DAYS', config.retentionDays.toString()), + updateConfiguration('ALLOWED_FILE_TYPES', config.allowedFileTypes) + ]); + + toast.success('Document policy saved successfully'); + + // Reload to get updated values + await loadConfigurations(); + } catch (error: any) { + console.error('Failed to save document configuration:', error); + toast.error(error.response?.data?.error || 'Failed to save document configuration'); + } finally { + setSaving(false); + } + }; + + const updateConfig = (updates: Partial) => { + setConfig(prev => ({ ...prev, ...updates })); + }; + + if (loading) { + return ( + + + +

Loading document policy configuration...

+
+
+ ); + } + + return ( + + +
+
+ +
+
+ Document Upload Policy + + Configure file upload limits, allowed types, and retention policies + +
+
+
+ + updateConfig({ maxFileSizeMB: size })} + onRetentionDaysChange={(days) => updateConfig({ retentionDays: days })} + /> + + + + updateConfig({ allowedFileTypes: types })} + /> + +
+ +
+
+
+ ); +} diff --git a/src/components/admin/DocumentConfig/DocumentUploadSettings.tsx b/src/components/admin/DocumentConfig/DocumentUploadSettings.tsx new file mode 100644 index 0000000..e6a08e6 --- /dev/null +++ b/src/components/admin/DocumentConfig/DocumentUploadSettings.tsx @@ -0,0 +1,76 @@ +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Upload, Archive } from 'lucide-react'; + +interface DocumentUploadSettingsProps { + maxFileSizeMB: number; + retentionDays: number; + onMaxFileSizeChange: (size: number) => void; + onRetentionDaysChange: (days: number) => void; +} + +export function DocumentUploadSettings({ + maxFileSizeMB, + retentionDays, + onMaxFileSizeChange, + onRetentionDaysChange +}: DocumentUploadSettingsProps) { + // Convert days to years for display (optional, or keep as days) + const retentionYears = Math.round((retentionDays / 365) * 10) / 10; + + return ( + + +
+ + Upload & Retention Settings +
+ + Configure file size limits and document retention period + +
+ +
+
+ + onMaxFileSizeChange(parseInt(e.target.value) || 10)} + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" + /> +

+ + Maximum allowed file size for document uploads +

+
+ +
+ + onRetentionDaysChange(parseInt(e.target.value) || 365)} + className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" + /> +

+ + Days to retain documents after workflow closure ({retentionYears} years) +

+
+
+
+
+ ); +} diff --git a/src/components/admin/DocumentConfig/index.ts b/src/components/admin/DocumentConfig/index.ts new file mode 100644 index 0000000..a1460f7 --- /dev/null +++ b/src/components/admin/DocumentConfig/index.ts @@ -0,0 +1,4 @@ +export { DocumentConfig } from './DocumentConfig'; +export { DocumentUploadSettings } from './DocumentUploadSettings'; +export { AllowedFileTypes } from './AllowedFileTypes'; + diff --git a/src/components/admin/NotificationConfig/EmailTemplateSection.tsx b/src/components/admin/NotificationConfig/EmailTemplateSection.tsx new file mode 100644 index 0000000..ed6933c --- /dev/null +++ b/src/components/admin/NotificationConfig/EmailTemplateSection.tsx @@ -0,0 +1,30 @@ +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; + +interface EmailTemplateSectionProps { + emailTemplate: string; + onEmailTemplateChange: (template: string) => void; +} + +export function EmailTemplateSection({ + emailTemplate, + onEmailTemplateChange +}: EmailTemplateSectionProps) { + return ( +
+ +