wulf-pulse/components/configuration-items/psa-tab.tsx
Lorentz Hinrichsen 3c3124d8c9 Restructure: rename to Pulse and move app to root
- Renamed project from PSA-Utils to Pulse
- Moved all app files from autotask-app/ to root
- Updated package.json name to 'pulse'
- Updated Docker container names to pulse-app and pulse-redis
- Updated Docker network name to pulse-network
2025-10-28 23:08:54 -04:00

456 lines
16 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import {
Server,
Edit,
Save,
Power,
Info,
Cpu,
RefreshCw,
User,
Receipt,
Ticket as TicketIcon
} from 'lucide-react';
import { format } from 'date-fns';
import { ConfigurationItem } from '@/lib/types/autotask';
import { PurchaseHistoryModal } from './purchase-history-modal';
import { RelatedTicketsModal } from './related-tickets-modal';
interface PSATabProps {
device?: ConfigurationItem;
onUpdate: (device: ConfigurationItem) => void;
}
export function PSATab({ device, onUpdate }: PSATabProps) {
const [editMode, setEditMode] = useState(false);
const [saving, setSaving] = useState(false);
const [editedData, setEditedData] = useState<Partial<ConfigurationItem>>(device || {});
const [error, setError] = useState<string | null>(null);
const [contactName, setContactName] = useState<string | null>(null);
const [loadingContact, setLoadingContact] = useState(false);
const [purchaseHistoryOpen, setPurchaseHistoryOpen] = useState(false);
const [relatedTicketsOpen, setRelatedTicketsOpen] = useState(false);
// Fetch contact information if contactID exists
useEffect(() => {
if (!device?.contactID) {
setContactName(null);
return;
}
const fetchContact = async () => {
setLoadingContact(true);
try {
const response = await fetch(`/api/contacts/${device.contactID}`);
if (response.ok) {
const data = await response.json();
setContactName(data.contact ? `${data.contact.firstName} ${data.contact.lastName}` : null);
}
} catch (err) {
console.error('Failed to fetch contact:', err);
} finally {
setLoadingContact(false);
}
};
fetchContact();
}, [device?.contactID]);
const handleSave = async () => {
if (!device) return;
setSaving(true);
setError(null);
try {
const response = await fetch(`/api/configuration-items/${device.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(editedData),
});
if (!response.ok) {
throw new Error('Failed to update configuration item');
}
const updated = await response.json();
onUpdate(updated.configurationItem);
setEditMode(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save changes');
} finally {
setSaving(false);
}
};
const handleMakeInactive = async () => {
if (!device) return;
setSaving(true);
setError(null);
try {
const response = await fetch(`/api/configuration-items/${device.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ isActive: false }),
});
if (!response.ok) {
throw new Error('Failed to update configuration item');
}
const updated = await response.json();
onUpdate(updated.configurationItem);
setEditedData({ ...editedData, isActive: false });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to make inactive');
} finally {
setSaving(false);
}
};
return (
<>
<Card className="border-0 shadow-lg">
<CardHeader className="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 rounded-t-lg">
<div className="flex items-center justify-between">
<CardTitle className="text-lg">PSA Configuration Item</CardTitle>
<div className="flex items-center gap-2">
{!editMode ? (
<>
<Button
variant="outline"
size="sm"
onClick={() => setPurchaseHistoryOpen(true)}
>
<Receipt className="w-4 h-4 mr-2" />
Purchase History
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setRelatedTicketsOpen(true)}
>
<TicketIcon className="w-4 h-4 mr-2" />
Related Tickets
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
setEditMode(true);
setEditedData(device || {});
}}
disabled={!device}
>
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
{device?.isActive && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
<Power className="w-4 h-4 mr-2" />
Make Inactive
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Make Configuration Item Inactive?</AlertDialogTitle>
<AlertDialogDescription>
This will mark the configuration item as inactive in Autotask PSA.
You can reactivate it later if needed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleMakeInactive}>
Make Inactive
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</>
) : (
<>
<Button
variant="outline"
size="sm"
onClick={() => {
setEditMode(false);
setEditedData(device || {});
setError(null);
}}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSave}
disabled={saving}
>
{saving ? (
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save
</Button>
</>
)}
</div>
</div>
</CardHeader>
<CardContent className="pt-6">
{error && (
<div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-md">
{error}
</div>
)}
{device ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Basic Information */}
<div className="space-y-4">
<h3 className="font-semibold flex items-center gap-2">
<Info className="w-4 h-4" />
Basic Information
</h3>
<div className="space-y-3">
<div>
<Label>Reference Title</Label>
{editMode ? (
<Input
value={editedData.referenceTitle || ''}
onChange={(e) => setEditedData({...editedData, referenceTitle: e.target.value})}
className="mt-1"
/>
) : (
<p className="text-sm text-muted-foreground mt-1">{device.referenceTitle}</p>
)}
</div>
<div>
<Label>Reference Number</Label>
{editMode ? (
<Input
value={editedData.referenceNumber || ''}
onChange={(e) => setEditedData({...editedData, referenceNumber: e.target.value})}
className="mt-1"
/>
) : (
<p className="text-sm text-muted-foreground font-mono mt-1">
{device.referenceNumber || '-'}
</p>
)}
</div>
<div>
<Label>Serial Number</Label>
{editMode ? (
<Input
value={editedData.serialNumber || ''}
onChange={(e) => setEditedData({...editedData, serialNumber: e.target.value})}
className="mt-1"
/>
) : (
<p className="text-sm text-muted-foreground font-mono mt-1">
{device.serialNumber || '-'}
</p>
)}
</div>
<div>
<Label>Location</Label>
{editMode ? (
<Input
value={editedData.location || ''}
onChange={(e) => setEditedData({...editedData, location: e.target.value})}
className="mt-1"
/>
) : (
<p className="text-sm text-muted-foreground mt-1">
{device.location || '-'}
</p>
)}
</div>
<div>
<Label>Active Status</Label>
{editMode ? (
<div className="flex items-center space-x-2 mt-1">
<Switch
checked={editedData.isActive}
onCheckedChange={(checked) => setEditedData({...editedData, isActive: checked})}
/>
<Label>{editedData.isActive ? 'Active' : 'Inactive'}</Label>
</div>
) : (
<div className="mt-1">
{device.isActive ? (
<Badge variant="default" className="bg-green-600">Active</Badge>
) : (
<Badge variant="secondary">Inactive</Badge>
)}
</div>
)}
</div>
<div>
<Label>Associated Contact</Label>
{loadingContact ? (
<p className="text-sm text-muted-foreground mt-1">Loading...</p>
) : contactName ? (
<div className="flex items-center gap-2 mt-1">
<Badge variant="default" className="bg-blue-600">
<User className="w-3 h-3 mr-1" />
{contactName}
</Badge>
</div>
) : device.contactID ? (
<p className="text-sm text-muted-foreground mt-1">Contact ID: {device.contactID}</p>
) : (
<p className="text-sm text-muted-foreground mt-1">No contact assigned</p>
)}
</div>
</div>
</div>
{/* Technical Details */}
<div className="space-y-4">
<h3 className="font-semibold flex items-center gap-2">
<Cpu className="w-4 h-4" />
Technical Details
</h3>
<div className="space-y-3">
<div>
<Label>Model Number</Label>
{editMode ? (
<Input
value={editedData.modelNumber || ''}
onChange={(e) => setEditedData({...editedData, modelNumber: e.target.value})}
className="mt-1"
/>
) : (
<p className="text-sm text-muted-foreground mt-1">
{device.modelNumber || '-'}
</p>
)}
</div>
<div>
<Label>MAC Address</Label>
{editMode ? (
<Input
value={editedData.macAddress || ''}
onChange={(e) => setEditedData({...editedData, macAddress: e.target.value})}
className="mt-1"
/>
) : (
<p className="text-sm text-muted-foreground font-mono mt-1">
{device.macAddress || '-'}
</p>
)}
</div>
<div>
<Label>Install Date</Label>
<p className="text-sm text-muted-foreground mt-1">
{device.installDate ?
format(new Date(device.installDate), 'MMM d, yyyy') :
'-'}
</p>
</div>
<div>
<Label>Warranty Expiration</Label>
<p className="text-sm text-muted-foreground mt-1">
{device.warrantyExpirationDate ?
format(new Date(device.warrantyExpirationDate), 'MMM d, yyyy') :
'-'}
</p>
</div>
<div>
<Label>RMM Device UID</Label>
<p className="text-sm text-muted-foreground font-mono mt-1">
{device.rmmDeviceUID || '-'}
</p>
</div>
</div>
</div>
{/* Notes */}
<div className="md:col-span-2 space-y-4">
<h3 className="font-semibold">Notes</h3>
{editMode ? (
<Textarea
value={editedData.notes || ''}
onChange={(e) => setEditedData({...editedData, notes: e.target.value})}
rows={4}
placeholder="Add notes..."
/>
) : (
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
{device.notes || 'No notes available'}
</p>
)}
</div>
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
<Server className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No PSA data available for this device</p>
</div>
)}
</CardContent>
</Card>
{/* Purchase History Modal */}
{device && (
<PurchaseHistoryModal
configItemId={device.id}
serialNumber={device.serialNumber}
createDate={device.createDate}
open={purchaseHistoryOpen}
onOpenChange={setPurchaseHistoryOpen}
/>
)}
{/* Related Tickets Modal */}
{device && (
<RelatedTicketsModal
configItemId={device.id}
open={relatedTicketsOpen}
onOpenChange={setRelatedTicketsOpen}
/>
)}
</>
);
}