prvng_platform/control-center-ui/src/components/audit/ComplianceReportGenerator.tsx
2025-10-07 10:59:52 +01:00

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>
);
};