diff --git a/CARRY_FORWARD_FEATURE.md b/CARRY_FORWARD_FEATURE.md
new file mode 100644
index 0000000..d2f4e18
--- /dev/null
+++ b/CARRY_FORWARD_FEATURE.md
@@ -0,0 +1,227 @@
+# Carry Forward Feature
+
+## Overview
+Added a "CarryTo" functionality that allows users to carry forward (clone) existing reports to today's date. This feature is useful for continuing operations with similar configurations from previous days.
+
+## Features
+
+### ✅ **Core Functionality**
+1. **Clone Report**: Creates an exact copy of an existing report with today's date
+2. **Smart Validation**: Prevents carrying forward reports from today or future dates
+3. **Conflict Prevention**: Checks for existing reports with same configuration for today
+4. **Sheet Management**: Automatically manages sheet creation and completion status
+5. **User Assignment**: Assigns the carried forward report to the current user
+
+### ✅ **Business Logic**
+- **Source Reports**: Only reports from previous days can be carried forward
+- **Target Date**: Always creates the new report with today's date and current time
+- **Data Preservation**: Copies all report data except stoppages (starts fresh for new day)
+- **Sheet Completion**: Automatically marks sheets as "completed" when both day and night shifts exist
+
+## User Interface
+
+### ✅ **Desktop Actions**
+```
+[View] [Duplicate] [CarryTo] [Edit] [Delete]
+```
+
+### ✅ **Mobile Actions**
+```
+[View Details]
+[Duplicate as Night Shift]
+[Carry Forward to Today]
+[Edit] [Delete]
+```
+
+### ✅ **Visual Design**
+- **Color**: Purple theme to distinguish from duplicate (green)
+- **Icon**: Forward arrow or calendar icon
+- **Confirmation**: User confirmation dialog before execution
+- **Feedback**: Success/error messages via toast notifications
+
+## Technical Implementation
+
+### ✅ **Validation Logic**
+```typescript
+const canCarryForwardReport = (report: any) => {
+ const reportDate = new Date(report.createdDate);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ reportDate.setHours(0, 0, 0, 0);
+
+ // Cannot carry forward reports from today or future
+ if (reportDate >= today) {
+ return false;
+ }
+
+ return true;
+};
+```
+
+### ✅ **Server-Side Processing**
+```typescript
+// 1. Validate source report exists and is from past
+// 2. Check for existing report with same configuration today
+// 3. Create new report with today's date
+// 4. Manage sheet creation/updates
+// 5. Check and update sheet completion status
+```
+
+### ✅ **Data Cloning**
+```typescript
+const carriedReport = await prisma.report.create({
+ data: {
+ employeeId: user.id, // Current user
+ shift: originalReport.shift, // Same shift
+ areaId: originalReport.areaId,
+ dredgerLocationId: originalReport.dredgerLocationId,
+ dredgerLineLength: originalReport.dredgerLineLength,
+ reclamationLocationId: originalReport.reclamationLocationId,
+ shoreConnection: originalReport.shoreConnection,
+ reclamationHeight: originalReport.reclamationHeight,
+ pipelineLength: originalReport.pipelineLength,
+ stats: originalReport.stats,
+ timeSheet: originalReport.timeSheet,
+ stoppages: [], // Fresh start for new day
+ notes: originalReport.notes,
+ createdDate: new Date() // Today's date
+ }
+});
+```
+
+## Business Rules
+
+### ✅ **Eligibility Criteria**
+1. **Date Restriction**: Source report must be from a previous day
+2. **User Access**: All authenticated users can carry forward reports
+3. **Conflict Prevention**: Cannot create if same configuration exists for today
+4. **Data Integrity**: Maintains referential integrity with areas, locations, etc.
+
+### ✅ **What Gets Copied**
+- ✅ Shift type (day/night)
+- ✅ Area and location assignments
+- ✅ Equipment configurations
+- ✅ Pipeline lengths and heights
+- ✅ Statistics and personnel counts
+- ✅ Time sheet entries
+- ✅ Notes and comments
+
+### ✅ **What Gets Reset**
+- ❌ Stoppages (starts with empty array)
+- ❌ Creation date (set to current date/time)
+- ❌ Employee assignment (assigned to current user)
+
+## Sheet Management
+
+### ✅ **Automatic Sheet Creation**
+- Creates or updates sheet for today's date
+- Links the carried forward report to appropriate shift slot
+- Maintains proper sheet structure and relationships
+
+### ✅ **Completion Logic**
+```typescript
+// Check if sheet has both day and night shifts
+if (todaySheet && todaySheet.dayShiftId && todaySheet.nightShiftId) {
+ await prisma.sheet.update({
+ where: { id: todaySheet.id },
+ data: { status: 'completed' }
+ });
+}
+```
+
+## Use Cases
+
+### **Operational Scenarios**
+
+1. **Continuous Operations**
+ - Carry forward yesterday's day shift to today
+ - Maintain same equipment and location setup
+ - Start fresh with stoppages for new day
+
+2. **Shift Handover**
+ - Night shift carries forward day shift configuration
+ - Ensures consistency in operational setup
+ - Reduces setup time for similar operations
+
+3. **Recurring Operations**
+ - Weekly operations with similar patterns
+ - Monthly maintenance cycles
+ - Seasonal operational configurations
+
+### **Workflow Examples**
+
+1. **Daily Operations**
+ ```
+ Yesterday Day Shift → [CarryTo] → Today Day Shift
+ - Same location, equipment, personnel
+ - Fresh stoppages tracking
+ - Current user assignment
+ ```
+
+2. **Shift Completion**
+ ```
+ Day Shift Carried Forward → Night Shift Created → Sheet Completed
+ - Automatic sheet status update
+ - Complete operational cycle
+ - Ready for reporting
+ ```
+
+## Error Handling
+
+### ✅ **Validation Errors**
+- **Future Date**: "Cannot carry forward reports from today or future dates"
+- **Duplicate Configuration**: "A [shift] shift report already exists for today with the same location configuration"
+- **Missing Report**: "Report not found"
+- **System Error**: "Failed to carry forward report"
+
+### ✅ **User Feedback**
+- **Success**: "Report carried forward to today as [shift] shift!"
+- **Confirmation**: User must confirm before execution
+- **Visual Indicators**: Button states show availability
+- **Toast Notifications**: Success/error messages
+
+## Security & Permissions
+
+### ✅ **Access Control**
+- All authenticated users (auth level 1+) can carry forward reports
+- Users can carry forward any report, not just their own
+- Carried forward report is assigned to current user
+- Maintains audit trail with creation timestamps
+
+### ✅ **Data Validation**
+- Server-side validation of all business rules
+- Prevents duplicate configurations for same day
+- Ensures data integrity and consistency
+- Proper error handling and user feedback
+
+## Performance Considerations
+
+### ✅ **Database Operations**
+- Single transaction for report creation and sheet management
+- Efficient queries for validation checks
+- Proper indexing on date and location fields
+- Minimal data transfer with focused queries
+
+### ✅ **User Experience**
+- Fast execution with immediate feedback
+- Clear visual indicators for button availability
+- Intuitive confirmation dialogs
+- Consistent behavior across desktop and mobile
+
+## Future Enhancements
+
+### **Potential Improvements**
+1. **Bulk Carry Forward**: Carry forward multiple reports at once
+2. **Scheduled Carry Forward**: Automatic carry forward for recurring operations
+3. **Template System**: Save common configurations as templates
+4. **Selective Data Copy**: Choose which data elements to carry forward
+5. **Carry Forward History**: Track which reports were carried forward from where
+
+### **Advanced Features**
+1. **Smart Suggestions**: Suggest reports to carry forward based on patterns
+2. **Batch Operations**: Carry forward entire sheets or multiple shifts
+3. **Custom Date Selection**: Carry forward to specific future dates
+4. **Approval Workflow**: Require approval for certain carry forward operations
+5. **Integration**: Connect with scheduling systems for automated carry forward
+
+The Carry Forward feature streamlines operational continuity by allowing users to quickly replicate successful operational configurations from previous days, reducing setup time and ensuring consistency in operations."
\ No newline at end of file
diff --git a/app/components/DashboardLayout.tsx b/app/components/DashboardLayout.tsx
index 330bc21..632da92 100644
--- a/app/components/DashboardLayout.tsx
+++ b/app/components/DashboardLayout.tsx
@@ -1,6 +1,6 @@
import { Form, Link, useLocation } from "@remix-run/react";
import type { Employee } from "@prisma/client";
-import { useState } from "react";
+import { useState, useEffect } from "react";
interface DashboardLayoutProps {
children: React.ReactNode;
@@ -9,24 +9,23 @@ interface DashboardLayoutProps {
export default function DashboardLayout({ children, user }: DashboardLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
- const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
- // Initialize from localStorage if available
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('sidebar-collapsed');
- return saved ? JSON.parse(saved) : false;
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+
+ // Initialize sidebar state from localStorage after hydration
+ useEffect(() => {
+ const saved = localStorage.getItem('sidebar-collapsed');
+ if (saved) {
+ setSidebarCollapsed(JSON.parse(saved));
}
- return false;
- });
+ }, []);
const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
-
+
const toggleCollapse = () => {
const newCollapsed = !sidebarCollapsed;
setSidebarCollapsed(newCollapsed);
// Persist to localStorage
- if (typeof window !== 'undefined') {
- localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
- }
+ localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
};
return (
@@ -69,7 +68,7 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
@@ -177,8 +176,8 @@ function SidebarContent({
onItemClick();
}}
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors duration-150 ${active
- ? 'bg-indigo-100 text-indigo-900 border-r-2 border-indigo-500'
- : 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
+ ? 'bg-indigo-100 text-indigo-900 border-r-2 border-indigo-500'
+ : 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
}`}
title={collapsed ? children?.toString() : undefined}
>
diff --git a/app/routes/areas.tsx b/app/routes/areas.tsx
index a4dace2..d0798a4 100644
--- a/app/routes/areas.tsx
+++ b/app/routes/areas.tsx
@@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Areas Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Areas Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);
diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx
index 6e80077..8434c9c 100644
--- a/app/routes/dashboard.tsx
+++ b/app/routes/dashboard.tsx
@@ -5,7 +5,7 @@ import { requireAuthLevel } from "~/utils/auth.server";
import DashboardLayout from "~/components/DashboardLayout";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Dashboard - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Dashboard - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 1);
diff --git a/app/routes/dredger-locations.tsx b/app/routes/dredger-locations.tsx
index 6c173a3..cf2413b 100644
--- a/app/routes/dredger-locations.tsx
+++ b/app/routes/dredger-locations.tsx
@@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Dredger Locations Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);
diff --git a/app/routes/employees.tsx b/app/routes/employees.tsx
index 8b2172c..13e18f8 100644
--- a/app/routes/employees.tsx
+++ b/app/routes/employees.tsx
@@ -9,7 +9,7 @@ import { useState, useEffect } from "react";
import bcrypt from "bcryptjs";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Employee Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Employee Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);
diff --git a/app/routes/equipment.tsx b/app/routes/equipment.tsx
index 4a0c0b6..c9c5994 100644
--- a/app/routes/equipment.tsx
+++ b/app/routes/equipment.tsx
@@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Equipment Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Equipment Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);
diff --git a/app/routes/foreman.tsx b/app/routes/foreman.tsx
index 00949cd..b3ea82f 100644
--- a/app/routes/foreman.tsx
+++ b/app/routes/foreman.tsx
@@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Foreman Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Foreman Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);
diff --git a/app/routes/reclamation-locations.tsx b/app/routes/reclamation-locations.tsx
index 6f7348d..af53497 100644
--- a/app/routes/reclamation-locations.tsx
+++ b/app/routes/reclamation-locations.tsx
@@ -8,7 +8,7 @@ import Toast from "~/components/Toast";
import { useState, useEffect } from "react";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Reclamation Locations Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 2);
diff --git a/app/routes/report-sheet.tsx b/app/routes/report-sheet.tsx
index ad072c0..0eb1a6e 100644
--- a/app/routes/report-sheet.tsx
+++ b/app/routes/report-sheet.tsx
@@ -7,7 +7,7 @@ import ReportSheetViewModal from "~/components/ReportSheetViewModal";
import { useState } from "react";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Report Sheets - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Report Sheets - Phosphat Report" }];
interface ReportSheet {
id: string;
diff --git a/app/routes/reports.tsx b/app/routes/reports.tsx
index 9e927d5..b916fa1 100644
--- a/app/routes/reports.tsx
+++ b/app/routes/reports.tsx
@@ -10,7 +10,7 @@ import { useState, useEffect } from "react";
import { manageSheet, removeFromSheet } from "~/utils/sheet.server";
import { prisma } from "~/utils/db.server";
-export const meta: MetaFunction = () => [{ title: "Reports Management - Alhaffer Reporting System" }];
+export const meta: MetaFunction = () => [{ title: "Reports Management - Phosphat Report" }];
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = await requireAuthLevel(request, 1); // All employees can access reports
@@ -457,6 +457,112 @@ export const action = async ({ request }: ActionFunctionArgs) => {
}
}
+ if (intent === "carryTo") {
+ if (typeof id !== "string") {
+ return json({ errors: { form: "Invalid report ID" } }, { status: 400 });
+ }
+
+ // Get the original report with all its data
+ const originalReport = await prisma.report.findUnique({
+ where: { id: parseInt(id) },
+ include: {
+ area: true,
+ dredgerLocation: true,
+ reclamationLocation: true
+ }
+ });
+
+ if (!originalReport) {
+ return json({ errors: { form: "Report not found" } }, { status: 404 });
+ }
+
+ // Check if report is from today or future (cannot carry forward)
+ const reportDate = new Date(originalReport.createdDate);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ reportDate.setHours(0, 0, 0, 0);
+
+ if (reportDate >= today) {
+ return json({ errors: { form: "Cannot carry forward reports from today or future dates" } }, { status: 400 });
+ }
+
+ // Check if a report with same shift, area, and locations already exists for today
+ const todayString = today.toISOString().split('T')[0];
+ const existingTodayReport = await prisma.report.findFirst({
+ where: {
+ createdDate: {
+ gte: today,
+ lt: new Date(today.getTime() + 24 * 60 * 60 * 1000)
+ },
+ shift: originalReport.shift,
+ areaId: originalReport.areaId,
+ dredgerLocationId: originalReport.dredgerLocationId,
+ reclamationLocationId: originalReport.reclamationLocationId
+ }
+ });
+
+ if (existingTodayReport) {
+ return json({ errors: { form: `A ${originalReport.shift} shift report already exists for today with the same location configuration` } }, { status: 400 });
+ }
+
+ try {
+ // Create the carried forward report with today's date
+ const carriedReport = await prisma.report.create({
+ data: {
+ employeeId: user.id, // Assign to current user
+ shift: originalReport.shift,
+ areaId: originalReport.areaId,
+ dredgerLocationId: originalReport.dredgerLocationId,
+ dredgerLineLength: originalReport.dredgerLineLength,
+ reclamationLocationId: originalReport.reclamationLocationId,
+ shoreConnection: originalReport.shoreConnection,
+ reclamationHeight: originalReport.reclamationHeight,
+ pipelineLength: originalReport.pipelineLength,
+ stats: originalReport.stats,
+ timeSheet: originalReport.timeSheet,
+ stoppages: [], // Empty stoppages array for new day
+ notes: originalReport.notes,
+ createdDate: new Date() // Set to current date/time
+ }
+ });
+
+ // Manage sheet for the new report
+ await manageSheet(
+ carriedReport.id,
+ originalReport.shift,
+ originalReport.areaId,
+ originalReport.dredgerLocationId,
+ originalReport.reclamationLocationId,
+ carriedReport.createdDate
+ );
+
+ // Check if the sheet should be marked as completed
+ const todaySheet = await prisma.sheet.findUnique({
+ where: {
+ areaId_dredgerLocationId_reclamationLocationId_date: {
+ areaId: originalReport.areaId,
+ dredgerLocationId: originalReport.dredgerLocationId,
+ reclamationLocationId: originalReport.reclamationLocationId,
+ date: todayString
+ }
+ }
+ });
+
+ // If sheet has both day and night shifts, mark as completed
+ if (todaySheet && todaySheet.dayShiftId && todaySheet.nightShiftId) {
+ await prisma.sheet.update({
+ where: { id: todaySheet.id },
+ data: { status: 'completed' }
+ });
+ }
+
+ return json({ success: `Report carried forward to today as ${originalReport.shift} shift!` });
+ } catch (error) {
+ console.error('CarryTo error:', error);
+ return json({ errors: { form: "Failed to carry forward report" } }, { status: 400 });
+ }
+ }
+
if (intent === "delete") {
if (typeof id !== "string") {
return json({ errors: { form: "Invalid report ID" } }, { status: 400 });
@@ -781,6 +887,22 @@ export default function Reports() {
return true;
};
+ const canCarryForwardReport = (report: any) => {
+ // Check if report is from today or future (cannot carry forward)
+ const reportDate = new Date(report.createdDate);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ reportDate.setHours(0, 0, 0, 0);
+
+ // If report is from today or future, don't allow carry forward
+ if (reportDate >= today) {
+ return false;
+ }
+
+ // All users (auth level 1+) can carry forward reports
+ return true;
+ };
+
const isReportTooOld = (report: any) => {
const reportDate = new Date(report.createdDate);
const dayBeforeToday = new Date();
@@ -1244,6 +1366,24 @@ export default function Reports() {
Duplicate
) : null}
+ {canCarryForwardReport(report) && (
+