This commit is contained in:
yznahmad 2025-08-01 05:00:14 +03:00
parent b43819aa7b
commit 17f1acfbb0
25 changed files with 2139 additions and 747 deletions

View File

@ -12,6 +12,7 @@ DATABASE_URL="file:/app/data/production.db"
# Security
SESSION_SECRET="your-super-secure-session-secret-change-this-in-production-min-32-chars"
ENCRYPTION_KEY="production-secure-encryption-key!"
# Super Admin Account (created on first run)
SUPER_ADMIN="superadmin"

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
/.cache
/build
.env
.env.*
/generated/prisma

View File

@ -0,0 +1,135 @@
# Mobile Responsiveness Update Summary
## Overview
All page routes in the Phosphat Report application have been updated to be fully responsive and mobile-friendly. The updates follow a mobile-first approach with consistent design patterns across all pages.
## Key Improvements Made
### 1. **Dashboard (dashboard.tsx)**
- ✅ Responsive stats grid: `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`
- ✅ Responsive typography: `text-xl sm:text-2xl`
- ✅ Mobile-friendly recent reports layout with stacked content on small screens
- ✅ Improved spacing and padding for mobile devices
### 2. **Reports Management (reports.tsx)**
- ✅ **Dual Layout System**: Desktop table + Mobile cards
- ✅ Desktop: Traditional table layout (hidden on mobile with `hidden md:block`)
- ✅ Mobile: Card-based layout (visible on mobile with `md:hidden`)
- ✅ Mobile cards include all essential information in a compact format
- ✅ Responsive header with stacked layout on mobile
- ✅ Mobile-friendly action buttons (full-width on mobile)
### 3. **Employee Management (employees.tsx)**
- ✅ **Dual Layout System**: Desktop table + Mobile cards
- ✅ Desktop: Full table with all columns (hidden on mobile with `hidden lg:block`)
- ✅ Mobile: Detailed cards showing employee info, status, and actions
- ✅ Responsive form layout in modal
- ✅ Mobile-friendly button layouts
### 4. **Report Sheets (report-sheet.tsx)**
- ✅ **Dual Layout System**: Desktop table + Mobile cards
- ✅ Mobile cards show date, area, locations, shifts, and employees
- ✅ Responsive shift badges and status indicators
- ✅ Mobile-optimized action buttons
### 5. **New Report Form (reports_.new.tsx)**
- ✅ Responsive multi-step progress indicator
- ✅ Mobile-friendly step navigation with smaller indicators on mobile
- ✅ Responsive form grids: `grid-cols-1 sm:grid-cols-2`
- ✅ Mobile-optimized navigation buttons with stacked layout
- ✅ Responsive spacing and padding throughout the form
### 6. **Areas Management (areas.tsx)**
- ✅ **Dual Layout System**: Desktop table + Mobile cards
- ✅ Mobile cards with area name, report count, and actions
- ✅ Responsive header layout
- ✅ Mobile-friendly action buttons
### 7. **Equipment Management (equipment.tsx)**
- ✅ **Dual Layout System**: Desktop table + Mobile cards
- ✅ Mobile cards showing equipment details, category badges, and actions
- ✅ Responsive form layout in modal
- ✅ Mobile-optimized category indicators
### 8. **Authentication Pages**
- ✅ **Sign In (signin.tsx)**: Responsive logo sizing, typography, and spacing
- ✅ **Sign Up (signup.tsx)**: Mobile-friendly form layout and responsive design
### 9. **Settings Pages**
- ✅ **Mail Settings (mail-settings.tsx)**: Responsive form layout, stacked buttons on mobile
- ✅ **Test Email (test-email.tsx)**: Mobile-friendly form and responsive typography
## Design Patterns Used
### 1. **Responsive Breakpoints**
- `sm:` (640px+) - Small tablets and larger phones
- `md:` (768px+) - Tablets
- `lg:` (1024px+) - Small desktops
- `xl:` (1280px+) - Large desktops
### 2. **Layout Strategies**
- **Tables → Cards**: Desktop tables convert to mobile cards for better readability
- **Flex Direction**: `flex-col sm:flex-row` for stacking on mobile
- **Grid Responsiveness**: `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`
- **Spacing**: Reduced padding/margins on mobile (`p-4 sm:p-6`)
### 3. **Typography Scaling**
- Headers: `text-xl sm:text-2xl`
- Body text: `text-sm sm:text-base`
- Responsive line heights and spacing
### 4. **Button Layouts**
- Mobile: Full-width buttons (`w-full`)
- Desktop: Inline buttons with proper spacing
- Action groups: Stacked on mobile, inline on desktop
### 5. **Navigation**
- Mobile-first navigation with collapsible elements
- Touch-friendly button sizes (minimum 44px touch targets)
- Proper spacing for thumb navigation
## Mobile-Specific Features
### 1. **Card-Based Layouts**
- All data tables have mobile card alternatives
- Cards include essential information in a scannable format
- Proper visual hierarchy with typography and spacing
### 2. **Touch-Friendly Interactions**
- Larger touch targets for buttons and links
- Proper spacing between interactive elements
- Mobile-optimized form inputs
### 3. **Progressive Enhancement**
- Desktop-first table layouts with mobile fallbacks
- Responsive images and icons
- Adaptive spacing and typography
## Testing Recommendations
### 1. **Device Testing**
- Test on actual mobile devices (iOS/Android)
- Use browser dev tools for responsive testing
- Test in both portrait and landscape orientations
### 2. **Breakpoint Testing**
- Test all major breakpoints (320px, 768px, 1024px, 1280px)
- Ensure smooth transitions between breakpoints
- Verify no horizontal scrolling on mobile
### 3. **Functionality Testing**
- All forms work properly on mobile
- Modal dialogs are mobile-friendly
- Navigation is accessible on touch devices
## Browser Support
- Modern mobile browsers (iOS Safari, Chrome Mobile, Firefox Mobile)
- Responsive design works across all major desktop browsers
- Graceful degradation for older browsers
## Performance Considerations
- Tailwind CSS provides optimized responsive utilities
- No additional JavaScript required for responsive behavior
- Minimal impact on bundle size
All routes now provide an excellent mobile experience while maintaining full desktop functionality.

14
Y_Todo.md Normal file
View File

@ -0,0 +1,14 @@
TODO
- [✅] Update Sidebar menu to be collapselble
- [✅] make the Sidebar compatible with mobile devices
- [ ] Update all the page routes to be responsive and mobile friendly
- [ ] Summation of stoppage hours (sum if contains) and show them under the report

View File

@ -1,5 +1,6 @@
import { Form, Link } from "@remix-run/react";
import { Form, Link, useLocation } from "@remix-run/react";
import type { Employee } from "@prisma/client";
import { useState } from "react";
interface DashboardLayoutProps {
children: React.ReactNode;
@ -7,50 +8,87 @@ 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;
}
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));
}
};
return (
<div className="min-h-screen bg-gray-50">
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-gray-600 bg-opacity-75 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Navigation */}
<nav className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-full mx-auto px-0 sm:px-6 lg:px-8">
<nav className="bg-white shadow-sm border-b border-gray-200 fixed w-full top-0 z-30">
<div className="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
{/* Mobile menu button */}
<button
type="button"
className="lg:hidden inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 mr-3"
onClick={toggleSidebar}
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
{/* Desktop collapse button */}
<button
type="button"
className="hidden lg:inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 mr-3"
onClick={toggleCollapse}
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<img
className="h-12 w-auto justify-self-start"
src="/clogo-sm.png"
className="h-14 w-auto"
src="/logo03.png"
alt="Phosphat Report"
/>
{/* <div className="ml-4">
<h1 className="text-xl font-semibold text-gray-900">
Phosphat Report Dashboard
</h1>
</div> */}
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<div className="hidden sm:flex items-center space-x-2">
<div className="text-sm text-gray-700">
Welcome, <span className="font-medium">{user.name}</span>
</div>
{/* <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
user.authLevel === 1
? 'bg-red-100 text-red-800'
: user.authLevel === 2
? 'bg-yellow-100 text-yellow-800'
: 'bg-green-100 text-green-800'
}`}>
Level {user.authLevel}
</span> */}
</div>
<Form method="post" action="/logout">
<button
type="submit"
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm 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-red-500 shadow-sm transition duration-150 ease-in-out"
className="inline-flex items-center px-3 py-2 border border-gray-300 text-sm 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-red-500 shadow-sm transition duration-150 ease-in-out"
>
<svg className="-ml-0.5 mr-2 h-4 w-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="h-4 w-4 sm:mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Logout
<span className="hidden sm:inline">Logout</span>
</button>
</Form>
</div>
@ -58,176 +96,253 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
</div>
</nav>
{/* Sidebar */}
<div className="flex">
<div className="w-64 bg-white shadow-sm min-h-screen">
<nav className="mt-8 px-4">
<ul className="space-y-2">
<li>
<Link
to="/dashboard"
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="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6a2 2 0 01-2 2H10a2 2 0 01-2-2V5z" />
</svg>
Dashboard
</Link>
</li>
<div className="flex pt-16">
{/* Desktop Sidebar */}
<div className={`hidden lg:flex lg:flex-shrink-0 transition-all duration-300 ${sidebarCollapsed ? 'lg:w-16' : 'lg:w-64'
}`}>
<div className="flex flex-col w-full">
<div className="flex flex-col flex-grow bg-white shadow-sm border-r border-gray-200 pt-5 pb-4 overflow-y-auto">
<SidebarContent
user={user}
collapsed={sidebarCollapsed}
onItemClick={() => { }}
/>
</div>
</div>
</div>
<li>
<Link
to="/reports"
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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Shifts
</Link>
</li>
<li>
<Link
to="/report-sheet"
className="group flex items-center px-2 py-2 text-base font-medium rounded-md text-gray-900 hover:bg-gray-50"
>
<svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
Reports
</Link>
</li>
{(user.authLevel >= 2) && (
<>
{/* Management Section */}
<li className="mt-6">
<div className="px-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Management
</div>
</li>
<li>
<Link
to="/areas"
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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Areas
</Link>
</li>
<li>
<Link
to="/dredger-locations"
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="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
Dredger Locations
</Link>
</li>
<li>
<Link
to="/reclamation-locations"
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="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Reclamation Locations
</Link>
</li>
<li>
<Link
to="/equipment"
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.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
Equipment
</Link>
</li>
<li>
<Link
to="/foreman"
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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
Foreman
</Link>
</li>
<li>
<Link
to="/employees"
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="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
Employees
</Link>
</li>
</>
)}
{user.authLevel === 3 && (
<>
{/* Admin Section */}
<li className="mt-6">
<div className="px-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Administration
</div>
</li>
<li>
<Link
to="/mail-settings"
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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
Mail Settings
</Link>
</li>
<li>
<Link
to="/test-email"
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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Test Email
</Link>
</li>
</>
)}
</ul>
</nav>
{/* Mobile Sidebar */}
<div className={`lg:hidden fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}>
<div className="flex flex-col h-full pt-16">
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<SidebarContent
user={user}
collapsed={false}
onItemClick={() => setSidebarOpen(false)}
/>
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 p-8">
{children}
<div className="flex-1 flex flex-col">
<main className="flex-1 p-4 sm:p-6 lg:p-8">
{children}
</main>
</div>
</div>
</div>
);
}
// Sidebar Content Component
function SidebarContent({
user,
collapsed,
onItemClick
}: {
user: Pick<Employee, "id" | "name" | "username" | "authLevel">;
collapsed: boolean;
onItemClick: () => void;
}) {
const location = useLocation();
const isActive = (path: string) => {
if (path === '/dashboard') {
return location.pathname === '/dashboard';
}
return location.pathname.startsWith(path);
};
const NavItem = ({
to,
icon,
children,
onClick
}: {
to: string;
icon: React.ReactNode;
children: React.ReactNode;
onClick?: () => void;
}) => {
const active = isActive(to);
return (
<li>
<Link
to={to}
onClick={() => {
onClick?.();
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'
}`}
title={collapsed ? children?.toString() : undefined}
>
<div className={`mr-3 flex-shrink-0 h-6 w-6 ${active ? 'text-indigo-500' : 'text-gray-400 group-hover:text-gray-500'
}`}>
{icon}
</div>
{!collapsed && (
<span className="truncate">{children}</span>
)}
</Link>
</li>
);
};
const SectionHeader = ({ children }: { children: React.ReactNode }) => (
<li className="mt-6">
{!collapsed && (
<div className="px-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
{children}
</div>
)}
{collapsed && <div className="border-t border-gray-200 mx-2"></div>}
</li>
);
return (
<nav className="mt-5 flex-1 px-2 space-y-1">
<ul className="space-y-1">
<NavItem
to="/dashboard"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6a2 2 0 01-2 2H10a2 2 0 01-2-2V5z" />
</svg>
}
>
Dashboard
</NavItem>
<NavItem
to="/reports"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
}
>
Shifts
</NavItem>
<NavItem
to="/report-sheet"
icon={
<svg 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
</NavItem>
{user.authLevel >= 2 && (
<>
<SectionHeader>Management</SectionHeader>
<NavItem
to="/areas"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
}
>
Areas
</NavItem>
<NavItem
to="/dredger-locations"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
}
>
Dredger Locations
</NavItem>
<NavItem
to="/reclamation-locations"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
}
>
Reclamation Locations
</NavItem>
<NavItem
to="/equipment"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
}
>
Equipment
</NavItem>
<NavItem
to="/foreman"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
}
>
Foreman
</NavItem>
<NavItem
to="/employees"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
}
>
Employees
</NavItem>
</>
)}
{user.authLevel === 3 && (
<>
<SectionHeader>Administration</SectionHeader>
<NavItem
to="/mail-settings"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
}
>
Mail Settings
</NavItem>
<NavItem
to="/test-email"
icon={
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
}
>
Test Email
</NavItem>
</>
)}
</ul>
</nav>
);
}

View File

@ -535,7 +535,11 @@ function TimeSheetSection({ timeSheetEntries, equipment, addTimeSheetEntry, remo
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 text-sm"
>
<option value="">Select Equipment</option>
{equipment.map((item: any) => (
{equipment.filter((item: any) => {
const machineValue = `${item.model} (${item.number})`;
// Show if not selected by any other entry, or if it's the current entry's selection
return !timeSheetEntries.some((e: any) => e.id !== entry.id && e.machine === machineValue);
}).map((item: any) => (
<option key={item.id} value={`${item.model} (${item.number})`}>
{item.category} - {item.model} ({item.number})
</option>

View File

@ -21,14 +21,77 @@ export default function ReportSheetViewModal({ isOpen, onClose, sheet }: ReportS
if (!isOpen || !sheet) return null;
const handleExportExcel = async () => {
if (sheet.dayReport) {
await exportReportToExcel(sheet.dayReport);
}
if (sheet.nightReport) {
await exportReportToExcel(sheet.nightReport);
}
// Export the entire sheet (both day and night reports combined)
await exportReportToExcel(sheet);
};
// Helper function to parse time string (HH:MM) to minutes
const parseTimeToMinutes = (timeStr: string): number => {
if (!timeStr || timeStr === '00:00') return 0;
const [hours, minutes] = timeStr.split(':').map(Number);
return (hours || 0) * 60 + (minutes || 0);
};
// Helper function to format minutes back to HH:MM
const formatMinutesToTime = (totalMinutes: number): string => {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
};
// Calculate stoppage statistics
const calculateStoppageStats = () => {
const dayStoppages = sheet.dayReport?.stoppages || [];
const nightStoppages = sheet.nightReport?.stoppages || [];
// Calculate total time for day shift stoppages
const totalDayMinutes = dayStoppages.reduce((sum, stoppage) => {
return sum + parseTimeToMinutes(stoppage.total || '00:00');
}, 0);
// Calculate total time for night shift stoppages
const totalNightMinutes = nightStoppages.reduce((sum, stoppage) => {
return sum + parseTimeToMinutes(stoppage.total || '00:00');
}, 0);
// Calculate total combined time
const totalMinutes = totalDayMinutes + totalNightMinutes;
// Calculate counted stoppages time (excluding "Brine" or "Change Shift" in notes)
const countedDayMinutes = dayStoppages
.filter(stoppage => {
const note = (stoppage.note || '').toLowerCase();
return !note.includes('brine') && !note.includes('change shift') && !note.includes('shift change');
})
.reduce((sum, stoppage) => {
return sum + parseTimeToMinutes(stoppage.total || '00:00');
}, 0);
const countedNightMinutes = nightStoppages
.filter(stoppage => {
const note = (stoppage.note || '').toLowerCase();
return !note.includes('brine') && !note.includes('change shift') && !note.includes('shift change');
})
.reduce((sum, stoppage) => {
return sum + parseTimeToMinutes(stoppage.total || '00:00');
}, 0);
const totalCountedMinutes = countedDayMinutes + countedNightMinutes;
return {
totalDayTime: formatMinutesToTime(totalDayMinutes),
totalNightTime: formatMinutesToTime(totalNightMinutes),
totalTime: formatMinutesToTime(totalMinutes),
countedDayTime: formatMinutesToTime(countedDayMinutes),
countedNightTime: formatMinutesToTime(countedNightMinutes),
totalCountedTime: formatMinutesToTime(totalCountedMinutes),
totalMinutes,
totalCountedMinutes
};
};
const stoppageStats = calculateStoppageStats();
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">
@ -177,6 +240,120 @@ export default function ReportSheetViewModal({ isOpen, onClose, sheet }: ReportS
<ReportSheetFooter />
</div>
{/* Summary Statistics Section */}
<div className="mt-6 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg border border-blue-200 p-6">
<h4 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Stoppage Summary Statistics
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Total Stoppage Time */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-gray-200">
<h5 className="text-sm font-medium text-gray-600 mb-3 uppercase tracking-wide">Total Stoppage Time</h5>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-700">Day Shift:</span>
<span className="text-lg font-semibold text-blue-600 font-mono">{stoppageStats.totalDayTime}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-700">Night Shift:</span>
<span className="text-lg font-semibold text-purple-600 font-mono">{stoppageStats.totalNightTime}</span>
</div>
<div className="border-t pt-2 mt-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-800">Total:</span>
<span className="text-xl font-bold text-gray-900 font-mono">{stoppageStats.totalTime}</span>
</div>
</div>
</div>
</div>
{/* Counted Stoppage Time */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-gray-200">
<h5 className="text-sm font-medium text-gray-600 mb-3 uppercase tracking-wide">Counted Stoppage Time</h5>
<div className="text-xs text-gray-500 mb-3">
{/* (Excluding "Brine" & "Change Shift") */}
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-700">Day Shift:</span>
<span className="text-lg font-semibold text-green-600 font-mono">{stoppageStats.countedDayTime}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-gray-700">Night Shift:</span>
<span className="text-lg font-semibold text-orange-600 font-mono">{stoppageStats.countedNightTime}</span>
</div>
<div className="border-t pt-2 mt-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-800">Total:</span>
<span className="text-xl font-bold text-red-600 font-mono">{stoppageStats.totalCountedTime}</span>
</div>
</div>
</div>
</div>
{/* Time Analysis */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-gray-200">
<h5 className="text-sm font-medium text-gray-600 mb-3 uppercase tracking-wide">Time Analysis</h5>
<div className="space-y-3">
<div>
<div className="flex justify-between items-center mb-1">
<span className="text-sm text-gray-700">Counted Rate:</span>
<span className="text-sm font-medium text-gray-900">
{stoppageStats.totalMinutes > 0
? `${Math.round((stoppageStats.totalCountedMinutes / stoppageStats.totalMinutes) * 100)}%`
: '0%'
}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-green-400 to-blue-500 h-2 rounded-full transition-all duration-300"
style={{
width: stoppageStats.totalMinutes > 0
? `${(stoppageStats.totalCountedMinutes / stoppageStats.totalMinutes) * 100}%`
: '0%'
}}
></div>
</div>
</div>
<div className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
<div className="flex justify-between">
<span>Excluded Time:</span>
<span className="font-medium font-mono">
{formatMinutesToTime(stoppageStats.totalMinutes - stoppageStats.totalCountedMinutes)}
</span>
</div>
</div>
{/* <div className="text-xs text-gray-600 bg-blue-50 p-2 rounded">
<div className="flex justify-between">
<span>Avg per Day:</span>
<span className="font-medium font-mono">
{formatMinutesToTime(Math.round(stoppageStats.totalCountedMinutes / 2))}
</span>
</div>
</div> */}
</div>
</div>
</div>
{/* Additional Info */}
<div className="mt-4 p-3 bg-blue-100 rounded-lg">
<p className="text-sm text-blue-800">
<svg className="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<strong>Note:</strong> Counted stoppage time excludes entries with "Brine" or "Change Shift" in the notes field,
as these are typically planned operational activities rather than unplanned stoppages. Times are displayed in HH:MM format.
</p>
</div>
</div>
</div>
</div>
</div>
@ -191,12 +368,22 @@ function ReportSheetHeader({ sheet }: { sheet: ReportSheet }) {
<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>
<td className=" p-2 text-center" style={{ width: '25%' }}>
<img
src="/logo03.png"
alt="Company Logo"
className="h-16 mx-auto"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
</td>
<td className="border-r-2 border-l-2 border-black p-2 text-center font-bold text-lg" style={{ width: '50%' }}>
<div>Reclamation Work Diary</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%' }}>
<td className="p-2 text-center" style={{ width: '25%' }}>
<img
src="/logo-light.png"
alt="Arab Potash Logo"

View File

@ -154,12 +154,23 @@ function ReportHeader() {
<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%' }}>
{/* border-r border-black */}
<td className=" p-2 text-center" style={{ width: '25%' }}>
<img
src="/logo03.png"
alt="Company Logo"
className="h-16 mx-auto"
onError={(e) => {
e.currentTarget.style.display = 'none';
}}
/>
</td>
<td className="border-r-2 border-l-2 border-black p-2 text-center font-bold text-lg" style={{ width: '50%' }}>
<div>Reclamation Work Diary</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%' }}>
<td className="p-2 text-center" style={{ width: '25%' }}>
<img
src="/logo-light.png"
alt="Arab Potash Logo"

View File

@ -125,14 +125,14 @@ export default function Areas() {
return (
<DashboardLayout user={user}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900">Areas Management</h1>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">Areas Management</h1>
<p className="mt-1 text-sm text-gray-600">Manage operational areas for your reports</p>
</div>
<button
onClick={handleAdd}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
>
<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" />
@ -141,10 +141,11 @@ export default function Areas() {
</button>
</div>
{/* Areas Table */}
{/* Areas Table - Desktop */}
<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">
<div className="hidden sm:block">
<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">
@ -211,7 +212,59 @@ export default function Areas() {
))}
</tbody>
</table>
</div>
</div>
{/* Areas Cards - Mobile */}
<div className="sm:hidden">
<div className="space-y-4 p-4">
{areas.map((area) => (
<div key={area.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center">
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div className="ml-3">
<div className="text-sm font-medium text-gray-900">{area.name}</div>
</div>
</div>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{area._count.reports} reports
</span>
</div>
<div className="flex flex-col space-y-2">
<button
onClick={() => handleEdit(area)}
className="w-full text-center px-3 py-2 text-sm text-indigo-600 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-150"
>
Edit Area
</button>
<Form method="post" className="w-full">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={area.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this area?")) {
e.preventDefault();
}
}}
className="w-full text-center px-3 py-2 text-sm text-red-600 bg-red-50 rounded-md hover:bg-red-100 transition-colors duration-150"
>
Delete Area
</button>
</Form>
</div>
</div>
))}
</div>
</div>
{areas.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">

View File

@ -47,17 +47,17 @@ export default function Dashboard() {
{/* Welcome Section */}
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 mb-2">
Welcome back, {user.name}!
</h2>
<p className="text-gray-600">
<p className="text-sm sm:text-base text-gray-600">
Here's what's happening with your phosphat operations today.
</p>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 gap-5 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:gap-5 sm:grid-cols-2 lg:grid-cols-3">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
@ -129,10 +129,10 @@ export default function Dashboard() {
{/* Recent Reports */}
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-5 sm:px-6">
<h3 className="text-lg leading-6 font-medium text-gray-900">
<h3 className="text-base sm:text-lg leading-6 font-medium text-gray-900">
Recent Reports
</h3>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
<p className="mt-1 max-w-2xl text-xs sm:text-sm text-gray-500">
Latest activity from your team
</p>
</div>
@ -141,7 +141,7 @@ export default function Dashboard() {
recentReports.map((report) => (
<li key={report.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-2 sm:space-y-0">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center">
@ -154,12 +154,12 @@ export default function Dashboard() {
<div className="text-sm font-medium text-gray-900">
{report.employee.name}
</div>
<div className="text-sm text-gray-500">
<div className="text-xs sm:text-sm text-gray-500">
{report.area.name} - {report.shift} shift
</div>
</div>
</div>
<div className="text-sm text-gray-500">
<div className="text-xs sm:text-sm text-gray-500 ml-12 sm:ml-0">
{new Date(report.createdDate).toLocaleDateString('en-GB')}
</div>
</div>
@ -169,7 +169,7 @@ export default function Dashboard() {
) : (
<li>
<div className="px-4 py-4 sm:px-6 text-center text-gray-500">
No reports yet. Create your first report to get started!
<p className="text-sm">No reports yet. Create your first report to get started!</p>
</div>
</li>
)}

View File

@ -342,14 +342,14 @@ export default function Employees() {
return (
<DashboardLayout user={user}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900">Employee Management</h1>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">Employee Management</h1>
<p className="mt-1 text-sm text-gray-600">Manage system users and their access levels</p>
</div>
<button
onClick={handleAdd}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
>
<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" />
@ -358,10 +358,11 @@ export default function Employees() {
</button>
</div>
{/* Employees Table */}
{/* Employees Table - Desktop */}
<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">
<div className="hidden lg:block">
<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">
@ -472,7 +473,91 @@ export default function Employees() {
))}
</tbody>
</table>
</div>
</div>
{/* Employees Cards - Mobile */}
<div className="lg:hidden">
<div className="space-y-4 p-4">
{employees.map((employee) => (
<div key={employee.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center">
<div className={`h-10 w-10 rounded-full flex items-center justify-center ${getAuthLevelBadge(employee.authLevel)}`}>
{getAuthLevelIcon(employee.authLevel)}
</div>
<div className="ml-3">
<div className="text-sm font-medium text-gray-900">{employee.name}</div>
<div className="text-xs text-gray-500 font-mono">{employee.username}</div>
</div>
</div>
<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>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Email:</span>
<span className="text-xs text-gray-900 truncate ml-2">{employee.email}</span>
</div>
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Access Level:</span>
<span className="text-xs text-gray-900">Level {employee.authLevel} - {getAuthLevelText(employee.authLevel)}</span>
</div>
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Reports:</span>
<span className="text-xs text-gray-900">{employee._count.reports} reports</span>
</div>
</div>
<div className="flex flex-col space-y-2">
<button
onClick={() => handleEdit(employee)}
className="w-full text-center px-3 py-2 text-sm text-indigo-600 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-150"
>
Edit Employee
</button>
{employee.id !== user.id && (
<div className="flex space-x-2">
<Form method="post" className="flex-1">
<input type="hidden" name="intent" value="toggleStatus" />
<input type="hidden" name="id" value={employee.id} />
<button
type="submit"
className={`w-full text-center px-3 py-2 text-sm rounded-md transition-colors duration-150 ${
employee.status === 'active'
? 'text-orange-600 bg-orange-50 hover:bg-orange-100'
: 'text-green-600 bg-green-50 hover:bg-green-100'
}`}
>
{employee.status === 'active' ? 'Deactivate' : 'Activate'}
</button>
</Form>
<Form method="post" className="flex-1">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={employee.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this employee?")) {
e.preventDefault();
}
}}
className="w-full text-center px-3 py-2 text-sm text-red-600 bg-red-50 rounded-md hover:bg-red-100 transition-colors duration-150"
>
Delete
</button>
</Form>
</div>
)}
</div>
</div>
))}
</div>
</div>
{employees.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">
@ -507,45 +592,45 @@ export default function Employees() {
<input type="hidden" name="intent" value={isEditing ? "update" : "create"} />
{isEditing && <input type="hidden" name="id" value={editingEmployee?.id} />}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input
type="text"
name="name"
id="name"
required
defaultValue={editingEmployee?.name || ""}
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"
placeholder="Enter full name"
/>
{actionData?.errors?.name && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.name}</p>
)}
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input
type="text"
name="name"
id="name"
required
defaultValue={editingEmployee?.name || ""}
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"
placeholder="Enter full name"
/>
{actionData?.errors?.name && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.name}</p>
)}
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input
type="text"
name="username"
id="username"
required
defaultValue={editingEmployee?.username || ""}
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"
placeholder="Enter username"
/>
{actionData?.errors?.username && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.username}</p>
)}
</div>
</div>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input
type="text"
name="username"
id="username"
required
defaultValue={editingEmployee?.username || ""}
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"
placeholder="Enter username"
/>
{actionData?.errors?.username && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.username}</p>
)}
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address
@ -563,47 +648,47 @@ export default function Employees() {
<p className="mt-1 text-sm text-red-600">{actionData.errors.email}</p>
)}
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Password {isEditing && <span className="text-gray-500">(leave blank to keep current)</span>}
</label>
<input
type="password"
name="password"
id="password"
required={!isEditing}
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"
placeholder={isEditing ? "Enter new password" : "Enter password"}
/>
{actionData?.errors?.password && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.password}</p>
)}
</div>
<div>
<label htmlFor="authLevel" className="block text-sm font-medium text-gray-700 mb-2">
Access Level
</label>
<select
name="authLevel"
id="authLevel"
required
defaultValue={editingEmployee?.authLevel || ""}
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="">Select access level</option>
<option value="1">Level 1 - User (Basic Access)</option>
<option value="2">Level 2 - Admin (Management Access)</option>
{user.authLevel === 3 && (
<option value="3">Level 3 - Super Admin (Full Access)</option>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Password {isEditing && <span className="text-gray-500">(leave blank to keep current)</span>}
</label>
<input
type="password"
name="password"
id="password"
required={!isEditing}
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"
placeholder={isEditing ? "Enter new password" : "Enter password"}
/>
{actionData?.errors?.password && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.password}</p>
)}
</select>
{actionData?.errors?.authLevel && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p>
)}
</div>
<div>
<label htmlFor="authLevel" className="block text-sm font-medium text-gray-700 mb-2">
Access Level
</label>
<select
name="authLevel"
id="authLevel"
required
defaultValue={editingEmployee?.authLevel || ""}
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="">Select access level</option>
<option value="1">Level 1 - User (Basic Access)</option>
<option value="2">Level 2 - Admin (Management Access)</option>
{user.authLevel === 3 && (
<option value="3">Level 3 - Super Admin (Full Access)</option>
)}
</select>
{actionData?.errors?.authLevel && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p>
)}
</div>
</div>
{isEditing && editingEmployee?.id !== user.id && (

View File

@ -166,14 +166,14 @@ export default function Equipment() {
return (
<DashboardLayout user={user}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900">Equipment Management</h1>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">Equipment Management</h1>
<p className="mt-1 text-sm text-gray-600">Manage your fleet equipment and machinery</p>
</div>
<button
onClick={handleAdd}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
>
<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" />
@ -182,10 +182,11 @@ export default function Equipment() {
</button>
</div>
{/* Equipment Table */}
{/* Equipment Table - Desktop */}
<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">
<div className="hidden sm:block">
<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">
@ -255,7 +256,57 @@ export default function Equipment() {
))}
</tbody>
</table>
</div>
</div>
{/* Equipment Cards - Mobile */}
<div className="sm:hidden">
<div className="space-y-4 p-4">
{equipment.map((item) => (
<div key={item.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center">
<div className={`h-10 w-10 rounded-full flex items-center justify-center ${getCategoryBadge(item.category)}`}>
{getCategoryIcon(item.category)}
</div>
<div className="ml-3">
<div className="text-sm font-medium text-gray-900">{item.model}</div>
<div className="text-xs text-gray-500 font-mono">#{item.number}</div>
</div>
</div>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getCategoryBadge(item.category)}`}>
{item.category}
</span>
</div>
<div className="flex flex-col space-y-2">
<button
onClick={() => handleEdit(item)}
className="w-full text-center px-3 py-2 text-sm text-indigo-600 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-150"
>
Edit Equipment
</button>
<Form method="post" className="w-full">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={item.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this equipment?")) {
e.preventDefault();
}
}}
className="w-full text-center px-3 py-2 text-sm text-red-600 bg-red-50 rounded-md hover:bg-red-100 transition-colors duration-150"
>
Delete Equipment
</button>
</Form>
</div>
</div>
))}
</div>
</div>
{equipment.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">

View File

@ -5,12 +5,23 @@ import { testEmailConnection } from "~/utils/mail.server";
import DashboardLayout from "~/components/DashboardLayout";
import { useState } from "react";
import { prisma } from "~/utils/db.server";
import { encryptMailSettings, decryptMailSettings, safeEncryptPassword } from "~/utils/encryption.server";
export async function loader({ request }: LoaderFunctionArgs) {
// Require auth level 3 to access mail settings
const user = await requireAuthLevel(request, 3);
const mailSettings = await prisma.mailSettings.findFirst();
const encryptedMailSettings = await prisma.mailSettings.findFirst();
// Decrypt settings for display (but mask the password)
let mailSettings = null;
if (encryptedMailSettings) {
const decrypted = decryptMailSettings(encryptedMailSettings);
mailSettings = {
...decrypted,
password: '••••••••' // Mask password for security
};
}
return json({ mailSettings, user });
}
@ -40,6 +51,9 @@ export async function action({ request }: ActionFunctionArgs) {
}
try {
// Encrypt the password before saving
const encryptedPassword = safeEncryptPassword(password);
// Check if settings exist
const existingSettings = await prisma.mailSettings.findFirst();
@ -52,7 +66,7 @@ export async function action({ request }: ActionFunctionArgs) {
port,
secure,
username,
password,
password: encryptedPassword, // Store encrypted password
fromName,
fromEmail,
},
@ -65,7 +79,7 @@ export async function action({ request }: ActionFunctionArgs) {
port,
secure,
username,
password,
password: encryptedPassword, // Store encrypted password
fromName,
fromEmail,
},
@ -74,6 +88,7 @@ export async function action({ request }: ActionFunctionArgs) {
return json({ success: "Mail settings saved successfully" });
} catch (error) {
console.error("Mail settings save error:", error);
return json({ error: "Failed to save mail settings" }, { status: 500 });
}
}
@ -85,13 +100,13 @@ export default function MailSettings() {
return (
<DashboardLayout user={user}>
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Mail Settings</h1>
<div className="max-w-2xl mx-auto p-4 sm:p-6">
<h1 className="text-xl sm:text-2xl font-bold mb-6">Mail Settings</h1>
{/* SMTP Configuration Examples */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="text-lg font-semibold text-blue-800 mb-2">Common SMTP Settings</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<h3 className="text-base sm:text-lg font-semibold text-blue-800 mb-2">Common SMTP Settings</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div>
<h4 className="font-medium text-blue-700">Gmail</h4>
<p>Host: smtp.gmail.com</p>
@ -240,7 +255,7 @@ export default function MailSettings() {
/>
</div>
<div className="flex space-x-4">
<div className="flex flex-col sm:flex-row sm:space-x-4 space-y-2 sm:space-y-0">
<button
type="submit"
className="flex-1 flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"

View File

@ -110,15 +110,16 @@ export default function ReportSheet() {
<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>
<h1 className="text-xl sm: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 */}
{/* Report Sheets Table - Desktop */}
<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">
<div className="hidden lg:block">
<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">
@ -216,7 +217,91 @@ export default function ReportSheet() {
))}
</tbody>
</table>
</div>
</div>
{/* Report Sheets Cards - Mobile */}
<div className="lg:hidden">
<div className="space-y-4 p-4">
{sheets.map((sheet) => (
<div key={sheet.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-start justify-between mb-3">
<div>
<div className="text-sm font-medium text-gray-900">
{new Date(sheet.date).toLocaleDateString('en-GB')}
</div>
<div className="text-xs text-gray-500">{sheet.area}</div>
</div>
<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>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Dredger:</span>
<span className="text-xs text-gray-900">{sheet.dredgerLocation}</span>
</div>
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Reclamation:</span>
<span className="text-xs text-gray-900">{sheet.reclamationLocation}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs font-medium text-gray-500">Shifts:</span>
<div className="flex space-x-1">
{sheet.dayReport && (
<span className={`inline-flex items-center px-2 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 py-0.5 rounded-full text-xs font-medium ${getShiftBadge('night')}`}>
{getShiftIcon('night')}
<span className="ml-1">Night</span>
</span>
)}
</div>
</div>
<div className="space-y-1">
{sheet.dayReport && (
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Day Employee:</span>
<span className="text-xs text-gray-900">{sheet.dayReport.employee.name}</span>
</div>
)}
{sheet.nightReport && (
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Night Employee:</span>
<span className="text-xs text-gray-900">{sheet.nightReport.employee.name}</span>
</div>
)}
</div>
</div>
<button
onClick={() => handleView(sheet)}
className="w-full text-center px-3 py-2 text-sm text-blue-600 bg-blue-50 rounded-md hover:bg-blue-100 transition-colors duration-150"
>
View Sheet Details
</button>
</div>
))}
</div>
</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">

View File

@ -407,20 +407,26 @@ export default function Reports() {
// First period
if (from1 && to1) {
const start1 = parseTime(from1);
const end1 = parseTime(to1);
let end1 = parseTime(to1);
if(end1 < start1)
end1 += 24 * 60;
totalMinutes += end1 - start1;
}
// Second period
if (from2 && to2) {
const start2 = parseTime(from2);
const end2 = parseTime(to2);
let end2 = parseTime(to2);
if(end2 < start2)
end2 += 24 * 60;
totalMinutes += end2 - start2;
}
return formatTime(Math.max(0, totalMinutes));
};
const calculateStoppageTime = (from: string, to: string) => {
if (!from || !to) return "00:00";
@ -436,7 +442,10 @@ export default function Reports() {
};
const startMinutes = parseTime(from);
const endMinutes = parseTime(to);
let endMinutes = parseTime(to);
if(endMinutes < startMinutes)
endMinutes += 24 * 60;
const totalMinutes = Math.max(0, endMinutes - startMinutes);
return formatTime(totalMinutes);
@ -540,14 +549,14 @@ export default function Reports() {
return (
<DashboardLayout user={user}>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900">Shifts Management</h1>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900">Shifts Management</h1>
<p className="mt-1 text-sm text-gray-600">Create and manage operational shifts</p>
</div>
<Link
to="/reports/new"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors duration-200"
>
<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" />
@ -556,107 +565,189 @@ export default function Reports() {
</Link>
</div>
{/* Reports Table */}
{/* Reports Table - Desktop */}
<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">
Shift Details
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Shift & 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">
Created
</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">
{reports.map((report) => (
<tr key={report.id} className="hover:bg-gray-50 transition-colors duration-150">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" 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>
<div className="hidden md:block">
<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">
Shift Details
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Shift & 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">
Created
</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">
{reports.map((report) => (
<tr key={report.id} className="hover:bg-gray-50 transition-colors duration-150">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" 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>
</div>
</div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">Shift #{report.id}</div>
<div className="text-sm text-gray-500">by {report.employee.name}</div>
</div>
</div>
<div className="ml-4">
<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>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col space-y-1">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge(report.shift)}`}>
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)} Shift
</span>
<span className="text-sm text-gray-900">{report.area.name}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
<div> {report.area.name} Dredger</div>
<div className="text-gray-500"> {report.dredgerLocation.name} - {report.reclamationLocation.name}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(report.createdDate).toLocaleDateString('en-GB')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<button
onClick={() => handleView(report)}
className="text-blue-600 hover:text-blue-900 transition-colors duration-150"
>
View
</button>
{canEditReport(report) && (
<>
<button
onClick={() => handleEdit(report)}
className="text-indigo-600 hover:text-indigo-900 transition-colors duration-150"
>
Edit
</button>
<Form method="post" className="inline">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={report.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this report?")) {
e.preventDefault();
}
}}
className="text-red-600 hover:text-red-900 transition-colors duration-150"
>
Delete
</button>
</Form>
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Reports Cards - Mobile */}
<div className="md:hidden">
<div className="space-y-4 p-4">
{reports.map((report) => (
<div key={report.id} className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center">
<div className="flex-shrink-0 h-10 w-10">
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<svg className="h-5 w-5 text-indigo-600" 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>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col space-y-1">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge(report.shift)}`}>
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)} Shift
</span>
<span className="text-sm text-gray-900">{report.area.name}</span>
<div className="ml-3">
<div className="text-sm font-medium text-gray-900">Shift #{report.id}</div>
<div className="text-xs text-gray-500">by {report.employee.name}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
<div> {report.area.name} Dredger</div>
<div className="text-gray-500"> {report.dredgerLocation.name} - {report.reclamationLocation.name}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{/* {new Date(report.createdDate).toLocaleDateString()} */}
{new Date(report.createdDate).toLocaleDateString('en-GB')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
</div>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge(report.shift)}`}>
{report.shift.charAt(0).toUpperCase() + report.shift.slice(1)}
</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Area:</span>
<span className="text-xs text-gray-900">{report.area.name}</span>
</div>
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Dredger:</span>
<span className="text-xs text-gray-900">{report.dredgerLocation.name}</span>
</div>
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Reclamation:</span>
<span className="text-xs text-gray-900">{report.reclamationLocation.name}</span>
</div>
<div className="flex justify-between">
<span className="text-xs font-medium text-gray-500">Created:</span>
<span className="text-xs text-gray-900">{new Date(report.createdDate).toLocaleDateString('en-GB')}</span>
</div>
</div>
<div className="flex flex-col space-y-2">
<button
onClick={() => handleView(report)}
className="w-full text-center px-3 py-2 text-sm text-blue-600 bg-blue-50 rounded-md hover:bg-blue-100 transition-colors duration-150"
>
View Details
</button>
{canEditReport(report) && (
<div className="flex space-x-2">
<button
onClick={() => handleView(report)}
className="text-blue-600 hover:text-blue-900 transition-colors duration-150"
onClick={() => handleEdit(report)}
className="flex-1 text-center px-3 py-2 text-sm text-indigo-600 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-150"
>
View
Edit
</button>
{canEditReport(report) && (
<>
<button
onClick={() => handleEdit(report)}
className="text-indigo-600 hover:text-indigo-900 transition-colors duration-150"
>
Edit
</button>
<Form method="post" className="inline">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={report.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this report?")) {
e.preventDefault();
}
}}
className="text-red-600 hover:text-red-900 transition-colors duration-150"
>
Delete
</button>
</Form>
</>
)}
<Form method="post" className="flex-1">
<input type="hidden" name="intent" value="delete" />
<input type="hidden" name="id" value={report.id} />
<button
type="submit"
onClick={(e) => {
if (!confirm("Are you sure you want to delete this report?")) {
e.preventDefault();
}
}}
className="w-full text-center px-3 py-2 text-sm text-red-600 bg-red-50 rounded-md hover:bg-red-100 transition-colors duration-150"
>
Delete
</button>
</Form>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
))}
</div>
</div>
{reports.length === 0 && (
<div className="text-center py-12">
<div className="text-center py-12 px-4">
<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>
@ -670,7 +761,7 @@ export default function Reports() {
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Create Shifs
Create Shifts
</Link>
</div>
</div>

View File

@ -35,10 +35,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const user = await requireAuthLevel(request, 1);
const formData = await request.formData();
// Debug logging
console.log("Form data received:", Object.fromEntries(formData.entries()));
const shift = formData.get("shift");
const areaId = formData.get("areaId");
const dredgerLocationId = formData.get("dredgerLocationId");
@ -50,8 +50,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
// Complex JSON fields
const reclamationHeightBase = formData.get("reclamationHeightBase");
const reclamationHeightExtra = formData.get("reclamationHeightExtra");
const pipelineMain = formData.get("pipelineMain");
const pipelineExt1 = formData.get("pipelineExt1");
const pipelineMain = formData.get("pipelineMain");
const pipelineExt1 = formData.get("pipelineExt1");
const pipelineReserve = formData.get("pipelineReserve");
const pipelineExt2 = formData.get("pipelineExt2");
const statsDozers = formData.get("statsDozers");
@ -64,7 +64,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
// Validation
// console.log("Validating fields:", { shift, areaId, dredgerLocationId, dredgerLineLength, reclamationLocationId, shoreConnection });
if (typeof shift !== "string" || !["day", "night"].includes(shift)) {
console.log("Shift validation failed:", shift);
return json({ errors: { shift: "Valid shift is required" } }, { status: 400 });
@ -114,8 +114,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
dredgerLocationId: parseInt(dredgerLocationId),
dredgerLineLength: parseInt(dredgerLineLength),
reclamationLocationId: parseInt(reclamationLocationId),
shoreConnection: parseInt(shoreConnection),
reclamationHeight: {
shoreConnection: parseInt(shoreConnection),
reclamationHeight: {
base: parseInt(reclamationHeightBase as string) || 0,
extra: parseInt(reclamationHeightExtra as string) || 0
},
@ -147,7 +147,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
parseInt(reclamationLocationId),
report.createdDate
);
// Redirect to reports page with success message
return redirect("/reports?success=Report created successfully!");
} catch (error) {
@ -193,7 +193,7 @@ export default function NewReport() {
total: string,
reason: string
}>>([]);
const [stoppageEntries, setStoppageEntries] = useState<Array<{
id: string,
from: string,
@ -207,8 +207,8 @@ export default function NewReport() {
const [currentStep, setCurrentStep] = useState(1);
const totalSteps = 4;
const isSubmitting = navigation.state === "submitting";
const isSubmitting = navigation.state === "submitting";
// Function to update form data
const updateFormData = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
@ -217,20 +217,20 @@ export default function NewReport() {
// Handle form submission - only allow on final step
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
// console.log("Form submit triggered, current step:", currentStep);
if (currentStep !== totalSteps) {
console.log("Preventing form submission - not on final step");
event.preventDefault();
event.stopPropagation();
return false;
}
// console.log("Allowing form submission");
// console.log("Form being submitted with data:", formData);
// console.log("Time sheet entries:", timeSheetEntries);
// console.log("Stoppage entries:", stoppageEntries);
};
// Helper functions for time calculations
const calculateTimeDifference = (from1: string, to1: string, from2: string, to2: string) => {
if (!from1 || !to1) return "00:00";
@ -250,17 +250,28 @@ export default function NewReport() {
if (from1 && to1) {
const start1 = parseTime(from1);
const end1 = parseTime(to1);
let end1 = parseTime(to1);
if (end1 < start1) {
end1 += 24 * 60;
}
totalMinutes += end1 - start1;
}
if (from2 && to2) {
const start2 = parseTime(from2);
const end2 = parseTime(to2);
let end2 = parseTime(to2);
if (end2 < start2) {
end2 += 24 * 60;
}
totalMinutes += end2 - start2;
}
return formatTime(Math.max(0, totalMinutes));
//return formatTime(Math.max(totalMinutes * -1, totalMinutes));
};
const calculateStoppageTime = (from: string, to: string) => {
@ -278,12 +289,14 @@ export default function NewReport() {
};
const startMinutes = parseTime(from);
const endMinutes = parseTime(to);
let endMinutes = parseTime(to);
if (endMinutes < startMinutes)
endMinutes += 24 * 60;
const totalMinutes = Math.max(0, endMinutes - startMinutes);
return formatTime(totalMinutes);
};
};
// Time Sheet management
const addTimeSheetEntry = () => {
const newEntry = {
@ -350,8 +363,8 @@ export default function NewReport() {
}
return entry;
}));
};
};
const nextStep = (event?: React.MouseEvent<HTMLButtonElement>) => {
if (event) {
event.preventDefault();
@ -384,15 +397,15 @@ export default function NewReport() {
<DashboardLayout user={user}>
<div className="max-w-full mx-auto">
{/* Header */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div className="mb-6 sm:mb-8">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
<div>
<h1 className="text-3xl font-bold text-gray-900">Create New Shifts</h1>
<p className="mt-2 text-gray-600">Fill out the operational shift details step by step</p>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Create New Shifts</h1>
<p className="mt-2 text-sm sm:text-base text-gray-600">Fill out the operational shift details step by step</p>
</div>
<Link
to="/reports"
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium 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 justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium 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="M10 19l-7-7m0 0l7-7m-7 7h18" />
@ -403,44 +416,42 @@ export default function NewReport() {
</div>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex items-center justify-between">
<div className="mb-6 sm:mb-8">
<div className="flex items-center justify-between px-2 sm:px-0">
{[1, 2, 3, 4].map((step) => (
<div key={step} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-2 ${
step <= currentStep
? 'bg-indigo-600 border-indigo-600 text-white'
: 'border-gray-300 text-gray-500'
}`}>
<div className={`flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full border-2 ${step <= currentStep
? 'bg-indigo-600 border-indigo-600 text-white'
: 'border-gray-300 text-gray-500'
}`}>
{step < currentStep ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg className="w-4 h-4 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<span className="text-sm font-medium">{step}</span>
<span className="text-xs sm:text-sm font-medium">{step}</span>
)}
</div>
{step < totalSteps && (
<div className={`flex-1 h-1 mx-4 ${
step < currentStep ? 'bg-indigo-600' : 'bg-gray-300'
}`} />
<div className={`flex-1 h-1 mx-2 sm:mx-4 ${step < currentStep ? 'bg-indigo-600' : 'bg-gray-300'
}`} />
)}
</div>
))}
</div>
<div className="mt-4 text-center">
<h2 className="text-xl font-semibold text-gray-900">{getStepTitle(currentStep)}</h2>
<p className="text-sm text-gray-500">Step {currentStep} of {totalSteps}</p>
</div>
<div className="mt-4 text-center">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">{getStepTitle(currentStep)}</h2>
<p className="text-xs sm:text-sm text-gray-500">Step {currentStep} of {totalSteps}</p>
</div>
</div>
{/* Form */}
<Form method="post" onSubmit={handleSubmit} className="bg-white shadow-lg rounded-lg overflow-hidden">
<div className="p-6">
<div className="p-4 sm:p-6">
{/* Step 1: Basic Information */}
{currentStep === 1 && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4 sm:space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div>
<label htmlFor="shift" className="block text-sm font-medium text-gray-700 mb-2">
Shift <span className="text-red-500">*</span>
@ -485,8 +496,8 @@ export default function NewReport() {
<p className="mt-1 text-sm text-red-600">{actionData.errors.areaId}</p>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div>
<label htmlFor="dredgerLocationId" className="block text-sm font-medium text-gray-700 mb-2">
Dredger Location <span className="text-red-500">*</span>
@ -532,11 +543,11 @@ export default function NewReport() {
</div>
</div>
</div>
)}
{/* Step 2: Location & Pipeline Details */}
)}
{/* Step 2: Location & Pipeline Details */}
{currentStep === 2 && (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4 sm:space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div>
<label htmlFor="reclamationLocationId" className="block text-sm font-medium text-gray-700 mb-2">
Reclamation Location <span className="text-red-500">*</span>
@ -560,15 +571,15 @@ export default function NewReport() {
</div>
</div>
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Reclamation Height</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Reclamation Height</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div><label htmlFor="reclamationHeightBase" className="block text-sm font-medium text-gray-700 mb-2">Base Height (m)</label><input type="number" id="reclamationHeightBase" name="reclamationHeightBase" min="0" value={formData.reclamationHeightBase} onChange={(e) => updateFormData('reclamationHeightBase', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="reclamationHeightExtra" className="block text-sm font-medium text-gray-700 mb-2">Extra Height (m)</label><input type="number" id="reclamationHeightExtra" name="reclamationHeightExtra" min="0" value={formData.reclamationHeightExtra} onChange={(e) => updateFormData('reclamationHeightExtra', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
</div>
</div>
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Pipeline Length</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Pipeline Length</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
<div><label htmlFor="pipelineMain" className="block text-sm font-medium text-gray-700 mb-2">Main</label><input type="number" id="pipelineMain" name="pipelineMain" min="0" value={formData.pipelineMain} onChange={(e) => updateFormData('pipelineMain', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="pipelineExt1" className="block text-sm font-medium text-gray-700 mb-2">Extension 1</label><input type="number" id="pipelineExt1" name="pipelineExt1" min="0" value={formData.pipelineExt1} onChange={(e) => updateFormData('pipelineExt1', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="pipelineReserve" className="block text-sm font-medium text-gray-700 mb-2">Reserve</label><input type="number" id="pipelineReserve" name="pipelineReserve" min="0" value={formData.pipelineReserve} onChange={(e) => updateFormData('pipelineReserve', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
@ -576,13 +587,13 @@ export default function NewReport() {
</div>
</div>
</div>
)}
{/* Step 3: Equipment & Time Sheet */}
)}
{/* Step 3: Equipment & Time Sheet */}
{currentStep === 3 && (
<div className="space-y-6">
<div className="space-y-4 sm:space-y-6">
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
<div><label htmlFor="statsDozers" className="block text-sm font-medium text-gray-700 mb-2">Dozers</label><input type="number" id="statsDozers" name="statsDozers" min="0" value={formData.statsDozers} onChange={(e) => updateFormData('statsDozers', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="statsExc" className="block text-sm font-medium text-gray-700 mb-2">Excavators</label><input type="number" id="statsExc" name="statsExc" min="0" value={formData.statsExc} onChange={(e) => updateFormData('statsExc', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label htmlFor="statsLoaders" className="block text-sm font-medium text-gray-700 mb-2">Loaders</label><input type="number" id="statsLoaders" name="statsLoaders" min="0" value={formData.statsLoaders} onChange={(e) => updateFormData('statsLoaders', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
@ -602,7 +613,11 @@ export default function NewReport() {
{timeSheetEntries.map((entry) => (
<div key={entry.id} className="bg-gray-50 p-4 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-7 gap-4">
<div><label className="block text-sm font-medium text-gray-700 mb-1">Equipment</label><select value={entry.machine} onChange={(e) => updateTimeSheetEntry(entry.id, 'machine', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"><option value="">Select Equipment</option>{equipment.map((item) => (<option key={item.id} value={`${item.model} (${item.number})`}>{item.category} - {item.model} ({item.number})</option>))}</select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">Equipment</label><select value={entry.machine} onChange={(e) => updateTimeSheetEntry(entry.id, 'machine', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"><option value="">Select Equipment</option>{equipment.filter((item) => {
const machineValue = `${item.model} (${item.number})`;
// Show if not selected by any other entry, or if it's the current entry's selection
return !timeSheetEntries.some(e => e.id !== entry.id && e.machine === machineValue);
}).map((item) => (<option key={item.id} value={`${item.model} (${item.number})`}>{item.category} - {item.model} ({item.number})</option>))}</select></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">From</label><input type="time" value={entry.from1} onChange={(e) => updateTimeSheetEntry(entry.id, 'from1', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">To</label><input type="time" value={entry.to1} onChange={(e) => updateTimeSheetEntry(entry.id, 'to1', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
<div><label className="block text-sm font-medium text-gray-700 mb-1">From 2</label><input type="time" value={entry.from2} onChange={(e) => updateTimeSheetEntry(entry.id, 'from2', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" /></div>
@ -619,8 +634,8 @@ export default function NewReport() {
)}
</div>
</div>
)}
{/* Step 4: Stoppages & Notes */}
)}
{/* Step 4: Stoppages & Notes */}
{currentStep === 4 && (
<div className="space-y-6">
<div>
@ -655,8 +670,8 @@ export default function NewReport() {
<textarea id="notes" name="notes" rows={4} value={formData.notes} onChange={(e) => updateFormData('notes', e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" placeholder="Enter any additional notes or comments about the operation..." />
</div>
</div>
)}
{/* Error Message */}
)}
{/* Error Message */}
{actionData?.errors?.form && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
<div className="flex">
@ -672,18 +687,18 @@ export default function NewReport() {
</div>
{/* Navigation Buttons */}
<div className="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-between">
<button type="button" onClick={prevStep} disabled={currentStep === 1} className={`inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium ${currentStep === 1 ? 'text-gray-400 bg-gray-100 cursor-not-allowed' : 'text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500'}`}>
<div className="px-4 sm:px-6 py-4 bg-gray-50 border-t border-gray-200 flex flex-col sm:flex-row sm:justify-between space-y-3 sm:space-y-0">
<button type="button" onClick={prevStep} disabled={currentStep === 1} className={`inline-flex items-center justify-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium ${currentStep === 1 ? 'text-gray-400 bg-gray-100 cursor-not-allowed' : '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="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>Previous
</button>
<div className="flex space-x-3">
{currentStep < totalSteps ? (
<button type="button" onClick={(e) => nextStep(e)} className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<button type="button" onClick={(e) => nextStep(e)} className="flex-1 sm:flex-none inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Next<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" /></svg>
</button>
) : (
<button type="submit" disabled={isSubmitting} className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed">
<button type="submit" disabled={isSubmitting} className="flex-1 sm:flex-none inline-flex items-center justify-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed">
{isSubmitting ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
@ -704,7 +719,7 @@ export default function NewReport() {
{/* Hidden inputs for dynamic data */}
<input type="hidden" name="timeSheetData" value={JSON.stringify(timeSheetEntries)} />
<input type="hidden" name="stoppagesData" value={JSON.stringify(stoppageEntries)} />
{/* Hidden inputs for form data from all steps */}
{currentStep !== 1 && (
<>

View File

@ -45,15 +45,15 @@ export default function SignIn() {
const actionData = useActionData<typeof action>();
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center py-8 px-4 sm:py-12 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-6 sm:space-y-8">
<div className="text-center">
<img
className="mx-auto h-28 w-auto"
className="mx-auto h-20 sm:h-28 w-auto"
src="/clogo-sm.png"
alt="Phosphat Report"
/>
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
<h2 className="mt-4 sm:mt-6 text-2xl sm:text-3xl font-extrabold text-gray-900">
Sign in to your account
</h2>
<p className="mt-2 text-sm text-gray-600">

View File

@ -81,15 +81,15 @@ export default function SignUp() {
const actionData = useActionData<typeof action>();
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center py-8 px-4 sm:py-12 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-6 sm:space-y-8">
<div className="text-center">
<img
className="mx-auto h-24 w-auto"
className="mx-auto h-20 sm:h-24 w-auto"
src="/clogo-sm.png"
alt="Phosphat Report"
/>
<h2 className="mt-6 text-3xl font-extrabold text-gray-900">
<h2 className="mt-4 sm:mt-6 text-2xl sm:text-3xl font-extrabold text-gray-900">
Create your account
</h2>
<p className="mt-2 text-sm text-gray-600">

View File

@ -43,8 +43,8 @@ export default function TestEmail() {
return (
<DashboardLayout user={user}>
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Test Email</h1>
<div className="max-w-2xl mx-auto p-4 sm:p-6">
<h1 className="text-xl sm:text-2xl font-bold mb-6">Test Email</h1>
<p className="text-gray-600 mb-6">
Use this form to test your email configuration. Only users with auth level 3 can access this feature.
</p>

View File

@ -0,0 +1,143 @@
import type { LoaderFunctionArgs } 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 { testEncryption, encrypt, decrypt } from "~/utils/encryption.server";
export async function loader({ request }: LoaderFunctionArgs) {
// Require auth level 3 to access encryption test
const user = await requireAuthLevel(request, 3);
// Test encryption with sample data
const testResults = testEncryption("sample-smtp-password-123");
// Test with different types of data
const tests = [
{ name: "Simple Password", data: "mypassword123" },
{ name: "Complex Password", data: "P@ssw0rd!@#$%^&*()" },
{ name: "Email Password", data: "smtp.gmail.app.password" },
{ name: "Special Characters", data: "test!@#$%^&*()_+-=[]{}|;:,.<>?" }
];
const testResults2 = tests.map(test => {
try {
const encrypted = encrypt(test.data);
const decrypted = decrypt(encrypted);
return {
...test,
encrypted,
decrypted,
success: decrypted === test.data,
encryptedLength: encrypted.length
};
} catch (error) {
return {
...test,
error: error instanceof Error ? error.message : 'Unknown error',
success: false
};
}
});
return json({
user,
basicTest: testResults,
detailedTests: testResults2
});
}
export default function TestEncryption() {
const { user, basicTest, detailedTests } = useLoaderData<typeof loader>();
return (
<DashboardLayout user={user}>
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Encryption Test Results</h1>
{/* Basic Test */}
<div className="bg-white shadow rounded-lg p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Basic Encryption Test</h2>
<div className="space-y-2">
<div className="flex items-center">
<span className="font-medium w-20">Status:</span>
<span className={`px-2 py-1 rounded text-sm ${
basicTest.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{basicTest.success ? 'PASSED' : 'FAILED'}
</span>
</div>
{basicTest.success ? (
<>
<div><span className="font-medium">Original:</span> {basicTest.original}</div>
<div><span className="font-medium">Encrypted:</span> <code className="bg-gray-100 px-2 py-1 rounded text-sm">{basicTest.encrypted}</code></div>
<div><span className="font-medium">Decrypted:</span> {basicTest.decrypted}</div>
<div><span className="font-medium">Valid:</span> {basicTest.isValid ? '✅' : '❌'}</div>
</>
) : (
<div className="text-red-600">Error: {basicTest.error}</div>
)}
</div>
</div>
{/* Detailed Tests */}
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-lg font-semibold mb-4">Detailed Encryption Tests</h2>
<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">Test Name</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">Original</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Encrypted Length</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Match</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{detailedTests.map((test, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{test.name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded text-xs ${
test.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
}`}>
{test.success ? 'PASS' : 'FAIL'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<code className="bg-gray-100 px-2 py-1 rounded text-xs">
{test.data.length > 20 ? test.data.substring(0, 20) + '...' : test.data}
</code>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{test.encryptedLength || 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{test.success ? '✅' : '❌'}
{test.error && <div className="text-red-500 text-xs mt-1">{test.error}</div>}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Security Notes */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mt-6">
<h3 className="text-lg font-semibold text-yellow-800 mb-2">Security Notes</h3>
<ul className="text-sm text-yellow-700 space-y-1">
<li> Passwords are encrypted using AES-256-CBC algorithm</li>
<li> Each encryption uses a random IV (Initialization Vector)</li>
<li> Encrypted data format: IV:EncryptedData</li>
<li> Encryption key should be set via ENCRYPTION_KEY environment variable</li>
<li> This test page should only be accessible to Super Admins</li>
</ul>
</div>
</div>
</DashboardLayout>
);
}

View File

@ -0,0 +1,152 @@
import crypto from 'crypto';
// Get encryption key from environment or generate a default one
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'phosphat-report-default-key-32b'; // Must be 32 characters
const ALGORITHM = 'aes-256-cbc';
const IV_LENGTH = 16; // For AES, this is always 16
// Ensure the key is exactly 32 bytes
function getEncryptionKey(): Buffer {
const key = ENCRYPTION_KEY.padEnd(32, '0').substring(0, 32);
return Buffer.from(key, 'utf8');
}
/**
* Encrypts a plain text string
* @param text - The plain text to encrypt
* @returns Encrypted string in format: iv:encryptedData
*/
export function encrypt(text: string): string {
if (!text) return '';
try {
const key = getEncryptionKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipher(ALGORITHM, key);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Return iv and encrypted data separated by ':'
return iv.toString('hex') + ':' + encrypted;
} catch (error) {
console.error('Encryption error:', error);
throw new Error('Failed to encrypt data');
}
}
/**
* Decrypts an encrypted string
* @param encryptedText - The encrypted text in format: iv:encryptedData
* @returns Decrypted plain text string
*/
export function decrypt(encryptedText: string): string {
if (!encryptedText) return '';
try {
const key = getEncryptionKey();
const textParts = encryptedText.split(':');
if (textParts.length !== 2) {
throw new Error('Invalid encrypted text format');
}
const iv = Buffer.from(textParts[0], 'hex');
const encryptedData = textParts[1];
const decipher = crypto.createDecipher(ALGORITHM, key);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('Decryption error:', error);
throw new Error('Failed to decrypt data');
}
}
/**
* Encrypts sensitive mail settings
* @param settings - Mail settings object
* @returns Mail settings with encrypted password
*/
export function encryptMailSettings(settings: {
host: string;
port: number;
secure: boolean;
username: string;
password: string;
fromName: string;
fromEmail: string;
}) {
return {
...settings,
password: encrypt(settings.password)
};
}
/**
* Decrypts sensitive mail settings
* @param settings - Mail settings object with encrypted password
* @returns Mail settings with decrypted password
*/
export function decryptMailSettings(settings: {
host: string;
port: number;
secure: boolean;
username: string;
password: string;
fromName: string;
fromEmail: string;
}) {
return {
...settings,
password: decrypt(settings.password)
};
}
/**
* Validates if a string is encrypted (contains ':' separator)
* @param text - Text to validate
* @returns True if text appears to be encrypted
*/
export function isEncrypted(text: string): boolean {
return text.includes(':') && text.split(':').length === 2;
}
/**
* Safely encrypts a password only if it's not already encrypted
* @param password - Password to encrypt
* @returns Encrypted password
*/
export function safeEncryptPassword(password: string): string {
if (isEncrypted(password)) {
return password; // Already encrypted
}
return encrypt(password);
}
/**
* Test encryption/decryption functionality
* @param testText - Text to test with
* @returns Test results
*/
export function testEncryption(testText: string = 'test-password-123') {
try {
const encrypted = encrypt(testText);
const decrypted = decrypt(encrypted);
return {
success: decrypted === testText,
original: testText,
encrypted: encrypted,
decrypted: decrypted,
isValid: decrypted === testText
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}

View File

@ -33,61 +33,203 @@ interface ReportData {
notes: string;
}
export async function exportReportToExcel(report: ReportData) {
interface ReportSheet {
id: string;
date: string;
area: string;
dredgerLocation: string;
reclamationLocation: string;
dayReport?: ReportData;
nightReport?: ReportData;
}
// Function to add logo images to worksheet
async function addLogosToWorksheet(worksheet: ExcelJS.Worksheet, currentRow: number) {
try {
// Try to add company logo (left side)
try {
const logoResponse = await fetch('/logo03.png');
if (logoResponse.ok) {
const logoBuffer = await logoResponse.arrayBuffer();
const logoImageId = worksheet.workbook.addImage({
buffer: logoBuffer,
extension: 'png',
});
worksheet.addImage(logoImageId, {
tl: { col: 0, row: currentRow - 1 },
ext: { width: 100, height: 60 }
});
}
} catch (error) {
console.log('Company logo not found, using text placeholder');
}
// Try to add Arab Potash logo (right side)
try {
const arabPotashResponse = await fetch('/logo-light.png');
if (arabPotashResponse.ok) {
const arabPotashBuffer = await arabPotashResponse.arrayBuffer();
const arabPotashImageId = worksheet.workbook.addImage({
buffer: arabPotashBuffer,
extension: 'png',
});
worksheet.addImage(arabPotashImageId, {
tl: { col: 3, row: currentRow - 1 },
ext: { width: 100, height: 60 }
});
}
} catch (error) {
console.log('Arab Potash logo not found, using text placeholder');
}
} catch (error) {
console.log('Error loading images:', error);
}
}
export async function exportReportToExcel(reportOrSheet: ReportData | ReportSheet) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Report');
// Set column widths to match the professional layout from the reference file
// Set RTL (Right-to-Left) layout
worksheet.views = [{ rightToLeft: false }];
// Set column widths to match the report view layout exactly
worksheet.columns = [
{ width: 30 }, // A - Labels/Machine names
{ width: 20 }, // B - Data/Values
{ width: 20 }, // C - Labels/Data
{ width: 20 }, // D - Data/Values
{ width: 15 }, // E - Pipeline data
{ width: 15 }, // F - Pipeline data
{ width: 25 } // G - Reason/Notes
{ width: 25 }, // A - First column (25% width)
{ width: 25 }, // B - Second column (25% width)
{ width: 25 }, // C - Third column (25% width)
{ width: 25 }, // D - Fourth column (25% width)
];
let currentRow = 1;
// 1. HEADER SECTION - Professional layout matching reference file
// Main header with company info
worksheet.mergeCells(`A${currentRow}:E${currentRow + 2}`);
const headerCell = worksheet.getCell(`A${currentRow}`);
headerCell.value = 'Reclamation Work Diary';
headerCell.style = {
// Check if this is a ReportSheet (combined day/night) or single Report
const isReportSheet = 'dayReport' in reportOrSheet || 'nightReport' in reportOrSheet;
if (isReportSheet) {
await exportReportSheet(worksheet, reportOrSheet as ReportSheet, currentRow);
} else {
await exportSingleReport(worksheet, reportOrSheet as ReportData, currentRow);
}
// Generate and save file
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
let fileName: string;
if (isReportSheet) {
const sheet = reportOrSheet as ReportSheet;
fileName = `ReportSheet_${sheet.id}_${new Date(sheet.date).toLocaleDateString('en-GB').replace(/\//g, '-')}.xlsx`;
} else {
const report = reportOrSheet as ReportData;
fileName = `Report_${report.id}_${new Date(report.createdDate).toLocaleDateString('en-GB').replace(/\//g, '-')}.xlsx`;
}
FileSaver.saveAs(blob, fileName);
}
async function exportReportSheet(worksheet: ExcelJS.Worksheet, sheet: ReportSheet, startRow: number) {
let currentRow = startRow;
// Add logos
await addLogosToWorksheet(worksheet, currentRow);
// 1. HEADER SECTION
currentRow = await addReportSheetHeader(worksheet, sheet, currentRow);
// 2. REPORT INFO SECTION
currentRow = await addReportSheetInfo(worksheet, sheet, currentRow);
// 3. DREDGER SECTION
currentRow = await addDredgerSection(worksheet, sheet.area, currentRow);
// 4. LOCATION DATA SECTION
const report = sheet.dayReport || sheet.nightReport;
if (report) {
currentRow = await addLocationData(worksheet, report, currentRow);
currentRow = await addPipelineLength(worksheet, report, currentRow);
}
// 5. DAY SHIFT SECTION
if (sheet.dayReport) {
currentRow = await addShiftSection(worksheet, sheet.dayReport, 'Day', currentRow);
}
// 6. NIGHT SHIFT SECTION
if (sheet.nightReport) {
currentRow = await addShiftSection(worksheet, sheet.nightReport, 'Night', currentRow);
}
// 7. FOOTER
await addFooter(worksheet, currentRow);
}
async function exportSingleReport(worksheet: ExcelJS.Worksheet, report: ReportData, startRow: number) {
let currentRow = startRow;
// Add logos
await addLogosToWorksheet(worksheet, currentRow);
// 1. HEADER SECTION
currentRow = await addSingleReportHeader(worksheet, report, currentRow);
// 2. REPORT INFO SECTION
currentRow = await addSingleReportInfo(worksheet, report, currentRow);
// 3. DREDGER SECTION
currentRow = await addDredgerSection(worksheet, report.area, currentRow);
// 4. LOCATION DATA SECTION
currentRow = await addLocationData(worksheet, report, currentRow);
// 5. PIPELINE LENGTH SECTION
currentRow = await addPipelineLength(worksheet, report, currentRow);
// 6. SHIFT SECTION
currentRow = await addShiftSection(worksheet, report, report.shift, currentRow);
// 7. FOOTER
await addFooter(worksheet, currentRow);
}
// Helper functions for building Excel sections
async function addReportSheetHeader(worksheet: ExcelJS.Worksheet, sheet: ReportSheet, currentRow: number): Promise<number> {
// Create the 3-column header layout
worksheet.mergeCells(`A${currentRow}:A${currentRow + 2}`);
const logoLeftCell = worksheet.getCell(`A${currentRow}`);
logoLeftCell.value = '';
logoLeftCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Middle section - main header
worksheet.mergeCells(`B${currentRow}:C${currentRow}`);
const headerMainCell = worksheet.getCell(`B${currentRow}`);
headerMainCell.value = 'Reclamation Work Diary';
headerMainCell.style = {
font: { name: 'Arial', size: 16, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thin', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Logo area
worksheet.mergeCells(`F${currentRow}:G${currentRow + 2}`);
const logoCell = worksheet.getCell(`F${currentRow}`);
logoCell.value = 'Arab Potash\nCompany Logo';
logoCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'center', vertical: 'middle', wrapText: true },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// Sub-header info
const qfCell = worksheet.getCell(`A${currentRow + 1}`);
// QF code
worksheet.mergeCells(`B${currentRow + 1}:C${currentRow + 1}`);
const qfCell = worksheet.getCell(`B${currentRow + 1}`);
qfCell.value = 'QF-3.6.1-08';
qfCell.style = {
font: { name: 'Arial', size: 10 },
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thin', color: { argb: '000000' } },
@ -97,10 +239,12 @@ export async function exportReportToExcel(report: ReportData) {
}
};
const revCell = worksheet.getCell(`A${currentRow + 2}`);
// Rev code
worksheet.mergeCells(`B${currentRow + 2}:C${currentRow + 2}`);
const revCell = worksheet.getCell(`B${currentRow + 2}`);
revCell.value = 'Rev. 1.0';
revCell.style = {
font: { name: 'Arial', size: 10 },
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thin', color: { argb: '000000' } },
@ -110,33 +254,90 @@ export async function exportReportToExcel(report: ReportData) {
}
};
currentRow += 4; // Skip to next section
// 2. REPORT INFO SECTION - Professional table layout
const infoRowCells = [
{ col: 'A', label: 'Date:', value: new Date(report.createdDate).toLocaleDateString('en-GB') },
{ col: 'C', label: 'Report No.', value: report.id.toString() }
];
// Create bordered info section
['A', 'B', 'C', 'D', 'E', 'F', 'G'].forEach(col => {
const cell = worksheet.getCell(`${col}${currentRow}`);
cell.style = {
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
// Right logo
worksheet.mergeCells(`D${currentRow}:D${currentRow + 2}`);
const logoRightCell = worksheet.getCell(`D${currentRow}`);
logoRightCell.value = '';
logoRightCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
return currentRow + 4;
}
async function addSingleReportHeader(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
return await addReportSheetHeader(worksheet, { area: report.area.name } as ReportSheet, currentRow);
}
async function addReportSheetInfo(worksheet: ExcelJS.Worksheet, sheet: ReportSheet, currentRow: number): Promise<number> {
const dateCell = worksheet.getCell(`A${currentRow}`);
dateCell.value = 'Date:';
dateCell.style = {
font: { name: 'Arial', size: 11, bold: true },
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'left', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const dateValueCell = worksheet.getCell(`B${currentRow}`);
dateValueCell.value = new Date(sheet.date).toLocaleDateString('en-GB');
dateValueCell.style = {
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'left', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const reportNoCell = worksheet.getCell(`C${currentRow}`);
reportNoCell.value = 'Report No.';
reportNoCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'left', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const reportNoValueCell = worksheet.getCell(`D${currentRow}`);
reportNoValueCell.value = sheet.id.toString();
reportNoValueCell.style = {
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'left', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
return currentRow + 2;
}
async function addSingleReportInfo(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
const dateCell = worksheet.getCell(`A${currentRow}`);
dateCell.value = 'Date:';
dateCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'left', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F0F0F0' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -148,22 +349,8 @@ export async function exportReportToExcel(report: ReportData) {
const dateValueCell = worksheet.getCell(`B${currentRow}`);
dateValueCell.value = new Date(report.createdDate).toLocaleDateString('en-GB');
dateValueCell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const reportNoCell = worksheet.getCell(`E${currentRow}`);
reportNoCell.value = 'Report No.';
reportNoCell.style = {
font: { name: 'Arial', size: 11, bold: true },
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'left', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F0F0F0' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -172,11 +359,11 @@ export async function exportReportToExcel(report: ReportData) {
}
};
const reportNoValueCell = worksheet.getCell(`F${currentRow}`);
reportNoValueCell.value = report.id.toString();
reportNoValueCell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
const shiftNoCell = worksheet.getCell(`C${currentRow}`);
shiftNoCell.value = 'Shift No.';
shiftNoCell.style = {
font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'left', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -185,34 +372,55 @@ export async function exportReportToExcel(report: ReportData) {
}
};
currentRow += 2; // Skip empty row
const shiftNoValueCell = worksheet.getCell(`D${currentRow}`);
shiftNoValueCell.value = report.id.toString();
shiftNoValueCell.style = {
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'left', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
// 3. DREDGER SECTION - Professional centered title
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
return currentRow + 2;
}
async function addDredgerSection(worksheet: ExcelJS.Worksheet, area: { name: string } | string, currentRow: number): Promise<number> {
const areaName = typeof area === 'string' ? area : area.name;
worksheet.mergeCells(`A${currentRow}:D${currentRow}`);
const dredgerCell = worksheet.getCell(`A${currentRow}`);
dredgerCell.value = `${report.area.name} Dredger`;
dredgerCell.value = `${areaName} Dredger`;
dredgerCell.style = {
font: { name: 'Arial', size: 18, bold: true, underline: true },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E6F3FF' } }
alignment: { horizontal: 'center', vertical: 'middle' }
};
currentRow += 2; // Skip empty row
// 4. LOCATION DATA SECTION - Professional table with green headers
const locationRows = [
['Dredger Location', report.dredgerLocation.name, '', 'Dredger Line Length', report.dredgerLineLength.toString()],
['Reclamation Location', report.reclamationLocation.name, '', 'Shore Connection', report.shoreConnection.toString()],
['Reclamation Height', `${report.reclamationHeight?.base || 0}m - ${(report.reclamationHeight?.base || 0) + (report.reclamationHeight?.extra || 0)}m`, '', '', '']
return currentRow + 2;
}
async function addLocationData(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
const locationData = [
['Dredger Location', report.dredgerLocation.name, 'Dredger Line Length', report.dredgerLineLength.toString()],
['Reclamation Location', report.reclamationLocation.name, 'Shore Connection', report.shoreConnection.toString()],
['Reclamation Height', `${report.reclamationHeight?.base || 0}m - ${(report.reclamationHeight?.base || 0) + (report.reclamationHeight?.extra || 0)}m`, '', '']
];
locationRows.forEach((rowData, index) => {
locationData.forEach((rowData, index) => {
const row = currentRow + index;
// Apply styling to all cells in the row first
for (let col = 1; col <= 7; col++) {
const cell = worksheet.getCell(row, col);
rowData.forEach((cellValue, colIndex) => {
const cell = worksheet.getCell(row, colIndex + 1);
cell.value = cellValue;
const isGreenHeader = (colIndex === 0 || colIndex === 2) && cellValue !== '';
cell.style = {
font: { name: 'Arial', size: 11, bold: isGreenHeader, color: isGreenHeader ? { argb: 'FFFFFF' } : { argb: '000000' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: isGreenHeader ? { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } } : { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -220,52 +428,31 @@ export async function exportReportToExcel(report: ReportData) {
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
rowData.forEach((cellValue, colIndex) => {
if (cellValue !== '') {
const cell = worksheet.getCell(row, colIndex + 1);
cell.value = cellValue;
const isGreenHeader = (colIndex === 0 || colIndex === 3);
cell.style = {
font: { name: 'Arial', size: 11, bold: isGreenHeader, color: isGreenHeader ? { argb: 'FFFFFF' } : { argb: '000000' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: isGreenHeader ? { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } } : { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
});
// Merge cells for better layout
if (index === 0) {
worksheet.mergeCells(`B${row}:C${row}`); // Dredger Location value
worksheet.mergeCells(`E${row}:G${row}`); // Dredger Line Length value
} else if (index === 1) {
worksheet.mergeCells(`B${row}:C${row}`); // Reclamation Location value
worksheet.mergeCells(`E${row}:G${row}`); // Shore Connection value
} else if (index === 2) {
worksheet.mergeCells(`B${row}:G${row}`); // Reclamation Height spans all remaining columns
}
});
currentRow += 4; // Skip empty row
return currentRow + 4;
}
async function addPipelineLength(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
// Expand to 7 columns for pipeline section
worksheet.columns = [
{ width: 30 }, // A - Pipeline header
{ width: 15 }, // B - Main
{ width: 15 }, // C - extension
{ width: 15 }, // D - total
{ width: 15 }, // E - Reserve
{ width: 15 }, // F - extension
{ width: 15 } // G - total
];
// 5. PIPELINE LENGTH SECTION - Professional table with green headers
const pipelineHeaderRow = currentRow;
// First row - main header with rowspan
const mainHeaderCell = worksheet.getCell(pipelineHeaderRow, 1);
mainHeaderCell.value = 'Pipeline Length "from Shore Connection"';
mainHeaderCell.style = {
// Pipeline header row
const pipelineHeaderCell = worksheet.getCell(`A${currentRow}`);
pipelineHeaderCell.value = 'Pipeline Length "from Shore Connection"';
pipelineHeaderCell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -274,15 +461,15 @@ export async function exportReportToExcel(report: ReportData) {
}
};
// Sub-headers
// Pipeline sub-headers
const pipelineSubHeaders = ['Main', 'extension', 'total', 'Reserve', 'extension', 'total'];
pipelineSubHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(pipelineHeaderRow, colIndex + 2);
const cell = worksheet.getCell(currentRow, colIndex + 2);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 10, bold: true, color: { argb: 'FFFFFF' } },
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -292,9 +479,20 @@ export async function exportReportToExcel(report: ReportData) {
};
});
// Data row
const pipelineDataRow = currentRow + 1;
const pipelineData = ['',
// Pipeline data row
currentRow++;
const pipelineDataCell = worksheet.getCell(`A${currentRow}`);
pipelineDataCell.value = '';
pipelineDataCell.style = {
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
const pipelineData = [
(report.pipelineLength?.main || 0).toString(),
(report.pipelineLength?.ext1 || 0).toString(),
((report.pipelineLength?.main || 0) + (report.pipelineLength?.ext1 || 0)).toString(),
@ -304,12 +502,11 @@ export async function exportReportToExcel(report: ReportData) {
];
pipelineData.forEach((data, colIndex) => {
const cell = worksheet.getCell(pipelineDataRow, colIndex + 1);
const cell = worksheet.getCell(currentRow, colIndex + 2);
cell.value = data;
cell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -319,16 +516,18 @@ export async function exportReportToExcel(report: ReportData) {
};
});
currentRow += 4; // Skip empty row
// 6. SHIFT HEADER SECTION - Professional full-width header
return currentRow + 2;
}
async function addShiftSection(worksheet: ExcelJS.Worksheet, report: ReportData, shiftName: string, currentRow: number): Promise<number> {
// SHIFT HEADER SECTION
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const shiftCell = worksheet.getCell(`A${currentRow}`);
shiftCell.value = `${report.shift.charAt(0).toUpperCase() + report.shift.slice(1)} Shift`;
shiftCell.value = `${shiftName.charAt(0).toUpperCase() + shiftName.slice(1)} Shift`;
shiftCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -337,33 +536,26 @@ export async function exportReportToExcel(report: ReportData) {
}
};
currentRow += 2; // Skip empty row
currentRow += 1;
// 7. EQUIPMENT STATS SECTION - Professional table with green headers
// EQUIPMENT STATS SECTION
const equipmentHeaders = ['Dozers', 'Exc.', 'Loader', 'Foreman', 'Laborer'];
// Apply borders to all cells in the equipment section
for (let col = 1; col <= 7; col++) {
for (let row = currentRow; row <= currentRow + 1; row++) {
const cell = worksheet.getCell(row, col);
cell.style = {
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
}
// Adjust columns back to 5 for equipment section
worksheet.columns = [
{ width: 20 }, // A - Dozers
{ width: 20 }, // B - Exc.
{ width: 20 }, // C - Loader
{ width: 20 }, // D - Foreman
{ width: 20 } // E - Laborer
];
equipmentHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
font: { name: 'Arial', size: 11, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -373,6 +565,7 @@ export async function exportReportToExcel(report: ReportData) {
};
});
currentRow++;
const equipmentData = [
(report.stats?.Dozers || 0).toString(),
(report.stats?.Exc || 0).toString(),
@ -382,12 +575,11 @@ export async function exportReportToExcel(report: ReportData) {
];
equipmentData.forEach((data, colIndex) => {
const cell = worksheet.getCell(currentRow + 1, colIndex + 1);
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = data;
cell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -397,18 +589,51 @@ export async function exportReportToExcel(report: ReportData) {
};
});
currentRow += 4; // Skip empty row
currentRow += 2;
// 8. TIME SHEET SECTION - Professional table
const createProfessionalTable = (headers: string[], data: any[][], startRow: number) => {
// Headers
headers.forEach((header, colIndex) => {
const cell = worksheet.getCell(startRow, colIndex + 1);
cell.value = header;
// TIME SHEET SECTION
// Expand to 7 columns for time sheet
worksheet.columns = [
{ width: 20 }, // A - Time Sheet
{ width: 15 }, // B - From
{ width: 15 }, // C - To
{ width: 15 }, // D - From
{ width: 15 }, // E - To
{ width: 15 }, // F - Total
{ width: 30 } // G - Reason
];
const timeSheetHeaders = ['Time Sheet', 'From', 'To', 'From', 'To', 'Total', 'Reason'];
timeSheetHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 11, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
currentRow++;
const timeSheetData = Array.isArray(report.timeSheet) && report.timeSheet.length > 0
? report.timeSheet
: [{ machine: 'No time sheet entries', from1: '', to1: '', from2: '', to2: '', total: '', reason: '' }];
timeSheetData.forEach((entry) => {
const rowData = [entry.machine, entry.from1, entry.to1, entry.from2, entry.to2, entry.total, entry.reason];
rowData.forEach((data, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = data;
cell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
font: { name: 'Arial', size: 10, bold: colIndex === 0 },
alignment: { horizontal: colIndex === 0 ? 'left' : 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -417,47 +642,19 @@ export async function exportReportToExcel(report: ReportData) {
}
};
});
// Data rows
data.forEach((rowData, rowIndex) => {
const row = startRow + rowIndex + 1;
rowData.forEach((cellData, colIndex) => {
const cell = worksheet.getCell(row, colIndex + 1);
cell.value = cellData;
cell.style = {
font: { name: 'Arial', size: 10, bold: colIndex === 0 },
alignment: { horizontal: colIndex === 0 ? 'left' : 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
});
return startRow + data.length + 1;
};
currentRow++;
});
const timeSheetHeaders = ['Time Sheet', 'From', 'To', 'From', 'To', 'Total', 'Reason'];
const timeSheetData = Array.isArray(report.timeSheet) && report.timeSheet.length > 0
? report.timeSheet.map(entry => [entry.machine, entry.from1, entry.to1, entry.from2, entry.to2, entry.total, entry.reason])
: [['No time sheet entries', '', '', '', '', '', '']];
currentRow += 1;
currentRow = createProfessionalTable(timeSheetHeaders, timeSheetData, currentRow);
currentRow += 2; // Skip empty row
// 9. STOPPAGES SECTION - Professional section with header
// STOPPAGES SECTION
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const stoppagesHeaderCell = worksheet.getCell(`A${currentRow}`);
stoppagesHeaderCell.value = 'Dredger Stoppages';
stoppagesHeaderCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -468,23 +665,68 @@ export async function exportReportToExcel(report: ReportData) {
currentRow++;
const stoppagesHeaders = ['From', 'To', 'Total', 'Reason', 'Responsible', 'Notes', ''];
// Adjust columns for stoppages (6 columns)
worksheet.columns = [
{ width: 15 }, // A - From
{ width: 15 }, // B - To
{ width: 15 }, // C - Total
{ width: 25 }, // D - Reason
{ width: 20 }, // E - Responsible
{ width: 30 } // F - Notes
];
const stoppagesHeaders = ['From', 'To', 'Total', 'Reason', 'Responsible', 'Notes'];
stoppagesHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = header;
cell.style = {
font: { name: 'Arial', size: 11, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
currentRow++;
const stoppagesData = Array.isArray(report.stoppages) && report.stoppages.length > 0
? report.stoppages.map(entry => [entry.from, entry.to, entry.total, entry.reason, entry.responsible, entry.note, ''])
: [['No stoppages recorded', '', '', '', '', '', '']];
? report.stoppages
: [{ from: 'No stoppages recorded', to: '', total: '', reason: '', responsible: '', note: '' }];
currentRow = createProfessionalTable(stoppagesHeaders, stoppagesData, currentRow);
stoppagesData.forEach((entry) => {
const rowData = [entry.from, entry.to, entry.total, entry.reason, entry.responsible, entry.note];
rowData.forEach((data, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = data;
cell.style = {
font: { name: 'Arial', size: 10 },
alignment: { horizontal: 'center', vertical: 'middle' },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
});
currentRow++;
});
currentRow += 2; // Skip empty row
currentRow += 1;
// 10. NOTES SECTION - Professional notes section
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
// NOTES SECTION
worksheet.mergeCells(`A${currentRow}:F${currentRow}`);
const notesHeaderCell = worksheet.getCell(`A${currentRow}`);
notesHeaderCell.value = 'Notes & Comments';
notesHeaderCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -495,13 +737,12 @@ export async function exportReportToExcel(report: ReportData) {
currentRow++;
worksheet.mergeCells(`A${currentRow}:G${currentRow + 3}`);
worksheet.mergeCells(`A${currentRow}:F${currentRow + 3}`);
const notesContentCell = worksheet.getCell(`A${currentRow}`);
notesContentCell.value = report.notes || 'No additional notes';
notesContentCell.style = {
font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'left', vertical: 'top', wrapText: true },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle', wrapText: true },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -510,16 +751,16 @@ export async function exportReportToExcel(report: ReportData) {
}
};
currentRow += 6; // Skip to footer
// 11. FOOTER SECTION - Professional footer
worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
return currentRow + 5;
}
async function addFooter(worksheet: ExcelJS.Worksheet, currentRow: number): Promise<number> {
worksheet.mergeCells(`A${currentRow}:F${currentRow}`);
const footerCell = worksheet.getCell(`A${currentRow}`);
footerCell.value = 'موقعة لأعمال الصيانة';
footerCell.value = '';
footerCell.style = {
font: { name: 'Arial', size: 12, bold: true },
font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E6F3FF' } },
border: {
top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } },
@ -528,24 +769,15 @@ export async function exportReportToExcel(report: ReportData) {
}
};
// Set row heights for professional appearance
// Set row heights for better appearance
worksheet.eachRow((row, rowNumber) => {
if (rowNumber <= 3) {
row.height = 25; // Header rows
} else if (row.getCell(1).value && typeof row.getCell(1).value === 'string' &&
(row.getCell(1).value.includes('Shift') ||
row.getCell(1).value.includes('Stoppages') ||
row.getCell(1).value.includes('Notes'))) {
row.height = 22; // Section headers
} else {
row.height = 18; // Standard rows
}
row.height = 20;
});
// Set print settings for professional output
// Set print settings
worksheet.pageSetup = {
paperSize: 9, // A4
orientation: 'landscape',
orientation: 'portrait',
fitToPage: true,
fitToWidth: 1,
fitToHeight: 0,
@ -559,10 +791,5 @@ export async function exportReportToExcel(report: ReportData) {
}
};
// Generate and save file
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const fileName = `Report_${report.id}_${new Date(report.createdDate).toLocaleDateString('en-GB').replace(/\//g, '-')}.xlsx`;
FileSaver.saveAs(blob, fileName);
return currentRow + 1;
}

View File

@ -1,5 +1,6 @@
import * as nodemailer from "nodemailer";
import { prisma } from "~/utils/db.server";
import { decryptMailSettings } from "~/utils/encryption.server";
interface EmailOptions {
to: string | string[];
@ -16,12 +17,15 @@ interface EmailOptions {
export async function sendEmail(options: EmailOptions) {
try {
// Get mail settings from database
const mailSettings = await prisma.mailSettings.findFirst();
const encryptedMailSettings = await prisma.mailSettings.findFirst();
if (!mailSettings) {
if (!encryptedMailSettings) {
throw new Error("Mail settings not configured. Please configure SMTP settings first.");
}
// Decrypt the mail settings
const mailSettings = decryptMailSettings(encryptedMailSettings);
// Create transporter with enhanced configuration
const transportConfig: any = {
host: mailSettings.host,
@ -29,7 +33,7 @@ export async function sendEmail(options: EmailOptions) {
secure: mailSettings.secure,
auth: {
user: mailSettings.username,
pass: mailSettings.password,
pass: mailSettings.password, // Now decrypted
},
};
@ -77,22 +81,25 @@ export async function sendEmail(options: EmailOptions) {
export async function testEmailConnection() {
try {
const mailSettings = await prisma.mailSettings.findFirst();
const encryptedMailSettings = await prisma.mailSettings.findFirst();
if (!mailSettings) {
if (!encryptedMailSettings) {
return {
success: false,
error: "Mail settings not configured",
};
}
// Decrypt the mail settings
const mailSettings = decryptMailSettings(encryptedMailSettings);
const transportConfig: any = {
host: mailSettings.host,
port: mailSettings.port,
secure: mailSettings.secure, // true for 465, false for other ports
auth: {
user: mailSettings.username,
pass: mailSettings.password,
pass: mailSettings.password, // Now decrypted
},
};

Binary file not shown.

BIN
public/logo03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB