diff --git a/app/components/DashboardLayout.tsx b/app/components/DashboardLayout.tsx index 2bbfab2..c2197db 100644 --- a/app/components/DashboardLayout.tsx +++ b/app/components/DashboardLayout.tsx @@ -1,4 +1,4 @@ -import { Form, Link, useLoaderData } from "@remix-run/react"; +import { Form, Link } from "@remix-run/react"; import type { Employee } from "@prisma/client"; interface DashboardLayoutProps { @@ -84,6 +84,18 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps + Shifts + + + +
  • + + + + Reports
  • diff --git a/app/components/ReportSheetViewModal.tsx b/app/components/ReportSheetViewModal.tsx new file mode 100644 index 0000000..e982c06 --- /dev/null +++ b/app/components/ReportSheetViewModal.tsx @@ -0,0 +1,524 @@ +import React from 'react'; +import { exportReportToExcel } from '~/utils/excelExport'; + +interface ReportSheet { + id: string; + date: string; + area: string; + dredgerLocation: string; + reclamationLocation: string; + dayReport?: any; + nightReport?: any; +} + +interface ReportSheetViewModalProps { + isOpen: boolean; + onClose: () => void; + sheet: ReportSheet | null; +} + +export default function ReportSheetViewModal({ isOpen, onClose, sheet }: ReportSheetViewModalProps) { + if (!isOpen || !sheet) return null; + + const handleExportExcel = async () => { + if (sheet.dayReport) { + await exportReportToExcel(sheet.dayReport); + } + if (sheet.nightReport) { + await exportReportToExcel(sheet.nightReport); + } + }; + + return ( +
    +
    +
    + +
    +
    +
    +

    Report Sheet - {new Date(sheet.date).toLocaleDateString('en-GB')}

    +
    + + + +
    +
    + + {/* Combined Report Sheet Layout */} +
    + + + + + + + {/* Day Shift Section */} + {sheet.dayReport && ( + <> + + + + + + + )} + + {/* Night Shift Section */} + {sheet.nightReport && ( + <> + + + + + + + )} + + +
    +
    +
    +
    +
    + ); +} + +// Header Section Component +function ReportSheetHeader({ sheet }: { sheet: ReportSheet }) { + return ( +
    + + + + + + + +
    +
    Reclamation Work Diary - Daily Sheet
    +
    QF-3.6.1-08
    +
    Rev. 1.0
    +
    + Arab Potash Logo { + e.currentTarget.style.display = 'none'; + }} + /> +
    +
    + ); +} + +// Report Info Component +function ReportSheetInfo({ sheet }: { sheet: ReportSheet }) { + return ( +
    + + + + + + + + + +
    Date: + {new Date(sheet.date).toLocaleDateString('en-GB')} + Report No. {sheet.id}
    +
    + ); +} + +// Dredger Section Component +function ReportSheetDredgerSection({ sheet }: { sheet: ReportSheet }) { + return ( +
    + {sheet.area} Dredger +
    + ); +} + +// Location Data Component (using data from either available report) +function ReportSheetLocationData({ sheet }: { sheet: ReportSheet }) { + const report = sheet.dayReport || sheet.nightReport; + if (!report) return null; + + return ( +
    + + + + + + + + + + + + + + + + + + + + +
    + Dredger Location + + {report.dredgerLocation.name} + + Dredger Line Length + + {report.dredgerLineLength} +
    + Reclamation Location + + {report.reclamationLocation.name} + + Shore Connection + + {report.shoreConnection} +
    + Reclamation Height + + {report.reclamationHeight?.base || 0}m - {(report.reclamationHeight?.extra + report.reclamationHeight?.base || 0 || 0)}m +
    +
    + ); +} + +// Pipeline Length Component (using data from either available report) +function ReportSheetPipelineLength({ sheet }: { sheet: ReportSheet }) { + const report = sheet.dayReport || sheet.nightReport; + if (!report) return null; + + return ( +
    + + + + + + + + + + + + + + + + + + + + +
    + Pipeline Length "from Shore Connection" + MainextensiontotalReserveextensiontotal
    + {report.pipelineLength?.main || 0} + + {report.pipelineLength?.ext1 || 0} + + {(report.pipelineLength?.main || 0) + (report.pipelineLength?.ext1 || 0)} + + {report.pipelineLength?.reserve || 0} + + {report.pipelineLength?.ext2 || 0} + + {(report.pipelineLength?.reserve || 0) + (report.pipelineLength?.ext2 || 0)} +
    +
    + ); +} + +// Shift Header Component +function ReportSheetShiftHeader({ shift }: { shift: string }) { + return ( +
    + {shift.charAt(0).toUpperCase() + shift.slice(1)} Shift +
    + ); +} + +// Equipment Statistics Component +function ReportSheetEquipmentStats({ report }: { report: any }) { + return ( +
    + + + + + + + + + + + + + + + + + +
    DozersExc.LoaderForemanLaborer
    + {report.stats?.Dozers || 0} + + {report.stats?.Exc || 0} + + {report.stats?.Loaders || 0} + + {report.stats?.Foreman || ''} + + {report.stats?.Laborer || 0} +
    +
    + ); +} + +// Time Sheet Component +function ReportSheetTimeSheet({ report }: { report: any }) { + return ( +
    + + + + + + + + + + + + + + {Array.isArray(report.timeSheet) && report.timeSheet.length > 0 ? ( + report.timeSheet.map((entry: any, index: number) => ( + + + + + + + + + + )) + ) : ( + + + + )} + +
    Time SheetFromToFromToTotalReason
    + {entry.machine} + + {entry.from1} + + {entry.to1} + + {entry.from2} + + {entry.to2} + + {entry.total} + + {entry.reason} +
    + No time sheet entries +
    +
    + ); +} + +// Stoppages Component +function ReportSheetStoppages({ report }: { report: any }) { + return ( + <> +
    + Dredger Stoppages +
    +
    + + + + + + + + + + + + + {Array.isArray(report.stoppages) && report.stoppages.length > 0 ? ( + report.stoppages.map((entry: any, index: number) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
    FromToTotalReasonResponsibleNotes
    + {entry.from} + + {entry.to} + + {entry.total} + + {entry.reason} + + {entry.responsible} + + {entry.note} +
    + No stoppages recorded +
    +
    + + ); +} + +// Notes Component +function ReportSheetNotes({ report }: { report: any }) { + return ( + <> +
    + Notes & Comments +
    +
    +
    + {report.notes || 'No additional notes'} +
    +
    + + ); +} + +// Footer Component +function ReportSheetFooter() { + return ( +
    + {/* موقعة لأعمال الصيانة */} +
    + ); +} \ No newline at end of file diff --git a/app/components/ReportViewModal.tsx b/app/components/ReportViewModal.tsx index 8b98428..4163e77 100644 --- a/app/components/ReportViewModal.tsx +++ b/app/components/ReportViewModal.tsx @@ -32,7 +32,81 @@ export default function ReportViewModal({ isOpen, onClose, report }: ReportViewM {employee.id !== user.id && ( -
    - - - -
    + <> +
    + + + +
    +
    + + + +
    + )} @@ -516,6 +605,23 @@ export default function Employees() {

    {actionData.errors.authLevel}

    )} + + {isEditing && editingEmployee?.id !== user.id && ( +
    + + +
    + )} {isEditing && ( diff --git a/app/routes/equipment.tsx b/app/routes/equipment.tsx index 9541af6..135eb06 100644 --- a/app/routes/equipment.tsx +++ b/app/routes/equipment.tsx @@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; import FormModal from "~/components/FormModal"; import Toast from "~/components/Toast"; -import { PrismaClient } from "@prisma/client"; import { useState, useEffect } from "react"; - -const prisma = new PrismaClient(); +import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }]; diff --git a/app/routes/foreman.tsx b/app/routes/foreman.tsx index 6938eb4..00f3a01 100644 --- a/app/routes/foreman.tsx +++ b/app/routes/foreman.tsx @@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; import FormModal from "~/components/FormModal"; import Toast from "~/components/Toast"; -import { PrismaClient } from "@prisma/client"; import { useState, useEffect } from "react"; - -const prisma = new PrismaClient(); +import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }]; diff --git a/app/routes/mail-settings.tsx b/app/routes/mail-settings.tsx index d9c126c..e30e5fc 100644 --- a/app/routes/mail-settings.tsx +++ b/app/routes/mail-settings.tsx @@ -1,12 +1,10 @@ import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; -import { PrismaClient } from "@prisma/client"; import { requireAuthLevel } from "~/utils/auth.server"; import { testEmailConnection } from "~/utils/mail.server"; import DashboardLayout from "~/components/DashboardLayout"; import { useState } from "react"; - -const prisma = new PrismaClient(); +import { prisma } from "~/utils/db.server"; export async function loader({ request }: LoaderFunctionArgs) { // Require auth level 3 to access mail settings diff --git a/app/routes/reclamation-locations.tsx b/app/routes/reclamation-locations.tsx index a081c06..0422023 100644 --- a/app/routes/reclamation-locations.tsx +++ b/app/routes/reclamation-locations.tsx @@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; import FormModal from "~/components/FormModal"; import Toast from "~/components/Toast"; -import { PrismaClient } from "@prisma/client"; import { useState, useEffect } from "react"; - -const prisma = new PrismaClient(); +import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }]; diff --git a/app/routes/report-sheet.tsx b/app/routes/report-sheet.tsx new file mode 100644 index 0000000..55b8c8d --- /dev/null +++ b/app/routes/report-sheet.tsx @@ -0,0 +1,240 @@ +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { requireAuthLevel } from "~/utils/auth.server"; +import DashboardLayout from "~/components/DashboardLayout"; +import ReportSheetViewModal from "~/components/ReportSheetViewModal"; +import { useState } from "react"; +import { prisma } from "~/utils/db.server"; + +export const meta: MetaFunction = () => [{ title: "Report Sheets - Phosphat Report" }]; + +interface ReportSheet { + id: string; + date: string; + area: string; + dredgerLocation: string; + reclamationLocation: string; + status: string; + dayReport?: any; + nightReport?: any; +} + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const user = await requireAuthLevel(request, 1); + + // Get sheets with related data + let sheets = await prisma.sheet.findMany({ + orderBy: { createdAt: 'desc' }, + include: { + area: { select: { name: true } }, + dredgerLocation: { select: { name: true, class: true } }, + reclamationLocation: { select: { name: true } }, + dayShift: { + include: { + employee: { select: { name: true } }, + area: { select: { name: true } }, + dredgerLocation: { select: { name: true, class: true } }, + reclamationLocation: { select: { name: true } } + } + }, + nightShift: { + include: { + employee: { select: { name: true } }, + area: { select: { name: true } }, + dredgerLocation: { select: { name: true, class: true } }, + reclamationLocation: { select: { name: true } } + } + } + } + }); + + // Filter sheets for level 1 users (only show sheets where user has at least one shift) + if (user.authLevel === 1) { + sheets = sheets.filter((sheet: any) => + (sheet.dayShift && sheet.dayShift.employeeId === user.id) || + (sheet.nightShift && sheet.nightShift.employeeId === user.id) + ); + } + + // Transform sheets to match the expected interface + const transformedSheets = sheets.map((sheet: any) => ({ + id: sheet.id.toString(), + date: sheet.date, + area: sheet.area.name, + dredgerLocation: sheet.dredgerLocation.name, + reclamationLocation: sheet.reclamationLocation.name, + status: sheet.status, + dayReport: sheet.dayShift, + nightReport: sheet.nightShift + })); + + return json({ user, sheets: transformedSheets }); +}; + +export default function ReportSheet() { + const { user, sheets } = useLoaderData(); + const [viewingSheet, setViewingSheet] = useState(null); + const [showViewModal, setShowViewModal] = useState(false); + + const handleView = (sheet: ReportSheet) => { + setViewingSheet(sheet); + setShowViewModal(true); + }; + + const handleCloseViewModal = () => { + setShowViewModal(false); + setViewingSheet(null); + }; + + const getShiftBadge = (shift: string) => { + return shift === "day" + ? "bg-yellow-100 text-yellow-800" + : "bg-blue-100 text-blue-800"; + }; + + const getShiftIcon = (shift: string) => { + return shift === "day" ? ( + + + + ) : ( + + + + ); + }; + + return ( + +
    +
    +
    +

    Report Sheets

    +

    View grouped reports by location and date

    +
    +
    + + {/* Report Sheets Table */} +
    +
    + + + + + + + + + + + + + + {sheets.map((sheet) => ( + + + + + + + + + + ))} + +
    + Date + + Area + + Locations + + Available Shifts + + Status + + Employees + + Actions +
    +
    + {new Date(sheet.date).toLocaleDateString('en-GB')} +
    +
    +
    {sheet.area}
    +
    +
    +
    Dredger: {sheet.dredgerLocation}
    +
    Reclamation: {sheet.reclamationLocation}
    +
    +
    +
    + {sheet.dayReport && ( + + {getShiftIcon('day')} + Day + + )} + {sheet.nightReport && ( + + {getShiftIcon('night')} + Night + + )} +
    +
    + + {sheet.status === 'completed' ? ( + + + + ) : ( + + + + )} + {sheet.status.charAt(0).toUpperCase() + sheet.status.slice(1)} + + +
    + {sheet.dayReport && ( +
    Day: {sheet.dayReport.employee.name}
    + )} + {sheet.nightReport && ( +
    Night: {sheet.nightReport.employee.name}
    + )} +
    +
    + +
    +
    + {sheets.length === 0 && ( +
    + + + +

    No report sheets

    +

    Report sheets will appear here when reports are created.

    +
    + )} +
    + + {/* View Modal */} + +
    +
    + ); +} \ No newline at end of file diff --git a/app/routes/reports.tsx b/app/routes/reports.tsx index 90ad0ce..bd259cb 100644 --- a/app/routes/reports.tsx +++ b/app/routes/reports.tsx @@ -6,10 +6,9 @@ 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(); +import { manageSheet, removeFromSheet } from "~/utils/sheet.server"; +import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }]; @@ -84,7 +83,14 @@ export const action = async ({ request }: ActionFunctionArgs) => { // 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 } + select: { + employeeId: true, + createdDate: true, + shift: true, + areaId: true, + dredgerLocationId: true, + reclamationLocationId: true + } }); if (!existingReport) { @@ -173,7 +179,21 @@ export const action = async ({ request }: ActionFunctionArgs) => { } } - await prisma.report.update({ + // First, remove from old sheet if location/date changed + if (existingReport.areaId !== parseInt(areaId) || + existingReport.dredgerLocationId !== parseInt(dredgerLocationId) || + existingReport.reclamationLocationId !== parseInt(reclamationLocationId)) { + await removeFromSheet( + parseInt(id), + existingReport.shift, + existingReport.areaId, + existingReport.dredgerLocationId, + existingReport.reclamationLocationId, + existingReport.createdDate + ); + } + + const updatedReport = await prisma.report.update({ where: { id: parseInt(id) }, data: { shift, @@ -204,6 +224,16 @@ export const action = async ({ request }: ActionFunctionArgs) => { notes: notes || null } }); + + // Manage sheet for new location/date + await manageSheet( + parseInt(id), + shift, + parseInt(areaId), + parseInt(dredgerLocationId), + parseInt(reclamationLocationId), + updatedReport.createdDate // Use original creation date, not update date + ); return json({ success: "Report updated successfully!" }); } catch (error) { return json({ errors: { form: "Failed to update report" } }, { status: 400 }); @@ -218,7 +248,14 @@ export const action = async ({ request }: ActionFunctionArgs) => { // 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 } + select: { + employeeId: true, + createdDate: true, + shift: true, + areaId: true, + dredgerLocationId: true, + reclamationLocationId: true + } }); if (!existingReport) { @@ -244,6 +281,16 @@ export const action = async ({ request }: ActionFunctionArgs) => { } try { + // Remove from sheet before deleting report + await removeFromSheet( + parseInt(id), + existingReport.shift, + existingReport.areaId, + existingReport.dredgerLocationId, + existingReport.reclamationLocationId, + existingReport.createdDate + ); + await prisma.report.delete({ where: { id: parseInt(id) } }); @@ -495,8 +542,8 @@ export default function Reports() {
    -

    Reports Management

    -

    Create and manage operational reports

    +

    Shifts Management

    +

    Create and manage operational shifts

    - Create New Report + Create New Shift
    @@ -516,7 +563,7 @@ export default function Reports() { - Report Details + Shift Details Shift & Area @@ -545,7 +592,7 @@ export default function Reports() {
    -
    Report #{report.id}
    +
    Shift #{report.id}
    by {report.employee.name}
    @@ -623,7 +670,7 @@ export default function Reports() { - Create Report + Create Shifs diff --git a/app/routes/reports_.new.tsx b/app/routes/reports_.new.tsx index a26b100..2a79901 100644 --- a/app/routes/reports_.new.tsx +++ b/app/routes/reports_.new.tsx @@ -3,10 +3,9 @@ import { json, redirect } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react"; import { requireAuthLevel } from "~/utils/auth.server"; import DashboardLayout from "~/components/DashboardLayout"; -import { PrismaClient } from "@prisma/client"; import { useState, useEffect } from "react"; - -const prisma = new PrismaClient(); +import { manageSheet } from "~/utils/sheet.server"; +import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "New Report - Phosphat Report" }]; @@ -107,7 +106,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { } } - await prisma.report.create({ + const report = await prisma.report.create({ data: { employeeId: user.id, shift, @@ -138,6 +137,16 @@ export const action = async ({ request }: ActionFunctionArgs) => { notes: notes || null } }); + + // Manage sheet creation/update + await manageSheet( + report.id, + shift, + parseInt(areaId), + parseInt(dredgerLocationId), + parseInt(reclamationLocationId), + report.createdDate + ); // Redirect to reports page with success message return redirect("/reports?success=Report created successfully!"); @@ -378,8 +387,8 @@ export default function NewReport() {
    -

    Create New Report

    -

    Fill out the operational report details step by step

    +

    Create New Shifts

    +

    Fill out the operational shift details step by step

    - Back to Reports + Back to Shifts
    diff --git a/app/routes/signup.tsx b/app/routes/signup.tsx index 118f420..4781612 100644 --- a/app/routes/signup.tsx +++ b/app/routes/signup.tsx @@ -2,9 +2,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remi import { json, redirect } from "@remix-run/node"; import { Form, Link, useActionData } from "@remix-run/react"; import { createUser, createUserSession, getUserId } from "~/utils/auth.server"; -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); +import { prisma } from "~/utils/db.server"; export const meta: MetaFunction = () => [{ title: "Sign Up - Phosphat Report" }]; diff --git a/app/utils/auth.server.ts b/app/utils/auth.server.ts index d005bc3..4e80f7e 100644 --- a/app/utils/auth.server.ts +++ b/app/utils/auth.server.ts @@ -1,8 +1,6 @@ import { createCookieSessionStorage, redirect } from "@remix-run/node"; -import { PrismaClient } from "@prisma/client"; import bcrypt from "bcryptjs"; - -const prisma = new PrismaClient(); +import { prisma } from "~/utils/db.server"; // Session storage const sessionSecret = process.env.SESSION_SECRET || "default-secret"; @@ -58,8 +56,14 @@ export async function getUser(request: Request) { try { const user = await prisma.employee.findUnique({ where: { id: userId }, - select: { id: true, name: true, username: true, email: true, authLevel: true }, + select: { id: true, name: true, username: true, email: true, authLevel: true, status: true }, }); + + // If user is inactive, logout and return null + if (user && user.status === 'inactive') { + throw logout(request); + } + return user; } catch { throw logout(request); @@ -109,7 +113,12 @@ export async function verifyLogin(usernameOrEmail: string, password: string) { return null; } - return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel }; + // Check if user is inactive + if (user.status === 'inactive') { + return null; + } + + return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel, status: user.status }; } export async function createUser(name: string, username: string, email: string, password: string, authLevel: number = 1) { @@ -122,8 +131,9 @@ export async function createUser(name: string, username: string, email: string, email, password: hashedPassword, authLevel, + status: 'active', }, }); - return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel }; + return { id: user.id, username: user.username, email: user.email, name: user.name, authLevel: user.authLevel, status: user.status }; } \ No newline at end of file diff --git a/app/utils/sheet.server.ts b/app/utils/sheet.server.ts new file mode 100644 index 0000000..47bc24e --- /dev/null +++ b/app/utils/sheet.server.ts @@ -0,0 +1,121 @@ +import { prisma } from "~/utils/db.server"; + +export async function manageSheet(reportId: number, shift: string, areaId: number, dredgerLocationId: number, reclamationLocationId: number, createdDate: Date) { + // Format date as YYYY-MM-DD + const dateString = createdDate.toISOString().split('T')[0]; + + try { + // Check if a sheet already exists for this combination + const existingSheet = await prisma.sheet.findUnique({ + where: { + areaId_dredgerLocationId_reclamationLocationId_date: { + areaId, + dredgerLocationId, + reclamationLocationId, + date: dateString + } + } + }); + + if (existingSheet) { + // Sheet exists, update it with the new shift + if (shift === 'day' && !existingSheet.dayShiftId) { + await prisma.sheet.update({ + where: { id: existingSheet.id }, + data: { + dayShiftId: reportId, + status: existingSheet.nightShiftId ? 'completed' : 'pending' + } + }); + } else if (shift === 'night' && !existingSheet.nightShiftId) { + await prisma.sheet.update({ + where: { id: existingSheet.id }, + data: { + nightShiftId: reportId, + status: existingSheet.dayShiftId ? 'completed' : 'pending' + } + }); + } + } else { + // No sheet exists, create a new one + const sheetData: any = { + areaId, + dredgerLocationId, + reclamationLocationId, + date: dateString, + status: 'pending' + }; + + if (shift === 'day') { + sheetData.dayShiftId = reportId; + } else if (shift === 'night') { + sheetData.nightShiftId = reportId; + } + + await prisma.sheet.create({ + data: sheetData + }); + } + } catch (error) { + console.error('Error managing sheet:', error); + throw error; + } +} + +export async function removeFromSheet(reportId: number, shift: string, areaId: number, dredgerLocationId: number, reclamationLocationId: number, createdDate: Date) { + // Format date as YYYY-MM-DD + const dateString = createdDate.toISOString().split('T')[0]; + + try { + // Find the sheet for this combination + const existingSheet = await prisma.sheet.findUnique({ + where: { + areaId_dredgerLocationId_reclamationLocationId_date: { + areaId, + dredgerLocationId, + reclamationLocationId, + date: dateString + } + } + }); + + if (existingSheet) { + if (shift === 'day' && existingSheet.dayShiftId === reportId) { + if (existingSheet.nightShiftId) { + // Keep sheet but remove day shift + await prisma.sheet.update({ + where: { id: existingSheet.id }, + data: { + dayShiftId: null, + status: 'pending' + } + }); + } else { + // Delete sheet if no other shift + await prisma.sheet.delete({ + where: { id: existingSheet.id } + }); + } + } else if (shift === 'night' && existingSheet.nightShiftId === reportId) { + if (existingSheet.dayShiftId) { + // Keep sheet but remove night shift + await prisma.sheet.update({ + where: { id: existingSheet.id }, + data: { + nightShiftId: null, + status: 'pending' + } + }); + } else { + // Delete sheet if no other shift + await prisma.sheet.delete({ + where: { id: existingSheet.id } + }); + } + } + } + } catch (error) { + console.error('Error removing from sheet:', error); + throw error; + } +} \ No newline at end of file diff --git a/prisma/dev.db b/prisma/dev.db index 30aabeb..4ff5ab4 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/prisma/dev.sqbpro b/prisma/dev.sqbpro new file mode 100644 index 0000000..75af1a5 --- /dev/null +++ b/prisma/dev.sqbpro @@ -0,0 +1 @@ +
    diff --git a/prisma/migrations/20250729120622_add_status_and_sheet/migration.sql b/prisma/migrations/20250729120622_add_status_and_sheet/migration.sql new file mode 100644 index 0000000..967c4ea --- /dev/null +++ b/prisma/migrations/20250729120622_add_status_and_sheet/migration.sql @@ -0,0 +1,41 @@ +-- CreateTable +CREATE TABLE "Sheet" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "dayShiftId" INTEGER, + "nightShiftId" INTEGER, + "status" TEXT NOT NULL DEFAULT 'pending', + "areaId" INTEGER NOT NULL, + "dredgerLocationId" INTEGER NOT NULL, + "reclamationLocationId" INTEGER NOT NULL, + "date" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Sheet_areaId_fkey" FOREIGN KEY ("areaId") REFERENCES "Area" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Sheet_dredgerLocationId_fkey" FOREIGN KEY ("dredgerLocationId") REFERENCES "DredgerLocation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Sheet_reclamationLocationId_fkey" FOREIGN KEY ("reclamationLocationId") REFERENCES "ReclamationLocation" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Sheet_dayShiftId_fkey" FOREIGN KEY ("dayShiftId") REFERENCES "Report" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Sheet_nightShiftId_fkey" FOREIGN KEY ("nightShiftId") REFERENCES "Report" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Employee" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "authLevel" INTEGER NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'active' +); +INSERT INTO "new_Employee" ("authLevel", "email", "id", "name", "password", "username") SELECT "authLevel", "email", "id", "name", "password", "username" FROM "Employee"; +DROP TABLE "Employee"; +ALTER TABLE "new_Employee" RENAME TO "Employee"; +CREATE UNIQUE INDEX "Employee_username_key" ON "Employee"("username"); +CREATE UNIQUE INDEX "Employee_email_key" ON "Employee"("email"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; + +-- CreateIndex +CREATE UNIQUE INDEX "Sheet_areaId_dredgerLocationId_reclamationLocationId_date_key" ON "Sheet"("areaId", "dredgerLocationId", "reclamationLocationId", "date"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 891c991..fdaf617 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -31,12 +31,17 @@ model Report { timeSheet Json // JSON: Array of timesheet objects stoppages Json // JSON: Array of stoppage records notes String? + + // Sheet relations + daySheetFor Sheet[] @relation("DayShift") + nightSheetFor Sheet[] @relation("NightShift") } model Area { id Int @id @default(autoincrement()) name String @unique reports Report[] + sheets Sheet[] } model DredgerLocation { @@ -44,33 +49,36 @@ model DredgerLocation { name String @unique class String // 's', 'd', 'sp' reports Report[] + sheets Sheet[] } model ReclamationLocation { id Int @id @default(autoincrement()) name String @unique reports Report[] + sheets Sheet[] } model Employee { - id Int @id @default(autoincrement()) - name String - authLevel Int - username String @unique - email String @unique - password String - reports Report[] + id Int @id @default(autoincrement()) + name String + authLevel Int + username String @unique + email String @unique + password String + status String @default("active") // 'active' or 'inactive' + reports Report[] passwordResetTokens PasswordResetToken[] } model PasswordResetToken { - id Int @id @default(autoincrement()) - token String @unique + id Int @id @default(autoincrement()) + token String @unique employeeId Int - employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) - expiresAt DateTime - used Boolean @default(false) - createdAt DateTime @default(now()) + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + expiresAt DateTime + used Boolean @default(false) + createdAt DateTime @default(now()) } model Foreman { @@ -85,14 +93,36 @@ model Equipment { number Int } +model Sheet { + id Int @id @default(autoincrement()) + dayShiftId Int? + nightShiftId Int? + status String @default("pending") // 'pending', 'completed' + areaId Int + dredgerLocationId Int + reclamationLocationId Int + date String // Store as string in YYYY-MM-DD format + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + area Area @relation(fields: [areaId], references: [id]) + dredgerLocation DredgerLocation @relation(fields: [dredgerLocationId], references: [id]) + reclamationLocation ReclamationLocation @relation(fields: [reclamationLocationId], references: [id]) + dayShift Report? @relation("DayShift", fields: [dayShiftId], references: [id]) + nightShift Report? @relation("NightShift", fields: [nightShiftId], references: [id]) + + @@unique([areaId, dredgerLocationId, reclamationLocationId, date]) +} + model MailSettings { - id Int @id @default(autoincrement()) - host String - port Int - secure Boolean @default(false) - username String - password String - fromName String + id Int @id @default(autoincrement()) + host String + port Int + secure Boolean @default(false) + username String + password String + fromName String fromEmail String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/prisma/seed.ts b/prisma/seed.ts index 2024205..a482859 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -86,7 +86,8 @@ async function main() { authLevel: 3, username: process.env.SUPER_ADMIN, email: process.env.SUPER_ADMIN_EMAIL, - password: bcrypt.hashSync(process.env.SUPER_ADMIN_PASSWORD, 10) + password: bcrypt.hashSync(process.env.SUPER_ADMIN_PASSWORD, 10), + status: 'active' } })