vvv
This commit is contained in:
parent
e8c83df854
commit
b43819aa7b
@ -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";
|
import type { Employee } from "@prisma/client";
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
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">
|
<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" />
|
<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>
|
</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
|
Reports
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
524
app/components/ReportSheetViewModal.tsx
Normal file
524
app/components/ReportSheetViewModal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -32,7 +32,81 @@ export default function ReportViewModal({ isOpen, onClose, report }: ReportViewM
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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"
|
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">
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* ISO Standard Report Layout */}
|
{/* 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 />
|
<ReportHeader />
|
||||||
<ReportInfo report={report} />
|
<ReportInfo report={report} />
|
||||||
<ReportDredgerSection 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%' }}>
|
<td className="border-r border-black p-2" style={{ width: '35%' }}>
|
||||||
{new Date(report.createdDate).toLocaleDateString('en-GB')}
|
{new Date(report.createdDate).toLocaleDateString('en-GB')}
|
||||||
</td>
|
</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>
|
<td className="p-2" style={{ width: '30%' }}>{report.id}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import FormModal from "~/components/FormModal";
|
import FormModal from "~/components/FormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Areas Management - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Areas Management - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,7 @@ import { json } from "@remix-run/node";
|
|||||||
import { useLoaderData } from "@remix-run/react";
|
import { useLoaderData } from "@remix-run/react";
|
||||||
import { requireAuthLevel } from "~/utils/auth.server";
|
import { requireAuthLevel } from "~/utils/auth.server";
|
||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { prisma } from "~/utils/db.server";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import FormModal from "~/components/FormModal";
|
import FormModal from "~/components/FormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,9 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import FormModal from "~/components/FormModal";
|
import FormModal from "~/components/FormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Employee Management - Phosphat Report" }];
|
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 email = formData.get("email");
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const authLevel = formData.get("authLevel");
|
const authLevel = formData.get("authLevel");
|
||||||
|
const status = formData.get("status");
|
||||||
|
|
||||||
if (intent === "create") {
|
if (intent === "create") {
|
||||||
if (typeof name !== "string" || name.length === 0) {
|
if (typeof name !== "string" || name.length === 0) {
|
||||||
@ -82,7 +81,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
authLevel: parseInt(authLevel)
|
authLevel: parseInt(authLevel),
|
||||||
|
status: 'active'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return json({ success: "Employee created successfully!" });
|
return json({ success: "Employee created successfully!" });
|
||||||
@ -141,6 +141,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
authLevel: parseInt(authLevel)
|
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
|
// Only update password if provided
|
||||||
if (typeof password === "string" && password.length >= 6) {
|
if (typeof password === "string" && password.length >= 6) {
|
||||||
updateData.password = bcrypt.hashSync(password, 10);
|
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 (intent === "delete") {
|
||||||
if (typeof id !== "string") {
|
if (typeof id !== "string") {
|
||||||
return json({ errors: { form: "Invalid employee ID" } }, { status: 400 });
|
return json({ errors: { form: "Invalid employee ID" } }, { status: 400 });
|
||||||
@ -195,7 +241,7 @@ export default function Employees() {
|
|||||||
const { user, employees } = useLoaderData<typeof loader>();
|
const { user, employees } = useLoaderData<typeof loader>();
|
||||||
const actionData = useActionData<typeof action>();
|
const actionData = useActionData<typeof action>();
|
||||||
const navigation = useNavigation();
|
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 [showModal, setShowModal] = useState(false);
|
||||||
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null);
|
||||||
|
|
||||||
@ -213,7 +259,7 @@ export default function Employees() {
|
|||||||
}
|
}
|
||||||
}, [actionData]);
|
}, [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);
|
setEditingEmployee(employee);
|
||||||
setShowModal(true);
|
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 (
|
return (
|
||||||
<DashboardLayout user={user}>
|
<DashboardLayout user={user}>
|
||||||
<div className="space-y-6">
|
<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">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Access Level
|
Access Level
|
||||||
</th>
|
</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">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Reports Count
|
Reports Count
|
||||||
</th>
|
</th>
|
||||||
@ -347,6 +414,12 @@ export default function Employees() {
|
|||||||
Level {employee.authLevel} - {getAuthLevelText(employee.authLevel)}
|
Level {employee.authLevel} - {getAuthLevelText(employee.authLevel)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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">
|
<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">
|
<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
|
{employee._count.reports} reports
|
||||||
@ -361,6 +434,21 @@ export default function Employees() {
|
|||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
{employee.id !== user.id && (
|
{employee.id !== user.id && (
|
||||||
|
<>
|
||||||
|
<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">
|
<Form method="post" className="inline">
|
||||||
<input type="hidden" name="intent" value="delete" />
|
<input type="hidden" name="intent" value="delete" />
|
||||||
<input type="hidden" name="id" value={employee.id} />
|
<input type="hidden" name="id" value={employee.id} />
|
||||||
@ -376,6 +464,7 @@ export default function Employees() {
|
|||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -516,6 +605,23 @@ export default function Employees() {
|
|||||||
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p>
|
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
|
|||||||
@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import FormModal from "~/components/FormModal";
|
import FormModal from "~/components/FormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import FormModal from "~/components/FormModal";
|
import FormModal from "~/components/FormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||||
import { Form, useActionData, useLoaderData } from "@remix-run/react";
|
import { Form, useActionData, useLoaderData } from "@remix-run/react";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { requireAuthLevel } from "~/utils/auth.server";
|
import { requireAuthLevel } from "~/utils/auth.server";
|
||||||
import { testEmailConnection } from "~/utils/mail.server";
|
import { testEmailConnection } from "~/utils/mail.server";
|
||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
export async function loader({ request }: LoaderFunctionArgs) {
|
||||||
// Require auth level 3 to access mail settings
|
// Require auth level 3 to access mail settings
|
||||||
|
|||||||
@ -5,10 +5,8 @@ import { requireAuthLevel } from "~/utils/auth.server";
|
|||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import FormModal from "~/components/FormModal";
|
import FormModal from "~/components/FormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
240
app/routes/report-sheet.tsx
Normal file
240
app/routes/report-sheet.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,10 +6,9 @@ import DashboardLayout from "~/components/DashboardLayout";
|
|||||||
import ReportViewModal from "~/components/ReportViewModal";
|
import ReportViewModal from "~/components/ReportViewModal";
|
||||||
import ReportFormModal from "~/components/ReportFormModal";
|
import ReportFormModal from "~/components/ReportFormModal";
|
||||||
import Toast from "~/components/Toast";
|
import Toast from "~/components/Toast";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { manageSheet, removeFromSheet } from "~/utils/sheet.server";
|
||||||
const prisma = new PrismaClient();
|
import { prisma } from "~/utils/db.server";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }];
|
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
|
// Check if user owns this report or has admin privileges
|
||||||
const existingReport = await prisma.report.findUnique({
|
const existingReport = await prisma.report.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { id: parseInt(id) },
|
||||||
select: { employeeId: true, createdDate: true }
|
select: {
|
||||||
|
employeeId: true,
|
||||||
|
createdDate: true,
|
||||||
|
shift: true,
|
||||||
|
areaId: true,
|
||||||
|
dredgerLocationId: true,
|
||||||
|
reclamationLocationId: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingReport) {
|
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) },
|
where: { id: parseInt(id) },
|
||||||
data: {
|
data: {
|
||||||
shift,
|
shift,
|
||||||
@ -204,6 +224,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
notes: notes || null
|
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!" });
|
return json({ success: "Report updated successfully!" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return json({ errors: { form: "Failed to update report" } }, { status: 400 });
|
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
|
// Check if user owns this report or has admin privileges
|
||||||
const existingReport = await prisma.report.findUnique({
|
const existingReport = await prisma.report.findUnique({
|
||||||
where: { id: parseInt(id) },
|
where: { id: parseInt(id) },
|
||||||
select: { employeeId: true, createdDate: true }
|
select: {
|
||||||
|
employeeId: true,
|
||||||
|
createdDate: true,
|
||||||
|
shift: true,
|
||||||
|
areaId: true,
|
||||||
|
dredgerLocationId: true,
|
||||||
|
reclamationLocationId: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingReport) {
|
if (!existingReport) {
|
||||||
@ -244,6 +281,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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({
|
await prisma.report.delete({
|
||||||
where: { id: parseInt(id) }
|
where: { id: parseInt(id) }
|
||||||
});
|
});
|
||||||
@ -495,8 +542,8 @@ export default function Reports() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Reports Management</h1>
|
<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 reports</p>
|
<p className="mt-1 text-sm text-gray-600">Create and manage operational shifts</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
to="/reports/new"
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
Create New Report
|
Create New Shift
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -516,7 +563,7 @@ export default function Reports() {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Report Details
|
Shift Details
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Shift & Area
|
Shift & Area
|
||||||
@ -545,7 +592,7 @@ export default function Reports() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<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 className="text-sm text-gray-500">by {report.employee.name}</div>
|
||||||
</div>
|
</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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
Create Report
|
Create Shifs
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,10 +3,9 @@ import { json, redirect } from "@remix-run/node";
|
|||||||
import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react";
|
import { Form, useActionData, useLoaderData, useNavigation, Link } from "@remix-run/react";
|
||||||
import { requireAuthLevel } from "~/utils/auth.server";
|
import { requireAuthLevel } from "~/utils/auth.server";
|
||||||
import DashboardLayout from "~/components/DashboardLayout";
|
import DashboardLayout from "~/components/DashboardLayout";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { manageSheet } from "~/utils/sheet.server";
|
||||||
const prisma = new PrismaClient();
|
import { prisma } from "~/utils/db.server";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "New Report - Phosphat Report" }];
|
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: {
|
data: {
|
||||||
employeeId: user.id,
|
employeeId: user.id,
|
||||||
shift,
|
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
|
// Redirect to reports page with success message
|
||||||
return redirect("/reports?success=Report created successfully!");
|
return redirect("/reports?success=Report created successfully!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -378,8 +387,8 @@ export default function NewReport() {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Create New Report</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Create New Shifts</h1>
|
||||||
<p className="mt-2 text-gray-600">Fill out the operational report details step by step</p>
|
<p className="mt-2 text-gray-600">Fill out the operational shift details step by step</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
to="/reports"
|
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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
</svg>
|
</svg>
|
||||||
Back to Reports
|
Back to Shifts
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,9 +2,7 @@ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remi
|
|||||||
import { json, redirect } from "@remix-run/node";
|
import { json, redirect } from "@remix-run/node";
|
||||||
import { Form, Link, useActionData } from "@remix-run/react";
|
import { Form, Link, useActionData } from "@remix-run/react";
|
||||||
import { createUser, createUserSession, getUserId } from "~/utils/auth.server";
|
import { createUser, createUserSession, getUserId } from "~/utils/auth.server";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { prisma } from "~/utils/db.server";
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export const meta: MetaFunction = () => [{ title: "Sign Up - Phosphat Report" }];
|
export const meta: MetaFunction = () => [{ title: "Sign Up - Phosphat Report" }];
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { createCookieSessionStorage, redirect } from "@remix-run/node";
|
import { createCookieSessionStorage, redirect } from "@remix-run/node";
|
||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
import { prisma } from "~/utils/db.server";
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// Session storage
|
// Session storage
|
||||||
const sessionSecret = process.env.SESSION_SECRET || "default-secret";
|
const sessionSecret = process.env.SESSION_SECRET || "default-secret";
|
||||||
@ -58,8 +56,14 @@ export async function getUser(request: Request) {
|
|||||||
try {
|
try {
|
||||||
const user = await prisma.employee.findUnique({
|
const user = await prisma.employee.findUnique({
|
||||||
where: { id: userId },
|
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;
|
return user;
|
||||||
} catch {
|
} catch {
|
||||||
throw logout(request);
|
throw logout(request);
|
||||||
@ -109,7 +113,12 @@ export async function verifyLogin(usernameOrEmail: string, password: string) {
|
|||||||
return null;
|
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) {
|
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,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
authLevel,
|
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
121
app/utils/sheet.server.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
1
prisma/dev.sqbpro
Normal file
1
prisma/dev.sqbpro
Normal 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>
|
||||||
@ -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");
|
||||||
@ -31,12 +31,17 @@ model Report {
|
|||||||
timeSheet Json // JSON: Array of timesheet objects
|
timeSheet Json // JSON: Array of timesheet objects
|
||||||
stoppages Json // JSON: Array of stoppage records
|
stoppages Json // JSON: Array of stoppage records
|
||||||
notes String?
|
notes String?
|
||||||
|
|
||||||
|
// Sheet relations
|
||||||
|
daySheetFor Sheet[] @relation("DayShift")
|
||||||
|
nightSheetFor Sheet[] @relation("NightShift")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Area {
|
model Area {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
reports Report[]
|
reports Report[]
|
||||||
|
sheets Sheet[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model DredgerLocation {
|
model DredgerLocation {
|
||||||
@ -44,12 +49,14 @@ model DredgerLocation {
|
|||||||
name String @unique
|
name String @unique
|
||||||
class String // 's', 'd', 'sp'
|
class String // 's', 'd', 'sp'
|
||||||
reports Report[]
|
reports Report[]
|
||||||
|
sheets Sheet[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ReclamationLocation {
|
model ReclamationLocation {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
reports Report[]
|
reports Report[]
|
||||||
|
sheets Sheet[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Employee {
|
model Employee {
|
||||||
@ -59,6 +66,7 @@ model Employee {
|
|||||||
username String @unique
|
username String @unique
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
|
status String @default("active") // 'active' or 'inactive'
|
||||||
reports Report[]
|
reports Report[]
|
||||||
passwordResetTokens PasswordResetToken[]
|
passwordResetTokens PasswordResetToken[]
|
||||||
}
|
}
|
||||||
@ -85,6 +93,28 @@ model Equipment {
|
|||||||
number Int
|
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 {
|
model MailSettings {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
host String
|
host String
|
||||||
|
|||||||
@ -86,7 +86,8 @@ async function main() {
|
|||||||
authLevel: 3,
|
authLevel: 3,
|
||||||
username: process.env.SUPER_ADMIN,
|
username: process.env.SUPER_ADMIN,
|
||||||
email: process.env.SUPER_ADMIN_EMAIL,
|
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'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user