diff --git a/application/src/components/schedule-incident/IncidentManagementTab.tsx b/application/src/components/schedule-incident/IncidentManagementTab.tsx
new file mode 100644
index 0000000..59bed3d
--- /dev/null
+++ b/application/src/components/schedule-incident/IncidentManagementTab.tsx
@@ -0,0 +1,13 @@
+
+import React from 'react';
+import { IncidentManagementContainer } from './incident-management';
+
+interface IncidentManagementTabProps {
+ refreshTrigger?: number;
+}
+
+export const IncidentManagementTab = React.memo(({ refreshTrigger = 0 }: IncidentManagementTabProps) => {
+ return ;
+});
+
+IncidentManagementTab.displayName = 'IncidentManagementTab';
diff --git a/application/src/components/schedule-incident/ScheduleIncidentContent.tsx b/application/src/components/schedule-incident/ScheduleIncidentContent.tsx
new file mode 100644
index 0000000..a3b8422
--- /dev/null
+++ b/application/src/components/schedule-incident/ScheduleIncidentContent.tsx
@@ -0,0 +1,126 @@
+
+import React, { useState, useEffect } from 'react';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Plus, CalendarClock, AlertCircle } from "lucide-react";
+import { useLanguage } from "@/contexts/LanguageContext";
+import { ScheduledMaintenanceTab } from "./ScheduledMaintenanceTab";
+import { IncidentManagementTab } from "./IncidentManagementTab";
+import { CreateMaintenanceDialog } from './maintenance/CreateMaintenanceDialog';
+import { CreateIncidentDialog } from './incident/CreateIncidentDialog';
+import { useToast } from '@/hooks/use-toast';
+import { initMaintenanceNotifications, stopMaintenanceNotifications } from '@/services/maintenance/maintenanceNotificationService';
+
+export const ScheduleIncidentContent = () => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+ const [activeTab, setActiveTab] = useState("maintenance");
+ const [createMaintenanceDialogOpen, setCreateMaintenanceDialogOpen] = useState(false);
+ const [createIncidentDialogOpen, setCreateIncidentDialogOpen] = useState(false);
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+ const [incidentRefreshTrigger, setIncidentRefreshTrigger] = useState(0);
+
+ // Initialize maintenance notifications when the component mounts
+ useEffect(() => {
+ console.log("Initializing maintenance notifications");
+ initMaintenanceNotifications();
+
+ // Clean up when the component unmounts
+ return () => {
+ console.log("Cleaning up maintenance notifications");
+ stopMaintenanceNotifications();
+ };
+ }, []);
+
+ const handleCreateButtonClick = () => {
+ if (activeTab === "maintenance") {
+ setCreateMaintenanceDialogOpen(true);
+ } else {
+ setCreateIncidentDialogOpen(true);
+ }
+ };
+
+ const handleMaintenanceCreated = () => {
+ // Refresh data by incrementing the refresh trigger
+ const newTriggerValue = refreshTrigger + 1;
+ console.log("Maintenance created, refreshing data with new trigger value:", newTriggerValue);
+ setRefreshTrigger(newTriggerValue);
+
+ // Show success toast
+ toast({
+ title: t('success'),
+ description: t('maintenanceCreatedSuccess'),
+ });
+ };
+
+ const handleIncidentCreated = () => {
+ // Refresh data by incrementing the refresh trigger
+ const newTriggerValue = incidentRefreshTrigger + 1;
+ console.log("Incident created, refreshing data with new trigger value:", newTriggerValue);
+ setIncidentRefreshTrigger(newTriggerValue);
+
+ // Show success toast
+ toast({
+ title: t('success'),
+ description: t('incidentCreatedSuccess'),
+ });
+ };
+
+ return (
+
+
+
+
+ {t('scheduleIncidentManagement')}
+
+
+
+
+
setActiveTab(value)}
+ >
+
+
+
+ {t('scheduledMaintenance')}
+
+
+
+ {t('incidentManagement')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Maintenance creation dialog */}
+
+
+ {/* Incident creation dialog */}
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/ScheduledMaintenanceTab.tsx b/application/src/components/schedule-incident/ScheduledMaintenanceTab.tsx
new file mode 100644
index 0000000..da82c1a
--- /dev/null
+++ b/application/src/components/schedule-incident/ScheduledMaintenanceTab.tsx
@@ -0,0 +1,160 @@
+
+import React, { useEffect } from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import { MaintenanceTable, MaintenanceStatusChecker } from './maintenance';
+import { EmptyMaintenanceState } from './maintenance/EmptyMaintenanceState';
+import { OverviewCard } from './common/OverviewCard';
+import { Calendar, Clock, CheckCircle } from 'lucide-react';
+import { useMaintenanceData } from './hooks/useMaintenanceData';
+import { Skeleton } from '@/components/ui/skeleton';
+import { useToast } from '@/hooks/use-toast';
+import { LoadingState } from '@/components/services/LoadingState';
+
+interface ScheduledMaintenanceTabProps {
+ refreshTrigger?: number;
+}
+
+export const ScheduledMaintenanceTab = ({ refreshTrigger = 0 }: ScheduledMaintenanceTabProps) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+ const [manualRefresh, setManualRefresh] = React.useState(0);
+
+ // Combine the external refresh trigger with our internal one
+ const combinedRefreshTrigger = refreshTrigger + manualRefresh;
+
+ const {
+ loading,
+ filter,
+ setFilter,
+ maintenanceData,
+ overviewStats,
+ fetchMaintenanceData,
+ isEmpty,
+ error,
+ initialized
+ } = useMaintenanceData({ refreshTrigger: combinedRefreshTrigger });
+
+ // Display toast when error occurs
+ useEffect(() => {
+ if (error) {
+ toast({
+ title: t('error'),
+ description: error,
+ variant: 'destructive',
+ });
+ }
+ }, [error, toast, t]);
+
+ // Force fetch data on initial load
+ useEffect(() => {
+ console.log("ScheduledMaintenanceTab is mounted, fetching data");
+ fetchMaintenanceData(true);
+ }, [fetchMaintenanceData]);
+
+ // Handle maintenance updates
+ const handleMaintenanceUpdated = () => {
+ console.log("Maintenance updated, refreshing data");
+ setManualRefresh(prev => prev + 1);
+ };
+
+ // Handle tab changes
+ const handleTabChange = (value: string) => {
+ setFilter(value);
+ };
+
+ // Show full-page loading state during initial load
+ if (loading && !initialized) {
+ return ;
+ }
+
+ return (
+ <>
+ {/* Status checker for automatic updates */}
+
+
+ {/* Overview Cards */}
+
+ }
+ isLoading={loading}
+ color="blue"
+ />
+ }
+ isLoading={loading}
+ color="amber"
+ />
+ }
+ isLoading={loading}
+ color="green"
+ />
+ }
+ isLoading={loading}
+ color="purple"
+ />
+
+
+
+
+ {t('scheduledMaintenance')}
+
+ {t('scheduledMaintenanceDesc')}
+
+
+
+
+
+ {t('upcomingMaintenance')}
+ {t('ongoingMaintenance')}
+ {t('completedMaintenance')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/application/src/components/schedule-incident/common/OverviewCard.tsx b/application/src/components/schedule-incident/common/OverviewCard.tsx
new file mode 100644
index 0000000..6c96010
--- /dev/null
+++ b/application/src/components/schedule-incident/common/OverviewCard.tsx
@@ -0,0 +1,112 @@
+
+import React, { ReactNode } from 'react';
+import { Card, CardContent } from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useTheme } from "@/contexts/ThemeContext";
+
+interface OverviewCardProps {
+ title: string;
+ value: string | number;
+ description?: string;
+ icon: ReactNode;
+ trend?: {
+ value: number;
+ isPositive: boolean;
+ };
+ className?: string;
+ valueClassName?: string;
+ isLoading?: boolean;
+ color?: string;
+}
+
+export const OverviewCard = ({
+ title,
+ value,
+ description,
+ icon,
+ trend,
+ className,
+ valueClassName,
+ isLoading = false,
+ color = "blue",
+}: OverviewCardProps) => {
+ const { theme } = useTheme();
+
+ // Map color prop to gradient colors
+ const getGradientBackground = () => {
+ const colors = {
+ blue: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(25, 118, 210, 0.8) 0%, rgba(66, 165, 245, 0.6) 100%)"
+ : "linear-gradient(135deg, #1976d2 0%, #42a5f5 100%)",
+ green: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(67, 160, 71, 0.8) 0%, rgba(102, 187, 106, 0.6) 100%)"
+ : "linear-gradient(135deg, #43a047 0%, #66bb6a 100%)",
+ amber: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(255, 152, 0, 0.8) 0%, rgba(255, 183, 77, 0.6) 100%)"
+ : "linear-gradient(135deg, #ff9800 0%, #ffb74d 100%)",
+ red: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(229, 57, 53, 0.8) 0%, rgba(239, 83, 80, 0.6) 100%)"
+ : "linear-gradient(135deg, #e53935 0%, #ef5350 100%)",
+ purple: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(123, 31, 162, 0.8) 0%, rgba(156, 39, 176, 0.6) 100%)"
+ : "linear-gradient(135deg, #7b1fa2 0%, #9c27b0 100%)",
+ orange: theme === 'dark'
+ ? "linear-gradient(135deg, rgba(230, 81, 0, 0.8) 0%, rgba(255, 109, 0, 0.6) 100%)"
+ : "linear-gradient(135deg, #e65100 0%, #ff6d00 100%)",
+ };
+
+ return colors[color as keyof typeof colors] || colors.blue;
+ };
+
+ return (
+
+ {/* Grid Pattern Overlay */}
+
+
+
+
+
+
{title}
+ {isLoading ? (
+
+ ) : (
+
{value}
+ )}
+ {description && (
+
{description}
+ )}
+ {trend && (
+
+
+ {trend.isPositive ? '↑' : '↓'}
+
+ {Math.abs(trend.value)}%
+ vs last month
+
+ )}
+
+
+ {icon}
+
+
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/hooks/index.ts b/application/src/components/schedule-incident/hooks/index.ts
new file mode 100644
index 0000000..eb66169
--- /dev/null
+++ b/application/src/components/schedule-incident/hooks/index.ts
@@ -0,0 +1,3 @@
+
+export * from './useMaintenanceData';
+export * from './useIncidentData';
diff --git a/application/src/components/schedule-incident/hooks/useIncidentData.ts b/application/src/components/schedule-incident/hooks/useIncidentData.ts
new file mode 100644
index 0000000..d6b32cb
--- /dev/null
+++ b/application/src/components/schedule-incident/hooks/useIncidentData.ts
@@ -0,0 +1,200 @@
+import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
+import { incidentService, IncidentItem } from '@/services/incident';
+
+interface UseIncidentDataProps {
+ refreshTrigger?: number;
+}
+
+export const useIncidentData = ({ refreshTrigger = 0 }: UseIncidentDataProps) => {
+ const [incidents, setIncidents] = useState([]);
+ const [filter, setFilter] = useState("unresolved");
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [initialized, setInitialized] = useState(false);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+
+ // Use a ref to prevent multiple simultaneous fetch requests
+ const isFetchingRef = useRef(false);
+ // Use a ref to track the refresh trigger
+ const lastRefreshTriggerRef = useRef(refreshTrigger);
+
+ // Simplified fetch function with improved controls to prevent duplicate calls
+ const fetchIncidentData = useCallback(async (force = false) => {
+ // Skip if already fetching
+ if (isFetchingRef.current) {
+ console.log('Already fetching data, skipping additional request');
+ return;
+ }
+
+ // Skip if not forced and already initialized
+ if (initialized && !force) {
+ console.log('Data already initialized and no force refresh, skipping fetch');
+ return;
+ }
+
+ // Set fetching flags
+ isFetchingRef.current = true;
+
+ if (!initialized || force) {
+ setLoading(true);
+ }
+
+ if (force) {
+ setIsRefreshing(true);
+ }
+
+ setError(null);
+
+ try {
+ console.log(`Fetching incident data (force=${force})`);
+ const allIncidents = await incidentService.getAllIncidents(force);
+
+ if (Array.isArray(allIncidents)) {
+ setIncidents(allIncidents);
+ console.log(`Successfully set ${allIncidents.length} incidents to state`);
+ } else {
+ setIncidents([]);
+ console.warn('No incidents returned from service');
+ }
+
+ setInitialized(true);
+ setLoading(false);
+ setIsRefreshing(false);
+ } catch (error) {
+ console.error('Error fetching incident data:', error);
+ setError('Failed to load incident data. Please try again later.');
+ setIncidents([]);
+ setInitialized(true);
+ setLoading(false);
+ setIsRefreshing(false);
+ } finally {
+ // Reset fetching flag after a slight delay to prevent rapid consecutive calls
+ setTimeout(() => {
+ isFetchingRef.current = false;
+ }, 500);
+ }
+ }, [initialized]);
+
+ // Only fetch when component mounts or refreshTrigger changes
+ useEffect(() => {
+ // Skip if the refresh trigger hasn't changed (prevents duplicate effect calls)
+ if (refreshTrigger === lastRefreshTriggerRef.current && initialized) {
+ console.log('Refresh trigger unchanged, skipping fetch');
+ return;
+ }
+
+ // Update last refresh trigger ref
+ lastRefreshTriggerRef.current = refreshTrigger;
+
+ // Create an abort controller for cleanup
+ const abortController = new AbortController();
+ let isMounted = true;
+
+ console.log(`useIncidentData effect running, refreshTrigger: ${refreshTrigger}`);
+
+ // Use a longer delay to ensure we don't trigger too many API calls
+ const fetchTimer = setTimeout(() => {
+ if (isMounted) {
+ fetchIncidentData(refreshTrigger > 0);
+ }
+ }, 500); // Use a longer delay
+
+ // Cleanup function to abort any in-flight requests and clear timers
+ return () => {
+ console.log('Cleaning up incident data fetch effect');
+ isMounted = false;
+ clearTimeout(fetchTimer);
+ abortController.abort();
+ };
+ }, [fetchIncidentData, refreshTrigger, initialized]);
+
+ // Filter the data based on the current filter
+ const incidentData = useMemo(() => {
+ if (!initialized || incidents.length === 0) return [];
+
+ console.log(`Filtering incidents by: ${filter}`);
+
+ if (filter === "unresolved") {
+ return incidents.filter(item => {
+ const status = (item.status || item.impact_status || '').toLowerCase();
+ return status !== 'resolved';
+ });
+ } else if (filter === "resolved") {
+ return incidents.filter(item => {
+ const status = (item.status || item.impact_status || '').toLowerCase();
+ return status === 'resolved';
+ });
+ }
+
+ return [];
+ }, [filter, incidents, initialized]);
+
+ // Calculate stats for overview cards
+ const overviewStats = useMemo(() => {
+ if (!initialized || incidents.length === 0) {
+ return {
+ unresolved: 0,
+ resolved: 0,
+ critical: 0,
+ highPriority: 0,
+ avgResolutionTime: "0h"
+ };
+ }
+
+ const unresolvedIncidents = incidents.filter(item => {
+ const status = (item.status || item.impact_status || '').toLowerCase();
+ return status !== 'resolved';
+ });
+
+ const resolvedIncidents = incidents.filter(item => {
+ const status = (item.status || item.impact_status || '').toLowerCase();
+ return status === 'resolved';
+ });
+
+ const criticalCount = unresolvedIncidents.filter(item =>
+ (item.priority?.toLowerCase() === 'critical') ||
+ (item.impact?.toLowerCase() === 'critical')
+ ).length;
+
+ const highPriorityCount = unresolvedIncidents.filter(item =>
+ (item.priority?.toLowerCase() === 'high')
+ ).length;
+
+ let avgResolutionTime = "0h";
+ if (resolvedIncidents.length > 0) {
+ const totalHours = resolvedIncidents.reduce((total, item) => {
+ if (!item.created || !item.resolution_time) return total;
+
+ const createdAt = new Date(item.created).getTime();
+ const resolvedAt = new Date(item.resolution_time).getTime();
+ const durationHours = (resolvedAt - createdAt) / (1000 * 60 * 60);
+ return isNaN(durationHours) ? total : total + durationHours;
+ }, 0);
+
+ avgResolutionTime = `${(totalHours / resolvedIncidents.length).toFixed(1)}h`;
+ }
+
+ return {
+ unresolved: unresolvedIncidents.length,
+ resolved: resolvedIncidents.length,
+ critical: criticalCount,
+ highPriority: highPriorityCount,
+ avgResolutionTime
+ };
+ }, [incidents, initialized]);
+
+ const isEmpty = !incidentData.length && initialized && !loading;
+
+ return {
+ filter,
+ setFilter,
+ incidentData,
+ overviewStats,
+ fetchIncidentData,
+ isEmpty,
+ error,
+ loading,
+ initialized,
+ isRefreshing
+ };
+};
diff --git a/application/src/components/schedule-incident/hooks/useMaintenanceData.ts b/application/src/components/schedule-incident/hooks/useMaintenanceData.ts
new file mode 100644
index 0000000..3af4626
--- /dev/null
+++ b/application/src/components/schedule-incident/hooks/useMaintenanceData.ts
@@ -0,0 +1,173 @@
+
+import { useState, useEffect, useCallback, useMemo } from 'react';
+import { maintenanceService, MaintenanceItem } from '@/services/maintenance';
+import { useToast } from '@/hooks/use-toast';
+import { useLanguage } from '@/contexts/LanguageContext';
+
+interface UseMaintenanceDataProps {
+ refreshTrigger?: number;
+}
+
+export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataProps) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+ const [loading, setLoading] = useState(true);
+ const [allMaintenanceData, setAllMaintenanceData] = useState([]);
+ const [filter, setFilter] = useState("upcoming");
+ const [error, setError] = useState(null);
+ const [initialized, setInitialized] = useState(false);
+ const [lastFetchTime, setLastFetchTime] = useState(0);
+
+ // Memoize categorized data to prevent unnecessary recalculations
+ const categorizedData = useMemo(() => {
+ if (!allMaintenanceData.length) return { upcoming: [], ongoing: [], completed: [] };
+
+ const currentDate = new Date();
+ const upcoming: MaintenanceItem[] = [];
+ const ongoing: MaintenanceItem[] = [];
+ const completed: MaintenanceItem[] = [];
+
+ allMaintenanceData.forEach(item => {
+ const status = item.status?.toLowerCase() || '';
+ const startTime = new Date(item.start_time);
+ const endTime = new Date(item.end_time);
+
+ if (status === 'completed' || status === 'cancelled') {
+ completed.push(item);
+ } else if (status === 'in_progress' ||
+ (status === 'scheduled' && startTime <= currentDate && endTime >= currentDate)) {
+ ongoing.push(item);
+ } else if (status === 'scheduled' && startTime > currentDate) {
+ upcoming.push(item);
+ } else {
+ // Default case: treat as upcoming if we can't determine
+ upcoming.push(item);
+ }
+ });
+
+ return { upcoming, ongoing, completed };
+ }, [allMaintenanceData]);
+
+ // Optimized fetch function with improved debouncing
+ const fetchMaintenanceData = useCallback(async (force = false) => {
+ // Prevent excessive fetching with improved debounce logic
+ const now = Date.now();
+ if (!force && now - lastFetchTime < 10000 && lastFetchTime > 0 && initialized) {
+ console.log("Skipping fetch - recently fetched (within 10s)");
+ return;
+ }
+
+ // Only show loading state for initial load, not refreshes
+ if (!initialized) {
+ setLoading(true);
+ }
+
+ setError(null);
+ setLastFetchTime(now);
+
+ try {
+ console.log("Fetching maintenance data from service...");
+ const data = await maintenanceService.getMaintenanceRecords();
+ console.log("Fetched maintenance data, count:", data.length);
+
+ // Log a sample of the first item's assigned_users for debugging
+ if (data.length > 0) {
+ console.log("Sample maintenance item assigned_users:", data[0].id, data[0].assigned_users);
+ }
+
+ // Update state with fetched data
+ setAllMaintenanceData(data);
+ setInitialized(true);
+ } catch (error) {
+ console.error('Error fetching maintenance data:', error);
+ setError('Failed to load maintenance data');
+ toast({
+ title: t('error'),
+ description: t('errorFetchingMaintenanceData'),
+ variant: 'destructive',
+ });
+ } finally {
+ setLoading(false);
+ }
+ }, [t, toast, lastFetchTime, initialized]);
+
+ // Initial fetch on mount with proper cleanup
+ useEffect(() => {
+ console.log("useMaintenanceData hook mounted, fetching data");
+ let isMounted = true;
+
+ const fetchData = async () => {
+ try {
+ await fetchMaintenanceData(true);
+ } catch (err) {
+ console.error("Error in initial fetch:", err);
+ }
+ };
+
+ fetchData();
+
+ // Set up polling with longer interval (5 minutes instead of 3)
+ const intervalId = setInterval(() => {
+ if (isMounted) fetchMaintenanceData(false);
+ }, 300000); // 5 minutes
+
+ return () => {
+ isMounted = false;
+ clearInterval(intervalId);
+ };
+ }, [fetchMaintenanceData]);
+
+ // Handle refresh trigger changes
+ useEffect(() => {
+ if (refreshTrigger > 0) {
+ console.log("Refresh trigger changed, forcing data fetch");
+ fetchMaintenanceData(true);
+ }
+ }, [refreshTrigger, fetchMaintenanceData]);
+
+ // Get filtered data based on current tab
+ const maintenanceData = useMemo(() => {
+ if (!initialized) return [];
+ return categorizedData[filter as keyof typeof categorizedData] || [];
+ }, [filter, categorizedData, initialized]);
+
+ // Calculate overview stats with memoization
+ const overviewStats = useMemo(() => {
+ const { upcoming, ongoing, completed } = categorizedData;
+
+ // Calculate total hours more efficiently
+ const calculateTotalHours = (items: MaintenanceItem[]) => {
+ return items.reduce((total, item) => {
+ try {
+ const start = new Date(item.start_time).getTime();
+ const end = new Date(item.end_time).getTime();
+ const durationHours = (end - start) / (1000 * 60 * 60);
+ return total + (isNaN(durationHours) ? 0 : durationHours);
+ } catch (e) {
+ return total;
+ }
+ }, 0).toFixed(1);
+ };
+
+ return {
+ upcoming: upcoming.length,
+ ongoing: ongoing.length,
+ completed: completed.length,
+ totalDuration: calculateTotalHours([...upcoming, ...ongoing]),
+ };
+ }, [categorizedData]);
+
+ const isEmpty = !loading && maintenanceData.length === 0 && initialized;
+
+ return {
+ loading,
+ filter,
+ setFilter,
+ maintenanceData,
+ overviewStats,
+ fetchMaintenanceData,
+ isEmpty,
+ error,
+ initialized,
+ };
+};
diff --git a/application/src/components/schedule-incident/incident-management/ErrorState.tsx b/application/src/components/schedule-incident/incident-management/ErrorState.tsx
new file mode 100644
index 0000000..0292164
--- /dev/null
+++ b/application/src/components/schedule-incident/incident-management/ErrorState.tsx
@@ -0,0 +1,37 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { Button } from '@/components/ui/button';
+import { RefreshCw } from 'lucide-react';
+
+interface ErrorStateProps {
+ error: string | null;
+ onRefresh: () => void;
+ isRefreshing: boolean;
+}
+
+export const ErrorState: React.FC = ({
+ error,
+ onRefresh,
+ isRefreshing
+}) => {
+ const { t } = useLanguage();
+
+ if (!error) return null;
+
+ return (
+
+
{error}
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident-management/HeaderActions.tsx b/application/src/components/schedule-incident/incident-management/HeaderActions.tsx
new file mode 100644
index 0000000..fa1e9df
--- /dev/null
+++ b/application/src/components/schedule-incident/incident-management/HeaderActions.tsx
@@ -0,0 +1,37 @@
+
+import React from 'react';
+import { CardDescription, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { RefreshCw } from 'lucide-react';
+import { useLanguage } from '@/contexts/LanguageContext';
+
+interface HeaderActionsProps {
+ onRefresh: () => void;
+ isRefreshing: boolean;
+}
+
+export const HeaderActions: React.FC = ({ onRefresh, isRefreshing }) => {
+ const { t } = useLanguage();
+
+ return (
+
+
+ {t('incidentManagement')}
+
+ {t('incidentsManagementDesc')}
+
+
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident-management/IncidentManagementContainer.tsx b/application/src/components/schedule-incident/incident-management/IncidentManagementContainer.tsx
new file mode 100644
index 0000000..bca2878
--- /dev/null
+++ b/application/src/components/schedule-incident/incident-management/IncidentManagementContainer.tsx
@@ -0,0 +1,171 @@
+
+import React, { useState, useCallback, useRef } from 'react';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { useIncidentData } from '../hooks/useIncidentData';
+import { IncidentItem } from '@/services/incident';
+import { useToast } from '@/hooks/use-toast';
+import { OverviewCards } from './OverviewCards';
+import { HeaderActions } from './HeaderActions';
+import { TabContent } from './TabContent';
+import { LoadingState } from '@/components/services/LoadingState';
+import { IncidentDetailDialog } from '../incident/detail-dialog/IncidentDetailDialog';
+
+interface IncidentManagementContainerProps {
+ refreshTrigger?: number;
+}
+
+export const IncidentManagementContainer: React.FC = ({
+ refreshTrigger = 0
+}) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+ const [selectedIncident, setSelectedIncident] = useState(null);
+ const [detailDialogOpen, setDetailDialogOpen] = useState(false);
+ const [manualRefresh, setManualRefresh] = useState(0);
+
+ // Use a ref to debounce multiple refresh requests
+ const refreshTimerRef = useRef(null);
+
+ // Combine the external refresh trigger with our internal one
+ const combinedRefreshTrigger = refreshTrigger + manualRefresh;
+
+ const {
+ filter,
+ setFilter,
+ incidentData,
+ overviewStats,
+ isEmpty,
+ loading,
+ error,
+ initialized,
+ isRefreshing
+ } = useIncidentData({ refreshTrigger: combinedRefreshTrigger });
+
+ // Handle incident updates with debouncing
+ const handleIncidentUpdated = useCallback(() => {
+ console.log('Incident updated, triggering refresh');
+
+ // Clear any existing timer
+ if (refreshTimerRef.current !== null) {
+ window.clearTimeout(refreshTimerRef.current);
+ }
+
+ // Set a new timer to debounce multiple quick updates
+ refreshTimerRef.current = window.setTimeout(() => {
+ setManualRefresh(prev => prev + 1);
+ refreshTimerRef.current = null;
+
+ toast({
+ title: t('incidentUpdated'),
+ description: t('incidentUpdateSuccess'),
+ });
+ }, 300);
+
+ }, [t, toast]);
+
+ // Handle tab changes
+ const handleTabChange = useCallback((value: string) => {
+ console.log(`Tab changed to: ${value}`);
+ setFilter(value);
+ }, [setFilter]);
+
+ // Handle view incident details
+ const handleViewIncidentDetails = useCallback((incident: IncidentItem) => {
+ setSelectedIncident(incident);
+ setDetailDialogOpen(true);
+ }, []);
+
+ // Handle manual refresh
+ const handleManualRefresh = useCallback(() => {
+ console.log('Manual refresh triggered by user');
+ setManualRefresh(prev => prev + 1);
+ }, []);
+
+ // Handle detail dialog close with refresh
+ const handleDetailDialogClose = useCallback((open: boolean) => {
+ setDetailDialogOpen(open);
+ if (!open) {
+ // When dialog closes, refresh data
+ handleManualRefresh();
+ }
+ }, [handleManualRefresh]);
+
+ // Clean up timer on unmount
+ React.useEffect(() => {
+ return () => {
+ if (refreshTimerRef.current !== null) {
+ window.clearTimeout(refreshTimerRef.current);
+ }
+ };
+ }, []);
+
+ // Show full-page loading state during initial load
+ if (loading && !initialized) {
+ return ;
+ }
+
+ return (
+ <>
+ {/* Overview Cards */}
+
+
+
+
+
+
+
+
+
+ {t('unresolvedIncidents')}
+ {t('resolvedIncidents')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Incident Detail Dialog */}
+
+ >
+ );
+};
diff --git a/application/src/components/schedule-incident/incident-management/OverviewCards.tsx b/application/src/components/schedule-incident/incident-management/OverviewCards.tsx
new file mode 100644
index 0000000..454c21d
--- /dev/null
+++ b/application/src/components/schedule-incident/incident-management/OverviewCards.tsx
@@ -0,0 +1,67 @@
+
+import React from 'react';
+import { AlertCircle, CheckCircle, Clock, AlertTriangle, Flag } from 'lucide-react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { OverviewCard } from '../common/OverviewCard';
+
+interface OverviewStatsProps {
+ unresolved: number;
+ resolved: number;
+ critical: number;
+ highPriority: number;
+ avgResolutionTime: string;
+}
+
+interface OverviewCardsProps {
+ overviewStats: OverviewStatsProps;
+ loading: boolean;
+ initialized: boolean;
+}
+
+export const OverviewCards: React.FC = ({
+ overviewStats,
+ loading,
+ initialized
+}) => {
+ const { t } = useLanguage();
+
+ return (
+
+ }
+ isLoading={loading && initialized}
+ color="red"
+ />
+ }
+ isLoading={loading && initialized}
+ color="amber"
+ />
+ }
+ isLoading={loading && initialized}
+ color="orange"
+ />
+ }
+ isLoading={loading && initialized}
+ color="green"
+ />
+ }
+ isLoading={loading && initialized}
+ color="blue"
+ />
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident-management/TabContent.tsx b/application/src/components/schedule-incident/incident-management/TabContent.tsx
new file mode 100644
index 0000000..a457694
--- /dev/null
+++ b/application/src/components/schedule-incident/incident-management/TabContent.tsx
@@ -0,0 +1,47 @@
+
+import React from 'react';
+import { IncidentTable } from '../incident/table/IncidentTable';
+import { EmptyIncidentState } from '../incident/EmptyIncidentState';
+import { ErrorState } from './ErrorState';
+import { IncidentItem } from '@/services/incident';
+
+interface TabContentProps {
+ error: string | null;
+ isEmpty: boolean;
+ data: IncidentItem[];
+ loading: boolean;
+ initialized: boolean;
+ isRefreshing: boolean;
+ onIncidentUpdated: () => void;
+ onViewDetails: (incident: IncidentItem) => void;
+ onRefresh: () => void;
+}
+
+export const TabContent: React.FC = ({
+ error,
+ isEmpty,
+ data,
+ loading,
+ initialized,
+ isRefreshing,
+ onIncidentUpdated,
+ onViewDetails,
+ onRefresh
+}) => {
+ if (error) {
+ return ;
+ }
+
+ if (isEmpty) {
+ return ;
+ }
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident-management/index.ts b/application/src/components/schedule-incident/incident-management/index.ts
new file mode 100644
index 0000000..4ad0804
--- /dev/null
+++ b/application/src/components/schedule-incident/incident-management/index.ts
@@ -0,0 +1,6 @@
+
+export * from './IncidentManagementContainer';
+export * from './OverviewCards';
+export * from './TabContent';
+export * from './ErrorState';
+export * from './HeaderActions';
diff --git a/application/src/components/schedule-incident/incident/CreateIncidentDialog.tsx b/application/src/components/schedule-incident/incident/CreateIncidentDialog.tsx
new file mode 100644
index 0000000..08dbf1c
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/CreateIncidentDialog.tsx
@@ -0,0 +1,96 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Form } from '@/components/ui/form';
+import { useIncidentForm } from './hooks/useIncidentForm';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import {
+ IncidentBasicFields,
+ IncidentAffectedFields,
+ IncidentConfigFields,
+ IncidentDetailsFields,
+} from './form';
+
+interface CreateIncidentDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onIncidentCreated: () => void;
+}
+
+export const CreateIncidentDialog: React.FC = ({
+ open,
+ onOpenChange,
+ onIncidentCreated,
+}) => {
+ const { t } = useLanguage();
+ const { form, onSubmit } = useIncidentForm(
+ onIncidentCreated,
+ () => onOpenChange(false)
+ );
+
+ return (
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/EditIncidentDialog.tsx b/application/src/components/schedule-incident/incident/EditIncidentDialog.tsx
new file mode 100644
index 0000000..4ea6a4a
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/EditIncidentDialog.tsx
@@ -0,0 +1,105 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Form } from '@/components/ui/form';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { useIncidentEditForm } from './hooks/useIncidentEditForm';
+import {
+ IncidentBasicFields,
+ IncidentAffectedFields,
+ IncidentConfigFields,
+ IncidentDetailsFields,
+} from './form';
+import { IncidentItem } from '@/services/incident/types';
+
+interface EditIncidentDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ incident: IncidentItem;
+ onIncidentUpdated: () => void;
+}
+
+export const EditIncidentDialog: React.FC = ({
+ open,
+ onOpenChange,
+ incident,
+ onIncidentUpdated,
+}) => {
+ const { t } = useLanguage();
+
+ const handleClose = () => {
+ onOpenChange(false);
+ };
+
+ const { form, onSubmit } = useIncidentEditForm(
+ incident,
+ onIncidentUpdated,
+ handleClose
+ );
+
+ return (
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/EmptyIncidentState.tsx b/application/src/components/schedule-incident/incident/EmptyIncidentState.tsx
new file mode 100644
index 0000000..25aefa0
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/EmptyIncidentState.tsx
@@ -0,0 +1,18 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { AlertCircle } from 'lucide-react';
+
+export const EmptyIncidentState = () => {
+ const { t } = useLanguage();
+
+ return (
+
+
+
{t('noIncidents')}
+
+ {t('noServices')}
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/IncidentActionsMenu.tsx b/application/src/components/schedule-incident/incident/IncidentActionsMenu.tsx
new file mode 100644
index 0000000..37b6615
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/IncidentActionsMenu.tsx
@@ -0,0 +1,114 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { Button } from '@/components/ui/button';
+import { MoreHorizontal, Eye, Edit, Trash, Check } from 'lucide-react';
+import { useToast } from '@/hooks/use-toast';
+import { updateIncidentStatus, deleteIncident } from '@/services/incident/incidentOperations';
+import { IncidentItem } from '@/services/incident/types';
+
+interface IncidentActionsMenuProps {
+ item: IncidentItem;
+ onIncidentUpdated: () => void;
+ onViewDetails?: (incident: IncidentItem) => void;
+ onEditIncident?: (incident: IncidentItem) => void;
+}
+
+export const IncidentActionsMenu = ({
+ item,
+ onIncidentUpdated,
+ onViewDetails,
+ onEditIncident
+}: IncidentActionsMenuProps) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+
+ const handleResolveIncident = async () => {
+ try {
+ await updateIncidentStatus(item.id, 'resolved');
+ toast({
+ title: t('success'),
+ description: t('incidentResolved'),
+ });
+ onIncidentUpdated();
+ } catch (error) {
+ console.error('Error resolving incident:', error);
+ toast({
+ title: t('error'),
+ description: t('errorResolvingIncident'),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const handleDeleteIncident = async () => {
+ try {
+ await deleteIncident(item.id);
+ toast({
+ title: t('success'),
+ description: t('incidentDeleted'),
+ });
+ onIncidentUpdated();
+ } catch (error) {
+ console.error('Error deleting incident:', error);
+ toast({
+ title: t('error'),
+ description: t('errorDeletingIncident'),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const handleEditClick = () => {
+ if (onEditIncident) {
+ onEditIncident(item);
+ } else {
+ console.log(`Edit incident ${item.id}`);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {t('actions')}
+
+ {onViewDetails && (
+ onViewDetails(item)}>
+
+ {t('view')}
+
+ )}
+
+
+ {t('edit')}
+
+
+
+ {t('resolve')}
+
+
+
+
+ {t('delete')}
+
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/IncidentStatusBadge.tsx b/application/src/components/schedule-incident/incident/IncidentStatusBadge.tsx
new file mode 100644
index 0000000..3e76f66
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/IncidentStatusBadge.tsx
@@ -0,0 +1,105 @@
+
+import React from 'react';
+import { Badge } from '@/components/ui/badge';
+import { useLanguage } from '@/contexts/LanguageContext';
+import {
+ AlertCircle,
+ CheckCircle,
+ Gauge,
+ Search,
+ Wrench,
+ LucideIcon
+} from 'lucide-react';
+
+interface IncidentStatusBadgeProps {
+ status: string;
+}
+
+type StatusConfig = {
+ label: string;
+ variant: 'outline' | 'default' | 'secondary' | 'destructive';
+ icon: LucideIcon;
+ className?: string;
+}
+
+export const IncidentStatusBadge = ({ status }: IncidentStatusBadgeProps) => {
+ const { t } = useLanguage();
+
+ // Normalize the status string
+ const normalizedStatus = (status || '').toLowerCase();
+
+ // Status configuration map
+ const statusConfigs: Record = {
+ 'investigating': {
+ label: t('investigating'),
+ variant: 'destructive',
+ icon: Search,
+ className: 'bg-red-100 border-red-200 text-red-700 hover:bg-red-100',
+ },
+ 'identified': {
+ label: t('identified'),
+ variant: 'secondary',
+ icon: AlertCircle,
+ className: 'bg-amber-100 border-amber-200 text-amber-700 hover:bg-amber-100',
+ },
+ 'found_root_cause': {
+ label: t('rootCauseFound'),
+ variant: 'secondary',
+ icon: AlertCircle,
+ className: 'bg-amber-100 border-amber-200 text-amber-700 hover:bg-amber-100',
+ },
+ 'completed': {
+ label: t('completed'),
+ variant: 'default',
+ icon: CheckCircle,
+ className: 'bg-green-100 border-green-200 text-green-700 hover:bg-green-100',
+ },
+ 'in_progress': {
+ label: t('inProgress'),
+ variant: 'default',
+ icon: Wrench,
+ className: 'bg-blue-100 border-blue-200 text-blue-700 hover:bg-blue-100',
+ },
+ 'inprogress': {
+ label: t('inProgress'),
+ variant: 'default',
+ icon: Wrench,
+ className: 'bg-blue-100 border-blue-200 text-blue-700 hover:bg-blue-100',
+ },
+ 'monitoring': {
+ label: t('monitoring'),
+ variant: 'outline',
+ icon: Gauge,
+ className: 'bg-purple-100 border-purple-200 text-purple-700 hover:bg-purple-100',
+ },
+ 'resolved': {
+ label: t('resolved'),
+ variant: 'default',
+ icon: CheckCircle,
+ className: 'bg-green-100 border-green-200 text-green-700 hover:bg-green-100',
+ }
+ };
+
+ // Find the appropriate config, defaulting to investigating if not found
+ const getStatusConfig = (): StatusConfig => {
+ for (const [key, config] of Object.entries(statusConfigs)) {
+ if (normalizedStatus.includes(key)) {
+ return config;
+ }
+ }
+ return statusConfigs['investigating'];
+ };
+
+ const config = getStatusConfig();
+ const Icon = config.icon;
+
+ return (
+
+
+ {config.label}
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/IncidentStatusDropdown.tsx b/application/src/components/schedule-incident/incident/IncidentStatusDropdown.tsx
new file mode 100644
index 0000000..661e23a
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/IncidentStatusDropdown.tsx
@@ -0,0 +1,105 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { AlertCircle, CheckCircle, Gauge, Search, Wrench } from 'lucide-react';
+import { useToast } from '@/hooks/use-toast';
+import { updateIncidentStatus } from '@/services/incident/incidentOperations';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { IncidentStatusBadge } from './IncidentStatusBadge';
+
+interface IncidentStatusDropdownProps {
+ status: string;
+ id: string;
+ onStatusUpdated: () => void;
+ disabled?: boolean;
+}
+
+export const IncidentStatusDropdown = ({
+ status,
+ id,
+ onStatusUpdated,
+ disabled = false
+}: IncidentStatusDropdownProps) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+ const [localStatus, setLocalStatus] = React.useState(status);
+
+ // Update local status when prop changes
+ React.useEffect(() => {
+ setLocalStatus(status);
+ }, [status]);
+
+ const statusOptions = [
+ { value: 'investigating', label: t('investigating'), icon: },
+ { value: 'identified', label: t('identified'), icon: },
+ { value: 'found_root_cause', label: t('foundRootCause'), icon: },
+ { value: 'in_progress', label: t('inProgress'), icon: },
+ { value: 'monitoring', label: t('monitoring'), icon: },
+ { value: 'resolved', label: t('resolved'), icon: },
+ ];
+
+ const handleStatusChange = async (newStatus: string) => {
+ try {
+ // Don't update if the status is the same
+ if (localStatus === newStatus) {
+ return;
+ }
+
+ console.log(`Changing incident status from ${localStatus} to ${newStatus}`);
+
+ // Optimistically update the UI immediately
+ setLocalStatus(newStatus);
+
+ // Make the API call in the background
+ await updateIncidentStatus(id, newStatus);
+
+ toast({
+ title: t('statusUpdated'),
+ description: t('incidentStatusUpdated'),
+ });
+
+ // Notify parent components about the status change
+ onStatusUpdated();
+ console.log('Status update complete, UI refresh triggered');
+ } catch (error) {
+ console.error('Error updating incident status:', error);
+
+ // Revert to the original status on error
+ setLocalStatus(status);
+
+ toast({
+ title: t('error'),
+ description: t('failedToUpdateStatus'),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+ {statusOptions.map((option) => (
+ {
+ e.stopPropagation(); // Prevent event from bubbling to table row click
+ handleStatusChange(option.value);
+ }}
+ >
+ {option.icon}
+ {option.label}
+
+ ))}
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/IncidentTable.tsx b/application/src/components/schedule-incident/incident/IncidentTable.tsx
new file mode 100644
index 0000000..5512abc
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/IncidentTable.tsx
@@ -0,0 +1,3 @@
+
+// Re-export the refactored IncidentTable component
+export { IncidentTable } from './table/IncidentTable';
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailContent.tsx b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailContent.tsx
new file mode 100644
index 0000000..7fe8737
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailContent.tsx
@@ -0,0 +1,76 @@
+
+import React from 'react';
+import { IncidentItem } from '@/services/incident/types';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { IncidentDetailHeader } from './IncidentDetailHeader';
+import { Separator } from '@/components/ui/separator';
+import {
+ BasicInfoSection,
+ TimelineSection,
+ AffectedSystemsSection,
+ ResolutionSection
+} from './sections';
+import { IncidentDetailFooter } from './IncidentDetailFooter';
+import { useQuery } from '@tanstack/react-query';
+import { userService } from '@/services/userService';
+
+interface IncidentDetailContentProps {
+ incident: IncidentItem;
+ onClose: () => void;
+ assignedUser: any | null;
+}
+
+export const IncidentDetailContent = ({
+ incident,
+ onClose,
+ assignedUser
+}: IncidentDetailContentProps) => {
+ // Fetch assigned user details if one wasn't provided and there's an assigned_to field
+ const { data: fetchedUser } = useQuery({
+ queryKey: ['user', incident?.assigned_to],
+ queryFn: async () => {
+ if (!incident?.assigned_to) return null;
+ try {
+ return await userService.getUser(incident.assigned_to);
+ } catch (error) {
+ console.error("Failed to fetch assigned user:", error);
+ return null;
+ }
+ },
+ enabled: !!incident?.assigned_to && !assignedUser,
+ staleTime: 300000 // Cache for 5 minutes
+ });
+
+ // Use the provided assignedUser or the one we fetched
+ const userToDisplay = assignedUser || fetchedUser;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailDialog.tsx b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailDialog.tsx
new file mode 100644
index 0000000..e0d26bf
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailDialog.tsx
@@ -0,0 +1,54 @@
+
+import React from 'react';
+import { Dialog, DialogContent } from '@/components/ui/dialog';
+import { IncidentItem } from '@/services/incident/types';
+import { useQuery } from '@tanstack/react-query';
+import { userService } from '@/services/userService';
+import { IncidentDetailContent } from './IncidentDetailContent';
+
+interface IncidentDetailDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ incident: IncidentItem | null;
+}
+
+export const IncidentDetailDialog = ({
+ open,
+ onOpenChange,
+ incident
+}: IncidentDetailDialogProps) => {
+ // Fetch user data for assigned_to field
+ const { data: users = [] } = useQuery({
+ queryKey: ['users'],
+ queryFn: async () => {
+ const usersList = await userService.getUsers();
+ return Array.isArray(usersList) ? usersList : [];
+ },
+ staleTime: 300000, // Cache for 5 minutes
+ enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
+ });
+
+ // Find the assigned user
+ const assignedUser = incident?.assigned_to
+ ? users.find(user => user.id === incident.assigned_to)
+ : null;
+
+ if (!incident) return null;
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailFooter.tsx b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailFooter.tsx
new file mode 100644
index 0000000..e2bbcce
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailFooter.tsx
@@ -0,0 +1,31 @@
+
+import React from 'react';
+import { IncidentItem } from '@/services/incident';
+import { Separator } from '@/components/ui/separator';
+import { CloseButton, DownloadPdfButton, PrintButton } from './components';
+import { useLanguage } from '@/contexts/LanguageContext';
+
+interface IncidentDetailFooterProps {
+ onClose: () => void;
+ incident: IncidentItem;
+}
+
+export const IncidentDetailFooter: React.FC = ({
+ onClose,
+ incident,
+}) => {
+ const { t } = useLanguage();
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailHeader.tsx b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailHeader.tsx
new file mode 100644
index 0000000..ee34373
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailHeader.tsx
@@ -0,0 +1,19 @@
+
+import React from 'react';
+import { DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import { IncidentItem } from '@/services/incident/types';
+
+interface IncidentDetailHeaderProps {
+ incident: IncidentItem;
+}
+
+export const IncidentDetailHeader = ({ incident }: IncidentDetailHeaderProps) => {
+ return (
+
+
+ {incident.title || 'Incident Details'}
+ #{incident.id}
+
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailSections.tsx b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailSections.tsx
new file mode 100644
index 0000000..b33f15e
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/IncidentDetailSections.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+import { Separator } from '@/components/ui/separator';
+import { useQuery } from '@tanstack/react-query';
+import { userService } from '@/services/userService';
+
+// Import all section components from the new location
+import {
+ BasicInfoSection,
+ TimelineSection,
+ AffectedSystemsSection,
+ ResolutionSection
+} from './sections';
+
+// Re-export all section components for compatibility with existing imports
+export {
+ BasicInfoSection,
+ TimelineSection,
+ AffectedSystemsSection,
+ ResolutionSection
+};
+
+// Legacy component - keeping this for backward compatibility with other imports
+export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
+ // Fetch assigned user details if there's an assigned_to field
+ const { data: assignedUser } = useQuery({
+ queryKey: ['user', incident?.assigned_to],
+ queryFn: async () => {
+ if (!incident?.assigned_to) return null;
+ try {
+ return await userService.getUser(incident.assigned_to);
+ } catch (error) {
+ console.error("Failed to fetch assigned user:", error);
+ return null;
+ }
+ },
+ enabled: !!incident?.assigned_to,
+ staleTime: 300000 // Cache for 5 minutes
+ });
+
+ if (!incident) return null;
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/components/CloseButton.tsx b/application/src/components/schedule-incident/incident/detail-dialog/components/CloseButton.tsx
new file mode 100644
index 0000000..bec23d4
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/components/CloseButton.tsx
@@ -0,0 +1,26 @@
+
+import React from 'react';
+import { Button } from '@/components/ui/button';
+import { useLanguage } from '@/contexts/LanguageContext';
+
+interface CloseButtonProps {
+ onClose: () => void;
+ className?: string;
+}
+
+export const CloseButton: React.FC = ({
+ onClose,
+ className
+}) => {
+ const { t } = useLanguage();
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/components/DownloadPdfButton.tsx b/application/src/components/schedule-incident/incident/detail-dialog/components/DownloadPdfButton.tsx
new file mode 100644
index 0000000..d3b3ebe
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/components/DownloadPdfButton.tsx
@@ -0,0 +1,31 @@
+
+import React from 'react';
+import { Button } from '@/components/ui/button';
+import { Download } from 'lucide-react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { useDownloadIncidentPdf } from '../hooks';
+import { IncidentItem } from '@/services/incident';
+
+interface DownloadPdfButtonProps {
+ incident: IncidentItem;
+ className?: string;
+}
+
+export const DownloadPdfButton: React.FC = ({
+ incident,
+ className
+}) => {
+ const { t } = useLanguage();
+ const { handleDownloadPDF } = useDownloadIncidentPdf();
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/components/PrintButton.tsx b/application/src/components/schedule-incident/incident/detail-dialog/components/PrintButton.tsx
new file mode 100644
index 0000000..27aa1b4
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/components/PrintButton.tsx
@@ -0,0 +1,26 @@
+
+import React from 'react';
+import { Button } from '@/components/ui/button';
+import { Printer } from 'lucide-react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { usePrintIncident } from '../hooks';
+
+interface PrintButtonProps {
+ className?: string;
+}
+
+export const PrintButton: React.FC = ({ className }) => {
+ const { t } = useLanguage();
+ const { handlePrint } = usePrintIncident();
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/components/index.ts b/application/src/components/schedule-incident/incident/detail-dialog/components/index.ts
new file mode 100644
index 0000000..b66885a
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/components/index.ts
@@ -0,0 +1,4 @@
+
+export * from './PrintButton';
+export * from './DownloadPdfButton';
+export * from './CloseButton';
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/hooks/index.ts b/application/src/components/schedule-incident/incident/detail-dialog/hooks/index.ts
new file mode 100644
index 0000000..2862893
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/hooks/index.ts
@@ -0,0 +1,3 @@
+
+export * from './usePrintIncident';
+export * from './useDownloadIncidentPdf';
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/hooks/useDownloadIncidentPdf.ts b/application/src/components/schedule-incident/incident/detail-dialog/hooks/useDownloadIncidentPdf.ts
new file mode 100644
index 0000000..8762384
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/hooks/useDownloadIncidentPdf.ts
@@ -0,0 +1,29 @@
+
+import { useToast } from '@/hooks/use-toast';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem, incidentService } from '@/services/incident';
+
+export const useDownloadIncidentPdf = () => {
+ const { toast } = useToast();
+ const { t } = useLanguage();
+
+ const handleDownloadPDF = async (incident: IncidentItem) => {
+ try {
+ await incidentService.generateIncidentPDF(incident);
+
+ toast({
+ title: t('success'),
+ description: t('pdfDownloaded'),
+ });
+ } catch (error) {
+ console.error("Error generating PDF:", error);
+ toast({
+ title: t('error'),
+ description: t('pdfGenerationFailed'),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return { handleDownloadPDF };
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/hooks/usePrintIncident.ts b/application/src/components/schedule-incident/incident/detail-dialog/hooks/usePrintIncident.ts
new file mode 100644
index 0000000..f9554dd
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/hooks/usePrintIncident.ts
@@ -0,0 +1,173 @@
+
+import React from 'react';
+import { useToast } from '@/hooks/use-toast';
+import { useLanguage } from '@/contexts/LanguageContext';
+
+export const usePrintIncident = () => {
+ const { toast } = useToast();
+ const { t } = useLanguage();
+
+ const handlePrint = React.useCallback(() => {
+ try {
+ // Add print-specific stylesheet temporarily
+ const style = document.createElement('style');
+ style.id = 'print-style';
+ style.textContent = `
+ @page {
+ size: A4;
+ margin: 10mm;
+ }
+ @media print {
+ body * {
+ visibility: hidden;
+ }
+
+ .dialog-content, .dialog-content * {
+ visibility: visible;
+ }
+
+ .dialog-content {
+ position: absolute !important;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: auto;
+ padding: 15mm !important;
+ margin: 0 !important;
+ background-color: white !important;
+ box-shadow: none;
+ overflow: visible !important;
+ display: block !important;
+ transform: none !important;
+ }
+
+ /* Remove any black backgrounds */
+ html, body {
+ background-color: white !important;
+ color: black !important;
+ }
+
+ /* Optimize spacing for single page */
+ .print-section {
+ margin-bottom: 2mm !important;
+ page-break-inside: avoid !important;
+ }
+
+ /* Reduce vertical spacing */
+ h4 {
+ margin-bottom: 1mm !important;
+ margin-top: 1mm !important;
+ color: #1e40af !important; /* blue-800 */
+ font-weight: bold !important;
+ }
+
+ .print-compact-text {
+ font-size: 9pt !important;
+ line-height: 1.2 !important;
+ }
+
+ .print-compact-spacing > * {
+ margin-top: 1mm !important;
+ margin-bottom: 1mm !important;
+ }
+
+ .print-grid {
+ display: grid !important;
+ grid-template-columns: 1fr 1fr !important;
+ gap: 1mm !important;
+ }
+
+ .badge-print {
+ background-color: #dbeafe !important; /* blue-100 */
+ color: #1e40af !important; /* blue-800 */
+ border: 1px solid #93c5fd !important; /* blue-300 */
+ padding: 0.5mm 1mm !important;
+ margin: 0.5mm !important;
+ display: inline-block !important;
+ font-size: 8pt !important;
+ }
+
+ /* More compact spacing */
+ .print-compact-margin {
+ margin: 1mm 0 !important;
+ }
+
+ .print-smaller-font {
+ font-size: 8pt !important;
+ }
+
+ .header-print {
+ background-color: #1e40af !important; /* blue-800 */
+ color: #ffffff !important;
+ padding: 2mm 3mm !important;
+ margin-bottom: 2mm !important;
+ }
+
+ /* More compact header */
+ .header-print h1 {
+ font-size: 12pt !important;
+ margin-bottom: 0 !important;
+ }
+
+ .header-print p {
+ font-size: 9pt !important;
+ margin-top: 1mm !important;
+ }
+
+ /* Footer stays at bottom */
+ .footer-print {
+ font-size: 7pt !important;
+ color: #6b7280 !important; /* gray-500 */
+ border-top: 1px solid #e5e7eb !important; /* gray-200 */
+ position: fixed !important;
+ bottom: 10mm !important;
+ left: 15mm !important;
+ right: 15mm !important;
+ padding-top: 2mm !important;
+ background-color: white !important;
+ }
+
+ /* Hide any unnecessary elements */
+ .print-hide {
+ display: none !important;
+ }
+
+ /* Optimize description text */
+ .print-description {
+ max-height: 100px !important;
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Wait a bit to ensure styles are applied
+ setTimeout(() => {
+ window.print();
+
+ // Remove the style after printing dialog closes
+ setTimeout(() => {
+ const printStyle = document.getElementById('print-style');
+ if (printStyle) {
+ printStyle.remove();
+ }
+ }, 1000);
+ }, 100);
+
+ toast({
+ title: t('success'),
+ description: t('printJobStarted'),
+ });
+ } catch (error) {
+ console.error("Error printing:", error);
+ toast({
+ title: t('error'),
+ description: t('printingFailed'),
+ variant: 'destructive',
+ });
+ }
+ }, [t, toast]);
+
+ return { handlePrint };
+};
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/index.ts b/application/src/components/schedule-incident/incident/detail-dialog/index.ts
new file mode 100644
index 0000000..6d33ea6
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/index.ts
@@ -0,0 +1,6 @@
+
+export * from './IncidentDetailDialog';
+export * from './IncidentDetailHeader';
+export * from './IncidentDetailContent';
+export * from './IncidentDetailSections';
+export * from './IncidentDetailFooter';
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/AffectedSystemsSection.tsx b/application/src/components/schedule-incident/incident/detail-dialog/sections/AffectedSystemsSection.tsx
new file mode 100644
index 0000000..a029b94
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/AffectedSystemsSection.tsx
@@ -0,0 +1,66 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+import { Badge } from '@/components/ui/badge';
+import { getAffectedSystemsArray } from './utils';
+
+interface AffectedSystemsSectionProps {
+ incident: IncidentItem | null;
+ assignedUser?: any | null;
+}
+
+export const AffectedSystemsSection: React.FC = ({ incident }) => {
+ const { t } = useLanguage();
+
+ if (!incident) return null;
+
+ return (
+
+
{t('affectedSystems')}
+
+
+
+
{t('systems')}
+
+ {getAffectedSystemsArray(incident.affected_systems).map((system, idx) => (
+ {system}
+ ))}
+ {getAffectedSystemsArray(incident.affected_systems).length === 0 &&
+ {t('noSystems')}
+ }
+
+
+
+
+
+
{t('impact')}
+
+
+ {t(incident.impact?.toLowerCase() || 'low')}
+
+
+
+
+
+
{t('priority')}
+
+
+ {t(incident.priority?.toLowerCase() || 'low')}
+
+
+
+
+
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/AssignmentSection.tsx b/application/src/components/schedule-incident/incident/detail-dialog/sections/AssignmentSection.tsx
new file mode 100644
index 0000000..2b41230
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/AssignmentSection.tsx
@@ -0,0 +1,44 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { User } from '@/services/userService';
+import { getUserInitials } from './utils';
+
+interface AssignmentSectionProps {
+ incident: IncidentItem | null;
+ assignedUser?: User | null;
+}
+
+export const AssignmentSection: React.FC = ({ incident, assignedUser }) => {
+ const { t } = useLanguage();
+
+ if (!incident) return null;
+
+ return (
+
+
{t('assignment')}
+
+
+
{t('assignedTo')}
+
+ {assignedUser ? (
+
+
+
+ {getUserInitials(assignedUser)}
+
+
{assignedUser.full_name || assignedUser.username}
+
+ ) : incident.assigned_to ? (
+
{incident.assigned_to}
+ ) : (
+
{t('unassigned')}
+ )}
+
+
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/BasicInfoSection.tsx b/application/src/components/schedule-incident/incident/detail-dialog/sections/BasicInfoSection.tsx
new file mode 100644
index 0000000..e343a2d
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/BasicInfoSection.tsx
@@ -0,0 +1,69 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+import { User } from '@/services/userService';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { IncidentStatusBadge } from '../../IncidentStatusBadge';
+import { getUserInitials } from './utils';
+
+interface BasicInfoSectionProps {
+ incident: IncidentItem | null;
+ assignedUser?: User | null;
+}
+
+export const BasicInfoSection: React.FC = ({ incident, assignedUser }) => {
+ const { t } = useLanguage();
+
+ if (!incident) return null;
+
+ return (
+
+
{t('basicInfo')}
+
+
+
+
{t('title')}
+
{incident.title || '-'}
+
+
+
+
+
+
{t('serviceId')}
+
{incident.service_id || '-'}
+
+
+
+
{t('assignedTo')}
+
+ {assignedUser ? (
+
+
+
+ {getUserInitials(assignedUser)}
+
+
{assignedUser.full_name || assignedUser.username}
+
+ ) : incident.assigned_to ? (
+
{incident.assigned_to}
+ ) : (
+
{t('unassigned')}
+ )}
+
+
+
+
+
+
{t('description')}
+
{incident.description || '-'}
+
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/ImpactAnalysisSection.tsx b/application/src/components/schedule-incident/incident/detail-dialog/sections/ImpactAnalysisSection.tsx
new file mode 100644
index 0000000..2e9fe90
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/ImpactAnalysisSection.tsx
@@ -0,0 +1,51 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+import { Badge } from '@/components/ui/badge';
+
+interface ImpactAnalysisSectionProps {
+ incident: IncidentItem | null;
+ assignedUser?: any | null;
+}
+
+export const ImpactAnalysisSection: React.FC = ({ incident }) => {
+ const { t } = useLanguage();
+
+ if (!incident) return null;
+
+ return (
+
+
{t('impactAnalysis')}
+
+
+
+
{t('impact')}
+
+
+ {t(incident.impact?.toLowerCase() || 'low')}
+
+
+
+
+
+
{t('priority')}
+
+
+ {t(incident.priority?.toLowerCase() || 'low')}
+
+
+
+
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/ResolutionSection.tsx b/application/src/components/schedule-incident/incident/detail-dialog/sections/ResolutionSection.tsx
new file mode 100644
index 0000000..7f94059
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/ResolutionSection.tsx
@@ -0,0 +1,39 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+
+interface ResolutionSectionProps {
+ incident: IncidentItem | null;
+ assignedUser?: any | null;
+}
+
+export const ResolutionSection: React.FC = ({ incident }) => {
+ const { t } = useLanguage();
+
+ if (!incident) return null;
+
+ return (
+
+
{t('resolutionDetails')}
+
+
+
+
{t('rootCause')}
+
{incident.root_cause || '-'}
+
+
+
+
{t('resolutionSteps')}
+
{incident.resolution_steps || '-'}
+
+
+
+
{t('lessonsLearned')}
+
{incident.lessons_learned || '-'}
+
+
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/TimelineSection.tsx b/application/src/components/schedule-incident/incident/detail-dialog/sections/TimelineSection.tsx
new file mode 100644
index 0000000..2f2b203
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/TimelineSection.tsx
@@ -0,0 +1,45 @@
+
+import React from 'react';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentItem } from '@/services/incident';
+import { formatDate } from './utils';
+
+interface TimelineSectionProps {
+ incident: IncidentItem | null;
+ assignedUser?: any | null;
+}
+
+export const TimelineSection: React.FC = ({ incident }) => {
+ const { t } = useLanguage();
+
+ if (!incident) return null;
+
+ return (
+
+
{t('timeline')}
+
+
+
+
{t('created')}
+
{formatDate(incident.created)}
+
+
+
+
{t('lastUpdated')}
+
{formatDate(incident.updated)}
+
+
+
+
{t('incidentTime')}
+
{formatDate(incident.timestamp)}
+
+
+
+
{t('resolutionTime')}
+
{formatDate(incident.resolution_time)}
+
+
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/index.ts b/application/src/components/schedule-incident/incident/detail-dialog/sections/index.ts
new file mode 100644
index 0000000..a3245ff
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/index.ts
@@ -0,0 +1,7 @@
+
+// Export all section components
+export * from './BasicInfoSection';
+export * from './TimelineSection';
+export * from './AffectedSystemsSection';
+export * from './ResolutionSection';
+export * from './utils';
diff --git a/application/src/components/schedule-incident/incident/detail-dialog/sections/utils.ts b/application/src/components/schedule-incident/incident/detail-dialog/sections/utils.ts
new file mode 100644
index 0000000..b4409e1
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/detail-dialog/sections/utils.ts
@@ -0,0 +1,32 @@
+
+import { User } from '@/services/userService';
+import { format } from 'date-fns';
+
+// Helper function to get user initials
+export const getUserInitials = (user: User): string => {
+ if (user.full_name) {
+ const nameParts = user.full_name.split(' ');
+ if (nameParts.length > 1) {
+ return `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase();
+ }
+ return user.full_name.substring(0, 2).toUpperCase();
+ }
+ return user.username.substring(0, 2).toUpperCase();
+};
+
+// Format date helper
+export const formatDate = (dateStr?: string) => {
+ if (!dateStr) return '-';
+ try {
+ return format(new Date(dateStr), 'PPp');
+ } catch (error) {
+ console.error('Error formatting date:', dateStr, error);
+ return dateStr;
+ }
+};
+
+// Get affected systems helper
+export const getAffectedSystemsArray = (systems?: string) => {
+ if (!systems) return [];
+ return systems.split(',').map(system => system.trim()).filter(Boolean);
+};
diff --git a/application/src/components/schedule-incident/incident/form/IncidentAffectedFields.tsx b/application/src/components/schedule-incident/incident/form/IncidentAffectedFields.tsx
new file mode 100644
index 0000000..8213bc2
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/form/IncidentAffectedFields.tsx
@@ -0,0 +1,56 @@
+
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentFormValues } from '../hooks/useIncidentForm';
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+
+export const IncidentAffectedFields: React.FC = () => {
+ const { t } = useLanguage();
+ const { control } = useFormContext();
+
+ return (
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/form/IncidentBasicFields.tsx b/application/src/components/schedule-incident/incident/form/IncidentBasicFields.tsx
new file mode 100644
index 0000000..33b35d5
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/form/IncidentBasicFields.tsx
@@ -0,0 +1,201 @@
+
+import React, { useEffect } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentFormValues } from '../hooks/useIncidentForm';
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { useQuery } from '@tanstack/react-query';
+import { userService } from '@/services/userService';
+import { Badge } from '@/components/ui/badge';
+import { X, Users } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+
+export const IncidentBasicFields: React.FC = () => {
+ const { t } = useLanguage();
+ const form = useFormContext();
+ const currentAssignedUserId = form.watch('assigned_to');
+
+ // For assigned users functionality
+ const [selectedUserIds, setSelectedUserIds] = React.useState([]);
+
+ // Update selectedUserIds when form value changes (for edit mode)
+ useEffect(() => {
+ if (currentAssignedUserId) {
+ setSelectedUserIds([currentAssignedUserId]);
+ }
+ }, [currentAssignedUserId]);
+
+ // Fetch users for assignment
+ const { data: users = [], isLoading } = useQuery({
+ queryKey: ['users'],
+ queryFn: async () => {
+ try {
+ const usersList = await userService.getUsers();
+ console.log("Fetched users for incident assignment:", usersList);
+ return Array.isArray(usersList) ? usersList : [];
+ } catch (error) {
+ console.error("Failed to fetch users:", error);
+ return [];
+ }
+ },
+ staleTime: 300000 // Cache for 5 minutes
+ });
+
+ // Add user to assigned_to
+ const addUser = (userId: string) => {
+ // For now, we're using a single user assignment
+ console.log("Setting user ID in form:", userId);
+ form.setValue('assigned_to', userId, { shouldValidate: true, shouldDirty: true });
+ setSelectedUserIds([userId]);
+ };
+
+ // Remove assigned user
+ const removeUser = () => {
+ form.setValue('assigned_to', '', { shouldValidate: true, shouldDirty: true });
+ setSelectedUserIds([]);
+ };
+
+ // Get selected user data
+ const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
+
+ // Function to get user initials from name
+ const getUserInitials = (user: any): string => {
+ if (user.full_name) {
+ const nameParts = user.full_name.split(' ');
+ if (nameParts.length > 1) {
+ return `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase();
+ }
+ return user.full_name.substring(0, 2).toUpperCase();
+ }
+ return user.username.substring(0, 2).toUpperCase();
+ };
+
+ return (
+
+
(
+
+ {t('title')}
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ {t('description')}
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ {t('serviceId')}
+
+
+
+
+
+ )}
+ />
+
+ (
+
+
+ {t('assignedTo')}
+
+
+
+
+
+
+ {selectedUser ? (
+
+
+
+
+
+ {getUserInitials(selectedUser)}
+
+
+ {selectedUser.full_name || selectedUser.username}
+
+
+
+ ) : (
+
+ {t('noAssignedUser')}
+
+ )}
+
+
+
+ )}
+ />
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/form/IncidentConfigFields.tsx b/application/src/components/schedule-incident/incident/form/IncidentConfigFields.tsx
new file mode 100644
index 0000000..56b86ed
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/form/IncidentConfigFields.tsx
@@ -0,0 +1,119 @@
+
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentFormValues } from '../hooks/useIncidentForm';
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormMessage,
+} from '@/components/ui/form';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
+import { Label } from '@/components/ui/label';
+
+export const IncidentConfigFields: React.FC = () => {
+ const { t } = useLanguage();
+ const { control } = useFormContext();
+
+ return (
+
+
(
+
+ {t('status')}
+
+
+
+ )}
+ />
+
+ (
+
+ {t('impact')}
+
+
+
+
+
+
+ •
+
+
+
+
+ •
+
+
+
+
+ •
+
+
+
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ {t('priority')}
+
+
+
+ )}
+ />
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/form/IncidentDetailsFields.tsx b/application/src/components/schedule-incident/incident/form/IncidentDetailsFields.tsx
new file mode 100644
index 0000000..0134a8d
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/form/IncidentDetailsFields.tsx
@@ -0,0 +1,59 @@
+
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentFormValues } from '../hooks/useIncidentForm';
+import {
+ FormField,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormMessage,
+} from '@/components/ui/form';
+import { Textarea } from '@/components/ui/textarea';
+
+export const IncidentDetailsFields: React.FC = () => {
+ const { t } = useLanguage();
+ const { control } = useFormContext();
+
+ return (
+
+ (
+
+ {t('resolutionSteps')}
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ {t('lessonsLearned')}
+
+
+
+
+
+ )}
+ />
+
+ );
+};
+
diff --git a/application/src/components/schedule-incident/incident/form/index.ts b/application/src/components/schedule-incident/incident/form/index.ts
new file mode 100644
index 0000000..9b06576
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/form/index.ts
@@ -0,0 +1,5 @@
+
+export * from './IncidentBasicFields';
+export * from './IncidentAffectedFields';
+export * from './IncidentConfigFields';
+export * from './IncidentDetailsFields';
diff --git a/application/src/components/schedule-incident/incident/hooks/useIncidentEditForm.ts b/application/src/components/schedule-incident/incident/hooks/useIncidentEditForm.ts
new file mode 100644
index 0000000..24ae314
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/hooks/useIncidentEditForm.ts
@@ -0,0 +1,84 @@
+
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useToast } from '@/hooks/use-toast';
+import { incidentService, IncidentItem } from '@/services/incident';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { incidentFormSchema, IncidentFormValues } from './useIncidentForm';
+
+export const useIncidentEditForm = (
+ incident: IncidentItem,
+ onSuccess: () => void,
+ onClose: () => void
+) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+
+ // Initialize form with existing incident data
+ const form = useForm({
+ resolver: zodResolver(incidentFormSchema),
+ defaultValues: {
+ title: incident.title || '',
+ description: incident.description || '',
+ affected_systems: incident.affected_systems || '',
+ status: (incident.status?.toLowerCase() || incident.impact_status?.toLowerCase() || 'investigating') as any,
+ impact: (incident.impact?.toLowerCase() || 'minor') as any,
+ priority: (incident.priority?.toLowerCase() || 'medium') as any,
+ service_id: incident.service_id || '',
+ assigned_to: incident.assigned_to || '',
+ root_cause: incident.root_cause || '',
+ resolution_steps: incident.resolution_steps || '',
+ lessons_learned: incident.lessons_learned || '',
+ },
+ });
+
+ const onSubmit = async (data: IncidentFormValues) => {
+ try {
+ console.log("Form data for update:", data);
+ console.log("Assigned user ID for update:", data.assigned_to);
+
+ await incidentService.updateIncident(incident.id, {
+ title: data.title,
+ description: data.description,
+ status: data.status,
+ affected_systems: data.affected_systems,
+ impact: data.impact,
+ priority: data.priority,
+ service_id: data.service_id,
+ assigned_to: data.assigned_to, // This is the user ID from the form
+ root_cause: data.root_cause,
+ resolution_steps: data.resolution_steps,
+ lessons_learned: data.lessons_learned,
+ });
+
+ toast({
+ title: t('incidentUpdated'),
+ description: t('incidentUpdatedDesc'),
+ });
+
+ onClose();
+ onSuccess();
+ } catch (error) {
+ console.error('Error updating incident:', error);
+
+ if (error instanceof Error) {
+ toast({
+ title: t('error'),
+ description: `${t('errorUpdatingIncident')}: ${error.message}`,
+ variant: 'destructive',
+ });
+ } else {
+ toast({
+ title: t('error'),
+ description: t('errorUpdatingIncident'),
+ variant: 'destructive',
+ });
+ }
+ }
+ };
+
+ return {
+ form,
+ onSubmit: form.handleSubmit(onSubmit),
+ };
+};
diff --git a/application/src/components/schedule-incident/incident/hooks/useIncidentForm.ts b/application/src/components/schedule-incident/incident/hooks/useIncidentForm.ts
new file mode 100644
index 0000000..13ca86f
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/hooks/useIncidentForm.ts
@@ -0,0 +1,105 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import * as z from 'zod';
+import { pb } from '@/lib/pocketbase';
+import { useToast } from '@/hooks/use-toast';
+import { incidentService, CreateIncidentInput } from '@/services/incident';
+import { useLanguage } from '@/contexts/LanguageContext';
+
+// Define the schema for our incident form
+export const incidentFormSchema = z.object({
+ title: z.string().min(1, { message: 'Title is required' }),
+ description: z.string().min(1, { message: 'Description is required' }),
+ affected_systems: z.string().min(1, { message: 'Affected systems are required' }),
+ status: z.enum(['investigating', 'found_root_cause', 'in_progress', 'monitoring', 'resolved']),
+ impact: z.enum(['none', 'minor', 'major', 'critical']),
+ priority: z.enum(['low', 'medium', 'high', 'critical']),
+ service_id: z.string().optional(),
+ assigned_to: z.string().optional(),
+ root_cause: z.string().optional(),
+ resolution_steps: z.string().optional(),
+ lessons_learned: z.string().optional(),
+});
+
+export type IncidentFormValues = z.infer;
+
+export const useIncidentForm = (onSuccess: () => void, onClose: () => void) => {
+ const { t } = useLanguage();
+ const { toast } = useToast();
+
+ const form = useForm({
+ resolver: zodResolver(incidentFormSchema),
+ defaultValues: {
+ title: '',
+ description: '',
+ affected_systems: '',
+ status: 'investigating',
+ impact: 'minor',
+ priority: 'medium',
+ service_id: '',
+ assigned_to: '',
+ root_cause: '',
+ resolution_steps: '',
+ lessons_learned: '',
+ },
+ });
+
+ const onSubmit = async (data: IncidentFormValues) => {
+ try {
+ console.log("Form data before submission:", data);
+
+ const formattedData: CreateIncidentInput = {
+ title: data.title,
+ description: data.description,
+ status: data.status,
+ affected_systems: data.affected_systems,
+ impact: data.impact,
+ priority: data.priority,
+ service_id: data.service_id,
+ assigned_to: data.assigned_to,
+ root_cause: data.root_cause,
+ resolution_steps: data.resolution_steps,
+ lessons_learned: data.lessons_learned,
+ timestamp: new Date().toISOString(),
+ created_by: pb.authStore.model?.id || '',
+ };
+
+ console.log("Formatted data for API:", formattedData);
+
+ await incidentService.createIncident(formattedData);
+
+ toast({
+ title: t('incidentCreated'),
+ description: t('incidentCreatedDesc'),
+ });
+
+ console.log("Incident created successfully, about to call onSuccess and onClose");
+
+ form.reset();
+ onClose();
+ onSuccess();
+ } catch (error) {
+ console.error('Error creating incident:', error);
+
+ if (error instanceof Error) {
+ console.error('Error details:', error.message);
+ toast({
+ title: t('error'),
+ description: `${t('errorCreatingIncident')}: ${error.message}`,
+ variant: 'destructive',
+ });
+ } else {
+ toast({
+ title: t('error'),
+ description: t('errorCreatingIncident'),
+ variant: 'destructive',
+ });
+ }
+ }
+ };
+
+ return {
+ form,
+ onSubmit: form.handleSubmit(onSubmit),
+ };
+};
diff --git a/application/src/components/schedule-incident/incident/index.ts b/application/src/components/schedule-incident/incident/index.ts
new file mode 100644
index 0000000..2421b33
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/index.ts
@@ -0,0 +1,9 @@
+
+export * from './IncidentTable';
+export * from './IncidentActionsMenu';
+export * from './IncidentStatusBadge';
+export * from './IncidentStatusDropdown';
+export * from './CreateIncidentDialog';
+export * from './EditIncidentDialog';
+export * from './EmptyIncidentState';
+export * from './detail-dialog';
diff --git a/application/src/components/schedule-incident/incident/table/IncidentTable.tsx b/application/src/components/schedule-incident/incident/table/IncidentTable.tsx
new file mode 100644
index 0000000..a163694
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/table/IncidentTable.tsx
@@ -0,0 +1,150 @@
+
+import React, { useState, useCallback } from 'react';
+import {
+ Table,
+ TableBody,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { format } from 'date-fns';
+import { IncidentItem } from '@/services/incident/types';
+import { IncidentTableRow } from './IncidentTableRow';
+import { IncidentDetailDialog } from '../detail-dialog/IncidentDetailDialog';
+import { EditIncidentDialog } from '../EditIncidentDialog';
+import { useLanguage } from '@/contexts/LanguageContext';
+import { IncidentTableSkeleton } from './IncidentTableSkeleton';
+
+interface IncidentTableProps {
+ data: IncidentItem[];
+ isLoading: boolean;
+ onIncidentUpdated: () => void;
+ onViewDetails?: (incident: IncidentItem) => void;
+ onEditIncident?: (incident: IncidentItem) => void;
+}
+
+export const IncidentTable = ({
+ data,
+ isLoading,
+ onIncidentUpdated,
+ onViewDetails,
+ onEditIncident
+}: IncidentTableProps) => {
+ const { t } = useLanguage();
+ const [selectedIncident, setSelectedIncident] = useState(null);
+ const [isDetailOpen, setIsDetailOpen] = useState(false);
+ const [isEditOpen, setIsEditOpen] = useState(false);
+
+ const formatDate = useCallback((dateString: string | undefined) => {
+ if (!dateString) return '-';
+ try {
+ return format(new Date(dateString), 'PPp');
+ } catch (error) {
+ console.error('Error formatting date:', dateString, error);
+ return dateString;
+ }
+ }, []);
+
+ const getAffectedSystemsArray = useCallback((affectedSystems: string | undefined): string[] => {
+ if (!affectedSystems) return [];
+ return affectedSystems.split(',').map(system => system.trim()).filter(Boolean);
+ }, []);
+
+ const handleViewDetails = useCallback((incident: IncidentItem) => {
+ setSelectedIncident(incident);
+ setIsDetailOpen(true);
+ }, []);
+
+ const handleEditIncident = useCallback((incident: IncidentItem) => {
+ setSelectedIncident(incident);
+ setIsEditOpen(true);
+ }, []);
+
+ // Handle status updates efficiently
+ const handleIncidentUpdated = useCallback(() => {
+ console.log("Incident updated in IncidentTable, propagating event");
+ onIncidentUpdated();
+ }, [onIncidentUpdated]);
+
+ // Handle dialog closing
+ const handleDetailDialogClose = useCallback((open: boolean) => {
+ setIsDetailOpen(open);
+ if (!open) {
+ onIncidentUpdated();
+ }
+ }, [onIncidentUpdated]);
+
+ // Handle edit dialog closing
+ const handleEditDialogClose = useCallback((open: boolean) => {
+ setIsEditOpen(open);
+ if (!open) {
+ onIncidentUpdated();
+ }
+ }, [onIncidentUpdated]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ // Add a safety check to prevent map of undefined error
+ if (!data || !Array.isArray(data)) {
+ console.error('Data is not an array:', data);
+ return (
+
+
No incident data available
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ {t('title')}
+ {t('status')}
+ {t('priority')}
+ {t('time')}
+ {t('affected')}
+ {t('impact')}
+ {t('assignedTo')}
+ {t('actions')}
+
+
+
+ {data.map((item) => (
+
+ ))}
+
+
+
+
+ {/* Incident detail dialog */}
+
+
+ {/* Edit incident dialog */}
+ {selectedIncident && (
+
+ )}
+ >
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/table/IncidentTableRow.tsx b/application/src/components/schedule-incident/incident/table/IncidentTableRow.tsx
new file mode 100644
index 0000000..8f068ff
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/table/IncidentTableRow.tsx
@@ -0,0 +1,119 @@
+
+import React, { memo, useState } from 'react';
+import { TableRow, TableCell } from '@/components/ui/table';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Eye } from 'lucide-react';
+import { IncidentStatusDropdown } from '../IncidentStatusDropdown';
+import { IncidentActionsMenu } from '../IncidentActionsMenu';
+import { IncidentItem } from '@/services/incident';
+import { AssignedUserCell } from './IncidentTableUtils';
+
+interface IncidentTableRowProps {
+ item: IncidentItem;
+ formatDate: (date: string | undefined) => string;
+ getAffectedSystemsArray: (systems: string | undefined) => string[];
+ onViewDetails?: (incident: IncidentItem) => void;
+ onEditIncident?: (incident: IncidentItem) => void;
+ onIncidentUpdated: () => void;
+ t: (key: string) => string;
+}
+
+export const IncidentTableRow = memo(({
+ item,
+ formatDate,
+ getAffectedSystemsArray,
+ onViewDetails,
+ onEditIncident,
+ onIncidentUpdated,
+ t
+}: IncidentTableRowProps) => {
+ // Use local state for optimistic UI updates
+ const [localItem, setLocalItem] = useState(item);
+
+ // Update local state when props change
+ React.useEffect(() => {
+ setLocalItem(item);
+ }, [item]);
+
+ // Handle status updates locally
+ const handleStatusUpdated = () => {
+ console.log("Status updated in TableRow, calling onIncidentUpdated");
+ onIncidentUpdated();
+ };
+
+ return (
+ onViewDetails && onViewDetails(localItem)}
+ >
+
+ {localItem.title || localItem.description || '-'}
+
+ e.stopPropagation()}>
+
+
+
+
+ {t(localItem.priority?.toLowerCase() || 'low')}
+
+
+ {formatDate(localItem.created)}
+
+
+ {getAffectedSystemsArray(localItem.affected_systems).map((system, idx) => (
+ {system}
+ ))}
+ {getAffectedSystemsArray(localItem.affected_systems).length === 0 && '-'}
+
+
+
+
+ {t(localItem.impact?.toLowerCase() || 'low')}
+
+
+
+
+
+ e.stopPropagation()}>
+
+ {onViewDetails && (
+
+ )}
+
+
+
+
+ );
+});
+
+IncidentTableRow.displayName = 'IncidentTableRow';
diff --git a/application/src/components/schedule-incident/incident/table/IncidentTableSkeleton.tsx b/application/src/components/schedule-incident/incident/table/IncidentTableSkeleton.tsx
new file mode 100644
index 0000000..1ae40df
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/table/IncidentTableSkeleton.tsx
@@ -0,0 +1,27 @@
+
+import React from 'react';
+import { TableRow, TableCell } from '@/components/ui/table';
+import { Skeleton } from '@/components/ui/skeleton';
+
+export const IncidentTableRowSkeleton = () => (
+
+
+
+
+
+
+
+
+
+
+);
+
+export const IncidentTableSkeleton = () => (
+
+
+ {Array(3).fill(0).map((_, index) => (
+
+ ))}
+
+
+);
diff --git a/application/src/components/schedule-incident/incident/table/IncidentTableUtils.tsx b/application/src/components/schedule-incident/incident/table/IncidentTableUtils.tsx
new file mode 100644
index 0000000..2dd14d5
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/table/IncidentTableUtils.tsx
@@ -0,0 +1,57 @@
+
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { userService, User } from '@/services/userService';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Skeleton } from '@/components/ui/skeleton';
+
+// Helper function to get user initials
+export const getUserInitials = (user: User): string => {
+ if (user.full_name) {
+ const nameParts = user.full_name.split(' ');
+ if (nameParts.length > 1) {
+ return `${nameParts[0][0]}${nameParts[1][0]}`.toUpperCase();
+ }
+ return user.full_name.substring(0, 2).toUpperCase();
+ }
+ return user.username.substring(0, 2).toUpperCase();
+};
+
+interface AssignedUserCellProps {
+ userId?: string;
+}
+
+export const AssignedUserCell: React.FC = ({ userId }) => {
+ const { data: user, isLoading } = useQuery({
+ queryKey: ['user', userId],
+ queryFn: async () => {
+ if (!userId) return null;
+ try {
+ return await userService.getUser(userId);
+ } catch (error) {
+ console.error("Failed to fetch assigned user:", error);
+ return null;
+ }
+ },
+ enabled: !!userId,
+ staleTime: 300000 // Cache for 5 minutes
+ });
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!user || !userId) {
+ return -;
+ }
+
+ return (
+
+
+
+ {getUserInitials(user)}
+
+
{user.full_name || user.username}
+
+ );
+};
diff --git a/application/src/components/schedule-incident/incident/table/index.ts b/application/src/components/schedule-incident/incident/table/index.ts
new file mode 100644
index 0000000..2d98564
--- /dev/null
+++ b/application/src/components/schedule-incident/incident/table/index.ts
@@ -0,0 +1,5 @@
+
+export * from './IncidentTable';
+export * from './IncidentTableRow';
+export * from './IncidentTableSkeleton';
+export * from './IncidentTableUtils';
diff --git a/application/src/components/schedule-incident/index.ts b/application/src/components/schedule-incident/index.ts
new file mode 100644
index 0000000..73f2997
--- /dev/null
+++ b/application/src/components/schedule-incident/index.ts
@@ -0,0 +1,18 @@
+
+// Export all components for easier imports
+export * from './ScheduleIncidentContent';
+export * from './ScheduledMaintenanceTab';
+export * from './IncidentManagementTab';
+export * from './maintenance/MaintenanceTable';
+export * from './maintenance/MaintenanceStatusBadge';
+export * from './maintenance/MaintenanceActionsMenu';
+export * from './maintenance/EmptyMaintenanceState';
+export * from './maintenance/CreateMaintenanceDialog';
+export * from './incident/IncidentTable';
+export * from './incident/IncidentStatusBadge';
+export * from './incident/IncidentActionsMenu';
+export * from './incident/EmptyIncidentState';
+export * from './incident/CreateIncidentDialog';
+export * from './incident-management';
+export * from './hooks';
+
diff --git a/application/src/pages/ScheduleIncident.tsx b/application/src/pages/ScheduleIncident.tsx
new file mode 100644
index 0000000..6adef1f
--- /dev/null
+++ b/application/src/pages/ScheduleIncident.tsx
@@ -0,0 +1,59 @@
+
+import React, { useState, useEffect } from 'react';
+import { Header } from "@/components/dashboard/Header";
+import { Sidebar } from "@/components/dashboard/Sidebar";
+import { useTheme } from "@/contexts/ThemeContext";
+import { useLanguage } from "@/contexts/LanguageContext";
+import { ScheduleIncidentContent } from "@/components/schedule-incident/ScheduleIncidentContent";
+import { authService } from "@/services/authService";
+import { useNavigate } from "react-router-dom";
+import { initMaintenanceNotifications, stopMaintenanceNotifications } from "@/services/maintenance/maintenanceNotificationService";
+
+const ScheduleIncident = () => {
+ // State for sidebar collapse functionality
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
+
+ // Get current theme and language
+ const { theme } = useTheme();
+ const { t } = useLanguage();
+
+ // Get current user
+ const currentUser = authService.getCurrentUser();
+ const navigate = useNavigate();
+
+ // Initialize maintenance notifications
+ useEffect(() => {
+ console.log("Initializing maintenance notifications");
+ initMaintenanceNotifications();
+
+ // Clean up on unmount
+ return () => {
+ console.log("Stopping maintenance notifications");
+ stopMaintenanceNotifications();
+ };
+ }, []);
+
+ // Handle logout
+ const handleLogout = () => {
+ authService.logout();
+ navigate("/login");
+ };
+
+ return (
+
+ );
+};
+
+export default ScheduleIncident;
\ No newline at end of file
diff --git a/application/src/services/incident/incidentCache.ts b/application/src/services/incident/incidentCache.ts
new file mode 100644
index 0000000..e34651b
--- /dev/null
+++ b/application/src/services/incident/incidentCache.ts
@@ -0,0 +1,14 @@
+
+import { IncidentItem } from './types';
+
+// Export functions to update and invalidate the cache
+export const updateCache = (data: IncidentItem[]) => {
+ console.log(`Updating cache with ${data.length} incidents`);
+ // The actual cache is now maintained in incidentFetch.ts
+};
+
+// Reset cache and request state
+export const invalidateCache = () => {
+ console.log('Invalidating incidents cache');
+ // The invalidation is handled in incidentFetch.ts implementation
+};
diff --git a/application/src/services/incident/incidentFetch.ts b/application/src/services/incident/incidentFetch.ts
new file mode 100644
index 0000000..ca1ea9b
--- /dev/null
+++ b/application/src/services/incident/incidentFetch.ts
@@ -0,0 +1,147 @@
+
+import { pb } from '@/lib/pocketbase';
+import { IncidentItem } from './types';
+import { normalizeFetchedItem } from './incidentUtils';
+import { updateCache, invalidateCache } from './incidentCache';
+
+// Internal state variables instead of importing them
+let incidentsCache: {
+ data: IncidentItem[];
+ timestamp: number;
+} | null = null;
+let pendingRequest: Promise | null = null;
+let isRequestInProgress = false;
+
+// Cache duration in milliseconds (30 minutes)
+const CACHE_DURATION = 30 * 60 * 1000;
+
+// Check if cache is valid
+export const isCacheValid = (): boolean => {
+ const now = Date.now();
+ return !!(incidentsCache && (now - incidentsCache.timestamp < CACHE_DURATION));
+};
+
+// Get all incidents with caching and error handling
+export const getAllIncidents = async (forceRefresh = false): Promise => {
+ // If a request is in progress, wait for it to complete rather than making a new one
+ if (isRequestInProgress) {
+ console.log('Request already in progress, waiting for completion');
+ try {
+ if (pendingRequest) {
+ await pendingRequest;
+ }
+ return incidentsCache?.data || [];
+ } catch (error) {
+ console.error('Error in existing incidents request:', error);
+ return incidentsCache?.data || [];
+ }
+ }
+
+ // Use cache if available, not expired, and not forced refresh
+ if (!forceRefresh && isCacheValid()) {
+ console.log('Using cached incidents data from', new Date(incidentsCache!.timestamp).toLocaleTimeString());
+ return incidentsCache!.data;
+ }
+
+ try {
+ console.log('Fetching all incidents from API...');
+ isRequestInProgress = true;
+
+ // Implement timeout for the request
+ const timeoutPromise = new Promise((_, reject) => {
+ setTimeout(() => reject(new Error('Request timeout')), 20000); // Longer timeout (20s)
+ });
+
+ // Create the fetch promise with a unique request key to prevent conflicts
+ const now = Date.now();
+ const requestKey = `incidents-${now}`;
+ const fetchPromise = pb.collection('incidents').getList(1, 100, {
+ sort: '-created',
+ requestKey,
+ $cancelKey: requestKey,
+ });
+
+ // Store the pending request
+ pendingRequest = Promise.race([fetchPromise, timeoutPromise]);
+
+ // Race between fetch and timeout
+ const result = await pendingRequest;
+
+ // Clear request flags
+ pendingRequest = null;
+ isRequestInProgress = false;
+
+ if (!result || !result.items) {
+ console.warn('No incidents found in API response');
+ return [];
+ }
+
+ const normalizedItems = result.items.map(normalizeFetchedItem);
+
+ // Update cache
+ updateCache(normalizedItems);
+
+ console.log(`Fetched ${normalizedItems.length} incidents at ${new Date().toLocaleTimeString()}`);
+ return normalizedItems;
+ } catch (error) {
+ if ((error as any)?.isAbort) {
+ console.log("Request aborted:", error);
+ return incidentsCache?.data || [];
+ }
+
+ console.error('Error fetching incidents:', error);
+
+ // Clear states to allow retry
+ pendingRequest = null;
+ isRequestInProgress = false;
+
+ // Improve error message
+ if (error instanceof Error) {
+ throw new Error(`Failed to fetch incidents: ${error.message}`);
+ }
+
+ // Still return cached data even on error
+ if (incidentsCache) {
+ console.log('Returning stale cached data after error');
+ return incidentsCache.data;
+ }
+
+ return [];
+ }
+};
+
+// Get incident by id
+export const getIncidentById = async (id: string): Promise => {
+ try {
+ console.log(`Fetching incident with ID: ${id}`);
+
+ // First check if the incident exists in the cache
+ if (isCacheValid() && incidentsCache) {
+ const cachedIncident = incidentsCache.data.find(incident => incident.id === id);
+ if (cachedIncident) {
+ console.log('Incident found in cache');
+ return cachedIncident;
+ }
+ }
+
+ // If not in cache, fetch from API
+ const result = await pb.collection('incidents').getOne(id);
+
+ if (!result) {
+ console.warn(`No incident found with ID: ${id}`);
+ return null;
+ }
+
+ const normalizedIncident = normalizeFetchedItem(result);
+ return normalizedIncident;
+
+ } catch (error) {
+ console.error(`Error fetching incident with ID ${id}:`, error);
+
+ if (error instanceof Error) {
+ throw new Error(`Failed to fetch incident: ${error.message}`);
+ }
+
+ return null;
+ }
+};
diff --git a/application/src/services/incident/incidentOperations.ts b/application/src/services/incident/incidentOperations.ts
new file mode 100644
index 0000000..c19905a
--- /dev/null
+++ b/application/src/services/incident/incidentOperations.ts
@@ -0,0 +1,109 @@
+
+import { pb } from '@/lib/pocketbase';
+import { CreateIncidentInput, IncidentItem } from './types';
+import { formatStatus } from './incidentUtils';
+import { invalidateCache } from './incidentCache';
+
+// Update incident status
+export const updateIncidentStatus = async (id: string, status: string): Promise => {
+ try {
+ const formattedStatus = formatStatus(status);
+ console.log(`Updating incident ${id} status to ${status} (formatted: ${formattedStatus})`);
+
+ // Update both status and impact_status fields
+ await pb.collection('incidents').update(id, {
+ status: formattedStatus,
+ impact_status: status.toLowerCase(), // Set impact_status to the lowercase status value
+ ...(status.toLowerCase() === 'resolved' ? { resolution_time: new Date().toISOString() } : {})
+ });
+
+ // Invalidate cache after update
+ invalidateCache();
+
+ console.log(`Incident ${id} status updated successfully to ${status}`);
+ } catch (error) {
+ console.error('Error updating incident status:', error);
+ throw error;
+ }
+};
+
+// Delete incident
+export const deleteIncident = async (id: string): Promise => {
+ try {
+ await pb.collection('incidents').delete(id);
+
+ // Invalidate cache after deletion
+ invalidateCache();
+
+ console.log(`Incident ${id} deleted successfully`);
+ } catch (error) {
+ console.error('Error deleting incident:', error);
+ throw error;
+ }
+};
+
+// Create incident
+export const createIncident = async (data: CreateIncidentInput): Promise => {
+ try {
+ // Format the payload according to API requirements
+ const payload = {
+ title: data.title,
+ description: data.description,
+ status: formatStatus(data.status),
+ impact_status: data.status.toLowerCase(),
+ // Use lowercase for impact and priority to match API expectations
+ impact: data.impact.toLowerCase(),
+ affected_systems: data.affected_systems,
+ priority: data.priority.toLowerCase(),
+ service_id: data.service_id || '',
+ assigned_to: data.assigned_to || '', // Direct user ID assignment
+ root_cause: data.root_cause || '',
+ resolution_steps: data.resolution_steps || '',
+ lessons_learned: data.lessons_learned || '',
+ timestamp: data.timestamp || new Date().toISOString(),
+ created_by: data.created_by,
+ resolution_time: data.status.toLowerCase() === 'resolved' ? new Date().toISOString() : null,
+ };
+
+ console.log('Creating new incident with payload:', payload);
+ await pb.collection('incidents').create(payload);
+
+ // Invalidate cache after create
+ invalidateCache();
+
+ console.log('Incident created successfully');
+ } catch (error) {
+ console.error('Error creating incident:', error);
+ throw error;
+ }
+};
+
+// Update incident
+export const updateIncident = async (id: string, data: Partial): Promise => {
+ try {
+ console.log(`Updating incident ${id} with:`, data);
+
+ // Make sure impact and priority are lowercase
+ const payload = {
+ ...data,
+ impact: data.impact?.toLowerCase(),
+ priority: data.priority?.toLowerCase(),
+ status: data.status ? formatStatus(data.status) : undefined,
+ impact_status: data.status ? data.status.toLowerCase() : undefined,
+ ...(data.status?.toLowerCase() === 'resolved' && !data.resolution_time
+ ? { resolution_time: new Date().toISOString() }
+ : {})
+ };
+
+ console.log("Final payload for update:", payload);
+ await pb.collection('incidents').update(id, payload);
+
+ // Invalidate cache after update
+ invalidateCache();
+
+ console.log(`Incident ${id} updated successfully`);
+ } catch (error) {
+ console.error('Error updating incident:', error);
+ throw error;
+ }
+};
diff --git a/application/src/services/incident/incidentPdfService.ts b/application/src/services/incident/incidentPdfService.ts
new file mode 100644
index 0000000..872a73c
--- /dev/null
+++ b/application/src/services/incident/incidentPdfService.ts
@@ -0,0 +1,5 @@
+
+import { generateIncidentPDF } from './pdf';
+
+export { generateIncidentPDF };
+
diff --git a/application/src/services/incident/incidentService.ts b/application/src/services/incident/incidentService.ts
new file mode 100644
index 0000000..6a73826
--- /dev/null
+++ b/application/src/services/incident/incidentService.ts
@@ -0,0 +1,22 @@
+
+import { CreateIncidentInput, IncidentItem, UpdateIncidentInput } from './types';
+import { createIncident, updateIncident, updateIncidentStatus, deleteIncident } from './incidentOperations';
+import { getAllIncidents, getIncidentById } from './incidentFetch';
+import { generateIncidentPDF } from './incidentPdfService';
+
+export const incidentService = {
+ // Fetch operations
+ getAllIncidents,
+ getIncidentById,
+
+ // CRUD operations
+ createIncident,
+ updateIncident,
+ updateIncidentStatus,
+ deleteIncident,
+
+ // PDF operations
+ generateIncidentPDF,
+};
+
+export default incidentService;
diff --git a/application/src/services/incident/incidentUtils.ts b/application/src/services/incident/incidentUtils.ts
new file mode 100644
index 0000000..c1e703b
--- /dev/null
+++ b/application/src/services/incident/incidentUtils.ts
@@ -0,0 +1,29 @@
+
+import { pb } from '@/lib/pocketbase';
+import { IncidentItem } from './types';
+
+// Helper function to get the API URL
+export const getApiUrl = (): string => {
+ try {
+ return pb.baseUrl;
+ } catch (error) {
+ console.error('Error getting API URL:', error);
+ return '';
+ }
+};
+
+// Normalize fetched items to ensure consistent data structure
+export const normalizeFetchedItem = (item: any): IncidentItem => {
+ return {
+ ...item,
+ affected_systems: item.affected_systems || '',
+ status: item.impact_status || item.status || 'Investigating',
+ impact: item.impact || 'Low',
+ priority: item.priority || 'Low',
+ } as IncidentItem;
+};
+
+// Format status with first letter capitalized
+export const formatStatus = (status: string): string => {
+ return status.charAt(0).toUpperCase() + status.slice(1);
+};
diff --git a/application/src/services/incident/index.ts b/application/src/services/incident/index.ts
new file mode 100644
index 0000000..51d5447
--- /dev/null
+++ b/application/src/services/incident/index.ts
@@ -0,0 +1,12 @@
+
+export * from './types';
+export * from './incidentFetch';
+export * from './incidentOperations';
+export * from './incidentCache';
+export * from './incidentUtils';
+export * from './incidentPdfService';
+export * from './incidentService';
+
+// Export the incidentService as the default export
+export { incidentService } from './incidentService';
+
diff --git a/application/src/services/incident/pdf/generator.ts b/application/src/services/incident/pdf/generator.ts
new file mode 100644
index 0000000..6337757
--- /dev/null
+++ b/application/src/services/incident/pdf/generator.ts
@@ -0,0 +1,112 @@
+
+import jsPDF from 'jspdf';
+import { IncidentItem } from '../types';
+import {
+ addBasicInfoSection,
+ addDescriptionSection,
+ addAffectedSystemsSection,
+ addRootCauseSection,
+ addResolutionSection,
+ addAssignmentSection,
+ addLessonsLearnedSection
+} from './sections';
+import { addHeader, addFooter } from './headerFooter';
+
+/**
+ * Generate a PDF for an incident report
+ */
+export const generatePdf = async (incident: IncidentItem): Promise => {
+ // Validate incident data
+ if (!incident?.id) {
+ console.error('Invalid incident data for PDF generation');
+ throw new Error('Invalid incident data');
+ }
+
+ try {
+ // Create new PDF document with portrait orientation
+ const doc = new jsPDF({
+ orientation: 'portrait',
+ unit: 'mm',
+ format: 'a4',
+ });
+
+ // Set title and filename
+ const title = incident.title || `Incident Report #${incident.id}`;
+ const filename = `incident-report-${incident.id}.pdf`;
+
+ // Add metadata
+ doc.setProperties({
+ title: title,
+ subject: 'Incident Report',
+ author: 'ReamStack System',
+ creator: 'ReamStack',
+ });
+
+ // Add header section
+ let yPos = addHeader(doc, incident);
+
+ // Add basic information section
+ yPos = addBasicInfoSection(doc, incident, yPos);
+
+ // Add description section
+ yPos = addDescriptionSection(doc, incident, yPos);
+
+ // Check if we need to add a new page
+ if (yPos > 250) {
+ doc.addPage();
+ yPos = 20;
+ }
+
+ // Add affected systems section
+ yPos = addAffectedSystemsSection(doc, incident, yPos);
+
+ // Check if we need to add a new page
+ if (yPos > 250) {
+ doc.addPage();
+ yPos = 20;
+ }
+
+ // Add root cause section
+ yPos = addRootCauseSection(doc, incident, yPos);
+
+ // Check if we need to add a new page
+ if (yPos > 250) {
+ doc.addPage();
+ yPos = 20;
+ }
+
+ // Add resolution steps section
+ yPos = addResolutionSection(doc, incident, yPos);
+
+ // Check if we need to add a new page
+ if (yPos > 250) {
+ doc.addPage();
+ yPos = 20;
+ }
+
+ // Add assignment section
+ yPos = addAssignmentSection(doc, incident, yPos);
+
+ // Check if we need to add a new page
+ if (yPos > 250) {
+ doc.addPage();
+ yPos = 20;
+ }
+
+ // Add lessons learned section if available
+ addLessonsLearnedSection(doc, incident, yPos);
+
+ // Add footer to all pages
+ addFooter(doc);
+
+ // Save the PDF
+ doc.save(filename);
+
+ console.log('PDF generated successfully:', filename);
+ return filename;
+ } catch (error) {
+ console.error('Error generating incident PDF:', error);
+ throw new Error(`Failed to generate PDF: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ }
+};
+
diff --git a/application/src/services/incident/pdf/headerFooter.ts b/application/src/services/incident/pdf/headerFooter.ts
new file mode 100644
index 0000000..9b3ff51
--- /dev/null
+++ b/application/src/services/incident/pdf/headerFooter.ts
@@ -0,0 +1,63 @@
+
+import jsPDF from 'jspdf';
+import { IncidentItem } from '../types';
+import { fonts } from './utils';
+
+/**
+ * Add the PDF header with title and metadata
+ */
+export const addHeader = (doc: jsPDF, incident: IncidentItem): number => {
+ // Set initial y position
+ let yPos = 15;
+
+ // Add header with title
+ doc.setFont(fonts.bold);
+ doc.setFontSize(18);
+ doc.setTextColor(0, 0, 0);
+ doc.text('INCIDENT REPORT', 105, yPos, { align: 'center' });
+
+ // Add incident title
+ yPos += 8;
+ doc.setFontSize(14);
+ const title = incident.title || `Incident Report #${incident.id}`;
+ doc.text(title, 105, yPos, { align: 'center' });
+
+ // Add current date
+ yPos += 8;
+ doc.setFont(fonts.normal);
+ doc.setFontSize(10);
+ doc.text(`Generated on: ${new Date().toLocaleDateString()}`, 105, yPos, { align: 'center' });
+
+ // Add ReamStack logo or text
+ yPos += 8;
+ doc.setFontSize(12);
+ doc.setFont(fonts.italic);
+ doc.text('ReamStack Incident Management', 105, yPos, { align: 'center' });
+
+ // Add horizontal line
+ yPos += 5;
+ doc.setLineWidth(0.5);
+ doc.line(15, yPos, 195, yPos);
+
+ // Return next Y position with some padding
+ return yPos + 10;
+};
+
+/**
+ * Add footer to all pages
+ */
+export const addFooter = (doc: jsPDF): void => {
+ const pageCount = doc.getNumberOfPages();
+ for (let i = 1; i <= pageCount; i++) {
+ doc.setPage(i);
+ doc.setFontSize(8);
+ doc.setTextColor(100, 100, 100);
+ doc.text(
+ `Page ${i} of ${pageCount} | Generated by ReamStack Incident Management System`,
+ 105,
+ 285,
+ { align: 'center' }
+ );
+ }
+};
+
diff --git a/application/src/services/incident/pdf/index.ts b/application/src/services/incident/pdf/index.ts
new file mode 100644
index 0000000..6ba7099
--- /dev/null
+++ b/application/src/services/incident/pdf/index.ts
@@ -0,0 +1,12 @@
+
+import { generatePdf } from './generator';
+import { IncidentItem } from '../types';
+
+/**
+ * Generate and download PDF for incident report
+ */
+export const generateIncidentPDF = async (incident: IncidentItem): Promise => {
+ await generatePdf(incident);
+ return Promise.resolve();
+};
+
diff --git a/application/src/services/incident/pdf/sections.ts b/application/src/services/incident/pdf/sections.ts
new file mode 100644
index 0000000..a3ee403
--- /dev/null
+++ b/application/src/services/incident/pdf/sections.ts
@@ -0,0 +1,192 @@
+
+import jsPDF from 'jspdf';
+import { IncidentItem } from '../types';
+import { fonts, formatDate, capitalize } from './utils';
+import autoTable from 'jspdf-autotable';
+
+/**
+ * Add the incident overview section to the PDF
+ */
+export const addBasicInfoSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ doc.setFont(fonts.bold);
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Incident Overview', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+ yPos += 10;
+
+ // Basic information table
+ autoTable(doc, {
+ startY: yPos,
+ head: [['Field', 'Information']],
+ body: [
+ ['ID', incident.id],
+ ['Status', capitalize(incident.status || 'Unknown')],
+ ['Priority', capitalize(incident.priority || 'Unknown')],
+ ['Impact', capitalize(incident.impact || 'Unknown')],
+ ['Created', formatDate(incident.created)],
+ ['Last Updated', formatDate(incident.updated)],
+ ],
+ headStyles: {
+ fillColor: [30, 64, 175], // Blue-800
+ textColor: [255, 255, 255],
+ fontStyle: 'bold',
+ },
+ alternateRowStyles: {
+ fillColor: [241, 245, 249], // Slate-100
+ },
+ margin: { left: 15, right: 15 },
+ });
+
+ // Return the new Y position after the table
+ return (doc as any).lastAutoTable.finalY + 10;
+};
+
+/**
+ * Add the description section to the PDF
+ */
+export const addDescriptionSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ doc.setFont(fonts.bold);
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Description', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+
+ // Add incident description
+ doc.setFont(fonts.normal);
+ doc.setFontSize(10);
+ doc.setTextColor(0, 0, 0);
+ const description = incident.description || 'No description provided';
+ const splitDescription = doc.splitTextToSize(description, 180);
+ doc.text(splitDescription, 15, yPos + 10);
+
+ // Update y position after the description
+ return yPos + 15 + (splitDescription.length * 5);
+};
+
+/**
+ * Add the affected systems section to the PDF
+ */
+export const addAffectedSystemsSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ doc.setFont(fonts.bold);
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Affected Systems', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+
+ // Add affected systems
+ doc.setFont(fonts.normal);
+ doc.setFontSize(10);
+ doc.setTextColor(0, 0, 0);
+ const affectedSystems = incident.affected_systems || 'None specified';
+ const systemsList = affectedSystems.split(',').map(system => system.trim());
+
+ let currentY = yPos + 10;
+ systemsList.forEach((system) => {
+ doc.text(`• ${system}`, 15, currentY);
+ currentY += 5;
+ });
+
+ return currentY + 5; // Return updated Y position with some padding
+};
+
+/**
+ * Add the root cause section to the PDF
+ */
+export const addRootCauseSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ doc.setFont(fonts.bold);
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Root Cause', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+
+ // Add root cause
+ doc.setFont(fonts.normal);
+ doc.setFontSize(10);
+ doc.setTextColor(0, 0, 0);
+ const rootCause = incident.root_cause || 'Root cause not identified yet';
+ const splitRootCause = doc.splitTextToSize(rootCause, 180);
+ doc.text(splitRootCause, 15, yPos + 10);
+
+ // Update y position after the root cause
+ return yPos + 15 + (splitRootCause.length * 5);
+};
+
+/**
+ * Add the resolution steps section to the PDF
+ */
+export const addResolutionSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ doc.setFont(fonts.bold);
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Resolution Steps', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+
+ // Create resolution data table - Using resolution_steps
+ const resolutionSteps = incident.resolution_steps || 'No resolution steps provided';
+ const splitResolutionSteps = doc.splitTextToSize(resolutionSteps, 180);
+
+ doc.setFontSize(10);
+ doc.setTextColor(0, 0, 0);
+ doc.text(splitResolutionSteps, 15, yPos + 10);
+
+ return yPos + 20 + (splitResolutionSteps.length * 5);
+};
+
+/**
+ * Add the assignment information section to the PDF
+ */
+export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Assignment Information', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+
+ // Add assigned user info
+ yPos += 10;
+ doc.setFontSize(10);
+ doc.setTextColor(0, 0, 0);
+ const assignedTo = incident.assigned_to || 'Not assigned';
+ doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
+
+ return yPos + 10;
+};
+
+/**
+ * Add the lessons learned section to the PDF if available
+ */
+export const addLessonsLearnedSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
+ if (!incident.lessons_learned) {
+ return yPos; // No lessons learned, return current position
+ }
+
+ doc.setFontSize(14);
+ doc.setTextColor(30, 64, 175); // Blue-800
+ doc.text('Lessons Learned', 15, yPos);
+
+ // Draw a line under the heading
+ doc.line(15, yPos + 2, 195, yPos + 2);
+
+ // Add lessons learned
+ const lessonsLearned = incident.lessons_learned;
+ const splitLessonsLearned = doc.splitTextToSize(lessonsLearned, 180);
+
+ doc.setFontSize(10);
+ doc.setTextColor(0, 0, 0);
+ doc.text(splitLessonsLearned, 15, yPos + 10);
+
+ return yPos + 15 + (splitLessonsLearned.length * 5);
+};
+
diff --git a/application/src/services/incident/pdf/utils.ts b/application/src/services/incident/pdf/utils.ts
new file mode 100644
index 0000000..65a6c48
--- /dev/null
+++ b/application/src/services/incident/pdf/utils.ts
@@ -0,0 +1,26 @@
+
+import { format } from 'date-fns';
+
+// Font configuration for the PDF
+export const fonts = {
+ normal: 'Helvetica',
+ bold: 'Helvetica-Bold',
+ italic: 'Helvetica-Oblique',
+};
+
+// Helper function to format dates
+export const formatDate = (dateString: string | undefined): string => {
+ if (!dateString) return 'N/A';
+ try {
+ return format(new Date(dateString), 'PPp');
+ } catch (e) {
+ return dateString || 'N/A';
+ }
+};
+
+// Helper function to capitalize first letter
+export const capitalize = (str: string): string => {
+ if (!str) return '';
+ return str.charAt(0).toUpperCase() + str.slice(1);
+};
+
diff --git a/application/src/services/incident/types.ts b/application/src/services/incident/types.ts
new file mode 100644
index 0000000..0297356
--- /dev/null
+++ b/application/src/services/incident/types.ts
@@ -0,0 +1,57 @@
+
+// Define the type for incident item
+export type IncidentItem = {
+ id: string;
+ service_id?: string;
+ timestamp?: string;
+ description: string;
+ assigned_to?: string;
+ resolution_time?: string;
+ impact: string;
+ affected_systems: string;
+ root_cause?: string;
+ resolution_steps?: string;
+ lessons_learned?: string;
+ operational_status_id?: string;
+ server_id?: string;
+ priority: string;
+ status: string;
+ impact_status?: string;
+ created: string;
+ updated: string;
+ category?: string;
+ title: string;
+};
+
+// Define the input type for creating an incident
+export type CreateIncidentInput = {
+ title: string;
+ description: string;
+ status: string;
+ impact: string;
+ affected_systems: string;
+ priority: string;
+ service_id?: string;
+ assigned_to?: string;
+ root_cause?: string;
+ resolution_steps?: string;
+ lessons_learned?: string;
+ timestamp?: string;
+ created_by: string;
+};
+
+// Define the input type for updating an incident
+export type UpdateIncidentInput = {
+ id: string;
+ title?: string;
+ description?: string;
+ status?: string;
+ impact?: string;
+ affected_systems?: string;
+ priority?: string;
+ service_id?: string;
+ assigned_to?: string;
+ root_cause?: string;
+ resolution_steps?: string;
+ lessons_learned?: string;
+};