diff --git a/components/admin/DetailModal.tsx b/components/admin/DetailModal.tsx index 6796ed5..d8008a3 100644 --- a/components/admin/DetailModal.tsx +++ b/components/admin/DetailModal.tsx @@ -1,21 +1,242 @@ 'use client'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; -import { Calendar, Check, X, FileText, Copy, CheckCircle2 } from 'lucide-react'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { Separator } from '@/components/ui/separator'; +import { Calendar, Check, X, Copy, CheckCircle2, Code2, LayoutTemplate, ExternalLink, Phone, Globe, MapPin, Mail } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useState } from 'react'; +// ── Autotask label maps ──────────────────────────────────────────────────────── + +const TICKET_STATUS: Record = { + 1: 'New', 5: 'Complete', 8: 'In Progress', 9: 'Waiting Customer', + 10: 'Waiting Materials', 11: 'Waiting Vendor', 12: 'Waiting Parts', + 13: 'Scheduled', 14: 'Escalated', 16: 'Waiting on 3rd Party', + 29: 'Customer Responded', 30: 'Dispatched', 31: 'Resolved', +}; + +const TICKET_PRIORITY: Record = { + 1: { label: 'Critical', variant: 'destructive' }, + 2: { label: 'High', variant: 'default' }, + 3: { label: 'Medium', variant: 'secondary' }, + 4: { label: 'Low', variant: 'outline' }, +}; + +const TICKET_SOURCE: Record = { + 1: 'Phone', 2: 'Email', 5: 'Web Portal', 6: 'Monitoring Alert', + 8: 'RMM Alert', 9: 'Chat', 10: 'In Person', 14: 'API', +}; + +const QUEUE: Record = { + 29482833: 'Client Services', 29482834: 'Network Operations', + 29482835: 'Help Desk', 29482836: 'Projects', +}; + +const COMPANY_TYPE: Record = { + 1: 'Customer', 2: 'Lead', 3: 'Prospect', 4: 'Dead', 6: 'Cancelation', + 7: 'Vendor', 8: 'Partner', +}; + +// ── Field metadata for formatted view ───────────────────────────────────────── + +type FieldGroup = { + label: string; + fields: Array<{ + key: string; + label: string; + type?: 'date' | 'bool' | 'status' | 'priority' | 'source' | 'queue' | 'company_type' | 'url' | 'phone' | 'email' | 'text' | 'hours' | 'id'; + }>; +}; + +const TICKET_GROUPS: FieldGroup[] = [ + { + label: 'Overview', + fields: [ + { key: 'ticket_number', label: 'Ticket #' }, + { key: 'title', label: 'Title' }, + { key: 'status', label: 'Status', type: 'status' }, + { key: 'priority', label: 'Priority', type: 'priority' }, + { key: 'source', label: 'Source', type: 'source' }, + { key: 'queue_id', label: 'Queue', type: 'queue' }, + ], + }, + { + label: 'Parties', + fields: [ + { key: 'company_id', label: 'Company ID', type: 'id' }, + { key: 'contact_id', label: 'Contact ID', type: 'id' }, + { key: 'assigned_resource_id', label: 'Assigned Resource ID', type: 'id' }, + ], + }, + { + label: 'Classification', + fields: [ + { key: 'issue_type', label: 'Issue Type' }, + { key: 'sub_issue_type', label: 'Sub-Issue Type' }, + ], + }, + { + label: 'Dates & Time', + fields: [ + { key: 'create_date', label: 'Created', type: 'date' }, + { key: 'due_date_time', label: 'Due', type: 'date' }, + { key: 'last_activity_date', label: 'Last Activity', type: 'date' }, + { key: 'completed_date', label: 'Completed', type: 'date' }, + { key: 'estimated_hours', label: 'Estimated Hours', type: 'hours' }, + ], + }, + { + label: 'System', + fields: [ + { key: 'id', label: 'Record ID', type: 'id' }, + { key: 'synced_at', label: 'Synced At', type: 'date' }, + { key: 'is_deleted', label: 'Deleted', type: 'bool' }, + ], + }, +]; + +const COMPANY_GROUPS: FieldGroup[] = [ + { + label: 'Identity', + fields: [ + { key: 'company_name', label: 'Company Name' }, + { key: 'company_number', label: 'Company #' }, + { key: 'company_type', label: 'Type', type: 'company_type' }, + { key: 'is_active', label: 'Active', type: 'bool' }, + ], + }, + { + label: 'Contact', + fields: [ + { key: 'phone', label: 'Phone', type: 'phone' }, + { key: 'alternate_phone1', label: 'Alt Phone 1', type: 'phone' }, + { key: 'alternate_phone2', label: 'Alt Phone 2', type: 'phone' }, + { key: 'fax', label: 'Fax', type: 'phone' }, + { key: 'web_site_url', label: 'Website', type: 'url' }, + ], + }, + { + label: 'Address', + fields: [ + { key: 'address1', label: 'Address 1' }, + { key: 'address2', label: 'Address 2' }, + { key: 'city', label: 'City' }, + { key: 'state', label: 'State' }, + { key: 'postal_code', label: 'Postal Code' }, + { key: 'country', label: 'Country' }, + ], + }, + { + label: 'System', + fields: [ + { key: 'id', label: 'Record ID', type: 'id' }, + { key: 'synced_at', label: 'Synced At', type: 'date' }, + { key: 'is_deleted', label: 'Deleted', type: 'bool' }, + ], + }, +]; + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +function resolveLabel(key: string, value: any, type?: string): { display: React.ReactNode; isEmpty: boolean } { + if (value === null || value === undefined || value === '') { + return { display: , isEmpty: true }; + } + + switch (type) { + case 'bool': + return { + display: ( + + {value ? : } + {value ? 'Yes' : 'No'} + + ), + isEmpty: false, + }; + case 'date': + try { + const d = new Date(value); + return { + display: ( + + + {d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })} + {d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} + + ), + isEmpty: false, + }; + } catch { break; } + case 'status': { + const label = TICKET_STATUS[Number(value)] ?? `Status ${value}`; + return { display: {label}, isEmpty: false }; + } + case 'priority': { + const p = TICKET_PRIORITY[Number(value)]; + return { display: {p?.label ?? `Priority ${value}`}, isEmpty: false }; + } + case 'source': { + const label = TICKET_SOURCE[Number(value)] ?? `Source ${value}`; + return { display: {label}, isEmpty: false }; + } + case 'queue': { + const label = QUEUE[Number(value)] ?? `Queue ${value}`; + return { display: {label}, isEmpty: false }; + } + case 'company_type': { + const label = COMPANY_TYPE[Number(value)] ?? `Type ${value}`; + return { display: {label}, isEmpty: false }; + } + case 'url': + return { + display: ( + + {value} + + + ), + isEmpty: false, + }; + case 'phone': + return { + display: ( + + {value} + + ), + isEmpty: false, + }; + case 'id': + return { display: {value}, isEmpty: false }; + case 'hours': + return { display: {Number(value).toFixed(1)} hrs, isEmpty: false }; + } + + if (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/)) { + return resolveLabel(key, value, 'date'); + } + + return { display: {String(value)}, isEmpty: false }; +} + +function detectGroups(data: Record): FieldGroup[] { + if ('ticket_number' in data) return TICKET_GROUPS; + if ('company_name' in data) return COMPANY_GROUPS; + return [{ label: 'Fields', fields: Object.keys(data).map(k => ({ key: k, label: k })) }]; +} + +// ── Component ────────────────────────────────────────────────────────────────── + interface DetailModalProps { open: boolean; onOpenChange: (open: boolean) => void; title: string; data: Record | null; - fields?: Array<{ - key: string; - label: string; - render?: (value: any) => React.ReactNode; - }>; + fields?: Array<{ key: string; label: string; render?: (value: any) => React.ReactNode }>; } export default function DetailModal({ open, onOpenChange, title, data, fields }: DetailModalProps) { @@ -29,101 +250,154 @@ export default function DetailModal({ open, onOpenChange, title, data, fields }: setTimeout(() => setCopiedField(null), 2000); }; - const renderValue = (value: any): React.ReactNode => { - if (value === null || value === undefined) { - return ( - - - null - - ); - } - if (typeof value === 'boolean') { - return ( - - {value ? : } - {value ? 'Yes' : 'No'} - - ); - } - if (value instanceof Date || (typeof value === 'string' && value.match(/^\d{4}-\d{2}-\d{2}/))) { - return ( - - - {new Date(value).toLocaleString()} - - ); - } - if (typeof value === 'object') { - return ( -
-          {JSON.stringify(value, null, 2)}
-        
- ); - } - return {String(value)}; - }; + const groups = detectGroups(data); - const displayFields = fields || Object.keys(data).map(key => ({ key, label: key, render: undefined })); + // Raw tab: all fields + const rawFields = fields || Object.keys(data).map(k => ({ key: k, label: k, render: undefined })); + + const renderRaw = (value: any): React.ReactNode => { + if (value === null || value === undefined) return null; + if (typeof value === 'boolean') return {value ? 'true' : 'false'}; + if (typeof value === 'object') return
{JSON.stringify(value, null, 2)}
; + return {String(value)}; + }; return ( - - + + {/* Header */} +
{title} - - Detailed view of record • {displayFields.length} fields + + Record ID: {data.id}
- - ID: {data.id} - -
- - -
-
- {displayFields.map((field, index) => { - const value = data[field.key]; - const stringValue = value !== null && value !== undefined ? String(value) : ''; - - return ( -
-
- -
- {field.label} -
-
-
-
- {field.render ? field.render(value) : renderValue(value)} -
- {stringValue && ( - - )} -
-
- ); - })} + {'is_active' in data && ( + + {data.is_active ? 'Active' : 'Inactive'} + + )} + {'status' in data && ( + + {TICKET_STATUS[Number(data.status)] ?? `Status ${data.status}`} + + )}
+ + {/* Tabs */} + +
+ + + + Formatted + + + + Raw + + +
+ + {/* ── Formatted Tab ── */} + +
+ {groups.map((group) => { + const visibleFields = group.fields.filter(f => f.key in data); + if (visibleFields.length === 0) return null; + return ( +
+

{group.label}

+
+ {visibleFields.map((field, idx) => { + const value = data[field.key]; + const { display, isEmpty } = resolveLabel(field.key, value, field.type); + const stringValue = value !== null && value !== undefined ? String(value) : ''; + return ( +
+ {idx > 0 && } +
+
+ {field.label} +
+
+
+ {display} +
+ {stringValue && !isEmpty && ( + + )} +
+
+
+ ); + })} +
+
+ ); + })} + + {/* Description block for tickets */} + {'description' in data && data.description && ( +
+

Description

+
+ {data.description} +
+
+ )} +
+
+ + {/* ── Raw Tab ── */} + +
+ {rawFields.map((field, idx) => { + const value = data[field.key]; + const stringValue = value !== null && value !== undefined ? String(value) : ''; + return ( +
+ {idx > 0 && } +
+
+ {field.key} +
+
+
+ {field.render ? field.render(value) : renderRaw(value)} +
+ {stringValue && ( + + )} +
+
+
+ ); + })} +
+
+
);