feat: Implement Incident Management and Tab Dashboard
This commit is contained in:
+76
@@ -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 (
|
||||
<ScrollArea className="h-[80vh] print:h-auto print:overflow-visible">
|
||||
<div className="px-6 py-6">
|
||||
<div className="print-section header-print">
|
||||
<IncidentDetailHeader incident={incident} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 print-compact-spacing">
|
||||
<div className="print-section">
|
||||
<BasicInfoSection incident={incident} assignedUser={userToDisplay} />
|
||||
</div>
|
||||
<Separator className="print:border-blue-200" />
|
||||
<div className="print-section">
|
||||
<TimelineSection incident={incident} assignedUser={userToDisplay} />
|
||||
</div>
|
||||
<Separator className="print:border-blue-200" />
|
||||
<div className="print-section">
|
||||
<AffectedSystemsSection incident={incident} assignedUser={userToDisplay} />
|
||||
</div>
|
||||
<Separator className="print:border-blue-200" />
|
||||
<div className="print-section">
|
||||
<ResolutionSection incident={incident} assignedUser={userToDisplay} />
|
||||
</div>
|
||||
|
||||
<IncidentDetailFooter onClose={onClose} incident={incident} />
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
+54
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="dialog-content sm:max-w-[700px] max-h-[90vh] p-0
|
||||
print:max-w-none print:max-h-none print:overflow-visible
|
||||
print:shadow-none print:m-0 print:p-0 print:border-none
|
||||
print:absolute print:left-0 print:top-0 print:w-full print:h-auto"
|
||||
>
|
||||
<IncidentDetailContent
|
||||
incident={incident}
|
||||
onClose={() => onOpenChange(false)}
|
||||
assignedUser={assignedUser}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+31
@@ -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<IncidentDetailFooterProps> = ({
|
||||
onClose,
|
||||
incident,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<div className="print:hidden">
|
||||
<Separator className="my-6" />
|
||||
<div className="flex justify-between items-center">
|
||||
<CloseButton onClose={onClose} />
|
||||
<div className="flex gap-2">
|
||||
<DownloadPdfButton incident={incident} />
|
||||
<PrintButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+19
@@ -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 (
|
||||
<DialogHeader className="mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<DialogTitle className="text-xl">{incident.title || 'Incident Details'}</DialogTitle>
|
||||
<span className="text-sm text-muted-foreground">#{incident.id}</span>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
);
|
||||
};
|
||||
+55
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
<BasicInfoSection incident={incident} assignedUser={assignedUser} />
|
||||
<Separator />
|
||||
<TimelineSection incident={incident} assignedUser={assignedUser} />
|
||||
<Separator />
|
||||
<AffectedSystemsSection incident={incident} assignedUser={assignedUser} />
|
||||
<Separator />
|
||||
<ResolutionSection incident={incident} assignedUser={assignedUser} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+26
@@ -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<CloseButtonProps> = ({
|
||||
onClose,
|
||||
className
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
className={className}
|
||||
>
|
||||
{t('close', 'common')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+31
@@ -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<DownloadPdfButtonProps> = ({
|
||||
incident,
|
||||
className
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { handleDownloadPDF } = useDownloadIncidentPdf();
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`flex items-center gap-2 ${className || ''}`}
|
||||
onClick={() => handleDownloadPDF(incident)}
|
||||
variant="outline"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{t('downloadPdf', 'incident')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+26
@@ -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<PrintButtonProps> = ({ className }) => {
|
||||
const { t } = useLanguage();
|
||||
const { handlePrint } = usePrintIncident();
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`flex items-center gap-2 ${className || ''}`}
|
||||
onClick={handlePrint}
|
||||
variant="default"
|
||||
>
|
||||
<Printer className="h-4 w-4" />
|
||||
{t('print', 'incident')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
|
||||
export * from './PrintButton';
|
||||
export * from './DownloadPdfButton';
|
||||
export * from './CloseButton';
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
export * from './usePrintIncident';
|
||||
export * from './useDownloadIncidentPdf';
|
||||
+29
@@ -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 };
|
||||
};
|
||||
+173
@@ -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 };
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
export * from './IncidentDetailDialog';
|
||||
export * from './IncidentDetailHeader';
|
||||
export * from './IncidentDetailContent';
|
||||
export * from './IncidentDetailSections';
|
||||
export * from './IncidentDetailFooter';
|
||||
+66
@@ -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<AffectedSystemsSectionProps> = ({ incident }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">{t('affectedSystems')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('systems')}</h4>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{getAffectedSystemsArray(incident.affected_systems).map((system, idx) => (
|
||||
<Badge key={idx} variant="outline">{system}</Badge>
|
||||
))}
|
||||
{getAffectedSystemsArray(incident.affected_systems).length === 0 &&
|
||||
<span className="text-muted-foreground italic">{t('noSystems')}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('impact')}</h4>
|
||||
<div className="mt-1">
|
||||
<Badge variant={
|
||||
incident.impact?.toLowerCase() === 'critical' ? 'destructive' :
|
||||
incident.impact?.toLowerCase() === 'high' ? 'default' :
|
||||
incident.impact?.toLowerCase() === 'medium' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{t(incident.impact?.toLowerCase() || 'low')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('priority')}</h4>
|
||||
<div className="mt-1">
|
||||
<Badge variant={
|
||||
incident.priority?.toLowerCase() === 'critical' ? 'destructive' :
|
||||
incident.priority?.toLowerCase() === 'high' ? 'default' :
|
||||
incident.priority?.toLowerCase() === 'medium' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{t(incident.priority?.toLowerCase() || 'low')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+44
@@ -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<AssignmentSectionProps> = ({ incident, assignedUser }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">{t('assignment')}</h3>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('assignedTo')}</h4>
|
||||
<div className="mt-1">
|
||||
{assignedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={assignedUser.avatar} alt={assignedUser.full_name || assignedUser.username} />
|
||||
<AvatarFallback>{getUserInitials(assignedUser)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{assignedUser.full_name || assignedUser.username}</span>
|
||||
</div>
|
||||
) : incident.assigned_to ? (
|
||||
<span>{incident.assigned_to}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+69
@@ -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<BasicInfoSectionProps> = ({ incident, assignedUser }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 print-compact-text">
|
||||
<h3 className="font-semibold text-lg">{t('basicInfo')}</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 print-grid">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('title')}</h4>
|
||||
<p className="mt-1">{incident.title || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('status')}</h4>
|
||||
<div className="mt-1">
|
||||
<IncidentStatusBadge status={incident.status || incident.impact_status || 'investigating'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('serviceId')}</h4>
|
||||
<p className="mt-1">{incident.service_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('assignedTo')}</h4>
|
||||
<div className="mt-1">
|
||||
{assignedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={assignedUser.avatar} alt={assignedUser.full_name || assignedUser.username} />
|
||||
<AvatarFallback>{getUserInitials(assignedUser)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{assignedUser.full_name || assignedUser.username}</span>
|
||||
</div>
|
||||
) : incident.assigned_to ? (
|
||||
<span>{incident.assigned_to}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">{t('unassigned')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('description')}</h4>
|
||||
<p className="mt-1 whitespace-pre-line print-description">{incident.description || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+51
@@ -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<ImpactAnalysisSectionProps> = ({ incident }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">{t('impactAnalysis')}</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('impact')}</h4>
|
||||
<div className="mt-1">
|
||||
<Badge variant={
|
||||
incident.impact?.toLowerCase() === 'critical' ? 'destructive' :
|
||||
incident.impact?.toLowerCase() === 'high' ? 'default' :
|
||||
incident.impact?.toLowerCase() === 'medium' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{t(incident.impact?.toLowerCase() || 'low')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('priority')}</h4>
|
||||
<div className="mt-1">
|
||||
<Badge variant={
|
||||
incident.priority?.toLowerCase() === 'critical' ? 'destructive' :
|
||||
incident.priority?.toLowerCase() === 'high' ? 'default' :
|
||||
incident.priority?.toLowerCase() === 'medium' ? 'secondary' : 'outline'
|
||||
}>
|
||||
{t(incident.priority?.toLowerCase() || 'low')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+39
@@ -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<ResolutionSectionProps> = ({ incident }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">{t('resolutionDetails')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('rootCause')}</h4>
|
||||
<p className="mt-1 whitespace-pre-line">{incident.root_cause || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('resolutionSteps')}</h4>
|
||||
<p className="mt-1 whitespace-pre-line">{incident.resolution_steps || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('lessonsLearned')}</h4>
|
||||
<p className="mt-1 whitespace-pre-line">{incident.lessons_learned || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+45
@@ -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<TimelineSectionProps> = ({ incident }) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!incident) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-lg">{t('timeline')}</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('created')}</h4>
|
||||
<p className="mt-1">{formatDate(incident.created)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('lastUpdated')}</h4>
|
||||
<p className="mt-1">{formatDate(incident.updated)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('incidentTime')}</h4>
|
||||
<p className="mt-1">{formatDate(incident.timestamp)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">{t('resolutionTime')}</h4>
|
||||
<p className="mt-1">{formatDate(incident.resolution_time)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
// Export all section components
|
||||
export * from './BasicInfoSection';
|
||||
export * from './TimelineSection';
|
||||
export * from './AffectedSystemsSection';
|
||||
export * from './ResolutionSection';
|
||||
export * from './utils';
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user