From 975fb65c3d16265d950667e1e0dca9738cd3735b Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 20 May 2025 15:15:11 +0800 Subject: [PATCH 01/16] Updated to clone a specific branch:develop for Local Developement --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f7c278..ca2d9bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ Click "Fork" on [GitHub](https://github.com/operacle/checkcle) to create your ow ### 2. Clone Your Fork ```bash -git clone https://github.com/operacle/checkcle.git +git clone --branch develop https://github.com/operacle/checkcle.git cd checkcle ``` From 34e78e5c46d925a2963ddbbf54f03c4bb686712d Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 20 May 2025 16:45:06 +0800 Subject: [PATCH 02/16] Removed DockerFile --- docker/Dockerfile | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 docker/Dockerfile diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index eba5abc..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# Stage 1: Build the frontend -FROM node:18-alpine AS frontend-builder -WORKDIR /app -COPY application/package*.json ./ -RUN npm install -COPY application/ ./ -RUN npm run build - -# Stage 2: Download PocketBase binary -FROM alpine:3.17 AS pb-builder -WORKDIR /pb -ARG PB_VERSION=0.27.2 -RUN apk add --no-cache wget unzip \ - && wget https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip \ - && unzip pocketbase_${PB_VERSION}_linux_amd64.zip \ - && chmod +x /pb/pocketbase - -# Stage 3: Final runtime image -FROM alpine:3.17 -WORKDIR /app - -# Copy PocketBase binary -COPY --from=pb-builder /pb/pocketbase /app/pocketbase - -# Copy frontend build to PocketBase public directory -COPY --from=frontend-builder /app/dist /app/pb_public - -# Copy backend files -COPY server/pb_migrations /app/pb_migrations -COPY server/pb_hooks /app/pb_hooks -COPY server/pb_data /app/pb_data - -# Mark pb_data as a volume for runtime persistence -VOLUME /app/pb_data - -# Expose default PocketBase port -EXPOSE 8090 - -# Launch PocketBase and bind to 0.0.0.0 for external access -CMD ["/app/pocketbase", "serve", "--http=0.0.0.0:8090", "--dir", "/app/pb_data"] From e4b5ba96c11343cb017351887e008440332bcd9d Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Tue, 20 May 2025 16:45:31 +0800 Subject: [PATCH 03/16] Updated Pocket API Connection for production --- application/src/lib/pocketbase.ts | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/application/src/lib/pocketbase.ts b/application/src/lib/pocketbase.ts index 4ad1dfb..8baea56 100644 --- a/application/src/lib/pocketbase.ts +++ b/application/src/lib/pocketbase.ts @@ -1,17 +1,16 @@ import PocketBase from 'pocketbase'; -// Dynamically detect API base URL from current host (for use in browser) +// Auto-detect base URL from browser location const dynamicBaseUrl = typeof window !== 'undefined' - ? `${window.location.protocol}//${window.location.hostname}:8090` + ? window.location.origin : 'http://localhost:8090'; -// Define available API endpoints +// Define API endpoints export const API_ENDPOINTS = { REMOTE: dynamicBaseUrl }; -// Get the current endpoint from localStorage or use remote as default export const getCurrentEndpoint = (): string => { if (typeof window !== 'undefined') { const savedEndpoint = localStorage.getItem('pocketbase_endpoint'); @@ -20,26 +19,19 @@ export const getCurrentEndpoint = (): string => { return API_ENDPOINTS.REMOTE; }; -// Set the API endpoint and reinitialize PocketBase export const setApiEndpoint = (endpoint: string): void => { if (typeof window !== 'undefined') { localStorage.setItem('pocketbase_endpoint', endpoint); - window.location.reload(); // Reload to reinitialize PocketBase with new endpoint + window.location.reload(); } }; -// Initialize the PocketBase client with the current API URL export const pb = new PocketBase(getCurrentEndpoint()); -// Helper to check if user is authenticated -export const isAuthenticated = () => { - return pb.authStore.isValid; -}; +export const isAuthenticated = () => pb.authStore.isValid; -// Export the auth store for use in components export const authStore = pb.authStore; -// Configure PocketBase to persist authentication between page reloads if (typeof window !== 'undefined') { const storedAuthData = localStorage.getItem('pocketbase_auth'); if (storedAuthData) { @@ -52,13 +44,15 @@ if (typeof window !== 'undefined') { } } - // Subscribe to authStore changes to persist authentication pb.authStore.onChange(() => { if (pb.authStore.isValid) { - localStorage.setItem('pocketbase_auth', JSON.stringify({ - token: pb.authStore.token, - model: pb.authStore.model - })); + localStorage.setItem( + 'pocketbase_auth', + JSON.stringify({ + token: pb.authStore.token, + model: pb.authStore.model + }) + ); } else { localStorage.removeItem('pocketbase_auth'); } From 1f13d5c6be42cf3ff61c5cb041ff76a989aac227 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Thu, 22 May 2025 23:10:46 +0800 Subject: [PATCH 04/16] Updated: About System Page --- .../src/components/settings/about-system/AboutSystem.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/application/src/components/settings/about-system/AboutSystem.tsx b/application/src/components/settings/about-system/AboutSystem.tsx index fdd5736..9509d2e 100644 --- a/application/src/components/settings/about-system/AboutSystem.tsx +++ b/application/src/components/settings/about-system/AboutSystem.tsx @@ -32,18 +32,13 @@ export const AboutSystem: React.FC = () => { {t('systemDescription')} - {systemName || 'ReamStack'}
-

- {t('systemVersion')} 1.0.0 {t('isAModern')} {t('serverMonitoring')} {t('solution')} {t('designedFor')} {t('enterpriseUse')}. {t('providesRealtime')} {t('uptimeTracking')}, {t('performanceAnalytics')}, {t('andAlertNotifications')}. -

-
{t('systemVersion')} - {t('version')} 1.0.0 + {t('version')} 1.1.0
@@ -53,7 +48,7 @@ export const AboutSystem: React.FC = () => {
{t('releasedOn')} - January 15, 2025 + May 16, 2025
From 705acfbe1dd4a012c89fb52c536435986f81c0eb Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 23 May 2025 18:48:03 +0800 Subject: [PATCH 05/16] Add Schedule & Incident menu sidebar --- application/src/App.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/application/src/App.tsx b/application/src/App.tsx index f0907e3..9c79abf 100644 --- a/application/src/App.tsx +++ b/application/src/App.tsx @@ -16,6 +16,7 @@ import Settings from "./pages/Settings"; import Profile from "./pages/Profile"; import NotFound from "./pages/NotFound"; import SslDomain from "./pages/SslDomain"; +import ScheduleIncident from "./pages/ScheduleIncident"; // Create a Protected route component const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { @@ -102,6 +103,14 @@ const App = () => { } /> + + + + } + /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> From 686634f0acfa2e015f17abb59c2c15b629e8be6a Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 23 May 2025 20:16:53 +0800 Subject: [PATCH 06/16] Update the word German to Deutsch --- application/src/components/dashboard/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/components/dashboard/Header.tsx b/application/src/components/dashboard/Header.tsx index 89fa488..bc98045 100644 --- a/application/src/components/dashboard/Header.tsx +++ b/application/src/components/dashboard/Header.tsx @@ -127,7 +127,7 @@ export const Header = ({ {t("khmer")} setLanguage("de")} className={language === "de" ? "bg-accent" : ""}> - {t("German")} + {t("Deutsch")} From 839fcc87711d4d15cceae95715c4576e9e1bf709 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 23 May 2025 20:20:06 +0800 Subject: [PATCH 07/16] Add necessary dependencies for jspdf & jspdf-autotable for PDF Print --- application/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/application/package.json b/application/package.json index df825c9..e047fff 100644 --- a/application/package.json +++ b/application/package.json @@ -30,7 +30,7 @@ "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", - "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-slider": "^1.2.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", @@ -46,6 +46,8 @@ "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", "input-otp": "^1.2.4", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", "pocketbase": "^0.19.0", @@ -80,4 +82,4 @@ "typescript-eslint": "^8.0.1", "vite": "^5.4.1" } -} +} \ No newline at end of file From 0692a78576f4aaf52da9d8529940a81be4000e97 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 23 May 2025 20:21:40 +0800 Subject: [PATCH 08/16] Added Schedule & Incident Menu Sidebar --- application/src/components/dashboard/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/components/dashboard/Sidebar.tsx b/application/src/components/dashboard/Sidebar.tsx index 8d3e4dc..6fe582c 100644 --- a/application/src/components/dashboard/Sidebar.tsx +++ b/application/src/components/dashboard/Sidebar.tsx @@ -67,10 +67,10 @@ export const Sidebar = ({ {!collapsed && {t("sslDomain")}} -
+ {!collapsed && {t("scheduleIncident")}} -
+
{!collapsed && {t("operationalPage")}} From 4c2f65b432e82b34702a5af3cc2c28dc3d0694e7 Mon Sep 17 00:00:00 2001 From: Tola Leng Date: Fri, 23 May 2025 20:34:18 +0800 Subject: [PATCH 09/16] feat: Implement Schedule Maintenance Management and Tab Dashboard --- .../maintenance/CreateMaintenanceDialog.tsx | 94 ++++++++ .../maintenance/EmptyMaintenanceState.tsx | 18 ++ .../maintenance/MaintenanceActionsMenu.tsx | 170 +++++++++++++++ .../maintenance/MaintenanceStatusBadge.tsx | 71 +++++++ .../maintenance/MaintenanceStatusChecker.tsx | 92 ++++++++ .../maintenance/MaintenanceStatusDropdown.tsx | 82 +++++++ .../maintenance/MaintenanceTable.tsx | 152 +++++++++++++ .../MaintenanceDetailContent.tsx | 176 +++++++++++++++ .../detail-dialog/MaintenanceDetailDialog.tsx | 37 ++++ .../detail-dialog/MaintenanceDetailFooter.tsx | 27 +++ .../detail-dialog/MaintenanceDetailHeader.tsx | 82 +++++++ .../MaintenanceDetailSections.tsx | 198 +++++++++++++++++ .../detail-dialog/components/CloseButton.tsx | 18 ++ .../components/DownloadPdfButton.tsx | 31 +++ .../detail-dialog/components/PrintButton.tsx | 27 +++ .../detail-dialog/components/index.ts | 4 + .../maintenance/detail-dialog/hooks/index.ts | 3 + .../hooks/useDownloadMaintenancePdf.ts | 47 ++++ .../hooks/usePrintMaintenance.ts | 173 +++++++++++++++ .../maintenance/detail-dialog/index.ts | 8 + .../edit-dialog/EditMaintenanceDialog.tsx | 97 +++++++++ .../maintenance/edit-dialog/index.ts | 2 + .../form/MaintenanceAffectedFields.tsx | 35 +++ .../form/MaintenanceBasicFields.tsx | 55 +++++ .../form/MaintenanceConfigFields.tsx | 32 +++ .../MaintenanceNotificationSettingsField.tsx | 170 +++++++++++++++ .../form/MaintenanceTimeFields.tsx | 195 +++++++++++++++++ .../form/config/AssignedUsersField.tsx | 185 ++++++++++++++++ .../form/config/ImpactLevelField.tsx | 56 +++++ .../maintenance/form/config/PriorityField.tsx | 56 +++++ .../maintenance/form/config/StatusField.tsx | 56 +++++ .../maintenance/form/config/index.ts | 5 + .../maintenance/form/index.ts | 7 + .../hooks/useMaintenanceEditForm.ts | 201 ++++++++++++++++++ .../maintenance/hooks/useMaintenanceForm.ts | 133 ++++++++++++ .../schedule-incident/maintenance/index.ts | 11 + .../src/services/maintenance/api/index.ts | 10 + .../maintenance/api/maintenanceCache.ts | 46 ++++ .../maintenance/api/maintenanceFetch.ts | 59 +++++ .../maintenance/api/maintenanceOperations.ts | 85 ++++++++ .../maintenance/api/maintenanceValidation.ts | 68 ++++++ application/src/services/maintenance/index.ts | 15 ++ .../maintenanceNotificationService.ts | 199 +++++++++++++++++ .../maintenance/maintenanceService.ts | 92 ++++++++ .../services/maintenance/maintenanceUtils.ts | 125 +++++++++++ .../src/services/maintenance/pdf/generator.ts | 91 ++++++++ .../services/maintenance/pdf/headerFooter.ts | 90 ++++++++ .../src/services/maintenance/pdf/index.ts | 16 ++ .../src/services/maintenance/pdf/sections.ts | 164 ++++++++++++++ .../src/services/maintenance/pdf/utils.ts | 50 +++++ .../src/services/types/maintenance.types.ts | 46 ++++ 51 files changed, 3962 insertions(+) create mode 100644 application/src/components/schedule-incident/maintenance/CreateMaintenanceDialog.tsx create mode 100644 application/src/components/schedule-incident/maintenance/EmptyMaintenanceState.tsx create mode 100644 application/src/components/schedule-incident/maintenance/MaintenanceActionsMenu.tsx create mode 100644 application/src/components/schedule-incident/maintenance/MaintenanceStatusBadge.tsx create mode 100644 application/src/components/schedule-incident/maintenance/MaintenanceStatusChecker.tsx create mode 100644 application/src/components/schedule-incident/maintenance/MaintenanceStatusDropdown.tsx create mode 100644 application/src/components/schedule-incident/maintenance/MaintenanceTable.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailContent.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailDialog.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailFooter.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailHeader.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailSections.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/components/CloseButton.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/components/DownloadPdfButton.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/components/PrintButton.tsx create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/components/index.ts create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/hooks/index.ts create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/hooks/useDownloadMaintenancePdf.ts create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/hooks/usePrintMaintenance.ts create mode 100644 application/src/components/schedule-incident/maintenance/detail-dialog/index.ts create mode 100644 application/src/components/schedule-incident/maintenance/edit-dialog/EditMaintenanceDialog.tsx create mode 100644 application/src/components/schedule-incident/maintenance/edit-dialog/index.ts create mode 100644 application/src/components/schedule-incident/maintenance/form/MaintenanceAffectedFields.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/MaintenanceBasicFields.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/MaintenanceConfigFields.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/MaintenanceNotificationSettingsField.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/MaintenanceTimeFields.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/config/AssignedUsersField.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/config/ImpactLevelField.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/config/PriorityField.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/config/StatusField.tsx create mode 100644 application/src/components/schedule-incident/maintenance/form/config/index.ts create mode 100644 application/src/components/schedule-incident/maintenance/form/index.ts create mode 100644 application/src/components/schedule-incident/maintenance/hooks/useMaintenanceEditForm.ts create mode 100644 application/src/components/schedule-incident/maintenance/hooks/useMaintenanceForm.ts create mode 100644 application/src/components/schedule-incident/maintenance/index.ts create mode 100644 application/src/services/maintenance/api/index.ts create mode 100644 application/src/services/maintenance/api/maintenanceCache.ts create mode 100644 application/src/services/maintenance/api/maintenanceFetch.ts create mode 100644 application/src/services/maintenance/api/maintenanceOperations.ts create mode 100644 application/src/services/maintenance/api/maintenanceValidation.ts create mode 100644 application/src/services/maintenance/index.ts create mode 100644 application/src/services/maintenance/maintenanceNotificationService.ts create mode 100644 application/src/services/maintenance/maintenanceService.ts create mode 100644 application/src/services/maintenance/maintenanceUtils.ts create mode 100644 application/src/services/maintenance/pdf/generator.ts create mode 100644 application/src/services/maintenance/pdf/headerFooter.ts create mode 100644 application/src/services/maintenance/pdf/index.ts create mode 100644 application/src/services/maintenance/pdf/sections.ts create mode 100644 application/src/services/maintenance/pdf/utils.ts create mode 100644 application/src/services/types/maintenance.types.ts diff --git a/application/src/components/schedule-incident/maintenance/CreateMaintenanceDialog.tsx b/application/src/components/schedule-incident/maintenance/CreateMaintenanceDialog.tsx new file mode 100644 index 0000000..2ddc398 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/CreateMaintenanceDialog.tsx @@ -0,0 +1,94 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from '@/components/ui/dialog'; +import { Form } from '@/components/ui/form'; +import { Button } from '@/components/ui/button'; +import { useMaintenanceForm } from './hooks/useMaintenanceForm'; +import { + MaintenanceBasicFields, + MaintenanceTimeFields, + MaintenanceAffectedFields, + MaintenanceConfigFields, + MaintenanceNotificationSettingsField +} from './form'; + +interface CreateMaintenanceDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onMaintenanceCreated: () => void; +} + +export const CreateMaintenanceDialog = ({ + open, + onOpenChange, + onMaintenanceCreated +}: CreateMaintenanceDialogProps) => { + const { t } = useLanguage(); + + const handleSuccess = () => { + console.log("CreateMaintenanceDialog: maintenance created successfully"); + onMaintenanceCreated(); + }; + + const handleClose = () => { + console.log("CreateMaintenanceDialog: closing dialog"); + onOpenChange(false); + }; + + const { form, onSubmit } = useMaintenanceForm(handleSuccess, handleClose); + + // Log the form state for debugging + React.useEffect(() => { + const subscription = form.watch((value) => { + console.log("Form values changed:", value); + }); + return () => subscription.unsubscribe(); + }, [form]); + + return ( + + + + {t('createMaintenanceWindow')} + + {t('createMaintenanceDesc')} + + + +
+ + + + + + + + + + + + + +
+
+ ); +}; diff --git a/application/src/components/schedule-incident/maintenance/EmptyMaintenanceState.tsx b/application/src/components/schedule-incident/maintenance/EmptyMaintenanceState.tsx new file mode 100644 index 0000000..60ce11d --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/EmptyMaintenanceState.tsx @@ -0,0 +1,18 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { CalendarClock } from 'lucide-react'; + +export const EmptyMaintenanceState = () => { + const { t } = useLanguage(); + + return ( +
+ +

{t('noScheduledMaintenance')}

+

+ {t('noMaintenanceWindows')} +

+
+ ); +}; diff --git a/application/src/components/schedule-incident/maintenance/MaintenanceActionsMenu.tsx b/application/src/components/schedule-incident/maintenance/MaintenanceActionsMenu.tsx new file mode 100644 index 0000000..9828100 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/MaintenanceActionsMenu.tsx @@ -0,0 +1,170 @@ + +import React, { useState } from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { MoreHorizontal, Eye, Edit, Trash, Play, CheckCircle, X } from 'lucide-react'; +import { useToast } from '@/hooks/use-toast'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { maintenanceService } from '@/services/maintenance'; +import { MaintenanceDetailDialog } from './detail-dialog/MaintenanceDetailDialog'; +import { EditMaintenanceDialog } from './edit-dialog/EditMaintenanceDialog'; + +interface MaintenanceActionsMenuProps { + item: MaintenanceItem; + onMaintenanceUpdated: () => void; +} + +export const MaintenanceActionsMenu = ({ item, onMaintenanceUpdated }: MaintenanceActionsMenuProps) => { + const { t } = useLanguage(); + const { toast } = useToast(); + const [detailDialogOpen, setDetailDialogOpen] = useState(false); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const handleStatusChange = async (newStatus: string) => { + try { + await maintenanceService.updateMaintenanceStatus(item.id, newStatus); + toast({ + title: t('statusUpdated'), + description: t('maintenanceStatusUpdated'), + }); + onMaintenanceUpdated(); + } catch (error) { + console.error('Error updating maintenance status:', error); + toast({ + title: t('error'), + description: t('errorUpdatingMaintenanceStatus'), + variant: 'destructive', + }); + } + }; + + const handleDelete = async () => { + try { + await maintenanceService.deleteMaintenance(item.id); + toast({ + title: t('maintenanceDeleted'), + description: t('maintenanceDeletedDesc'), + }); + onMaintenanceUpdated(); + } catch (error) { + console.error('Error deleting maintenance:', error); + toast({ + title: t('error'), + description: t('errorDeletingMaintenance'), + variant: 'destructive', + }); + } finally { + setDeleteDialogOpen(false); + } + }; + + // Convert status to lowercase for consistent comparison + const status = item.status.toLowerCase(); + + return ( + <> + + + + + + {t('actions')} + + setDetailDialogOpen(true)}> + + {t('view')} + + + setEditDialogOpen(true)}> + + {t('edit')} + + + {status === 'scheduled' && ( + handleStatusChange('in_progress')}> + + {t('markAsInProgress')} + + )} + + {(status === 'scheduled' || status === 'in_progress') && ( + handleStatusChange('completed')}> + + {t('markAsCompleted')} + + )} + + {status !== 'cancelled' && ( + handleStatusChange('cancelled')}> + + {t('markAsCancelled')} + + )} + + + setDeleteDialogOpen(true)} + className="text-red-600 focus:text-red-600" + > + + {t('delete')} + + + + + + + + + + + + {t('confirmDelete')} + + {t('deleteMaintenanceConfirmation')} + {item.title}? + {t('thisActionCannotBeUndone')} + + + + {t('cancel')} + + {t('delete')} + + + + + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/MaintenanceStatusBadge.tsx b/application/src/components/schedule-incident/maintenance/MaintenanceStatusBadge.tsx new file mode 100644 index 0000000..0df3a5b --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/MaintenanceStatusBadge.tsx @@ -0,0 +1,71 @@ + +import React from 'react'; +import { Badge } from '@/components/ui/badge'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { CalendarClock, Clock, CheckCircle, X } from 'lucide-react'; + +interface MaintenanceStatusBadgeProps { + status: string; +} + +export const MaintenanceStatusBadge = ({ status }: MaintenanceStatusBadgeProps) => { + const { t } = useLanguage(); + + // Ensure we have a string and normalize it + const normalizedStatus = typeof status === 'string' ? status.toLowerCase() : ''; + + const getStatusColor = () => { + switch (normalizedStatus) { + case 'scheduled': + return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'; + case 'in progress': + case 'in_progress': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + case 'completed': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + case 'cancelled': + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + default: + return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; + } + }; + + const getStatusIcon = () => { + switch (normalizedStatus) { + case 'scheduled': + return ; + case 'in progress': + case 'in_progress': + return ; + case 'completed': + return ; + case 'cancelled': + return ; + default: + return ; + } + }; + + const getDisplayStatus = () => { + switch (normalizedStatus) { + case 'scheduled': + return t('scheduled'); + case 'in progress': + case 'in_progress': + return t('inProgress'); + case 'completed': + return t('completed'); + case 'cancelled': + return t('cancelled'); + default: + return status || ''; + } + }; + + return ( + + {getStatusIcon()} + {getDisplayStatus()} + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/MaintenanceStatusChecker.tsx b/application/src/components/schedule-incident/maintenance/MaintenanceStatusChecker.tsx new file mode 100644 index 0000000..01e20f6 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/MaintenanceStatusChecker.tsx @@ -0,0 +1,92 @@ + +import { useEffect, useRef } from 'react'; +import { maintenanceService } from '@/services/maintenance'; +import { useToast } from '@/hooks/use-toast'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { maintenanceNotificationService } from '@/services/maintenance/maintenanceNotificationService'; + +interface MaintenanceStatusCheckerProps { + maintenanceData: MaintenanceItem[]; + onStatusUpdated: () => void; +} + +export const MaintenanceStatusChecker = ({ + maintenanceData, + onStatusUpdated +}: MaintenanceStatusCheckerProps) => { + const { t } = useLanguage(); + const { toast } = useToast(); + const checkedItemsRef = useRef>(new Set()); + const notificationSentRef = useRef>(new Set()); + + useEffect(() => { + if (!maintenanceData || maintenanceData.length === 0) return; + + // Check for maintenance items that need status update + const now = new Date(); + const checkAndUpdateStatus = async () => { + for (const item of maintenanceData) { + if (item.status.toLowerCase() !== 'scheduled') continue; + + // Skip if we've already checked this item in this session + if (checkedItemsRef.current.has(item.id)) continue; + + try { + const startTime = new Date(item.start_time); + const endTime = new Date(item.end_time); + + // If current time is past start time but before end time, update to in_progress + if (startTime <= now && now <= endTime) { + console.log(`Auto-updating maintenance ${item.title} to in_progress status`); + + // Update status to in_progress + await maintenanceService.updateMaintenanceStatus(item.id, 'in_progress'); + + // Only send notification if not sent before + if (!notificationSentRef.current.has(item.id)) { + console.log(`Sending start notification for maintenance ${item.title}`); + + // Send notification for maintenance start + await maintenanceNotificationService.sendMaintenanceNotification({ + maintenance: item, + notificationType: 'start' + }); + + // Mark as notified + notificationSentRef.current.add(item.id); + } + + toast({ + title: t('maintenanceInProgress'), + description: `${item.title} ${t('isNowInProgress')}`, + }); + + onStatusUpdated(); + } + + // Mark as checked regardless of outcome + checkedItemsRef.current.add(item.id); + } catch (error) { + console.error(`Error auto-updating maintenance status for ${item.id}:`, error); + + // Add a small delay before trying again to prevent rapid retry cycles + setTimeout(() => { + checkedItemsRef.current.delete(item.id); // Allow retry on next check + }, 300000); // 5 minutes before retry + } + } + }; + + // Run the check immediately + checkAndUpdateStatus(); + + // Set up interval to check every minute + const intervalId = setInterval(checkAndUpdateStatus, 60000); + + return () => clearInterval(intervalId); + }, [maintenanceData, onStatusUpdated, t, toast]); + + // This is a utility component with no UI render + return null; +}; diff --git a/application/src/components/schedule-incident/maintenance/MaintenanceStatusDropdown.tsx b/application/src/components/schedule-incident/maintenance/MaintenanceStatusDropdown.tsx new file mode 100644 index 0000000..9963728 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/MaintenanceStatusDropdown.tsx @@ -0,0 +1,82 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { CalendarClock, Clock, CheckCircle, X } from 'lucide-react'; +import { useToast } from '@/hooks/use-toast'; +import { maintenanceService } from '@/services/maintenance'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { MaintenanceStatusBadge } from './MaintenanceStatusBadge'; + +interface MaintenanceStatusDropdownProps { + status: string; + id: string; + onStatusUpdated: () => void; + disabled?: boolean; +} + +export const MaintenanceStatusDropdown = ({ + status, + id, + onStatusUpdated, + disabled = false +}: MaintenanceStatusDropdownProps) => { + const { t } = useLanguage(); + const { toast } = useToast(); + + const statusOptions = [ + { value: 'scheduled', label: t('scheduled'), icon: }, + { value: 'in_progress', label: t('inProgress'), icon: }, + { value: 'completed', label: t('completed'), icon: }, + { value: 'cancelled', label: t('cancelled'), icon: }, + ]; + + const handleStatusChange = async (newStatus: string) => { + // Don't update if the status is the same + if (status.toLowerCase() === newStatus) return; + + try { + await maintenanceService.updateMaintenanceStatus(id, newStatus); + + toast({ + title: t('statusUpdated'), + description: t('maintenanceStatusUpdated'), + }); + + onStatusUpdated(); + } catch (error) { + console.error('Error updating maintenance status:', error); + + toast({ + title: t('error'), + description: t('failedToUpdateStatus'), + variant: 'destructive', + }); + } + }; + + return ( + + + + + + {statusOptions.map((option) => ( + handleStatusChange(option.value)} + > + {option.icon} + {option.label} + + ))} + + + ); +}; + diff --git a/application/src/components/schedule-incident/maintenance/MaintenanceTable.tsx b/application/src/components/schedule-incident/maintenance/MaintenanceTable.tsx new file mode 100644 index 0000000..62295f6 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/MaintenanceTable.tsx @@ -0,0 +1,152 @@ + +import React, { useState, useMemo } from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { format } from 'date-fns'; +import { MaintenanceStatusDropdown } from './MaintenanceStatusDropdown'; +import { MaintenanceActionsMenu } from './MaintenanceActionsMenu'; +import { MaintenanceDetailDialog } from './detail-dialog/MaintenanceDetailDialog'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { Skeleton } from '@/components/ui/skeleton'; +import { EmptyMaintenanceState } from './EmptyMaintenanceState'; + +interface MaintenanceTableProps { + data: MaintenanceItem[]; + isLoading?: boolean; + onMaintenanceUpdated: () => void; +} + +export const MaintenanceTable = ({ data, isLoading = false, onMaintenanceUpdated }: MaintenanceTableProps) => { + const { t } = useLanguage(); + const [selectedMaintenance, setSelectedMaintenance] = useState(null); + const [detailDialogOpen, setDetailDialogOpen] = useState(false); + + // Memoize the formatted date function to avoid recreating it on each render + const formatDate = useMemo(() => (dateString: string) => { + if (!dateString) return '-'; + + try { + return format(new Date(dateString), 'MMM d, yyyy HH:mm'); + } catch (error) { + return dateString || '-'; + } + }, []); + + const handleViewMaintenance = (maintenance: MaintenanceItem) => { + setSelectedMaintenance(maintenance); + setDetailDialogOpen(true); + }; + + if (isLoading) { + // Render skeleton loading state with table structure for smoother transitions + return ( + + + + {t('title')} + {t('status')} + {t('scheduledStart')} + {t('scheduledEnd')} + {t('affectedServices')} + {t('impact')} + {t('actions')} + + + + {[1, 2, 3].map(i => ( + + + + + + + + + + ))} + +
+ ); + } + + if (!data || data.length === 0) { + return ; + } + + // Memoize the impact badge variant calculation + const getImpactBadgeVariant = (field: string | undefined) => { + const fieldLower = field?.toLowerCase() || ''; + if (fieldLower === 'critical') return 'destructive'; + if (fieldLower === 'major') return 'default'; + if (fieldLower === 'minor') return 'secondary'; + return 'outline'; + }; + + return ( + <> + + + + {t('title')} + {t('status')} + {t('scheduledStart')} + {t('scheduledEnd')} + {t('affectedServices')} + {t('impact')} + {t('actions')} + + + + {data.map((item) => ( + + handleViewMaintenance(item)}> + {item.title || '-'} + + e.stopPropagation()}> + + + handleViewMaintenance(item)}> + {formatDate(item.start_time)} + + handleViewMaintenance(item)}> + {formatDate(item.end_time)} + + handleViewMaintenance(item)}> +
+ {item.affected ? item.affected.split(',').slice(0, 2).map((service, index) => ( + {service.trim()} + )) : '-'} + {item.affected && item.affected.split(',').length > 2 && ( + +{item.affected.split(',').length - 2} + )} +
+
+ handleViewMaintenance(item)}> + + {item.field ? t(item.field.toLowerCase()) : '-'} + + + e.stopPropagation()}> + + +
+ ))} +
+
+ + + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailContent.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailContent.tsx new file mode 100644 index 0000000..778c8f5 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailContent.tsx @@ -0,0 +1,176 @@ + +import React, { useEffect, useState } from 'react'; +import { userService, User } from '@/services/userService'; +import { useQuery } from '@tanstack/react-query'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { MaintenanceDetailHeader } from './MaintenanceDetailHeader'; +import { MaintenanceDetailSections } from './MaintenanceDetailSections'; +import { MaintenanceDetailFooter } from './MaintenanceDetailFooter'; +import { alertConfigService } from '@/services/alertConfigService'; + +interface MaintenanceDetailContentProps { + maintenance: MaintenanceItem; + onClose: () => void; +} + +export const MaintenanceDetailContent = ({ + maintenance, + onClose +}: MaintenanceDetailContentProps) => { + const [assignedUsers, setAssignedUsers] = useState([]); + const [maintenanceWithDetails, setMaintenanceWithDetails] = useState(maintenance); + + // Fetch all users to resolve the assigned users and creator + const { data: users = [] } = useQuery({ + queryKey: ['users'], + queryFn: async () => { + const usersList = await userService.getUsers(); + console.log("Fetched users for maintenance detail:", usersList); + return usersList || []; + }, + enabled: true + }); + + // Fetch notification channels for notification channel display + const { data: notificationChannels = [] } = useQuery({ + queryKey: ['notificationChannels'], + queryFn: async () => { + const channels = await alertConfigService.getAlertConfigurations(); + console.log("Fetched notification channels:", channels); + return channels || []; + }, + enabled: true + }); + + // Process user information when users data is available + useEffect(() => { + if (users.length > 0 && maintenance) { + console.log("Processing maintenance data with users:", maintenance); + console.log("Maintenance assigned_users:", maintenance.assigned_users); + + // Create a copy of the maintenance object for modifications + const enhancedMaintenance = { ...maintenance }; + + // Process assigned users + if (maintenance.assigned_users) { + let userIds: string[] = []; + + // Step 1: Extract user IDs from various formats + if (Array.isArray(maintenance.assigned_users)) { + userIds = maintenance.assigned_users; + console.log("Assigned users is an array:", userIds); + } else if (typeof maintenance.assigned_users === 'string') { + // Try parsing the string to extract user IDs + try { + // Try parsing as JSON first + const parsedData = JSON.parse(maintenance.assigned_users); + if (Array.isArray(parsedData)) { + // Direct array format + userIds = parsedData; + console.log("Parsed assigned_users from JSON array:", userIds); + } else if (typeof parsedData === 'string') { + // Nested JSON string + try { + const nestedData = JSON.parse(parsedData); + if (Array.isArray(nestedData)) { + userIds = nestedData; + console.log("Parsed assigned_users from nested JSON:", userIds); + } + } catch (e) { + // If nested parsing fails, treat as single ID + userIds = [parsedData]; + console.log("Using parsed string as single ID:", userIds); + } + } else { + // Unknown format, use as is + userIds = [String(parsedData)]; + console.log("Using parsed data as single ID:", userIds); + } + } catch (e) { + // If JSON parsing fails, try comma splitting + if (maintenance.assigned_users.includes(',')) { + userIds = maintenance.assigned_users.split(',').map(id => id.trim()).filter(Boolean); + console.log("Split assigned_users by comma:", userIds); + } else { + // Single ID + userIds = [maintenance.assigned_users]; + console.log("Using string as single ID:", userIds); + } + } + } + + // Step 2: Clean up extracted IDs (remove quotes, brackets, etc.) + userIds = userIds.map(id => { + // Remove surrounding quotes if present + let cleanId = id.replace(/^["']|["']$/g, ''); + // Remove any remaining JSON artifacts + cleanId = cleanId.replace(/[\[\]"'\\]/g, ''); + return cleanId; + }).filter(Boolean); + + console.log("Cleaned user IDs:", userIds); + + // Step 3: Find matching users from the users array + if (userIds.length > 0) { + const matchedUsers = users.filter(user => userIds.includes(user.id)); + console.log("Matched assigned users:", matchedUsers); + setAssignedUsers(matchedUsers); + } else { + console.log("No user IDs found in assigned_users"); + setAssignedUsers([]); + } + } else { + console.log("No assigned users found in maintenance data"); + setAssignedUsers([]); + } + + // Process created_by field - replace ID with full name if available + if (maintenance?.created_by) { + const creator = users.find(user => user.id === maintenance.created_by); + if (creator) { + enhancedMaintenance.created_by = creator.full_name || creator.username || maintenance.created_by; + } + } + + // Process notification channel if needed + if (maintenance.notify_subscribers === 'yes') { + // Try notification_channel_id first, fall back to notification_id if needed + const channelId = maintenance.notification_channel_id || maintenance.notification_id; + + if (channelId && notificationChannels.length > 0) { + console.log("Looking for notification channel with ID:", channelId); + console.log("Available notification channels:", notificationChannels.map(c => ({ id: c.id, name: c.notify_name }))); + + const channel = notificationChannels.find(ch => ch.id === channelId); + if (channel) { + console.log("Found notification channel:", channel); + enhancedMaintenance.notification_channel_name = `${channel.notify_name} (${channel.notification_type})`; + } else { + console.log("No matching notification channel found for ID:", channelId); + enhancedMaintenance.notification_channel_name = `Channel ID: ${channelId}`; + } + } else { + console.log("No channel ID available or no notification channels loaded"); + } + } + + console.log("Enhanced maintenance with details:", enhancedMaintenance); + console.log("Assigned users for display:", assignedUsers); + setMaintenanceWithDetails(enhancedMaintenance); + } + }, [maintenance, users, notificationChannels]); + + return ( + <> + + + + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailDialog.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailDialog.tsx new file mode 100644 index 0000000..5940e34 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailDialog.tsx @@ -0,0 +1,37 @@ + +import React from 'react'; +import { + Dialog, + DialogContent +} from '@/components/ui/dialog'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { MaintenanceDetailContent } from './MaintenanceDetailContent'; + +interface MaintenanceDetailDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + maintenance: MaintenanceItem | null; +} + +export const MaintenanceDetailDialog = ({ + open, + onOpenChange, + maintenance +}: MaintenanceDetailDialogProps) => { + if (!maintenance) { + return null; + } + + return ( + + + onOpenChange(false)} /> + + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailFooter.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailFooter.tsx new file mode 100644 index 0000000..a59a8cc --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailFooter.tsx @@ -0,0 +1,27 @@ + +import React from 'react'; +import { DialogFooter } from '@/components/ui/dialog'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { + PrintButton, + DownloadPdfButton, + CloseButton +} from './components'; + +interface MaintenanceDetailFooterProps { + maintenance: MaintenanceItem; + onClose: () => void; +} + +export const MaintenanceDetailFooter = ({ + maintenance, + onClose +}: MaintenanceDetailFooterProps) => { + return ( + + + + + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailHeader.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailHeader.tsx new file mode 100644 index 0000000..0b70c76 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailHeader.tsx @@ -0,0 +1,82 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { CalendarClock } from 'lucide-react'; +import { + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { Badge } from '@/components/ui/badge'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; + +interface MaintenanceDetailHeaderProps { + maintenance: MaintenanceItem; +} + +export const MaintenanceDetailHeader = ({ maintenance }: MaintenanceDetailHeaderProps) => { + const { t } = useLanguage(); + + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case 'scheduled': + return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'; + case 'in_progress': + case 'in progress': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + case 'completed': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + case 'cancelled': + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + default: + return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; + } + }; + + const getImpactColor = (impact: string) => { + switch (impact.toLowerCase()) { + case 'critical': + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + case 'major': + return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300'; + case 'minor': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + case 'none': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + default: + return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; + } + }; + + const getPriorityColor = (priority: string) => { + switch (priority.toLowerCase()) { + case 'high': + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + case 'medium': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + case 'low': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + default: + return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'; + } + }; + + return ( + +
+ + {maintenance.title} +
+
+ + {maintenance.status} + + + {maintenance.field} {t('impact')} + + + {maintenance.priority} {t('priority')} + +
+
+ ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailSections.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailSections.tsx new file mode 100644 index 0000000..d3abf71 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/MaintenanceDetailSections.tsx @@ -0,0 +1,198 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { format } from 'date-fns'; +import { Separator } from '@/components/ui/separator'; +import { Badge } from '@/components/ui/badge'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { User } from '@/services/userService'; + +interface MaintenanceDetailSectionsProps { + maintenance: MaintenanceItem; + assignedUsers: User[]; +} + +export const MaintenanceDetailSections = ({ + maintenance, + assignedUsers +}: MaintenanceDetailSectionsProps) => { + const { t } = useLanguage(); + + const formatDateTime = (dateString: string) => { + try { + return format(new Date(dateString), 'PPP p'); + } catch (error) { + return dateString; + } + }; + + const calculateDuration = (start: string, end: string): string => { + try { + const startTime = new Date(start); + const endTime = new Date(end); + const durationMs = endTime.getTime() - startTime.getTime(); + + // Convert to hours and minutes + const hours = Math.floor(durationMs / (1000 * 60 * 60)); + const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours === 0) { + return `${minutes}m`; + } else if (minutes === 0) { + return `${hours}h`; + } else { + return `${hours}h ${minutes}m`; + } + } catch (error) { + return "N/A"; + } + }; + + const affectedServices = typeof maintenance.affected === 'string' + ? maintenance.affected.split(',').map(item => item.trim()) + : []; + + return ( +
+ {/* Print Header - Only visible when printing */} +
+
+
+
+

Scheduled Maintenance Report

+

{maintenance.title}

+
+
+

Reference ID: {maintenance.id}

+

Generated on {format(new Date(), 'PPP')}

+
+
+
+
+ + {/* Reference ID - Only visible in UI */} +
+ {t('referenceID')} + {maintenance.id} +
+ + {/* Description Section */} +
+

{t('description')}

+
+ {maintenance.description || t('noDescriptionProvided')} +
+
+ + + + {/* Time Information */} +
+

{t('timeInformation')}

+
+
+

{t('scheduledStart')}

+

{formatDateTime(maintenance.start_time)}

+
+
+

{t('scheduledEnd')}

+

{formatDateTime(maintenance.end_time)}

+
+
+

{t('duration')}

+

{calculateDuration(maintenance.start_time, maintenance.end_time)}

+
+
+

{t('maintenanceType')}

+

{maintenance.field}

+
+
+
+ + + + {/* Affected Services */} +
+

{t('affectedServices')}

+
+ {affectedServices.length > 0 ? ( +
+ {affectedServices.map((service, index) => ( + + {service} + + ))} +
+ ) : ( + {t('noAffectedServices')} + )} +
+
+ + {/* Assigned Users - Simplified for print */} +
+

{t('assignedPersonnel')}

+
+ {assignedUsers && assignedUsers.length > 0 ? ( +
+ {assignedUsers.map((user) => ( +
+
+ {user && (user.full_name?.[0] || user.username?.[0] || 'U').toUpperCase()} +
+ {user.full_name || user.username} +
+ ))} +
+ ) : ( + {t('noAssignedUsers')} + )} +
+
+ + + + {/* Metadata - Condensed for print */} +
+

{t('additionalInformation')}

+
+
+

{t('createdBy')}

+

{maintenance.created_by || t('notSpecified')}

+
+
+

{t('notificationChannel')}

+

{maintenance.notification_channel_name || t('notSpecified')}

+
+
+

{t('created')}

+

{formatDateTime(maintenance.created)}

+
+
+

{t('lastUpdated')}

+

{formatDateTime(maintenance.updated)}

+
+
+
+ + {/* Notification Settings - Simplified for print */} +
+

{t('notificationSettings')}

+

+ {maintenance.notify_subscribers === 'yes' + ? t('subscribersWillBeNotified') + : t('noNotifications')} +

+
+ + {/* Print Footer - Only visible when printing */} +
+
+ Maintenance ID: {maintenance.id} + Printed on {format(new Date(), 'PPP')} +
+

This document is confidential and intended for internal use only.

+
+
+ ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/components/CloseButton.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/components/CloseButton.tsx new file mode 100644 index 0000000..14df70b --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/components/CloseButton.tsx @@ -0,0 +1,18 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { Button } from '@/components/ui/button'; + +interface CloseButtonProps { + onClose: () => void; +} + +export const CloseButton: React.FC = ({ onClose }) => { + const { t } = useLanguage(); + + return ( + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/components/DownloadPdfButton.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/components/DownloadPdfButton.tsx new file mode 100644 index 0000000..dd2aa57 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/components/DownloadPdfButton.tsx @@ -0,0 +1,31 @@ + +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Download } from 'lucide-react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { useDownloadMaintenancePdf } from '../hooks'; +import { MaintenanceItem } from '@/services/maintenance'; + +interface DownloadPdfButtonProps { + maintenance: MaintenanceItem; + className?: string; +} + +export const DownloadPdfButton: React.FC = ({ + maintenance, + className +}) => { + const { t } = useLanguage(); + const { handleDownloadPDF } = useDownloadMaintenancePdf(); + + return ( + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/components/PrintButton.tsx b/application/src/components/schedule-incident/maintenance/detail-dialog/components/PrintButton.tsx new file mode 100644 index 0000000..4480daf --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/components/PrintButton.tsx @@ -0,0 +1,27 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { Printer } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { usePrintMaintenance } from '../hooks/usePrintMaintenance'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; + +interface PrintButtonProps { + maintenance: MaintenanceItem; +} + +export const PrintButton: React.FC = ({ maintenance }) => { + const { t } = useLanguage(); + const { handlePrint } = usePrintMaintenance(); + + return ( + + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/components/index.ts b/application/src/components/schedule-incident/maintenance/detail-dialog/components/index.ts new file mode 100644 index 0000000..d0f807c --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/components/index.ts @@ -0,0 +1,4 @@ + +export * from './DownloadPdfButton'; +export * from './PrintButton'; +export * from './CloseButton'; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/index.ts b/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/index.ts new file mode 100644 index 0000000..4150016 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/index.ts @@ -0,0 +1,3 @@ + +export * from './useDownloadMaintenancePdf'; +export * from './usePrintMaintenance'; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/useDownloadMaintenancePdf.ts b/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/useDownloadMaintenancePdf.ts new file mode 100644 index 0000000..a605437 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/useDownloadMaintenancePdf.ts @@ -0,0 +1,47 @@ + +import { useToast } from '@/hooks/use-toast'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { MaintenanceItem, generateMaintenancePDF } from '@/services/maintenance'; + +export const useDownloadMaintenancePdf = () => { + const { toast } = useToast(); + const { t } = useLanguage(); + + const handleDownloadPDF = async (maintenance: MaintenanceItem) => { + try { + // Show loading toast + toast({ + title: t('generating'), + description: t('preparingPdf'), + }); + + // Generate and download the PDF + const filename = await generateMaintenancePDF(maintenance); + + // Show success toast after successful generation + toast({ + title: t('success'), + description: t('pdfDownloaded'), + }); + + return filename; + } catch (error) { + console.error("Error generating PDF:", error); + + // Provide more detailed error message if available + const errorMessage = error instanceof Error + ? error.message + : t('pdfGenerationFailed'); + + toast({ + title: t('error'), + description: errorMessage, + variant: 'destructive', + }); + + throw error; + } + }; + + return { handleDownloadPDF }; +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/usePrintMaintenance.ts b/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/usePrintMaintenance.ts new file mode 100644 index 0000000..363308f --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/hooks/usePrintMaintenance.ts @@ -0,0 +1,173 @@ + +import { useLanguage } from '@/contexts/LanguageContext'; +import { useToast } from '@/hooks/use-toast'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; + +export const usePrintMaintenance = () => { + const { t } = useLanguage(); + const { toast } = useToast(); + + const handlePrint = (maintenance: MaintenanceItem) => { + try { + // Add print-specific stylesheet temporarily + const style = document.createElement('style'); + style.id = 'print-style'; + style.textContent = ` + @page { + size: A4; + margin: 10mm; + } + @media print { + body * { + visibility: hidden; + } + + .dialog-content, .dialog-content * { + visibility: visible; + } + + .dialog-content { + position: absolute !important; + left: 0; + top: 0; + width: 100%; + height: auto; + padding: 15mm !important; + margin: 0 !important; + background-color: white !important; + box-shadow: none; + overflow: visible !important; + display: block !important; + transform: none !important; + } + + /* Remove any black backgrounds */ + html, body { + background-color: white !important; + color: black !important; + } + + /* Optimize spacing for single page */ + .print-section { + margin-bottom: 2mm !important; + page-break-inside: avoid !important; + } + + /* Reduce vertical spacing */ + h4 { + margin-bottom: 1mm !important; + margin-top: 1mm !important; + color: #1e40af !important; /* blue-800 */ + font-weight: bold !important; + } + + .print-compact-text { + font-size: 9pt !important; + line-height: 1.2 !important; + } + + .print-compact-spacing > * { + margin-top: 1mm !important; + margin-bottom: 1mm !important; + } + + .print-grid { + display: grid !important; + grid-template-columns: 1fr 1fr !important; + gap: 1mm !important; + } + + .badge-print { + background-color: #dbeafe !important; /* blue-100 */ + color: #1e40af !important; /* blue-800 */ + border: 1px solid #93c5fd !important; /* blue-300 */ + padding: 0.5mm 1mm !important; + margin: 0.5mm !important; + display: inline-block !important; + font-size: 8pt !important; + } + + /* More compact spacing */ + .print-compact-margin { + margin: 1mm 0 !important; + } + + .print-smaller-font { + font-size: 8pt !important; + } + + .header-print { + background-color: #1e40af !important; /* blue-800 */ + color: #ffffff !important; + padding: 2mm 3mm !important; + margin-bottom: 2mm !important; + } + + /* More compact header */ + .header-print h1 { + font-size: 12pt !important; + margin-bottom: 0 !important; + } + + .header-print p { + font-size: 9pt !important; + margin-top: 1mm !important; + } + + /* Footer stays at bottom */ + .footer-print { + font-size: 7pt !important; + color: #6b7280 !important; /* gray-500 */ + border-top: 1px solid #e5e7eb !important; /* gray-200 */ + position: fixed !important; + bottom: 10mm !important; + left: 15mm !important; + right: 15mm !important; + padding-top: 2mm !important; + background-color: white !important; + } + + /* Hide any unnecessary elements */ + .print-hide { + display: none !important; + } + + /* Optimize description text */ + .print-description { + max-height: 100px !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } + } + `; + document.head.appendChild(style); + + // Wait a bit to ensure styles are applied + setTimeout(() => { + window.print(); + + // Remove the style after printing dialog closes + setTimeout(() => { + const printStyle = document.getElementById('print-style'); + if (printStyle) { + printStyle.remove(); + } + }, 1000); + }, 100); + + toast({ + title: t('success'), + description: t('printJobStarted'), + }); + } catch (error) { + console.error("Error printing:", error); + toast({ + title: t('error'), + description: t('printingFailed'), + variant: 'destructive', + }); + } + }; + + return { handlePrint }; +}; diff --git a/application/src/components/schedule-incident/maintenance/detail-dialog/index.ts b/application/src/components/schedule-incident/maintenance/detail-dialog/index.ts new file mode 100644 index 0000000..a559f25 --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/detail-dialog/index.ts @@ -0,0 +1,8 @@ + +export * from './MaintenanceDetailDialog'; +export * from './MaintenanceDetailContent'; +export * from './MaintenanceDetailHeader'; +export * from './MaintenanceDetailSections'; +export * from './MaintenanceDetailFooter'; +export * from './components'; +export * from './hooks'; diff --git a/application/src/components/schedule-incident/maintenance/edit-dialog/EditMaintenanceDialog.tsx b/application/src/components/schedule-incident/maintenance/edit-dialog/EditMaintenanceDialog.tsx new file mode 100644 index 0000000..d51184e --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/edit-dialog/EditMaintenanceDialog.tsx @@ -0,0 +1,97 @@ + +import React from 'react'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter +} from '@/components/ui/dialog'; +import { Form } from '@/components/ui/form'; +import { Button } from '@/components/ui/button'; +import { MaintenanceItem } from '@/services/types/maintenance.types'; +import { useMaintenanceEditForm } from '../hooks/useMaintenanceEditForm'; +import { + MaintenanceBasicFields, + MaintenanceTimeFields, + MaintenanceAffectedFields, + MaintenanceConfigFields, + MaintenanceNotificationSettingsField +} from '../form'; + +interface EditMaintenanceDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + maintenance: MaintenanceItem; + onMaintenanceUpdated: () => void; +} + +export const EditMaintenanceDialog = ({ + open, + onOpenChange, + maintenance, + onMaintenanceUpdated +}: EditMaintenanceDialogProps) => { + const { t } = useLanguage(); + + const handleSuccess = () => { + console.log("EditMaintenanceDialog: maintenance updated successfully"); + onMaintenanceUpdated(); + }; + + const handleClose = () => { + console.log("EditMaintenanceDialog: closing dialog"); + onOpenChange(false); + }; + + const { form, onSubmit } = useMaintenanceEditForm(maintenance, handleSuccess, handleClose); + + // Log the form state for debugging + React.useEffect(() => { + const subscription = form.watch((value) => { + console.log("Form values in edit dialog:", value); + }); + return () => subscription.unsubscribe(); + }, [form]); + + return ( + + + + {t('editMaintenanceWindow')} + + {t('editMaintenanceDesc')} + + + +
+ + + + + + + + + + + + + +
+
+ ); +}; diff --git a/application/src/components/schedule-incident/maintenance/edit-dialog/index.ts b/application/src/components/schedule-incident/maintenance/edit-dialog/index.ts new file mode 100644 index 0000000..e4f699c --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/edit-dialog/index.ts @@ -0,0 +1,2 @@ + +export * from './EditMaintenanceDialog'; diff --git a/application/src/components/schedule-incident/maintenance/form/MaintenanceAffectedFields.tsx b/application/src/components/schedule-incident/maintenance/form/MaintenanceAffectedFields.tsx new file mode 100644 index 0000000..2f933dd --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/form/MaintenanceAffectedFields.tsx @@ -0,0 +1,35 @@ + +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { MaintenanceFormValues } from '../hooks/useMaintenanceForm'; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; + +export const MaintenanceAffectedFields: React.FC = () => { + const { t } = useLanguage(); + const { control } = useFormContext(); + + return ( + ( + + {t('affectedServices')} + + + + +

{t('separateServicesWithComma')}

+
+ )} + /> + ); +}; diff --git a/application/src/components/schedule-incident/maintenance/form/MaintenanceBasicFields.tsx b/application/src/components/schedule-incident/maintenance/form/MaintenanceBasicFields.tsx new file mode 100644 index 0000000..0ec8a9f --- /dev/null +++ b/application/src/components/schedule-incident/maintenance/form/MaintenanceBasicFields.tsx @@ -0,0 +1,55 @@ + +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { useLanguage } from '@/contexts/LanguageContext'; +import { MaintenanceFormValues } from '../hooks/useMaintenanceForm'; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; + +export const MaintenanceBasicFields: React.FC = () => { + const { t } = useLanguage(); + const { control } = useFormContext(); + + return ( + <> + ( + + {t('title')} + + + + + + )} + /> + + ( + + {t('description')} + +