wulf-pulse/components/admin/DetailModal.tsx
root 6eee14f8af Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
2025-11-19 14:18:16 -05:00

130 lines
4.8 KiB
TypeScript

'use client';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Calendar, Check, X, FileText, Copy, CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useState } from 'react';
interface DetailModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
data: Record<string, any> | null;
fields?: Array<{
key: string;
label: string;
render?: (value: any) => React.ReactNode;
}>;
}
export default function DetailModal({ open, onOpenChange, title, data, fields }: DetailModalProps) {
const [copiedField, setCopiedField] = useState<string | null>(null);
if (!data) return null;
const copyToClipboard = (text: string, fieldKey: string) => {
navigator.clipboard.writeText(text);
setCopiedField(fieldKey);
setTimeout(() => setCopiedField(null), 2000);
};
const renderValue = (value: any): React.ReactNode => {
if (value === null || value === undefined) {
return (
<span className="inline-flex items-center gap-1.5 text-muted-foreground italic text-xs">
<X className="w-3 h-3" />
null
</span>
);
}
if (typeof value === 'boolean') {
return (
<Badge variant={value ? 'default' : 'secondary'} className="gap-1">
{value ? <Check className="w-3 h-3" /> : <X className="w-3 h-3" />}
{value ? 'Yes' : 'No'}
</Badge>
);
}
if (value instanceof Date || (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/))) {
return (
<span className="inline-flex items-center gap-1.5 text-sm">
<Calendar className="w-3.5 h-3.5 text-muted-foreground" />
{new Date(value).toLocaleString()}
</span>
);
}
if (typeof value === 'object') {
return (
<pre className="text-xs bg-muted p-3 rounded-md overflow-x-auto border">
{JSON.stringify(value, null, 2)}
</pre>
);
}
return <span className="text-sm">{String(value)}</span>;
};
const displayFields = fields || Object.keys(data).map(key => ({ key, label: key, render: undefined }));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="pb-4 border-b">
<div className="flex items-start justify-between gap-4">
<div>
<DialogTitle className="text-2xl font-bold">{title}</DialogTitle>
<DialogDescription className="mt-1.5">
Detailed view of record {displayFields.length} fields
</DialogDescription>
</div>
<Badge variant="outline" className="shrink-0">
ID: {data.id}
</Badge>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto pr-2 -mr-2">
<div className="space-y-1 py-4">
{displayFields.map((field, index) => {
const value = data[field.key];
const stringValue = value !== null && value !== undefined ? String(value) : '';
return (
<div
key={field.key}
className="group grid grid-cols-[200px_1fr] gap-6 py-3 px-4 rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex items-start gap-2">
<FileText className="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" />
<div className="font-medium text-sm text-muted-foreground">
{field.label}
</div>
</div>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 break-words min-w-0">
{field.render ? field.render(value) : renderValue(value)}
</div>
{stringValue && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
onClick={() => copyToClipboard(stringValue, field.key)}
>
{copiedField === field.key ? (
<CheckCircle2 className="w-3.5 h-3.5 text-green-500" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</Button>
)}
</div>
</div>
);
})}
</div>
</div>
</DialogContent>
</Dialog>
);
}