This commit is contained in:
yznahmad 2025-07-29 15:22:36 +03:00
parent e8c83df854
commit b43819aa7b
22 changed files with 1295 additions and 95 deletions

View File

@ -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
<svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Shifts
</Link>
</li>
<li>
<Link
to="/report-sheet"
className="group flex items-center px-2 py-2 text-base font-medium rounded-md text-gray-900 hover:bg-gray-50"
>
<svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
Reports
</Link>
</li>

View File

@ -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 (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onClick={onClose}></div>
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-7xl sm:w-full">
<div className="bg-white px-6 pt-6 pb-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-gray-900">Report Sheet - {new Date(sheet.date).toLocaleDateString('en-GB')}</h3>
<div className="flex space-x-2">
<button
type="button"
onClick={handleExportExcel}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Export Excel
</button>
<button
type="button"
onClick={() => {
const printContent = document.getElementById('reportframe');
if (printContent) {
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>Report Sheet Print</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
table { border-collapse: collapse; }
.border-2 { border: 2px solid black; }
.border { border: 1px solid black; }
.border-t { border-top: 1px solid black; }
.border-r { border-right: 1px solid black; }
.border-black { border-color: black; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
.text-center { text-align: center; }
.font-bold { font-weight: bold; }
.font-semibold { font-weight: 600; }
.p-2 { padding: 8px; }
.p-4 { padding: 16px; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mt-1 { margin-top: 4px; }
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.w-full { width: 100%; }
.h-16 { height: 64px; }
.mx-auto { margin-left: auto; margin-right: auto; }
.underline { text-decoration: underline; }
.text-sm { font-size: 14px; }
.text-lg { font-size: 18px; }
.min-h-[100px] { min-height: 100px; }
@media print {
body {
margin: 0;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.no-print { display: none; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
}
</style>
</head>
<body>
${printContent.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
printWindow.close();
}
}
}}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Print
</button>
<button
type="button"
onClick={onClose}
className="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Combined Report Sheet Layout */}
<div id="reportframe" className="max-h-[80vh] overflow-y-auto bg-white p-6 border border-gray-300" style={{ fontFamily: 'Arial, sans-serif' }}>
<ReportSheetHeader sheet={sheet} />
<ReportSheetInfo sheet={sheet} />
<ReportSheetDredgerSection sheet={sheet} />
<ReportSheetLocationData sheet={sheet} />
<ReportSheetPipelineLength sheet={sheet} />
{/* Day Shift Section */}
{sheet.dayReport && (
<>
<ReportSheetShiftHeader shift="day" />
<ReportSheetEquipmentStats report={sheet.dayReport} />
<ReportSheetTimeSheet report={sheet.dayReport} />
<ReportSheetStoppages report={sheet.dayReport} />
<ReportSheetNotes report={sheet.dayReport} />
</>
)}
{/* Night Shift Section */}
{sheet.nightReport && (
<>
<ReportSheetShiftHeader shift="night" />
<ReportSheetEquipmentStats report={sheet.nightReport} />
<ReportSheetTimeSheet report={sheet.nightReport} />
<ReportSheetStoppages report={sheet.nightReport} />
<ReportSheetNotes report={sheet.nightReport} />
</>
)}
<ReportSheetFooter />
</div>
</div>
</div>
</div>
</div>
);
}
// Header Section Component
function ReportSheetHeader({ sheet }: { sheet: ReportSheet }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse">
<tbody>
<tr>
<td className="border-r-2 border-black p-2 text-center font-bold text-lg" style={{ width: '70%' }}>
<div>Reclamation Work Diary - Daily Sheet</div>
<div className="border-t border-black mt-1 pt-1">QF-3.6.1-08</div>
<div className="border-t border-black mt-1 pt-1">Rev. 1.0</div>
</td>
<td className="p-2 text-center" style={{ width: '30%' }}>
<img
src="/logo-light.png"
alt="Arab Potash Logo"
className="h-16 mx-auto"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
</td>
</tr>
</tbody>
</table>
</div>
);
}
// Report Info Component
function ReportSheetInfo({ sheet }: { sheet: ReportSheet }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse">
<tbody>
<tr>
<td className="border-r border-black p-2 font-semibold" style={{ width: '15%' }}>Date:</td>
<td className="border-r border-black p-2" style={{ width: '35%' }}>
{new Date(sheet.date).toLocaleDateString('en-GB')}
</td>
<td className="border-r border-black p-2 font-semibold" style={{ width: '20%' }}> Report No. </td>
<td className="p-2" style={{ width: '30%' }}>{sheet.id}</td>
</tr>
</tbody>
</table>
</div>
);
}
// Dredger Section Component
function ReportSheetDredgerSection({ sheet }: { sheet: ReportSheet }) {
return (
<div className="text-center font-bold text-lg mb-2 underline">
{sheet.area} Dredger
</div>
);
}
// 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 (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black" style={{ width: '25%' }}>
Dredger Location
</td>
<td className="p-2 border-r border-black text-center" style={{ width: '25%' }}>
{report.dredgerLocation.name}
</td>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black" style={{ width: '25%' }}>
Dredger Line Length
</td>
<td className="p-2 text-center" style={{ width: '25%' }}>
{report.dredgerLineLength}
</td>
</tr>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black border-t border-black">
Reclamation Location
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.reclamationLocation.name}
</td>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black border-t border-black">
Shore Connection
</td>
<td className="p-2 border-t border-black text-center">
{report.shoreConnection}
</td>
</tr>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black border-t border-black">
Reclamation Height
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.reclamationHeight?.base || 0}m - {(report.reclamationHeight?.extra + report.reclamationHeight?.base || 0 || 0)}m
</td>
<td className="p-2 border-r border-black border-t border-black" colSpan={2}></td>
</tr>
</tbody>
</table>
</div>
);
}
// 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 (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<td className="bg-green-500 text-white p-2 font-semibold border-r border-black" rowSpan={2}>
Pipeline Length "from Shore Connection"
</td>
<td className="p-2 border-r border-black text-center font-semibold">Main</td>
<td className="p-2 border-r border-black text-center font-semibold">extension</td>
<td className="p-2 border-r border-black text-center font-semibold">total</td>
<td className="p-2 border-r border-black text-center font-semibold">Reserve</td>
<td className="p-2 border-r border-black text-center font-semibold">extension</td>
<td className="p-2 text-center font-semibold">total</td>
</tr>
<tr>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.main || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.ext1 || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{(report.pipelineLength?.main || 0) + (report.pipelineLength?.ext1 || 0)}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.reserve || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.pipelineLength?.ext2 || 0}
</td>
<td className="p-2 border-t border-black text-center">
{(report.pipelineLength?.reserve || 0) + (report.pipelineLength?.ext2 || 0)}
</td>
</tr>
</tbody>
</table>
</div>
);
}
// Shift Header Component
function ReportSheetShiftHeader({ shift }: { shift: string }) {
return (
<div className="bg-green-500 text-white p-2 text-center font-bold mb-2 mt-6">
{shift.charAt(0).toUpperCase() + shift.slice(1)} Shift
</div>
);
}
// Equipment Statistics Component
function ReportSheetEquipmentStats({ report }: { report: any }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<tbody>
<tr>
<td className="p-2 border-r border-black text-center font-semibold">Dozers</td>
<td className="p-2 border-r border-black text-center font-semibold">Exc.</td>
<td className="p-2 border-r border-black text-center font-semibold">Loader</td>
<td className="p-2 border-r border-black text-center font-semibold">Foreman</td>
<td className="p-2 text-center font-semibold">Laborer</td>
</tr>
<tr>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Dozers || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Exc || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Loaders || 0}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{report.stats?.Foreman || ''}
</td>
<td className="p-2 border-t border-black text-center">
{report.stats?.Laborer || 0}
</td>
</tr>
</tbody>
</table>
</div>
);
}
// Time Sheet Component
function ReportSheetTimeSheet({ report }: { report: any }) {
return (
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<thead>
<tr>
<td className="p-2 border-r border-black text-center font-semibold" rowSpan={2}>Time Sheet</td>
<td className="p-2 border-r border-black text-center font-semibold">From</td>
<td className="p-2 border-r border-black text-center font-semibold">To</td>
<td className="p-2 border-r border-black text-center font-semibold">From</td>
<td className="p-2 border-r border-black text-center font-semibold">To</td>
<td className="p-2 border-r border-black text-center font-semibold">Total</td>
<td className="p-2 text-center font-semibold">Reason</td>
</tr>
</thead>
<tbody>
{Array.isArray(report.timeSheet) && report.timeSheet.length > 0 ? (
report.timeSheet.map((entry: any, index: number) => (
<tr key={index}>
<td className="p-2 border-r border-black border-t border-black font-semibold">
{entry.machine}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.from1}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.to1}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.from2}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.to2}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.total}
</td>
<td className="p-2 border-t border-black">
{entry.reason}
</td>
</tr>
))
) : (
<tr>
<td className="p-2 border-r border-black border-t border-black text-center" colSpan={7}>
No time sheet entries
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
// Stoppages Component
function ReportSheetStoppages({ report }: { report: any }) {
return (
<>
<div className="bg-green-500 text-white p-2 text-center font-bold mb-2">
Dredger Stoppages
</div>
<div className="border-2 border-black mb-4">
<table className="w-full border-collapse text-sm">
<thead>
<tr>
<td className="p-2 border-r border-black text-center font-semibold">From</td>
<td className="p-2 border-r border-black text-center font-semibold">To</td>
<td className="p-2 border-r border-black text-center font-semibold">Total</td>
<td className="p-2 border-r border-black text-center font-semibold">Reason</td>
<td className="p-2 border-r border-black text-center font-semibold">Responsible</td>
<td className="p-2 text-center font-semibold">Notes</td>
</tr>
</thead>
<tbody>
{Array.isArray(report.stoppages) && report.stoppages.length > 0 ? (
report.stoppages.map((entry: any, index: number) => (
<tr key={index}>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.from}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.to}
</td>
<td className="p-2 border-r border-black border-t border-black text-center">
{entry.total}
</td>
<td className="p-2 border-r border-black border-t border-black">
{entry.reason}
</td>
<td className="p-2 border-r border-black border-t border-black">
{entry.responsible}
</td>
<td className="p-2 border-t border-black">
{entry.note}
</td>
</tr>
))
) : (
<tr>
<td className="p-2 border-r border-black border-t border-black text-center" colSpan={6}>
No stoppages recorded
</td>
</tr>
)}
</tbody>
</table>
</div>
</>
);
}
// Notes Component
function ReportSheetNotes({ report }: { report: any }) {
return (
<>
<div className="bg-green-500 text-white p-2 text-center font-bold mb-2">
Notes & Comments
</div>
<div className="border-2 border-black mb-4 min-h-[100px]">
<div className="p-4 text-center">
{report.notes || 'No additional notes'}
</div>
</div>
</>
);
}
// Footer Component
function ReportSheetFooter() {
return (
<div className="text-center text-sm mt-4 border-t border-black pt-2">
{/* موقعة لأعمال الصيانة */}
</div>
);
}

View File

@ -32,7 +32,81 @@ export default function ReportViewModal({ isOpen, onClose, report }: ReportViewM
</button>
<button
type="button"
onClick={() => window.print()}
onClick={() => {
const printContent = document.getElementById('reportframe');
if (printContent) {
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(`
<html>
<head>
<title>Report Print</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
table { border-collapse: collapse; }
.border-2 { border: 2px solid black; }
.border { border: 1px solid black; }
.border-t { border-top: 1px solid black; }
.border-r { border-right: 1px solid black; }
.border-black { border-color: black; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
.text-center { text-align: center; }
.font-bold { font-weight: bold; }
.font-semibold { font-weight: 600; }
.p-2 { padding: 8px; }
.p-4 { padding: 16px; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mt-1 { margin-top: 4px; }
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
.pt-1 { padding-top: 4px; }
.pt-2 { padding-top: 8px; }
.w-full { width: 100%; }
.h-16 { height: 64px; }
.mx-auto { margin-left: auto; margin-right: auto; }
.underline { text-decoration: underline; }
.text-sm { font-size: 14px; }
.text-lg { font-size: 18px; }
.min-h-[100px] { min-height: 100px; }
@media print {
body {
margin: 0;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.no-print { display: none; }
.bg-green-500 {
background-color: #10b981 !important;
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.text-white { color: white !important; }
}
</style>
</head>
<body>
${printContent.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.print();
printWindow.close();
}
}
}}
className="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -53,7 +127,7 @@ export default function ReportViewModal({ isOpen, onClose, report }: ReportViewM
</div>
{/* ISO Standard Report Layout */}
<div className="max-h-[80vh] overflow-y-auto bg-white p-6 border border-gray-300" style={{ fontFamily: 'Arial, sans-serif' }}>
<div id="reportframe" className="max-h-[80vh] overflow-y-auto bg-white p-6 border border-gray-300" style={{ fontFamily: 'Arial, sans-serif' }}>
<ReportHeader />
<ReportInfo report={report} />
<ReportDredgerSection report={report} />
@ -113,7 +187,7 @@ function ReportInfo({ report }: { report: any }) {
<td className="border-r border-black p-2" style={{ width: '35%' }}>
{new Date(report.createdDate).toLocaleDateString('en-GB')}
</td>
<td className="border-r border-black p-2 font-semibold" style={{ width: '20%' }}>Report No.</td>
<td className="border-r border-black p-2 font-semibold" style={{ width: '20%' }}>Shift No.</td>
<td className="p-2" style={{ width: '30%' }}>{report.id}</td>
</tr>
</tbody>

View File

@ -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: "Areas Management - Phosphat Report" }];

View File

@ -3,9 +3,7 @@ import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }];

View File

@ -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: "Dredger Locations Management - Phosphat Report" }];

View File

@ -5,11 +5,9 @@ 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";
import bcrypt from "bcryptjs";
const prisma = new PrismaClient();
import { prisma } from "~/utils/db.server";
export const meta: MetaFunction = () => [{ title: "Employee Management - Phosphat Report" }];
@ -46,6 +44,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const email = formData.get("email");
const password = formData.get("password");
const authLevel = formData.get("authLevel");
const status = formData.get("status");
if (intent === "create") {
if (typeof name !== "string" || name.length === 0) {
@ -82,7 +81,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
username,
email,
password: hashedPassword,
authLevel: parseInt(authLevel)
authLevel: parseInt(authLevel),
status: 'active'
}
});
return json({ success: "Employee created successfully!" });
@ -141,6 +141,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
authLevel: parseInt(authLevel)
};
// Add status if provided (but prevent users from changing their own status)
if (typeof status === "string" && ["active", "inactive"].includes(status)) {
if (parseInt(id) !== user.id) {
updateData.status = status;
}
}
// Only update password if provided
if (typeof password === "string" && password.length >= 6) {
updateData.password = bcrypt.hashSync(password, 10);
@ -158,6 +165,45 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}
}
if (intent === "toggleStatus") {
if (typeof id !== "string") {
return json({ errors: { form: "Invalid employee ID" } }, { status: 400 });
}
const employeeId = parseInt(id);
// Prevent users from changing their own status
if (employeeId === user.id) {
return json({ errors: { form: "You cannot change your own status" } }, { status: 403 });
}
// Check if the employee exists and if current user can edit them
const existingEmployee = await prisma.employee.findUnique({
where: { id: employeeId },
select: { authLevel: true, status: true }
});
if (!existingEmployee) {
return json({ errors: { form: "Employee not found" } }, { status: 404 });
}
// Level 2 users cannot edit Level 3 employees
if (user.authLevel === 2 && existingEmployee.authLevel === 3) {
return json({ errors: { form: "You don't have permission to edit Super Admin users" } }, { status: 403 });
}
try {
const newStatus = existingEmployee.status === 'active' ? 'inactive' : 'active';
await prisma.employee.update({
where: { id: employeeId },
data: { status: newStatus }
});
return json({ success: `Employee status changed to ${newStatus}!` });
} catch (error) {
return json({ errors: { form: "Failed to update employee status" } }, { status: 400 });
}
}
if (intent === "delete") {
if (typeof id !== "string") {
return json({ errors: { form: "Invalid employee ID" } }, { status: 400 });
@ -195,7 +241,7 @@ export default function Employees() {
const { user, employees } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const [editingEmployee, setEditingEmployee] = useState<{ id: number; name: string; username: string; email: string; authLevel: number } | null>(null);
const [editingEmployee, setEditingEmployee] = useState<{ id: number; name: string; username: string; email: string; authLevel: number; status: string } | null>(null);
const [showModal, setShowModal] = useState(false);
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
@ -213,7 +259,7 @@ export default function Employees() {
}
}, [actionData]);
const handleEdit = (employee: { id: number; name: string; username: string; email: string; authLevel: number }) => {
const handleEdit = (employee: { id: number; name: string; username: string; email: string; authLevel: number; status: string }) => {
setEditingEmployee(employee);
setShowModal(true);
};
@ -275,6 +321,24 @@ export default function Employees() {
}
};
const getStatusBadge = (status: string) => {
return status === 'active'
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800";
};
const getStatusIcon = (status: string) => {
return status === 'active' ? (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
};
return (
<DashboardLayout user={user}>
<div className="space-y-6">
@ -312,6 +376,9 @@ export default function Employees() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Access Level
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Reports Count
</th>
@ -347,6 +414,12 @@ export default function Employees() {
Level {employee.authLevel} - {getAuthLevelText(employee.authLevel)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusBadge(employee.status)}`}>
{getStatusIcon(employee.status)}
<span className="ml-1 capitalize">{employee.status}</span>
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{employee._count.reports} reports
@ -361,21 +434,37 @@ export default function Employees() {
Edit
</button>
{employee.id !== user.id && (
<Form method="post" className="inline">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={employee.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this employee?")) {
e.preventDefault();
}
}}
className="text-red-600 hover:text-red-900 transition-colors duration-150"
>
Delete
</button>
</Form>
<>
<Form method="post" className="inline">
<input type="hidden" name="intent" value="toggleStatus" />
<input type="hidden" name="id" value={employee.id} />
<button
type="submit"
className={`transition-colors duration-150 ${
employee.status === 'active'
? 'text-orange-600 hover:text-orange-900'
: 'text-green-600 hover:text-green-900'
}`}
>
{employee.status === 'active' ? 'Deactivate' : 'Activate'}
</button>
</Form>
<Form method="post" className="inline">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={employee.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this employee?")) {
e.preventDefault();
}
}}
className="text-red-600 hover:text-red-900 transition-colors duration-150"
>
Delete
</button>
</Form>
</>
)}
</div>
</td>
@ -516,6 +605,23 @@ export default function Employees() {
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p>
)}
</div>
{isEditing && editingEmployee?.id !== user.id && (
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
name="status"
id="status"
defaultValue={editingEmployee?.status || "active"}
className="block w-full border-gray-300 h-9 p-2 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
)}
</div>
{isEditing && (

View File

@ -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" }];

View File

@ -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" }];

View File

@ -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

View File

@ -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" }];

240
app/routes/report-sheet.tsx Normal file
View File

@ -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<typeof loader>();
const [viewingSheet, setViewingSheet] = useState<ReportSheet | null>(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" ? (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
);
};
return (
<DashboardLayout user={user}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">Report Sheets</h1>
<p className="mt-1 text-sm text-gray-600">View grouped reports by location and date</p>
</div>
</div>
{/* Report Sheets Table */}
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Area
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Locations
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Available Shifts
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Employees
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sheets.map((sheet) => (
<tr key={sheet.id} className="hover:bg-gray-50 transition-colors duration-150">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{new Date(sheet.date).toLocaleDateString('en-GB')}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{sheet.area}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
<div>Dredger: {sheet.dredgerLocation}</div>
<div className="text-gray-500">Reclamation: {sheet.reclamationLocation}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex space-x-2">
{sheet.dayReport && (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge('day')}`}>
{getShiftIcon('day')}
<span className="ml-1">Day</span>
</span>
)}
{sheet.nightReport && (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge('night')}`}>
{getShiftIcon('night')}
<span className="ml-1">Night</span>
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${sheet.status === 'completed'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{sheet.status === 'completed' ? (
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : (
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
{sheet.status.charAt(0).toUpperCase() + sheet.status.slice(1)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{sheet.dayReport && (
<div>Day: {sheet.dayReport.employee.name}</div>
)}
{sheet.nightReport && (
<div>Night: {sheet.nightReport.employee.name}</div>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
onClick={() => handleView(sheet)}
className="text-blue-600 hover:text-blue-900 transition-colors duration-150"
>
View Sheet
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{sheets.length === 0 && (
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">No report sheets</h3>
<p className="mt-1 text-sm text-gray-500">Report sheets will appear here when reports are created.</p>
</div>
)}
</div>
{/* View Modal */}
<ReportSheetViewModal
isOpen={showViewModal}
onClose={handleCloseViewModal}
sheet={viewingSheet}
/>
</div>
</DashboardLayout>
);
}

View File

@ -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() {
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">Reports Management</h1>
<p className="mt-1 text-sm text-gray-600">Create and manage operational reports</p>
<h1 className="text-2xl font-bold text-gray-900">Shifts Management</h1>
<p className="mt-1 text-sm text-gray-600">Create and manage operational shifts</p>
</div>
<Link
to="/reports/new"
@ -505,7 +552,7 @@ export default function Reports() {
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Create New Report
Create New Shift
</Link>
</div>
@ -516,7 +563,7 @@ export default function Reports() {
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Report Details
Shift Details
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Shift & Area
@ -545,7 +592,7 @@ export default function Reports() {
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">Report #{report.id}</div>
<div className="text-sm font-medium text-gray-900">Shift #{report.id}</div>
<div className="text-sm text-gray-500">by {report.employee.name}</div>
</div>
</div>
@ -623,7 +670,7 @@ export default function Reports() {
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Create Report
Create Shifs
</Link>
</div>
</div>

View File

@ -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,
@ -139,6 +138,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}
});
// 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!");
} catch (error) {
@ -378,8 +387,8 @@ export default function NewReport() {
<div className="mb-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Create New Report</h1>
<p className="mt-2 text-gray-600">Fill out the operational report details step by step</p>
<h1 className="text-3xl font-bold text-gray-900">Create New Shifts</h1>
<p className="mt-2 text-gray-600">Fill out the operational shift details step by step</p>
</div>
<Link
to="/reports"
@ -388,7 +397,7 @@ export default function NewReport() {
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Reports
Back to Shifts
</Link>
</div>
</div>

View File

@ -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" }];

View File

@ -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 };
}

121
app/utils/sheet.server.ts Normal file
View File

@ -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;
}
}

Binary file not shown.

1
prisma/dev.sqbpro Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><sqlb_project><db path="dev.db" readonly="0" foreign_keys="1" case_sensitive_like="0" temp_store="0" wal_autocheckpoint="1000" synchronous="2"/><attached/><window><main_tabs open="structure browser pragmas query" current="1"/></window><tab_structure><column_width id="0" width="300"/><column_width id="1" width="0"/><column_width id="2" width="100"/><column_width id="3" width="7352"/><column_width id="4" width="0"/><expanded_item id="0" parent="1"/><expanded_item id="1" parent="1"/><expanded_item id="2" parent="1"/><expanded_item id="3" parent="1"/></tab_structure><tab_browse><table title="Report" custom_title="0" dock_id="3" table="4,6:mainReport"/><table title="Report" custom_title="0" dock_id="2" table="4,6:mainReport"/><table title="Area" custom_title="0" dock_id="1" table="4,4:mainArea"/><table title="Area" custom_title="0" dock_id="4" table="4,4:mainArea"/><dock_state state="000000ff00000000fd00000001000000020000043b000002aefc0100000001fc000000000000043b0000011800fffffffa000000030100000004fb000000160064006f0063006b00420072006f00770073006500310100000000ffffffff0000011800fffffffb000000160064006f0063006b00420072006f00770073006500320100000000ffffffff0000011800fffffffb000000160064006f0063006b00420072006f00770073006500330100000000ffffffff0000011800fffffffb000000160064006f0063006b00420072006f00770073006500340100000000ffffffff0000011800ffffff000002580000000000000004000000040000000800000008fc00000000"/><default_encoding codec=""/><browse_table_settings><table schema="main" name="Area" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="29"/><column index="2" value="54"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table><table schema="main" name="Report" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_" freeze_columns="0"><sort/><column_widths><column index="1" value="29"/><column index="2" value="76"/><column index="3" value="109"/><column index="4" value="109"/><column index="5" value="46"/><column index="6" value="47"/><column index="7" value="113"/><column index="8" value="113"/><column index="9" value="136"/><column index="10" value="101"/><column index="11" value="171"/><column index="12" value="300"/><column index="13" value="300"/><column index="14" value="300"/><column index="15" value="300"/><column index="16" value="93"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table></browse_table_settings></tab_browse><tab_sql><sql name="SQL 1"></sql><current_tab id="0"/></tab_sql></sqlb_project>

View File

@ -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");

View File

@ -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

View File

@ -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'
}
})