@checkstack/maintenance-frontend 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/package.json +29 -0
- package/src/api.ts +10 -0
- package/src/components/MaintenanceEditor.tsx +317 -0
- package/src/components/MaintenanceMenuItems.tsx +33 -0
- package/src/components/MaintenanceUpdateForm.tsx +123 -0
- package/src/components/SystemMaintenanceBadge.tsx +49 -0
- package/src/components/SystemMaintenancePanel.tsx +176 -0
- package/src/index.tsx +71 -0
- package/src/pages/MaintenanceConfigPage.tsx +347 -0
- package/src/pages/MaintenanceDetailPage.tsx +295 -0
- package/src/pages/SystemMaintenanceHistoryPage.tsx +198 -0
- package/src/utils/badges.tsx +29 -0
- package/tsconfig.json +6 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { useApi, type SlotContext } from "@checkstack/frontend-api";
|
|
4
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
5
|
+
import { resolveRoute } from "@checkstack/common";
|
|
6
|
+
import { SystemDetailsSlot } from "@checkstack/catalog-common";
|
|
7
|
+
import { maintenanceApiRef } from "../api";
|
|
8
|
+
import {
|
|
9
|
+
maintenanceRoutes,
|
|
10
|
+
MAINTENANCE_UPDATED,
|
|
11
|
+
type MaintenanceWithSystems,
|
|
12
|
+
} from "@checkstack/maintenance-common";
|
|
13
|
+
import {
|
|
14
|
+
Card,
|
|
15
|
+
CardHeader,
|
|
16
|
+
CardTitle,
|
|
17
|
+
CardContent,
|
|
18
|
+
Badge,
|
|
19
|
+
LoadingSpinner,
|
|
20
|
+
Button,
|
|
21
|
+
} from "@checkstack/ui";
|
|
22
|
+
import { Wrench, Clock, Calendar, History, ChevronRight } from "lucide-react";
|
|
23
|
+
import { formatDistanceToNow, format } from "date-fns";
|
|
24
|
+
|
|
25
|
+
type Props = SlotContext<typeof SystemDetailsSlot>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Panel shown on system detail pages displaying active/upcoming maintenances.
|
|
29
|
+
* Listens for realtime updates via signals.
|
|
30
|
+
*/
|
|
31
|
+
export const SystemMaintenancePanel: React.FC<Props> = ({ system }) => {
|
|
32
|
+
const api = useApi(maintenanceApiRef);
|
|
33
|
+
const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
|
|
34
|
+
[]
|
|
35
|
+
);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
|
|
38
|
+
const refetch = useCallback(() => {
|
|
39
|
+
if (!system?.id) return;
|
|
40
|
+
|
|
41
|
+
api
|
|
42
|
+
.getMaintenancesForSystem({ systemId: system.id })
|
|
43
|
+
.then(setMaintenances)
|
|
44
|
+
.catch(console.error)
|
|
45
|
+
.finally(() => setLoading(false));
|
|
46
|
+
}, [system?.id, api]);
|
|
47
|
+
|
|
48
|
+
// Initial fetch
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
refetch();
|
|
51
|
+
}, [refetch]);
|
|
52
|
+
|
|
53
|
+
// Listen for realtime maintenance updates
|
|
54
|
+
useSignal(MAINTENANCE_UPDATED, ({ systemIds }) => {
|
|
55
|
+
if (system?.id && systemIds.includes(system.id)) {
|
|
56
|
+
refetch();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (loading) {
|
|
61
|
+
return (
|
|
62
|
+
<Card>
|
|
63
|
+
<CardContent className="p-6 flex justify-center">
|
|
64
|
+
<LoadingSpinner />
|
|
65
|
+
</CardContent>
|
|
66
|
+
</Card>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (maintenances.length === 0) {
|
|
71
|
+
// Show a subtle card with just the history button when no active maintenances
|
|
72
|
+
return (
|
|
73
|
+
<Card className="border-border/50">
|
|
74
|
+
<CardContent className="p-4">
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
77
|
+
<Wrench className="h-4 w-4" />
|
|
78
|
+
<span className="text-sm">No active maintenances</span>
|
|
79
|
+
</div>
|
|
80
|
+
<Button variant="ghost" size="sm" asChild>
|
|
81
|
+
<Link
|
|
82
|
+
to={resolveRoute(maintenanceRoutes.routes.systemHistory, {
|
|
83
|
+
systemId: system.id,
|
|
84
|
+
})}
|
|
85
|
+
>
|
|
86
|
+
<History className="h-4 w-4 mr-1" />
|
|
87
|
+
View History
|
|
88
|
+
</Link>
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const getStatusBadge = (status: string) => {
|
|
97
|
+
switch (status) {
|
|
98
|
+
case "in_progress": {
|
|
99
|
+
return <Badge variant="warning">In Progress</Badge>;
|
|
100
|
+
}
|
|
101
|
+
case "scheduled": {
|
|
102
|
+
return <Badge variant="info">Scheduled</Badge>;
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
return <Badge>{status}</Badge>;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Card className="border-warning/30 bg-warning/5">
|
|
112
|
+
<CardHeader className="border-b border-border bg-warning/10">
|
|
113
|
+
<div className="flex items-center justify-between">
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<Wrench className="h-5 w-5 text-warning" />
|
|
116
|
+
<CardTitle className="text-lg font-semibold">
|
|
117
|
+
Planned Maintenance
|
|
118
|
+
</CardTitle>
|
|
119
|
+
</div>
|
|
120
|
+
<Button variant="ghost" size="sm" asChild>
|
|
121
|
+
<Link
|
|
122
|
+
to={resolveRoute(maintenanceRoutes.routes.systemHistory, {
|
|
123
|
+
systemId: system.id,
|
|
124
|
+
})}
|
|
125
|
+
>
|
|
126
|
+
<History className="h-4 w-4 mr-1" />
|
|
127
|
+
View History
|
|
128
|
+
</Link>
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent className="p-4 space-y-3">
|
|
133
|
+
{maintenances.map((m) => (
|
|
134
|
+
<Link
|
|
135
|
+
key={m.id}
|
|
136
|
+
to={`${resolveRoute(maintenanceRoutes.routes.detail, {
|
|
137
|
+
maintenanceId: m.id,
|
|
138
|
+
})}?from=${system.id}`}
|
|
139
|
+
className="block p-3 rounded-lg border border-border bg-background hover:bg-muted/50 transition-colors"
|
|
140
|
+
>
|
|
141
|
+
<div className="flex items-start justify-between mb-2">
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<h4 className="font-medium text-foreground">{m.title}</h4>
|
|
144
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
145
|
+
</div>
|
|
146
|
+
{getStatusBadge(m.status)}
|
|
147
|
+
</div>
|
|
148
|
+
{m.description && (
|
|
149
|
+
<p className="text-sm text-muted-foreground mb-2">
|
|
150
|
+
{m.description}
|
|
151
|
+
</p>
|
|
152
|
+
)}
|
|
153
|
+
<div className="flex gap-4 text-xs text-muted-foreground">
|
|
154
|
+
<div className="flex items-center gap-1">
|
|
155
|
+
<Calendar className="h-3 w-3" />
|
|
156
|
+
<span>{format(new Date(m.startAt), "MMM d, HH:mm")}</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="flex items-center gap-1">
|
|
159
|
+
<Clock className="h-3 w-3" />
|
|
160
|
+
<span>
|
|
161
|
+
{m.status === "scheduled"
|
|
162
|
+
? `Starts ${formatDistanceToNow(new Date(m.startAt), {
|
|
163
|
+
addSuffix: true,
|
|
164
|
+
})}`
|
|
165
|
+
: `Ends ${formatDistanceToNow(new Date(m.endAt), {
|
|
166
|
+
addSuffix: true,
|
|
167
|
+
})}`}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</Link>
|
|
172
|
+
))}
|
|
173
|
+
</CardContent>
|
|
174
|
+
</Card>
|
|
175
|
+
);
|
|
176
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFrontendPlugin,
|
|
3
|
+
createSlotExtension,
|
|
4
|
+
rpcApiRef,
|
|
5
|
+
type ApiRef,
|
|
6
|
+
UserMenuItemsSlot,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { maintenanceApiRef, type MaintenanceApiClient } from "./api";
|
|
9
|
+
import {
|
|
10
|
+
maintenanceRoutes,
|
|
11
|
+
MaintenanceApi,
|
|
12
|
+
pluginMetadata,
|
|
13
|
+
permissions,
|
|
14
|
+
} from "@checkstack/maintenance-common";
|
|
15
|
+
import {
|
|
16
|
+
SystemDetailsTopSlot,
|
|
17
|
+
SystemStateBadgesSlot,
|
|
18
|
+
} from "@checkstack/catalog-common";
|
|
19
|
+
import { MaintenanceConfigPage } from "./pages/MaintenanceConfigPage";
|
|
20
|
+
import { SystemMaintenanceHistoryPage } from "./pages/SystemMaintenanceHistoryPage";
|
|
21
|
+
import { MaintenanceDetailPage } from "./pages/MaintenanceDetailPage";
|
|
22
|
+
import { SystemMaintenancePanel } from "./components/SystemMaintenancePanel";
|
|
23
|
+
import { SystemMaintenanceBadge } from "./components/SystemMaintenanceBadge";
|
|
24
|
+
import { MaintenanceMenuItems } from "./components/MaintenanceMenuItems";
|
|
25
|
+
|
|
26
|
+
export default createFrontendPlugin({
|
|
27
|
+
metadata: pluginMetadata,
|
|
28
|
+
routes: [
|
|
29
|
+
{
|
|
30
|
+
route: maintenanceRoutes.routes.config,
|
|
31
|
+
element: <MaintenanceConfigPage />,
|
|
32
|
+
title: "Maintenances",
|
|
33
|
+
permission: permissions.maintenanceManage,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
route: maintenanceRoutes.routes.systemHistory,
|
|
37
|
+
element: <SystemMaintenanceHistoryPage />,
|
|
38
|
+
title: "System Maintenance History",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
route: maintenanceRoutes.routes.detail,
|
|
42
|
+
element: <MaintenanceDetailPage />,
|
|
43
|
+
title: "Maintenance Details",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
apis: [
|
|
47
|
+
{
|
|
48
|
+
ref: maintenanceApiRef,
|
|
49
|
+
factory: (deps: {
|
|
50
|
+
get: <T>(ref: ApiRef<T>) => T;
|
|
51
|
+
}): MaintenanceApiClient => {
|
|
52
|
+
const rpcApi = deps.get(rpcApiRef);
|
|
53
|
+
return rpcApi.forPlugin(MaintenanceApi);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
extensions: [
|
|
58
|
+
createSlotExtension(UserMenuItemsSlot, {
|
|
59
|
+
id: "maintenance.user-menu.items",
|
|
60
|
+
component: MaintenanceMenuItems,
|
|
61
|
+
}),
|
|
62
|
+
createSlotExtension(SystemStateBadgesSlot, {
|
|
63
|
+
id: "maintenance.system-maintenance-badge",
|
|
64
|
+
component: SystemMaintenanceBadge,
|
|
65
|
+
}),
|
|
66
|
+
createSlotExtension(SystemDetailsTopSlot, {
|
|
67
|
+
id: "maintenance.system-details-top.panel",
|
|
68
|
+
component: SystemMaintenancePanel,
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
rpcApiRef,
|
|
6
|
+
permissionApiRef,
|
|
7
|
+
wrapInSuspense,
|
|
8
|
+
} from "@checkstack/frontend-api";
|
|
9
|
+
import { maintenanceApiRef } from "../api";
|
|
10
|
+
import type {
|
|
11
|
+
MaintenanceWithSystems,
|
|
12
|
+
MaintenanceStatus,
|
|
13
|
+
} from "@checkstack/maintenance-common";
|
|
14
|
+
import { CatalogApi, type System } from "@checkstack/catalog-common";
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardHeader,
|
|
18
|
+
CardTitle,
|
|
19
|
+
CardContent,
|
|
20
|
+
Button,
|
|
21
|
+
LoadingSpinner,
|
|
22
|
+
EmptyState,
|
|
23
|
+
Table,
|
|
24
|
+
TableHeader,
|
|
25
|
+
TableRow,
|
|
26
|
+
TableHead,
|
|
27
|
+
TableBody,
|
|
28
|
+
TableCell,
|
|
29
|
+
Select,
|
|
30
|
+
SelectTrigger,
|
|
31
|
+
SelectValue,
|
|
32
|
+
SelectContent,
|
|
33
|
+
SelectItem,
|
|
34
|
+
useToast,
|
|
35
|
+
ConfirmationModal,
|
|
36
|
+
PageLayout,
|
|
37
|
+
} from "@checkstack/ui";
|
|
38
|
+
import {
|
|
39
|
+
Plus,
|
|
40
|
+
Wrench,
|
|
41
|
+
Calendar,
|
|
42
|
+
Trash2,
|
|
43
|
+
Edit2,
|
|
44
|
+
Clock,
|
|
45
|
+
CheckCircle2,
|
|
46
|
+
} from "lucide-react";
|
|
47
|
+
import { format } from "date-fns";
|
|
48
|
+
import { MaintenanceEditor } from "../components/MaintenanceEditor";
|
|
49
|
+
import { getMaintenanceStatusBadge } from "../utils/badges";
|
|
50
|
+
|
|
51
|
+
const MaintenanceConfigPageContent: React.FC = () => {
|
|
52
|
+
const api = useApi(maintenanceApiRef);
|
|
53
|
+
const rpcApi = useApi(rpcApiRef);
|
|
54
|
+
const permissionApi = useApi(permissionApiRef);
|
|
55
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
56
|
+
|
|
57
|
+
const catalogApi = useMemo(() => rpcApi.forPlugin(CatalogApi), [rpcApi]);
|
|
58
|
+
const toast = useToast();
|
|
59
|
+
|
|
60
|
+
const { allowed: canManage, loading: permissionLoading } =
|
|
61
|
+
permissionApi.useResourcePermission("maintenance", "manage");
|
|
62
|
+
|
|
63
|
+
const [maintenances, setMaintenances] = useState<MaintenanceWithSystems[]>(
|
|
64
|
+
[]
|
|
65
|
+
);
|
|
66
|
+
const [systems, setSystems] = useState<System[]>([]);
|
|
67
|
+
const [loading, setLoading] = useState(true);
|
|
68
|
+
const [statusFilter, setStatusFilter] = useState<MaintenanceStatus | "all">(
|
|
69
|
+
"all"
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Editor state
|
|
73
|
+
const [editorOpen, setEditorOpen] = useState(false);
|
|
74
|
+
const [editingMaintenance, setEditingMaintenance] = useState<
|
|
75
|
+
MaintenanceWithSystems | undefined
|
|
76
|
+
>();
|
|
77
|
+
|
|
78
|
+
// Delete confirmation state
|
|
79
|
+
const [deleteId, setDeleteId] = useState<string | undefined>();
|
|
80
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
81
|
+
|
|
82
|
+
// Complete confirmation state
|
|
83
|
+
const [completeId, setCompleteId] = useState<string | undefined>();
|
|
84
|
+
const [isCompleting, setIsCompleting] = useState(false);
|
|
85
|
+
|
|
86
|
+
const loadData = async () => {
|
|
87
|
+
setLoading(true);
|
|
88
|
+
try {
|
|
89
|
+
const [maintenanceList, systemList] = await Promise.all([
|
|
90
|
+
api.listMaintenances(
|
|
91
|
+
statusFilter === "all" ? undefined : { status: statusFilter }
|
|
92
|
+
),
|
|
93
|
+
catalogApi.getSystems(),
|
|
94
|
+
]);
|
|
95
|
+
setMaintenances(maintenanceList);
|
|
96
|
+
setSystems(systemList);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : "Failed to load";
|
|
99
|
+
toast.error(message);
|
|
100
|
+
} finally {
|
|
101
|
+
setLoading(false);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
loadData();
|
|
107
|
+
}, [statusFilter]);
|
|
108
|
+
|
|
109
|
+
// Handle ?action=create URL parameter (from command palette)
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (searchParams.get("action") === "create" && canManage) {
|
|
112
|
+
setEditingMaintenance(undefined);
|
|
113
|
+
setEditorOpen(true);
|
|
114
|
+
// Clear the URL param after opening
|
|
115
|
+
searchParams.delete("action");
|
|
116
|
+
setSearchParams(searchParams, { replace: true });
|
|
117
|
+
}
|
|
118
|
+
}, [searchParams, canManage, setSearchParams]);
|
|
119
|
+
|
|
120
|
+
const handleCreate = () => {
|
|
121
|
+
setEditingMaintenance(undefined);
|
|
122
|
+
setEditorOpen(true);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleEdit = (m: MaintenanceWithSystems) => {
|
|
126
|
+
setEditingMaintenance(m);
|
|
127
|
+
setEditorOpen(true);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleDelete = async () => {
|
|
131
|
+
if (!deleteId) return;
|
|
132
|
+
|
|
133
|
+
setIsDeleting(true);
|
|
134
|
+
try {
|
|
135
|
+
await api.deleteMaintenance({ id: deleteId });
|
|
136
|
+
toast.success("Maintenance deleted");
|
|
137
|
+
loadData();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const message =
|
|
140
|
+
error instanceof Error ? error.message : "Failed to delete";
|
|
141
|
+
toast.error(message);
|
|
142
|
+
} finally {
|
|
143
|
+
setIsDeleting(false);
|
|
144
|
+
setDeleteId(undefined);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleComplete = async () => {
|
|
149
|
+
if (!completeId) return;
|
|
150
|
+
|
|
151
|
+
setIsCompleting(true);
|
|
152
|
+
try {
|
|
153
|
+
await api.closeMaintenance({ id: completeId });
|
|
154
|
+
toast.success("Maintenance completed");
|
|
155
|
+
loadData();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const message =
|
|
158
|
+
error instanceof Error ? error.message : "Failed to complete";
|
|
159
|
+
toast.error(message);
|
|
160
|
+
} finally {
|
|
161
|
+
setIsCompleting(false);
|
|
162
|
+
setCompleteId(undefined);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleSave = () => {
|
|
167
|
+
setEditorOpen(false);
|
|
168
|
+
loadData();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const getSystemNames = (systemIds: string[]): string => {
|
|
172
|
+
const names = systemIds
|
|
173
|
+
.map((id) => systems.find((s) => s.id === id)?.name ?? id)
|
|
174
|
+
.slice(0, 3);
|
|
175
|
+
if (systemIds.length > 3) {
|
|
176
|
+
names.push(`+${systemIds.length - 3} more`);
|
|
177
|
+
}
|
|
178
|
+
return names.join(", ");
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const canComplete = (status: MaintenanceStatus) =>
|
|
182
|
+
status !== "completed" && status !== "cancelled";
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<PageLayout
|
|
186
|
+
title="Planned Maintenances"
|
|
187
|
+
subtitle="Manage scheduled maintenance windows for systems"
|
|
188
|
+
loading={permissionLoading}
|
|
189
|
+
allowed={canManage}
|
|
190
|
+
actions={
|
|
191
|
+
<Button onClick={handleCreate}>
|
|
192
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
193
|
+
Create Maintenance
|
|
194
|
+
</Button>
|
|
195
|
+
}
|
|
196
|
+
>
|
|
197
|
+
<Card>
|
|
198
|
+
<CardHeader className="border-b border-border">
|
|
199
|
+
<div className="flex items-center justify-between">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<Wrench className="h-5 w-5 text-muted-foreground" />
|
|
202
|
+
<CardTitle>Maintenances</CardTitle>
|
|
203
|
+
</div>
|
|
204
|
+
<Select
|
|
205
|
+
value={statusFilter}
|
|
206
|
+
onValueChange={(v) =>
|
|
207
|
+
setStatusFilter(v as MaintenanceStatus | "all")
|
|
208
|
+
}
|
|
209
|
+
>
|
|
210
|
+
<SelectTrigger className="w-40">
|
|
211
|
+
<SelectValue placeholder="Filter by status" />
|
|
212
|
+
</SelectTrigger>
|
|
213
|
+
<SelectContent>
|
|
214
|
+
<SelectItem value="all">All Statuses</SelectItem>
|
|
215
|
+
<SelectItem value="scheduled">Scheduled</SelectItem>
|
|
216
|
+
<SelectItem value="in_progress">In Progress</SelectItem>
|
|
217
|
+
<SelectItem value="completed">Completed</SelectItem>
|
|
218
|
+
<SelectItem value="cancelled">Cancelled</SelectItem>
|
|
219
|
+
</SelectContent>
|
|
220
|
+
</Select>
|
|
221
|
+
</div>
|
|
222
|
+
</CardHeader>
|
|
223
|
+
<CardContent className="p-0">
|
|
224
|
+
{loading ? (
|
|
225
|
+
<div className="p-12 flex justify-center">
|
|
226
|
+
<LoadingSpinner />
|
|
227
|
+
</div>
|
|
228
|
+
) : maintenances.length === 0 ? (
|
|
229
|
+
<EmptyState
|
|
230
|
+
title="No maintenances found"
|
|
231
|
+
description="Create your first planned maintenance to get started."
|
|
232
|
+
/>
|
|
233
|
+
) : (
|
|
234
|
+
<Table>
|
|
235
|
+
<TableHeader>
|
|
236
|
+
<TableRow>
|
|
237
|
+
<TableHead>Title</TableHead>
|
|
238
|
+
<TableHead>Status</TableHead>
|
|
239
|
+
<TableHead>Systems</TableHead>
|
|
240
|
+
<TableHead>Schedule</TableHead>
|
|
241
|
+
<TableHead className="w-32">Actions</TableHead>
|
|
242
|
+
</TableRow>
|
|
243
|
+
</TableHeader>
|
|
244
|
+
<TableBody>
|
|
245
|
+
{maintenances.map((m) => (
|
|
246
|
+
<TableRow key={m.id}>
|
|
247
|
+
<TableCell>
|
|
248
|
+
<div>
|
|
249
|
+
<p className="font-medium">{m.title}</p>
|
|
250
|
+
{m.description && (
|
|
251
|
+
<p className="text-sm text-muted-foreground truncate max-w-xs">
|
|
252
|
+
{m.description}
|
|
253
|
+
</p>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</TableCell>
|
|
257
|
+
<TableCell>{getMaintenanceStatusBadge(m.status)}</TableCell>
|
|
258
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
259
|
+
{getSystemNames(m.systemIds)}
|
|
260
|
+
</TableCell>
|
|
261
|
+
<TableCell>
|
|
262
|
+
<div className="text-sm space-y-1">
|
|
263
|
+
<div className="flex items-center gap-1 text-muted-foreground">
|
|
264
|
+
<Calendar className="h-3 w-3" />
|
|
265
|
+
<span>
|
|
266
|
+
{format(new Date(m.startAt), "MMM d, HH:mm")}
|
|
267
|
+
</span>
|
|
268
|
+
</div>
|
|
269
|
+
<div className="flex items-center gap-1 text-muted-foreground">
|
|
270
|
+
<Clock className="h-3 w-3" />
|
|
271
|
+
<span>
|
|
272
|
+
{format(new Date(m.endAt), "MMM d, HH:mm")}
|
|
273
|
+
</span>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</TableCell>
|
|
277
|
+
<TableCell>
|
|
278
|
+
<div className="flex gap-2">
|
|
279
|
+
<Button
|
|
280
|
+
variant="ghost"
|
|
281
|
+
size="sm"
|
|
282
|
+
onClick={() => handleEdit(m)}
|
|
283
|
+
>
|
|
284
|
+
<Edit2 className="h-4 w-4" />
|
|
285
|
+
</Button>
|
|
286
|
+
{canComplete(m.status) && (
|
|
287
|
+
<Button
|
|
288
|
+
variant="ghost"
|
|
289
|
+
size="sm"
|
|
290
|
+
onClick={() => setCompleteId(m.id)}
|
|
291
|
+
>
|
|
292
|
+
<CheckCircle2 className="h-4 w-4 text-success" />
|
|
293
|
+
</Button>
|
|
294
|
+
)}
|
|
295
|
+
<Button
|
|
296
|
+
variant="ghost"
|
|
297
|
+
size="sm"
|
|
298
|
+
onClick={() => setDeleteId(m.id)}
|
|
299
|
+
>
|
|
300
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
301
|
+
</Button>
|
|
302
|
+
</div>
|
|
303
|
+
</TableCell>
|
|
304
|
+
</TableRow>
|
|
305
|
+
))}
|
|
306
|
+
</TableBody>
|
|
307
|
+
</Table>
|
|
308
|
+
)}
|
|
309
|
+
</CardContent>
|
|
310
|
+
</Card>
|
|
311
|
+
|
|
312
|
+
<MaintenanceEditor
|
|
313
|
+
open={editorOpen}
|
|
314
|
+
onOpenChange={setEditorOpen}
|
|
315
|
+
maintenance={editingMaintenance}
|
|
316
|
+
systems={systems}
|
|
317
|
+
onSave={handleSave}
|
|
318
|
+
/>
|
|
319
|
+
|
|
320
|
+
<ConfirmationModal
|
|
321
|
+
isOpen={!!deleteId}
|
|
322
|
+
onClose={() => setDeleteId(undefined)}
|
|
323
|
+
title="Delete Maintenance"
|
|
324
|
+
message="Are you sure you want to delete this maintenance? This action cannot be undone."
|
|
325
|
+
confirmText="Delete"
|
|
326
|
+
variant="danger"
|
|
327
|
+
onConfirm={handleDelete}
|
|
328
|
+
isLoading={isDeleting}
|
|
329
|
+
/>
|
|
330
|
+
|
|
331
|
+
<ConfirmationModal
|
|
332
|
+
isOpen={!!completeId}
|
|
333
|
+
onClose={() => setCompleteId(undefined)}
|
|
334
|
+
title="Complete Maintenance"
|
|
335
|
+
message="Are you sure you want to mark this maintenance as completed?"
|
|
336
|
+
confirmText="Complete"
|
|
337
|
+
variant="info"
|
|
338
|
+
onConfirm={handleComplete}
|
|
339
|
+
isLoading={isCompleting}
|
|
340
|
+
/>
|
|
341
|
+
</PageLayout>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export const MaintenanceConfigPage = wrapInSuspense(
|
|
346
|
+
MaintenanceConfigPageContent
|
|
347
|
+
);
|