668 lines
25 KiB
TypeScript
668 lines
25 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
||
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
||
|
|
import { useForm, Controller } from 'react-hook-form';
|
||
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { format, subDays, startOfMonth, endOfMonth, startOfQuarter, endOfQuarter, startOfYear, endOfYear } from 'date-fns';
|
||
|
|
import {
|
||
|
|
FileText,
|
||
|
|
Download,
|
||
|
|
Calendar,
|
||
|
|
Shield,
|
||
|
|
AlertTriangle,
|
||
|
|
CheckCircle,
|
||
|
|
Clock,
|
||
|
|
Settings,
|
||
|
|
X,
|
||
|
|
Play,
|
||
|
|
RefreshCw,
|
||
|
|
Eye,
|
||
|
|
Filter
|
||
|
|
} from 'lucide-react';
|
||
|
|
import { toast } from 'react-toastify';
|
||
|
|
import auditApi from '@/services/api';
|
||
|
|
import { ComplianceReport } from '@/types/audit';
|
||
|
|
|
||
|
|
interface ReportGeneratorProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FormData {
|
||
|
|
type: 'soc2' | 'hipaa' | 'pci' | 'gdpr' | 'custom';
|
||
|
|
startDate: string;
|
||
|
|
endDate: string;
|
||
|
|
template: string;
|
||
|
|
includeFindings: boolean;
|
||
|
|
includeRecommendations: boolean;
|
||
|
|
includeEvidence: boolean;
|
||
|
|
executiveSummary: boolean;
|
||
|
|
customTitle: string;
|
||
|
|
customDescription: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const complianceFrameworks = [
|
||
|
|
{
|
||
|
|
value: 'soc2' as const,
|
||
|
|
label: 'SOC 2 Type II',
|
||
|
|
description: 'Service Organization Control 2 Type II assessment',
|
||
|
|
icon: Shield,
|
||
|
|
color: 'text-blue-600',
|
||
|
|
requirements: ['Trust Services Criteria', 'Security', 'Availability', 'Confidentiality', 'Processing Integrity', 'Privacy']
|
||
|
|
},
|
||
|
|
{
|
||
|
|
value: 'hipaa' as const,
|
||
|
|
label: 'HIPAA',
|
||
|
|
description: 'Health Insurance Portability and Accountability Act',
|
||
|
|
icon: Shield,
|
||
|
|
color: 'text-green-600',
|
||
|
|
requirements: ['Administrative Safeguards', 'Physical Safeguards', 'Technical Safeguards', 'Breach Notification']
|
||
|
|
},
|
||
|
|
{
|
||
|
|
value: 'pci' as const,
|
||
|
|
label: 'PCI DSS',
|
||
|
|
description: 'Payment Card Industry Data Security Standard',
|
||
|
|
icon: Shield,
|
||
|
|
color: 'text-purple-600',
|
||
|
|
requirements: ['Network Security', 'Cardholder Data Protection', 'Vulnerability Management', 'Access Control']
|
||
|
|
},
|
||
|
|
{
|
||
|
|
value: 'gdpr' as const,
|
||
|
|
label: 'GDPR',
|
||
|
|
description: 'General Data Protection Regulation',
|
||
|
|
icon: Shield,
|
||
|
|
color: 'text-indigo-600',
|
||
|
|
requirements: ['Data Minimization', 'Consent Management', 'Data Subject Rights', 'Breach Notification']
|
||
|
|
},
|
||
|
|
{
|
||
|
|
value: 'custom' as const,
|
||
|
|
label: 'Custom Report',
|
||
|
|
description: 'Create a custom compliance report',
|
||
|
|
icon: FileText,
|
||
|
|
color: 'text-gray-600',
|
||
|
|
requirements: ['Flexible Requirements', 'Custom Controls', 'Tailored Assessment']
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const predefinedPeriods = [
|
||
|
|
{
|
||
|
|
label: 'Last 7 days',
|
||
|
|
getValue: () => ({
|
||
|
|
start: format(subDays(new Date(), 7), 'yyyy-MM-dd'),
|
||
|
|
end: format(new Date(), 'yyyy-MM-dd')
|
||
|
|
})
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Last 30 days',
|
||
|
|
getValue: () => ({
|
||
|
|
start: format(subDays(new Date(), 30), 'yyyy-MM-dd'),
|
||
|
|
end: format(new Date(), 'yyyy-MM-dd')
|
||
|
|
})
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Current Month',
|
||
|
|
getValue: () => ({
|
||
|
|
start: format(startOfMonth(new Date()), 'yyyy-MM-dd'),
|
||
|
|
end: format(endOfMonth(new Date()), 'yyyy-MM-dd')
|
||
|
|
})
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Current Quarter',
|
||
|
|
getValue: () => ({
|
||
|
|
start: format(startOfQuarter(new Date()), 'yyyy-MM-dd'),
|
||
|
|
end: format(endOfQuarter(new Date()), 'yyyy-MM-dd')
|
||
|
|
})
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Current Year',
|
||
|
|
getValue: () => ({
|
||
|
|
start: format(startOfYear(new Date()), 'yyyy-MM-dd'),
|
||
|
|
end: format(endOfYear(new Date()), 'yyyy-MM-dd')
|
||
|
|
})
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
export const ComplianceReportGenerator: React.FC<ReportGeneratorProps> = ({
|
||
|
|
isOpen,
|
||
|
|
onClose
|
||
|
|
}) => {
|
||
|
|
const queryClient = useQueryClient();
|
||
|
|
const [generatingReportId, setGeneratingReportId] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const { control, watch, setValue, getValues, handleSubmit } = useForm<FormData>({
|
||
|
|
defaultValues: {
|
||
|
|
type: 'soc2',
|
||
|
|
startDate: format(subDays(new Date(), 30), 'yyyy-MM-dd'),
|
||
|
|
endDate: format(new Date(), 'yyyy-MM-dd'),
|
||
|
|
template: 'standard',
|
||
|
|
includeFindings: true,
|
||
|
|
includeRecommendations: true,
|
||
|
|
includeEvidence: true,
|
||
|
|
executiveSummary: true,
|
||
|
|
customTitle: '',
|
||
|
|
customDescription: ''
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const watchedType = watch('type');
|
||
|
|
|
||
|
|
// Fetch available templates
|
||
|
|
const { data: templates = [] } = useQuery({
|
||
|
|
queryKey: ['complianceTemplates'],
|
||
|
|
queryFn: () => auditApi.getComplianceTemplates(),
|
||
|
|
enabled: isOpen
|
||
|
|
});
|
||
|
|
|
||
|
|
// Fetch existing reports
|
||
|
|
const { data: existingReports = [], refetch: refetchReports } = useQuery({
|
||
|
|
queryKey: ['complianceReports'],
|
||
|
|
queryFn: () => auditApi.getComplianceReports(),
|
||
|
|
enabled: isOpen
|
||
|
|
});
|
||
|
|
|
||
|
|
// Generate report mutation
|
||
|
|
const generateReportMutation = useMutation({
|
||
|
|
mutationFn: (data: { type: FormData['type'], period: { start: Date; end: Date }, template?: string }) =>
|
||
|
|
auditApi.generateComplianceReport(data.type, data.period, data.template),
|
||
|
|
onSuccess: (result) => {
|
||
|
|
setGeneratingReportId(result.reportId);
|
||
|
|
toast.success('Report generation started');
|
||
|
|
|
||
|
|
// Poll for completion
|
||
|
|
const pollInterval = setInterval(async () => {
|
||
|
|
try {
|
||
|
|
const report = await auditApi.getComplianceReport(result.reportId);
|
||
|
|
if (report) {
|
||
|
|
clearInterval(pollInterval);
|
||
|
|
setGeneratingReportId(null);
|
||
|
|
queryClient.invalidateQueries({ queryKey: ['complianceReports'] });
|
||
|
|
toast.success('Report generated successfully');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
// Report might not be ready yet, continue polling
|
||
|
|
}
|
||
|
|
}, 2000);
|
||
|
|
|
||
|
|
// Stop polling after 5 minutes
|
||
|
|
setTimeout(() => {
|
||
|
|
clearInterval(pollInterval);
|
||
|
|
setGeneratingReportId(null);
|
||
|
|
}, 5 * 60 * 1000);
|
||
|
|
},
|
||
|
|
onError: (error) => {
|
||
|
|
toast.error('Failed to generate report');
|
||
|
|
setGeneratingReportId(null);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const selectedFramework = complianceFrameworks.find(f => f.value === watchedType);
|
||
|
|
|
||
|
|
const handlePeriodSelect = (period: { start: string; end: string }) => {
|
||
|
|
setValue('startDate', period.start);
|
||
|
|
setValue('endDate', period.end);
|
||
|
|
};
|
||
|
|
|
||
|
|
const onSubmit = (data: FormData) => {
|
||
|
|
generateReportMutation.mutate({
|
||
|
|
type: data.type,
|
||
|
|
period: {
|
||
|
|
start: new Date(data.startDate),
|
||
|
|
end: new Date(data.endDate)
|
||
|
|
},
|
||
|
|
template: data.template
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDownloadReport = async (report: ComplianceReport) => {
|
||
|
|
try {
|
||
|
|
const blob = await auditApi.exportLogs({
|
||
|
|
format: 'pdf',
|
||
|
|
filters: {
|
||
|
|
dateRange: {
|
||
|
|
start: report.period.start,
|
||
|
|
end: report.period.end
|
||
|
|
},
|
||
|
|
complianceFrameworks: [report.type]
|
||
|
|
},
|
||
|
|
includeMetadata: true,
|
||
|
|
includeCompliance: true,
|
||
|
|
template: `compliance_${report.type}`
|
||
|
|
});
|
||
|
|
|
||
|
|
const url = URL.createObjectURL(blob);
|
||
|
|
const link = document.createElement('a');
|
||
|
|
link.href = url;
|
||
|
|
link.download = `${report.type}_compliance_report_${format(new Date(), 'yyyy-MM-dd')}.pdf`;
|
||
|
|
document.body.appendChild(link);
|
||
|
|
link.click();
|
||
|
|
document.body.removeChild(link);
|
||
|
|
URL.revokeObjectURL(url);
|
||
|
|
|
||
|
|
toast.success('Report downloaded successfully');
|
||
|
|
} catch (error) {
|
||
|
|
toast.error('Failed to download report');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!isOpen) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AnimatePresence>
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0 }}
|
||
|
|
animate={{ opacity: 1 }}
|
||
|
|
exit={{ opacity: 0 }}
|
||
|
|
className="modal modal-open"
|
||
|
|
>
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
||
|
|
animate={{ opacity: 1, scale: 1 }}
|
||
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
||
|
|
className="modal-box w-11/12 max-w-6xl max-h-[90vh] overflow-hidden"
|
||
|
|
>
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between mb-6">
|
||
|
|
<div>
|
||
|
|
<h2 className="text-2xl font-bold text-base-content">
|
||
|
|
Compliance Report Generator
|
||
|
|
</h2>
|
||
|
|
<p className="text-base-content/60 mt-1">
|
||
|
|
Generate comprehensive compliance reports for various frameworks
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<button onClick={onClose} className="btn btn-ghost btn-sm">
|
||
|
|
<X className="h-4 w-4" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-3 gap-6 h-full overflow-hidden">
|
||
|
|
{/* Configuration Panel */}
|
||
|
|
<div className="col-span-2 space-y-6 overflow-y-auto pr-4">
|
||
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
|
|
{/* Framework Selection */}
|
||
|
|
<div className="form-section">
|
||
|
|
<h3 className="form-section-title">
|
||
|
|
<Shield className="h-4 w-4 inline mr-2" />
|
||
|
|
Compliance Framework
|
||
|
|
</h3>
|
||
|
|
<Controller
|
||
|
|
name="type"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<div className="grid grid-cols-2 gap-3">
|
||
|
|
{complianceFrameworks.map((framework) => {
|
||
|
|
const Icon = framework.icon;
|
||
|
|
return (
|
||
|
|
<label
|
||
|
|
key={framework.value}
|
||
|
|
className={`block p-4 border rounded-lg cursor-pointer transition-all ${
|
||
|
|
field.value === framework.value
|
||
|
|
? 'border-primary bg-primary/5 ring-2 ring-primary/20'
|
||
|
|
: 'border-base-300 hover:border-base-400'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<input
|
||
|
|
type="radio"
|
||
|
|
{...field}
|
||
|
|
value={framework.value}
|
||
|
|
className="sr-only"
|
||
|
|
/>
|
||
|
|
<div className="flex items-start space-x-3">
|
||
|
|
<Icon className={`h-5 w-5 ${framework.color} mt-0.5`} />
|
||
|
|
<div className="flex-1">
|
||
|
|
<div className="font-medium text-base-content">
|
||
|
|
{framework.label}
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-base-content/60 mt-1">
|
||
|
|
{framework.description}
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
||
|
|
{framework.requirements.slice(0, 2).map((req) => (
|
||
|
|
<span key={req} className="badge badge-outline badge-xs">
|
||
|
|
{req}
|
||
|
|
</span>
|
||
|
|
))}
|
||
|
|
{framework.requirements.length > 2 && (
|
||
|
|
<span className="badge badge-outline badge-xs">
|
||
|
|
+{framework.requirements.length - 2}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</label>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Time Period */}
|
||
|
|
<div className="form-section">
|
||
|
|
<h3 className="form-section-title">
|
||
|
|
<Calendar className="h-4 w-4 inline mr-2" />
|
||
|
|
Reporting Period
|
||
|
|
</h3>
|
||
|
|
|
||
|
|
{/* Quick Period Selection */}
|
||
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
||
|
|
{predefinedPeriods.map((period) => (
|
||
|
|
<button
|
||
|
|
key={period.label}
|
||
|
|
type="button"
|
||
|
|
onClick={() => handlePeriodSelect(period.getValue())}
|
||
|
|
className="btn btn-outline btn-sm"
|
||
|
|
>
|
||
|
|
{period.label}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<Controller
|
||
|
|
name="startDate"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<div>
|
||
|
|
<label className="label">
|
||
|
|
<span className="label-text">Start Date</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
{...field}
|
||
|
|
type="date"
|
||
|
|
className="input input-bordered w-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
<Controller
|
||
|
|
name="endDate"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<div>
|
||
|
|
<label className="label">
|
||
|
|
<span className="label-text">End Date</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
{...field}
|
||
|
|
type="date"
|
||
|
|
className="input input-bordered w-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Template Selection */}
|
||
|
|
<div className="form-section">
|
||
|
|
<h3 className="form-section-title">
|
||
|
|
<FileText className="h-4 w-4 inline mr-2" />
|
||
|
|
Report Template
|
||
|
|
</h3>
|
||
|
|
<Controller
|
||
|
|
name="template"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<select {...field} className="select select-bordered w-full">
|
||
|
|
{templates.map((template) => (
|
||
|
|
<option key={template.id} value={template.id}>
|
||
|
|
{template.name} - {template.description}
|
||
|
|
</option>
|
||
|
|
))}
|
||
|
|
</select>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Report Options */}
|
||
|
|
<div className="form-section">
|
||
|
|
<h3 className="form-section-title">
|
||
|
|
<Settings className="h-4 w-4 inline mr-2" />
|
||
|
|
Report Options
|
||
|
|
</h3>
|
||
|
|
<div className="space-y-3">
|
||
|
|
<Controller
|
||
|
|
name="includeFindings"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
className="checkbox"
|
||
|
|
checked={field.value}
|
||
|
|
onChange={field.onChange}
|
||
|
|
/>
|
||
|
|
<span className="label-text">Include detailed findings</span>
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
<Controller
|
||
|
|
name="includeRecommendations"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
className="checkbox"
|
||
|
|
checked={field.value}
|
||
|
|
onChange={field.onChange}
|
||
|
|
/>
|
||
|
|
<span className="label-text">Include remediation recommendations</span>
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
<Controller
|
||
|
|
name="includeEvidence"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
className="checkbox"
|
||
|
|
checked={field.value}
|
||
|
|
onChange={field.onChange}
|
||
|
|
/>
|
||
|
|
<span className="label-text">Include supporting evidence</span>
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
<Controller
|
||
|
|
name="executiveSummary"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
className="checkbox"
|
||
|
|
checked={field.value}
|
||
|
|
onChange={field.onChange}
|
||
|
|
/>
|
||
|
|
<span className="label-text">Include executive summary</span>
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Custom Fields for Custom Reports */}
|
||
|
|
{watchedType === 'custom' && (
|
||
|
|
<div className="form-section">
|
||
|
|
<h3 className="form-section-title">Custom Report Details</h3>
|
||
|
|
<div className="space-y-4">
|
||
|
|
<Controller
|
||
|
|
name="customTitle"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<div>
|
||
|
|
<label className="label">
|
||
|
|
<span className="label-text">Report Title</span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
{...field}
|
||
|
|
type="text"
|
||
|
|
placeholder="Custom Compliance Assessment"
|
||
|
|
className="input input-bordered w-full"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
<Controller
|
||
|
|
name="customDescription"
|
||
|
|
control={control}
|
||
|
|
render={({ field }) => (
|
||
|
|
<div>
|
||
|
|
<label className="label">
|
||
|
|
<span className="label-text">Description</span>
|
||
|
|
</label>
|
||
|
|
<textarea
|
||
|
|
{...field}
|
||
|
|
placeholder="Describe the scope and objectives of this compliance assessment..."
|
||
|
|
className="textarea textarea-bordered w-full h-24"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="flex justify-end space-x-2 pt-4 border-t border-base-300">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onClose}
|
||
|
|
className="btn btn-ghost"
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
className="btn btn-primary"
|
||
|
|
disabled={generateReportMutation.isPending}
|
||
|
|
>
|
||
|
|
{generateReportMutation.isPending ? (
|
||
|
|
<>
|
||
|
|
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||
|
|
Generating...
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Play className="h-4 w-4 mr-2" />
|
||
|
|
Generate Report
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Reports Panel */}
|
||
|
|
<div className="space-y-4 overflow-y-auto">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<h3 className="text-lg font-semibold">Recent Reports</h3>
|
||
|
|
<button
|
||
|
|
onClick={() => refetchReports()}
|
||
|
|
className="btn btn-ghost btn-sm"
|
||
|
|
>
|
||
|
|
<RefreshCw className="h-3 w-3" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-3">
|
||
|
|
{existingReports.length === 0 ? (
|
||
|
|
<div className="text-center text-base-content/60 py-8">
|
||
|
|
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||
|
|
<p>No reports generated yet</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
existingReports.map((report) => (
|
||
|
|
<div
|
||
|
|
key={report.id}
|
||
|
|
className="bg-base-200 rounded-lg p-4 space-y-3"
|
||
|
|
>
|
||
|
|
<div className="flex items-start justify-between">
|
||
|
|
<div className="flex-1">
|
||
|
|
<div className="font-medium text-base-content">
|
||
|
|
{report.title}
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-base-content/60 mt-1">
|
||
|
|
{report.type.toUpperCase()} • {format(report.generatedAt, 'MMM dd, yyyy')}
|
||
|
|
</div>
|
||
|
|
<div className="text-sm text-base-content/60">
|
||
|
|
{format(report.period.start, 'MMM dd')} - {format(report.period.end, 'MMM dd, yyyy')}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Report Summary */}
|
||
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<CheckCircle className="h-3 w-3 text-success" />
|
||
|
|
<span>{report.summary.compliantEvents}</span>
|
||
|
|
<span className="text-base-content/60">compliant</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<AlertTriangle className="h-3 w-3 text-warning" />
|
||
|
|
<span>{report.summary.violations}</span>
|
||
|
|
<span className="text-base-content/60">violations</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<Activity className="h-3 w-3 text-primary" />
|
||
|
|
<span>{report.summary.totalEvents}</span>
|
||
|
|
<span className="text-base-content/60">events</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center space-x-1">
|
||
|
|
<AlertTriangle className="h-3 w-3 text-error" />
|
||
|
|
<span>{report.summary.criticalFindings}</span>
|
||
|
|
<span className="text-base-content/60">critical</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="flex justify-end space-x-2">
|
||
|
|
<button
|
||
|
|
onClick={() => {/* View report logic */}}
|
||
|
|
className="btn btn-ghost btn-xs"
|
||
|
|
>
|
||
|
|
<Eye className="h-3 w-3 mr-1" />
|
||
|
|
View
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={() => handleDownloadReport(report)}
|
||
|
|
className="btn btn-ghost btn-xs"
|
||
|
|
>
|
||
|
|
<Download className="h-3 w-3 mr-1" />
|
||
|
|
Download
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Framework Info */}
|
||
|
|
{selectedFramework && (
|
||
|
|
<div className="bg-base-200 rounded-lg p-4 mt-6">
|
||
|
|
<h4 className="font-medium mb-2">{selectedFramework.label}</h4>
|
||
|
|
<p className="text-sm text-base-content/70 mb-3">
|
||
|
|
{selectedFramework.description}
|
||
|
|
</p>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<div className="text-sm font-medium">Key Requirements:</div>
|
||
|
|
{selectedFramework.requirements.map((req) => (
|
||
|
|
<div key={req} className="text-xs text-base-content/60 flex items-center">
|
||
|
|
<CheckCircle className="h-3 w-3 mr-1 text-success" />
|
||
|
|
{req}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
</motion.div>
|
||
|
|
</AnimatePresence>
|
||
|
|
);
|
||
|
|
};
|