From b43819aa7b866a60b4e0a8deb3b7ba135c3bdd01 Mon Sep 17 00:00:00 2001 From: yznahmad Date: Tue, 29 Jul 2025 15:22:36 +0300 Subject: [PATCH] vvv --- app/components/DashboardLayout.tsx | 14 +- app/components/ReportSheetViewModal.tsx | 524 ++++++++++++++++++ app/components/ReportViewModal.tsx | 80 ++- app/routes/areas.tsx | 4 +- app/routes/dashboard.tsx | 4 +- app/routes/dredger-locations.tsx | 4 +- app/routes/employees.tsx | 148 ++++- app/routes/equipment.tsx | 4 +- app/routes/foreman.tsx | 4 +- app/routes/mail-settings.tsx | 4 +- app/routes/reclamation-locations.tsx | 4 +- app/routes/report-sheet.tsx | 240 ++++++++ app/routes/reports.tsx | 71 ++- app/routes/reports_.new.tsx | 23 +- app/routes/signup.tsx | 4 +- app/utils/auth.server.ts | 22 +- app/utils/sheet.server.ts | 121 ++++ prisma/dev.db | Bin 86016 -> 94208 bytes prisma/dev.sqbpro | 1 + .../migration.sql | 41 ++ prisma/schema.prisma | 70 ++- prisma/seed.ts | 3 +- 22 files changed, 1295 insertions(+), 95 deletions(-) create mode 100644 app/components/ReportSheetViewModal.tsx create mode 100644 app/routes/report-sheet.tsx create mode 100644 app/utils/sheet.server.ts create mode 100644 prisma/dev.sqbpro create mode 100644 prisma/migrations/20250729120622_add_status_and_sheet/migration.sql 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 30aabeb9023f0741d7600d3dc1025fdc71bb9d84..4ff5ab45d695f89acd1dbf5d1747cd8f64fcecd2 100644 GIT binary patch delta 3386 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 A^Z)<= 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' } })