import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation, Link, useSearchParams } from "@remix-run/react"; import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; import ReportViewModal from "~/components/ReportViewModal"; import ReportFormModal from "~/components/ReportFormModal"; import Toast from "~/components/Toast"; import { PrismaClient } from "@prisma/client"; import { useState, useEffect } from "react"; const prisma = new PrismaClient(); export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }]; export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireAuthLevel(request, 1); // All employees can access reports // Get all reports with related data let reports = await prisma.report.findMany({ // where: { employeeId: user.id }, orderBy: { createdDate: 'desc' }, include: { employee: { select: { name: true } }, area: { select: { name: true } }, dredgerLocation: { select: { name: true, class: true } }, reclamationLocation: { select: { name: true } } } }); if (user.authLevel === 1){ // filter report by user id reports = reports.filter((report: any) => report.employeeId === user.id); } // if (user.authLevel === 1) { // reports = await prisma.report.findMany({ // where: { employeeId: user.id }, // orderBy: { createdDate: 'desc' }, // include: { // employee: { select: { name: true } }, // area: { select: { name: true } }, // dredgerLocation: { select: { name: true, class: true } }, // reclamationLocation: { select: { name: true } } // } // }); // } // Get dropdown data for edit form only const [areas, dredgerLocations, reclamationLocations, foremen, equipment] = await Promise.all([ prisma.area.findMany({ orderBy: { name: 'asc' } }), prisma.dredgerLocation.findMany({ orderBy: { name: 'asc' } }), prisma.reclamationLocation.findMany({ orderBy: { name: 'asc' } }), prisma.foreman.findMany({ orderBy: { name: 'asc' } }), prisma.equipment.findMany({ orderBy: [{ category: 'asc' }, { model: 'asc' }, { number: 'asc' }] }) ]); return json({ user, reports, areas, dredgerLocations, reclamationLocations, foremen, equipment }); }; export const action = async ({ request }: ActionFunctionArgs) => { const user = await requireAuthLevel(request, 1); const formData = await request.formData(); const intent = formData.get("intent"); const id = formData.get("id"); if (intent === "update") { if (typeof id !== "string") { return json({ errors: { form: "Invalid report ID" } }, { status: 400 }); } // Check if user owns this report or has admin privileges const existingReport = await prisma.report.findUnique({ where: { id: parseInt(id) }, select: { employeeId: true, createdDate: true } }); if (!existingReport) { return json({ errors: { form: "Report not found" } }, { status: 404 }); } if (user.authLevel < 2) { // Regular users can only edit their own reports if (existingReport.employeeId !== user.id) { return json({ errors: { form: "You can only edit your own reports" } }, { status: 403 }); } // Regular users can only edit their latest report const latestUserReport = await prisma.report.findFirst({ where: { employeeId: user.id }, orderBy: { createdDate: 'desc' }, select: { id: true } }); if (!latestUserReport || latestUserReport.id !== parseInt(id)) { return json({ errors: { form: "You can only edit your latest report" } }, { status: 403 }); } } const shift = formData.get("shift"); const areaId = formData.get("areaId"); const dredgerLocationId = formData.get("dredgerLocationId"); const dredgerLineLength = formData.get("dredgerLineLength"); const reclamationLocationId = formData.get("reclamationLocationId"); const shoreConnection = formData.get("shoreConnection"); const notes = formData.get("notes"); // Complex JSON fields const reclamationHeightBase = formData.get("reclamationHeightBase"); const reclamationHeightExtra = formData.get("reclamationHeightExtra"); const pipelineMain = formData.get("pipelineMain"); const pipelineExt1 = formData.get("pipelineExt1"); const pipelineReserve = formData.get("pipelineReserve"); const pipelineExt2 = formData.get("pipelineExt2"); const statsDozers = formData.get("statsDozers"); const statsExc = formData.get("statsExc"); const statsLoaders = formData.get("statsLoaders"); const statsForeman = formData.get("statsForeman"); const statsLaborer = formData.get("statsLaborer"); const timeSheetData = formData.get("timeSheetData"); const stoppagesData = formData.get("stoppagesData"); // Validation (same as create) if (typeof shift !== "string" || !["day", "night"].includes(shift)) { return json({ errors: { shift: "Valid shift is required" } }, { status: 400 }); } if (typeof areaId !== "string" || !areaId) { return json({ errors: { areaId: "Area is required" } }, { status: 400 }); } if (typeof dredgerLocationId !== "string" || !dredgerLocationId) { return json({ errors: { dredgerLocationId: "Dredger location is required" } }, { status: 400 }); } if (typeof dredgerLineLength !== "string" || isNaN(parseInt(dredgerLineLength))) { return json({ errors: { dredgerLineLength: "Valid dredger line length is required" } }, { status: 400 }); } if (typeof reclamationLocationId !== "string" || !reclamationLocationId) { return json({ errors: { reclamationLocationId: "Reclamation location is required" } }, { status: 400 }); } if (typeof shoreConnection !== "string" || isNaN(parseInt(shoreConnection))) { return json({ errors: { shoreConnection: "Valid shore connection is required" } }, { status: 400 }); } try { // Parse JSON arrays let timeSheet = []; let stoppages = []; if (timeSheetData && typeof timeSheetData === "string") { try { timeSheet = JSON.parse(timeSheetData); } catch (e) { timeSheet = []; } } if (stoppagesData && typeof stoppagesData === "string") { try { stoppages = JSON.parse(stoppagesData); } catch (e) { stoppages = []; } } await prisma.report.update({ where: { id: parseInt(id) }, data: { shift, areaId: parseInt(areaId), dredgerLocationId: parseInt(dredgerLocationId), dredgerLineLength: parseInt(dredgerLineLength), reclamationLocationId: parseInt(reclamationLocationId), shoreConnection: parseInt(shoreConnection), reclamationHeight: { base: parseInt(reclamationHeightBase as string) || 0, extra: parseInt(reclamationHeightExtra as string) || 0 }, pipelineLength: { main: parseInt(pipelineMain as string) || 0, ext1: parseInt(pipelineExt1 as string) || 0, reserve: parseInt(pipelineReserve as string) || 0, ext2: parseInt(pipelineExt2 as string) || 0 }, stats: { Dozers: parseInt(statsDozers as string) || 0, Exc: parseInt(statsExc as string) || 0, Loaders: parseInt(statsLoaders as string) || 0, Foreman: statsForeman as string || "", Laborer: parseInt(statsLaborer as string) || 0 }, timeSheet, stoppages, notes: notes || null } }); return json({ success: "Report updated successfully!" }); } catch (error) { return json({ errors: { form: "Failed to update report" } }, { status: 400 }); } } if (intent === "delete") { if (typeof id !== "string") { return json({ errors: { form: "Invalid report ID" } }, { status: 400 }); } // Check if user owns this report or has admin privileges const existingReport = await prisma.report.findUnique({ where: { id: parseInt(id) }, select: { employeeId: true, createdDate: true } }); if (!existingReport) { return json({ errors: { form: "Report not found" } }, { status: 404 }); } if (user.authLevel < 2) { // Regular users can only delete their own reports if (existingReport.employeeId !== user.id) { return json({ errors: { form: "You can only delete your own reports" } }, { status: 403 }); } // Regular users can only delete their latest report const latestUserReport = await prisma.report.findFirst({ where: { employeeId: user.id }, orderBy: { createdDate: 'desc' }, select: { id: true } }); if (!latestUserReport || latestUserReport.id !== parseInt(id)) { return json({ errors: { form: "You can only delete your latest report" } }, { status: 403 }); } } try { await prisma.report.delete({ where: { id: parseInt(id) } }); return json({ success: "Report deleted successfully!" }); } catch (error) { return json({ errors: { form: "Failed to delete report" } }, { status: 400 }); } } return json({ errors: { form: "Invalid action" } }, { status: 400 }); }; export default function Reports() { const { user, reports, areas, dredgerLocations, reclamationLocations, foremen, equipment } = useLoaderData(); const actionData = useActionData(); const navigation = useNavigation(); const [searchParams] = useSearchParams(); const [editingReport, setEditingReport] = useState(null); const [viewingReport, setViewingReport] = useState(null); const [showModal, setShowModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); // Dynamic arrays state for editing only const [timeSheetEntries, setTimeSheetEntries] = useState>([]); const [stoppageEntries, setStoppageEntries] = useState>([]); const isSubmitting = navigation.state === "submitting"; const isEditing = editingReport !== null; // Handle success/error messages from URL params and action data useEffect(() => { const successMessage = searchParams.get("success"); const errorMessage = searchParams.get("error"); if (successMessage) { setToast({ message: successMessage, type: "success" }); // Clear the URL parameter window.history.replaceState({}, '', '/reports'); } else if (errorMessage) { setToast({ message: errorMessage, type: "error" }); // Clear the URL parameter window.history.replaceState({}, '', '/reports'); } else if (actionData?.success) { setToast({ message: actionData.success, type: "success" }); setShowModal(false); setEditingReport(null); } else if (actionData?.errors?.form) { setToast({ message: actionData.errors.form, type: "error" }); } }, [actionData, searchParams]); const handleView = (report: any) => { setViewingReport(report); setShowViewModal(true); }; const handleEdit = (report: any) => { setEditingReport(report); // Load existing timesheet and stoppages data setTimeSheetEntries(Array.isArray(report.timeSheet) ? report.timeSheet : []); setStoppageEntries(Array.isArray(report.stoppages) ? report.stoppages : []); setShowModal(true); }; // Remove handleAdd since we're using a separate page const handleCloseModal = () => { setShowModal(false); setEditingReport(null); setTimeSheetEntries([]); setStoppageEntries([]); }; const handleCloseViewModal = () => { setShowViewModal(false); setViewingReport(null); }; // Helper function to calculate time difference in hours:minutes format const calculateTimeDifference = (from1: string, to1: string, from2: string, to2: string) => { if (!from1 || !to1) return "00:00"; const parseTime = (timeStr: string) => { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; }; const formatTime = (minutes: number) => { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; }; let totalMinutes = 0; // First period if (from1 && to1) { const start1 = parseTime(from1); const end1 = parseTime(to1); totalMinutes += end1 - start1; } // Second period if (from2 && to2) { const start2 = parseTime(from2); const end2 = parseTime(to2); totalMinutes += end2 - start2; } return formatTime(Math.max(0, totalMinutes)); }; const calculateStoppageTime = (from: string, to: string) => { if (!from || !to) return "00:00"; const parseTime = (timeStr: string) => { const [hours, minutes] = timeStr.split(':').map(Number); return hours * 60 + minutes; }; const formatTime = (minutes: number) => { const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`; }; const startMinutes = parseTime(from); const endMinutes = parseTime(to); const totalMinutes = Math.max(0, endMinutes - startMinutes); return formatTime(totalMinutes); }; // TimeSheet management functions const addTimeSheetEntry = () => { const newEntry = { id: Date.now().toString(), machine: '', from1: '', to1: '', from2: '', to2: '', total: '00:00', reason: '' }; setTimeSheetEntries([...timeSheetEntries, newEntry]); }; const removeTimeSheetEntry = (id: string) => { setTimeSheetEntries(timeSheetEntries.filter(entry => entry.id !== id)); }; const updateTimeSheetEntry = (id: string, field: string, value: string) => { setTimeSheetEntries(timeSheetEntries.map(entry => { if (entry.id === id) { const updatedEntry = { ...entry, [field]: value }; // Auto-calculate total when time fields change if (['from1', 'to1', 'from2', 'to2'].includes(field)) { updatedEntry.total = calculateTimeDifference( updatedEntry.from1, updatedEntry.to1, updatedEntry.from2, updatedEntry.to2 ); } return updatedEntry; } return entry; })); }; // Stoppage management functions const addStoppageEntry = () => { const newEntry = { id: Date.now().toString(), from: '', to: '', total: '00:00', reason: '', responsible: '', note: '' }; setStoppageEntries([...stoppageEntries, newEntry]); }; const removeStoppageEntry = (id: string) => { setStoppageEntries(stoppageEntries.filter(entry => entry.id !== id)); }; const updateStoppageEntry = (id: string, field: string, value: string) => { setStoppageEntries(stoppageEntries.map(entry => { if (entry.id === id) { const updatedEntry = { ...entry, [field]: value }; // Auto-calculate total when time fields change if (['from', 'to'].includes(field)) { updatedEntry.total = calculateStoppageTime(updatedEntry.from, updatedEntry.to); } return updatedEntry; } return entry; })); }; const getShiftBadge = (shift: string) => { return shift === "day" ? "bg-yellow-100 text-yellow-800" : "bg-blue-100 text-blue-800"; }; const canEditReport = (report: any) => { // Admin users (auth level >= 2) can edit any report if (user.authLevel >= 2) { return true; } // Regular users (auth level 1) can only edit their own latest report if (report.employeeId === user.id) { // Find the latest report for this user const userReports = reports.filter(r => r.employeeId === user.id); const latestReport = userReports.reduce((latest, current) => new Date(current.createdDate) > new Date(latest.createdDate) ? current : latest ); return report.id === latestReport.id; } return false; }; return (

Reports Management

Create and manage operational reports

Create New Report
{/* Reports Table */}
{reports.map((report) => ( ))}
Report Details Shift & Area Locations Created Actions
Report #{report.id}
by {report.employee.name}
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)} Shift {report.area.name}
{report.area.name} Dredger
{report.dredgerLocation.name} - {report.reclamationLocation.name}
{/* {new Date(report.createdDate).toLocaleDateString()} */} {new Date(report.createdDate).toLocaleDateString('en-GB')}
{canEditReport(report) && ( <>
)}
{reports.length === 0 && (

No reports

Get started by creating your first report.

Create Report
)}
{/* Edit Form Modal - Only for editing existing reports */} {isEditing && ( )} {/* View Modal */} {/* Toast Notifications */} {toast && ( setToast(null)} /> )}
); }