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
This commit is contained in:
parent
e8462ef301
commit
6eee14f8af
171 changed files with 32671 additions and 621 deletions
130
components/admin/DetailModal.tsx
Normal file
130
components/admin/DetailModal.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
'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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue