- DetailModal: thread tz through resolveLabel(...) module helper + default export's 3 inline date/time calls. - IntegrationStatusTabs: thread tz through fmtDate helper + VeeamTab sub-component prop. - SyncScheduler: thread tz into closure-scoped formatDate helper. - audit-log-table, user-table, user-sessions, active-sessions: inline toLocale calls in component body. - analysis-view: useUserTimezone in AnalysisView; thread tz into 4 toLocaleString calls. - resolution-trend, volume-trend (recharts): module-scope fmtDate(iso) → fmtDate(iso, tz); useUserTimezone in named export; thread tz into axis tickFormatter + tooltip labelFormatter. - ticket-detail-modal: thread tz into formatDate arrow inside TicketDetailModal. - TimelineView: useUserTimezone; thread tz into 4 toLocale*String calls (hour/day/month/event-time formatters). - ScoreCard: useUserTimezone in AggregateScoreCard; thread tz into the date-range latest call. - addigy-tab: useUserTimezone in AddigyTab; thread tz into 2 inline calls. - activity-sparkline: module-scope fmtHour(iso) → fmtHour(iso, tz); useUserTimezone in ActivitySparkline; update 3 callsites in title/aria. - compliance-detail-table: thread tz from ComplianceDetailTable into ContractCoverageModal sub-component (2 inline date calls). - company-backup-detail: module-scope formatDate(d) → formatDate(d, tz); useUserTimezone in CompanyBackupDetail; update 3 callsites. Migrates 31 of 81 audit leak callsites.
211 lines
7.6 KiB
TypeScript
211 lines
7.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
|
|
|
interface CompanyBackupDetailProps {
|
|
companyId: number | null;
|
|
companyName: string;
|
|
}
|
|
|
|
function StatusBadge({ status }: { status: string }) {
|
|
const colors = status === 'Success'
|
|
? 'bg-green-500/10 text-green-500'
|
|
: status === 'Warning'
|
|
? 'bg-yellow-500/10 text-yellow-500'
|
|
: status === 'Failed'
|
|
? 'bg-red-500/10 text-red-500'
|
|
: status === 'Running'
|
|
? 'bg-blue-500/10 text-blue-500'
|
|
: 'bg-gray-500/10 text-gray-500';
|
|
return <Badge className={colors}>{status}</Badge>;
|
|
}
|
|
|
|
function formatBytes(bytes: number | null): string {
|
|
if (!bytes) return '-';
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
let i = 0;
|
|
let val = bytes;
|
|
while (val >= 1024 && i < units.length - 1) {
|
|
val /= 1024;
|
|
i++;
|
|
}
|
|
return `${val.toFixed(1)} ${units[i]}`;
|
|
}
|
|
|
|
function formatDate(dateStr: string | null, tz: string): string {
|
|
if (!dateStr) return 'Never';
|
|
return new Date(dateStr).toLocaleString(undefined, { timeZone: tz });
|
|
}
|
|
|
|
export function CompanyBackupDetail({ companyId, companyName }: CompanyBackupDetailProps) {
|
|
const tz = useUserTimezone();
|
|
const [workloads, setWorkloads] = useState<any[]>([]);
|
|
const [jobs, setJobs] = useState<{ serverJobs: any[]; agentJobs: any[] }>({ serverJobs: [], agentJobs: [] });
|
|
const [compliance, setCompliance] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!companyId) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
Promise.all([
|
|
fetch(`/api/veeam/companies/${companyId}/workloads`).then(r => r.json()),
|
|
fetch(`/api/veeam/companies/${companyId}/jobs`).then(r => r.json()),
|
|
fetch(`/api/veeam/compliance/${companyId}`).then(r => r.json()),
|
|
]).then(([w, j, c]) => {
|
|
setWorkloads(Array.isArray(w) ? w : []);
|
|
setJobs(j || { serverJobs: [], agentJobs: [] });
|
|
setCompliance(Array.isArray(c) ? c : []);
|
|
setLoading(false);
|
|
}).catch(() => setLoading(false));
|
|
}, [companyId]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<Skeleton className="h-32 w-full" />
|
|
<Skeleton className="h-32 w-full" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!companyId) {
|
|
return <p className="text-muted-foreground text-sm">No Autotask company linked to this organization.</p>;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Protected Workloads */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium">Protected Workloads ({workloads.length})</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{workloads.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No protected workloads</p>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Restore Points</TableHead>
|
|
<TableHead>Latest Restore</TableHead>
|
|
<TableHead>Size</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{workloads.map((w: any) => (
|
|
<TableRow key={w.instance_uid}>
|
|
<TableCell className="font-medium">{w.name}</TableCell>
|
|
<TableCell>{w.restore_points ?? '-'}</TableCell>
|
|
<TableCell className="text-sm">{formatDate(w.latest_restore_point_date, tz)}</TableCell>
|
|
<TableCell className="text-sm">{formatBytes(w.used_source_size)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Backup Jobs */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium">
|
|
Backup Jobs ({jobs.serverJobs.length} server, {jobs.agentJobs.length} agent)
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{jobs.serverJobs.length === 0 && jobs.agentJobs.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No backup jobs</p>
|
|
) : (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Last Run</TableHead>
|
|
<TableHead>Duration</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{jobs.serverJobs.map((j: any) => (
|
|
<TableRow key={j.instance_uid}>
|
|
<TableCell className="font-medium">{j.name}</TableCell>
|
|
<TableCell className="text-sm">{j.type || 'Server'}</TableCell>
|
|
<TableCell><StatusBadge status={j.status || 'Unknown'} /></TableCell>
|
|
<TableCell className="text-sm">{formatDate(j.last_run, tz)}</TableCell>
|
|
<TableCell className="text-sm">{j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
{jobs.agentJobs.map((j: any) => (
|
|
<TableRow key={j.instance_uid}>
|
|
<TableCell className="font-medium">{j.name}</TableCell>
|
|
<TableCell className="text-sm">{j.backup_mode || 'Agent'}</TableCell>
|
|
<TableCell><StatusBadge status={j.status || 'Unknown'} /></TableCell>
|
|
<TableCell className="text-sm">{formatDate(j.last_run, tz)}</TableCell>
|
|
<TableCell className="text-sm">{j.last_duration ? `${Math.round(j.last_duration / 60)}m` : '-'}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Compliance Mismatches */}
|
|
{compliance.length > 0 && (
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-yellow-500">
|
|
Compliance Issues ({compliance.length})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Device</TableHead>
|
|
<TableHead>Issue</TableHead>
|
|
<TableHead>Backup UDF</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{compliance.map((c: any) => (
|
|
<TableRow key={c.id}>
|
|
<TableCell className="font-medium">{c.device_name}</TableCell>
|
|
<TableCell>
|
|
<Badge className={
|
|
c.mismatch_type === 'contracted_not_backed_up'
|
|
? 'bg-red-500/10 text-red-500'
|
|
: 'bg-yellow-500/10 text-yellow-500'
|
|
}>
|
|
{c.mismatch_type === 'contracted_not_backed_up' ? 'Missing Backup' : 'No Contract'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm">{c.backup_type_udf || '-'}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|