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 # Security
SESSION_SECRET="your-super-secure-session-secret-change-this-in-production-min-32-chars" 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 Account (created on first run)
SUPER_ADMIN="superadmin" SUPER_ADMIN="superadmin"

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
/.cache /.cache
/build /build
.env .env
.env.*
/generated/prisma /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 type { Employee } from "@prisma/client";
import { useState } from "react";
interface DashboardLayoutProps { interface DashboardLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@ -7,50 +8,87 @@ interface DashboardLayoutProps {
} }
export default function DashboardLayout({ children, user }: 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 ( return (
<div className="min-h-screen bg-gray-50"> <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 */} {/* Navigation */}
<nav className="bg-white shadow-sm border-b border-gray-200"> <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-0 sm:px-6 lg:px-8"> <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 justify-between h-16">
<div className="flex items-center"> <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 <img
className="h-12 w-auto justify-self-start" className="h-14 w-auto"
src="/clogo-sm.png" src="/logo03.png"
alt="Phosphat Report" alt="Phosphat Report"
/> />
{/* <div className="ml-4">
<h1 className="text-xl font-semibold text-gray-900">
Phosphat Report Dashboard
</h1>
</div> */}
</div> </div>
<div className="flex items-center space-x-4"> <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"> <div className="text-sm text-gray-700">
Welcome, <span className="font-medium">{user.name}</span> Welcome, <span className="font-medium">{user.name}</span>
</div> </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> </div>
<Form method="post" action="/logout"> <Form method="post" action="/logout">
<button <button
type="submit" 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" /> <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> </svg>
Logout <span className="hidden sm:inline">Logout</span>
</button> </button>
</Form> </Form>
</div> </div>
@ -58,176 +96,253 @@ export default function DashboardLayout({ children, user }: DashboardLayoutProps
</div> </div>
</nav> </nav>
{/* Sidebar */} <div className="flex pt-16">
<div className="flex"> {/* Desktop Sidebar */}
<div className="w-64 bg-white shadow-sm min-h-screen"> <div className={`hidden lg:flex lg:flex-shrink-0 transition-all duration-300 ${sidebarCollapsed ? 'lg:w-16' : 'lg:w-64'
<nav className="mt-8 px-4"> }`}>
<ul className="space-y-2"> <div className="flex flex-col w-full">
<li> <div className="flex flex-col flex-grow bg-white shadow-sm border-r border-gray-200 pt-5 pb-4 overflow-y-auto">
<Link <SidebarContent
to="/dashboard" user={user}
className="group flex items-center px-2 py-2 text-base font-medium rounded-md text-gray-900 hover:bg-gray-50" collapsed={sidebarCollapsed}
> onItemClick={() => { }}
<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" /> </div>
<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" /> </div>
</svg> </div>
Dashboard
</Link>
</li>
<li> {/* Mobile Sidebar */}
<Link <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'
to="/reports" }`}>
className="group flex items-center px-2 py-2 text-base font-medium rounded-md text-gray-900 hover:bg-gray-50" <div className="flex flex-col h-full pt-16">
> <div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<svg className="mr-4 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <SidebarContent
<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" /> user={user}
</svg> collapsed={false}
Shifts onItemClick={() => setSidebarOpen(false)}
</Link> />
</li> </div>
</div>
<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>
</div> </div>
{/* Main content */} {/* Main content */}
<div className="flex-1 p-8"> <div className="flex-1 flex flex-col">
{children} <main className="flex-1 p-4 sm:p-6 lg:p-8">
{children}
</main>
</div> </div>
</div> </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" 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> <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})`}> <option key={item.id} value={`${item.model} (${item.number})`}>
{item.category} - {item.model} ({item.number}) {item.category} - {item.model} ({item.number})
</option> </option>

View File

@ -21,14 +21,77 @@ export default function ReportSheetViewModal({ isOpen, onClose, sheet }: ReportS
if (!isOpen || !sheet) return null; if (!isOpen || !sheet) return null;
const handleExportExcel = async () => { const handleExportExcel = async () => {
if (sheet.dayReport) { // Export the entire sheet (both day and night reports combined)
await exportReportToExcel(sheet.dayReport); await exportReportToExcel(sheet);
}
if (sheet.nightReport) {
await exportReportToExcel(sheet.nightReport);
}
}; };
// 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 ( return (
<div className="fixed inset-0 z-50 overflow-y-auto"> <div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <div className="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 /> <ReportSheetFooter />
</div> </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> </div>
</div> </div>
@ -191,12 +368,22 @@ function ReportSheetHeader({ sheet }: { sheet: ReportSheet }) {
<table className="w-full border-collapse"> <table className="w-full border-collapse">
<tbody> <tbody>
<tr> <tr>
<td className="border-r-2 border-black p-2 text-center font-bold text-lg" style={{ width: '70%' }}> <td className=" p-2 text-center" style={{ width: '25%' }}>
<div>Reclamation Work Diary - Daily Sheet</div> <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">QF-3.6.1-08</div>
<div className="border-t border-black mt-1 pt-1">Rev. 1.0</div> <div className="border-t border-black mt-1 pt-1">Rev. 1.0</div>
</td> </td>
<td className="p-2 text-center" style={{ width: '30%' }}> <td className="p-2 text-center" style={{ width: '25%' }}>
<img <img
src="/logo-light.png" src="/logo-light.png"
alt="Arab Potash Logo" alt="Arab Potash Logo"

View File

@ -154,12 +154,23 @@ function ReportHeader() {
<table className="w-full border-collapse"> <table className="w-full border-collapse">
<tbody> <tbody>
<tr> <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>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">QF-3.6.1-08</div>
<div className="border-t border-black mt-1 pt-1">Rev. 1.0</div> <div className="border-t border-black mt-1 pt-1">Rev. 1.0</div>
</td> </td>
<td className="p-2 text-center" style={{ width: '30%' }}> <td className="p-2 text-center" style={{ width: '25%' }}>
<img <img
src="/logo-light.png" src="/logo-light.png"
alt="Arab Potash Logo" alt="Arab Potash Logo"

View File

@ -125,14 +125,14 @@ export default function Areas() {
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="space-y-6"> <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> <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> <p className="mt-1 text-sm text-gray-600">Manage operational areas for your reports</p>
</div> </div>
<button <button
onClick={handleAdd} 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"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@ -141,10 +141,11 @@ export default function Areas() {
</button> </button>
</div> </div>
{/* Areas Table */} {/* Areas Table - Desktop */}
<div className="bg-white shadow-sm rounded-lg overflow-hidden"> <div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="hidden sm:block">
<table className="min-w-full divide-y divide-gray-200"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
@ -211,7 +212,59 @@ export default function Areas() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
</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 && ( {areas.length === 0 && (
<div className="text-center py-12"> <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"> <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 */} {/* Welcome Section */}
<div className="bg-white overflow-hidden shadow rounded-lg"> <div className="bg-white overflow-hidden shadow rounded-lg">
<div className="px-4 py-5 sm:p-6"> <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}! Welcome back, {user.name}!
</h2> </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. Here's what's happening with your phosphat operations today.
</p> </p>
</div> </div>
</div> </div>
{/* Stats Grid */} {/* 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="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5"> <div className="p-5">
<div className="flex items-center"> <div className="flex items-center">
@ -129,10 +129,10 @@ export default function Dashboard() {
{/* Recent Reports */} {/* Recent Reports */}
<div className="bg-white shadow overflow-hidden sm:rounded-md"> <div className="bg-white shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-5 sm:px-6"> <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 Recent Reports
</h3> </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 Latest activity from your team
</p> </p>
</div> </div>
@ -141,7 +141,7 @@ export default function Dashboard() {
recentReports.map((report) => ( recentReports.map((report) => (
<li key={report.id}> <li key={report.id}>
<div className="px-4 py-4 sm:px-6"> <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 items-center">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<div className="h-8 w-8 rounded-full bg-indigo-500 flex items-center justify-center"> <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"> <div className="text-sm font-medium text-gray-900">
{report.employee.name} {report.employee.name}
</div> </div>
<div className="text-sm text-gray-500"> <div className="text-xs sm:text-sm text-gray-500">
{report.area.name} - {report.shift} shift {report.area.name} - {report.shift} shift
</div> </div>
</div> </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')} {new Date(report.createdDate).toLocaleDateString('en-GB')}
</div> </div>
</div> </div>
@ -169,7 +169,7 @@ export default function Dashboard() {
) : ( ) : (
<li> <li>
<div className="px-4 py-4 sm:px-6 text-center text-gray-500"> <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> </div>
</li> </li>
)} )}

View File

@ -342,14 +342,14 @@ export default function Employees() {
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="space-y-6"> <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> <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> <p className="mt-1 text-sm text-gray-600">Manage system users and their access levels</p>
</div> </div>
<button <button
onClick={handleAdd} 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"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@ -358,10 +358,11 @@ export default function Employees() {
</button> </button>
</div> </div>
{/* Employees Table */} {/* Employees Table - Desktop */}
<div className="bg-white shadow-sm rounded-lg overflow-hidden"> <div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="hidden lg:block">
<table className="min-w-full divide-y divide-gray-200"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
@ -472,7 +473,91 @@ export default function Employees() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
</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 && ( {employees.length === 0 && (
<div className="text-center py-12"> <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"> <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"} /> <input type="hidden" name="intent" value={isEditing ? "update" : "create"} />
{isEditing && <input type="hidden" name="id" value={editingEmployee?.id} />} {isEditing && <input type="hidden" name="id" value={editingEmployee?.id} />}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="space-y-4">
<div> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2"> <div>
Full Name <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">
</label> Full Name
<input </label>
type="text" <input
name="name" type="text"
id="name" name="name"
required id="name"
defaultValue={editingEmployee?.name || ""} required
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" defaultValue={editingEmployee?.name || ""}
placeholder="Enter full 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> {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>
<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> <div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address Email Address
@ -563,47 +648,47 @@ export default function Employees() {
<p className="mt-1 text-sm text-red-600">{actionData.errors.email}</p> <p className="mt-1 text-sm text-red-600">{actionData.errors.email}</p>
)} )}
</div> </div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div> <div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2"> <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>} Password {isEditing && <span className="text-gray-500">(leave blank to keep current)</span>}
</label> </label>
<input <input
type="password" type="password"
name="password" name="password"
id="password" id="password"
required={!isEditing} 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" 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"} placeholder={isEditing ? "Enter new password" : "Enter password"}
/> />
{actionData?.errors?.password && ( {actionData?.errors?.password && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.password}</p> <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>
)} )}
</select> </div>
{actionData?.errors?.authLevel && (
<p className="mt-1 text-sm text-red-600">{actionData.errors.authLevel}</p> <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> </div>
{isEditing && editingEmployee?.id !== user.id && ( {isEditing && editingEmployee?.id !== user.id && (

View File

@ -166,14 +166,14 @@ export default function Equipment() {
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="space-y-6"> <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> <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> <p className="mt-1 text-sm text-gray-600">Manage your fleet equipment and machinery</p>
</div> </div>
<button <button
onClick={handleAdd} 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"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@ -182,10 +182,11 @@ export default function Equipment() {
</button> </button>
</div> </div>
{/* Equipment Table */} {/* Equipment Table - Desktop */}
<div className="bg-white shadow-sm rounded-lg overflow-hidden"> <div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="hidden sm:block">
<table className="min-w-full divide-y divide-gray-200"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
@ -255,7 +256,57 @@ export default function Equipment() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
</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 && ( {equipment.length === 0 && (
<div className="text-center py-12"> <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"> <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 DashboardLayout from "~/components/DashboardLayout";
import { useState } from "react"; import { useState } from "react";
import { prisma } from "~/utils/db.server"; import { prisma } from "~/utils/db.server";
import { encryptMailSettings, decryptMailSettings, safeEncryptPassword } from "~/utils/encryption.server";
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
// Require auth level 3 to access mail settings // Require auth level 3 to access mail settings
const user = await requireAuthLevel(request, 3); 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 }); return json({ mailSettings, user });
} }
@ -40,6 +51,9 @@ export async function action({ request }: ActionFunctionArgs) {
} }
try { try {
// Encrypt the password before saving
const encryptedPassword = safeEncryptPassword(password);
// Check if settings exist // Check if settings exist
const existingSettings = await prisma.mailSettings.findFirst(); const existingSettings = await prisma.mailSettings.findFirst();
@ -52,7 +66,7 @@ export async function action({ request }: ActionFunctionArgs) {
port, port,
secure, secure,
username, username,
password, password: encryptedPassword, // Store encrypted password
fromName, fromName,
fromEmail, fromEmail,
}, },
@ -65,7 +79,7 @@ export async function action({ request }: ActionFunctionArgs) {
port, port,
secure, secure,
username, username,
password, password: encryptedPassword, // Store encrypted password
fromName, fromName,
fromEmail, fromEmail,
}, },
@ -74,6 +88,7 @@ export async function action({ request }: ActionFunctionArgs) {
return json({ success: "Mail settings saved successfully" }); return json({ success: "Mail settings saved successfully" });
} catch (error) { } catch (error) {
console.error("Mail settings save error:", error);
return json({ error: "Failed to save mail settings" }, { status: 500 }); return json({ error: "Failed to save mail settings" }, { status: 500 });
} }
} }
@ -85,13 +100,13 @@ export default function MailSettings() {
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="max-w-2xl mx-auto p-6"> <div className="max-w-2xl mx-auto p-4 sm:p-6">
<h1 className="text-2xl font-bold mb-6">Mail Settings</h1> <h1 className="text-xl sm:text-2xl font-bold mb-6">Mail Settings</h1>
{/* SMTP Configuration Examples */} {/* SMTP Configuration Examples */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"> <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> <h3 className="text-base sm: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"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div> <div>
<h4 className="font-medium text-blue-700">Gmail</h4> <h4 className="font-medium text-blue-700">Gmail</h4>
<p>Host: smtp.gmail.com</p> <p>Host: smtp.gmail.com</p>
@ -240,7 +255,7 @@ export default function MailSettings() {
/> />
</div> </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 <button
type="submit" 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" 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="space-y-6">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900">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> <p className="mt-1 text-sm text-gray-600">View grouped reports by location and date</p>
</div> </div>
</div> </div>
{/* Report Sheets Table */} {/* Report Sheets Table - Desktop */}
<div className="bg-white shadow-sm rounded-lg overflow-hidden"> <div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="hidden lg:block">
<table className="min-w-full divide-y divide-gray-200"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
@ -216,7 +217,91 @@ export default function ReportSheet() {
))} ))}
</tbody> </tbody>
</table> </table>
</div>
</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 && ( {sheets.length === 0 && (
<div className="text-center py-12"> <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"> <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 // First period
if (from1 && to1) { if (from1 && to1) {
const start1 = parseTime(from1); const start1 = parseTime(from1);
const end1 = parseTime(to1); let end1 = parseTime(to1);
if(end1 < start1)
end1 += 24 * 60;
totalMinutes += end1 - start1; totalMinutes += end1 - start1;
} }
// Second period // Second period
if (from2 && to2) { if (from2 && to2) {
const start2 = parseTime(from2); const start2 = parseTime(from2);
const end2 = parseTime(to2); let end2 = parseTime(to2);
if(end2 < start2)
end2 += 24 * 60;
totalMinutes += end2 - start2; totalMinutes += end2 - start2;
} }
return formatTime(Math.max(0, totalMinutes)); return formatTime(Math.max(0, totalMinutes));
}; };
const calculateStoppageTime = (from: string, to: string) => { const calculateStoppageTime = (from: string, to: string) => {
if (!from || !to) return "00:00"; if (!from || !to) return "00:00";
@ -436,7 +442,10 @@ export default function Reports() {
}; };
const startMinutes = parseTime(from); 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); const totalMinutes = Math.max(0, endMinutes - startMinutes);
return formatTime(totalMinutes); return formatTime(totalMinutes);
@ -540,14 +549,14 @@ export default function Reports() {
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="space-y-6"> <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> <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> <p className="mt-1 text-sm text-gray-600">Create and manage operational shifts</p>
</div> </div>
<Link <Link
to="/reports/new" to="/reports/new"
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"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
@ -556,107 +565,189 @@ export default function Reports() {
</Link> </Link>
</div> </div>
{/* Reports Table */} {/* Reports Table - Desktop */}
<div className="bg-white shadow-sm rounded-lg overflow-hidden"> <div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="hidden md:block">
<table className="min-w-full divide-y divide-gray-200"> <div className="overflow-x-auto">
<thead className="bg-gray-50"> <table className="min-w-full divide-y divide-gray-200">
<tr> <thead className="bg-gray-50">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <tr>
Shift Details <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Shift Details
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Shift & Area <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Shift & Area
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Locations <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Locations
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Created <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Created
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Actions <th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Actions
</tr> </th>
</thead> </tr>
<tbody className="bg-white divide-y divide-gray-200"> </thead>
{reports.map((report) => ( <tbody className="bg-white divide-y divide-gray-200">
<tr key={report.id} className="hover:bg-gray-50 transition-colors duration-150"> {reports.map((report) => (
<td className="px-6 py-4 whitespace-nowrap"> <tr key={report.id} className="hover:bg-gray-50 transition-colors duration-150">
<div className="flex items-center"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex-shrink-0 h-10 w-10"> <div className="flex items-center">
<div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center"> <div className="flex-shrink-0 h-10 w-10">
<svg className="h-5 w-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div className="h-10 w-10 rounded-full bg-indigo-100 flex items-center justify-center">
<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 className="h-5 w-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</svg> <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> </div>
<div className="ml-4"> </td>
<div className="text-sm font-medium text-gray-900">Shift #{report.id}</div> <td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">by {report.employee.name}</div> <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>
</div> </div>
</td> <div className="ml-3">
<td className="px-6 py-4 whitespace-nowrap"> <div className="text-sm font-medium text-gray-900">Shift #{report.id}</div>
<div className="flex flex-col space-y-1"> <div className="text-xs text-gray-500">by {report.employee.name}</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)} Shift
</span>
<span className="text-sm text-gray-900">{report.area.name}</span>
</div> </div>
</td> </div>
<td className="px-6 py-4 whitespace-nowrap"> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getShiftBadge(report.shift)}`}>
<div className="text-sm text-gray-900"> {report.shift.charAt(0).toUpperCase() + report.shift.slice(1)}
<div> {report.area.name} Dredger</div> </span>
<div className="text-gray-500"> {report.dredgerLocation.name} - {report.reclamationLocation.name}</div> </div>
</div>
</td> <div className="space-y-2 mb-4">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <div className="flex justify-between">
{/* {new Date(report.createdDate).toLocaleDateString()} */} <span className="text-xs font-medium text-gray-500">Area:</span>
{new Date(report.createdDate).toLocaleDateString('en-GB')} <span className="text-xs text-gray-900">{report.area.name}</span>
</td> </div>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <div className="flex justify-between">
<div className="flex justify-end space-x-2"> <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 <button
onClick={() => handleView(report)} onClick={() => handleEdit(report)}
className="text-blue-600 hover:text-blue-900 transition-colors duration-150" 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> </button>
{canEditReport(report) && ( <Form method="post" className="flex-1">
<> <input type="hidden" name="intent" value="delete" />
<button <input type="hidden" name="id" value={report.id} />
onClick={() => handleEdit(report)} <button
className="text-indigo-600 hover:text-indigo-900 transition-colors duration-150" type="submit"
> onClick={(e) => {
Edit if (!confirm("Are you sure you want to delete this report?")) {
</button> e.preventDefault();
<Form method="post" className="inline"> }
<input type="hidden" name="intent" value="delete" /> }}
<input type="hidden" name="id" value={report.id} /> 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"
<button >
type="submit" Delete
onClick={(e) => { </button>
if (!confirm("Are you sure you want to delete this report?")) { </Form>
e.preventDefault();
}
}}
className="text-red-600 hover:text-red-900 transition-colors duration-150"
>
Delete
</button>
</Form>
</>
)}
</div> </div>
</td> )}
</tr> </div>
))} </div>
</tbody> ))}
</table> </div>
</div> </div>
{reports.length === 0 && ( {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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
@ -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"> <svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg> </svg>
Create Shifs Create Shifts
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -51,7 +51,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const reclamationHeightBase = formData.get("reclamationHeightBase"); const reclamationHeightBase = formData.get("reclamationHeightBase");
const reclamationHeightExtra = formData.get("reclamationHeightExtra"); const reclamationHeightExtra = formData.get("reclamationHeightExtra");
const pipelineMain = formData.get("pipelineMain"); const pipelineMain = formData.get("pipelineMain");
const pipelineExt1 = formData.get("pipelineExt1"); const pipelineExt1 = formData.get("pipelineExt1");
const pipelineReserve = formData.get("pipelineReserve"); const pipelineReserve = formData.get("pipelineReserve");
const pipelineExt2 = formData.get("pipelineExt2"); const pipelineExt2 = formData.get("pipelineExt2");
const statsDozers = formData.get("statsDozers"); const statsDozers = formData.get("statsDozers");
@ -115,7 +115,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
dredgerLineLength: parseInt(dredgerLineLength), dredgerLineLength: parseInt(dredgerLineLength),
reclamationLocationId: parseInt(reclamationLocationId), reclamationLocationId: parseInt(reclamationLocationId),
shoreConnection: parseInt(shoreConnection), shoreConnection: parseInt(shoreConnection),
reclamationHeight: { reclamationHeight: {
base: parseInt(reclamationHeightBase as string) || 0, base: parseInt(reclamationHeightBase as string) || 0,
extra: parseInt(reclamationHeightExtra as string) || 0 extra: parseInt(reclamationHeightExtra as string) || 0
}, },
@ -250,17 +250,28 @@ export default function NewReport() {
if (from1 && to1) { if (from1 && to1) {
const start1 = parseTime(from1); const start1 = parseTime(from1);
const end1 = parseTime(to1); let end1 = parseTime(to1);
if (end1 < start1) {
end1 += 24 * 60;
}
totalMinutes += end1 - start1; totalMinutes += end1 - start1;
} }
if (from2 && to2) { if (from2 && to2) {
const start2 = parseTime(from2); const start2 = parseTime(from2);
const end2 = parseTime(to2); let end2 = parseTime(to2);
if (end2 < start2) {
end2 += 24 * 60;
}
totalMinutes += end2 - start2; totalMinutes += end2 - start2;
} }
return formatTime(Math.max(0, totalMinutes)); return formatTime(Math.max(0, totalMinutes));
//return formatTime(Math.max(totalMinutes * -1, totalMinutes));
}; };
const calculateStoppageTime = (from: string, to: string) => { const calculateStoppageTime = (from: string, to: string) => {
@ -278,7 +289,9 @@ export default function NewReport() {
}; };
const startMinutes = parseTime(from); 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); const totalMinutes = Math.max(0, endMinutes - startMinutes);
return formatTime(totalMinutes); return formatTime(totalMinutes);
@ -384,15 +397,15 @@ export default function NewReport() {
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="max-w-full mx-auto"> <div className="max-w-full mx-auto">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Create New Shifts</h1> <h1 className="text-2xl sm: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> <p className="mt-2 text-sm sm:text-base text-gray-600">Fill out the operational shift details step by step</p>
</div> </div>
<Link <Link
to="/reports" 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"> <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
@ -403,44 +416,42 @@ export default function NewReport() {
</div> </div>
{/* Progress Steps */} {/* Progress Steps */}
<div className="mb-8"> <div className="mb-6 sm:mb-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between px-2 sm:px-0">
{[1, 2, 3, 4].map((step) => ( {[1, 2, 3, 4].map((step) => (
<div key={step} className="flex items-center"> <div key={step} className="flex items-center">
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-2 ${ <div className={`flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full border-2 ${step <= currentStep
step <= currentStep ? 'bg-indigo-600 border-indigo-600 text-white'
? 'bg-indigo-600 border-indigo-600 text-white' : 'border-gray-300 text-gray-500'
: 'border-gray-300 text-gray-500' }`}>
}`}>
{step < currentStep ? ( {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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg> </svg>
) : ( ) : (
<span className="text-sm font-medium">{step}</span> <span className="text-xs sm:text-sm font-medium">{step}</span>
)} )}
</div> </div>
{step < totalSteps && ( {step < totalSteps && (
<div className={`flex-1 h-1 mx-4 ${ <div className={`flex-1 h-1 mx-2 sm:mx-4 ${step < currentStep ? 'bg-indigo-600' : 'bg-gray-300'
step < currentStep ? 'bg-indigo-600' : 'bg-gray-300' }`} />
}`} />
)} )}
</div> </div>
))} ))}
</div> </div>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<h2 className="text-xl font-semibold text-gray-900">{getStepTitle(currentStep)}</h2> <h2 className="text-lg sm:text-xl font-semibold text-gray-900">{getStepTitle(currentStep)}</h2>
<p className="text-sm text-gray-500">Step {currentStep} of {totalSteps}</p> <p className="text-xs sm:text-sm text-gray-500">Step {currentStep} of {totalSteps}</p>
</div> </div>
</div> </div>
{/* Form */} {/* Form */}
<Form method="post" onSubmit={handleSubmit} className="bg-white shadow-lg rounded-lg overflow-hidden"> <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 */} {/* Step 1: Basic Information */}
{currentStep === 1 && ( {currentStep === 1 && (
<div className="space-y-6"> <div className="space-y-4 sm:space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div> <div>
<label htmlFor="shift" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="shift" className="block text-sm font-medium text-gray-700 mb-2">
Shift <span className="text-red-500">*</span> Shift <span className="text-red-500">*</span>
@ -486,7 +497,7 @@ export default function NewReport() {
)} )}
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div> <div>
<label htmlFor="dredgerLocationId" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="dredgerLocationId" className="block text-sm font-medium text-gray-700 mb-2">
Dredger Location <span className="text-red-500">*</span> Dredger Location <span className="text-red-500">*</span>
@ -533,10 +544,10 @@ export default function NewReport() {
</div> </div>
</div> </div>
)} )}
{/* Step 2: Location & Pipeline Details */} {/* Step 2: Location & Pipeline Details */}
{currentStep === 2 && ( {currentStep === 2 && (
<div className="space-y-6"> <div className="space-y-4 sm:space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div> <div>
<label htmlFor="reclamationLocationId" className="block text-sm font-medium text-gray-700 mb-2"> <label htmlFor="reclamationLocationId" className="block text-sm font-medium text-gray-700 mb-2">
Reclamation Location <span className="text-red-500">*</span> Reclamation Location <span className="text-red-500">*</span>
@ -560,15 +571,15 @@ export default function NewReport() {
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Reclamation Height</h3> <h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Reclamation Height</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <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="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><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> </div>
<div> <div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Pipeline Length</h3> <h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Pipeline Length</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <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="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="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> <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>
@ -577,12 +588,12 @@ export default function NewReport() {
</div> </div>
</div> </div>
)} )}
{/* Step 3: Equipment & Time Sheet */} {/* Step 3: Equipment & Time Sheet */}
{currentStep === 3 && ( {currentStep === 3 && (
<div className="space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3> <h3 className="text-base sm:text-lg font-medium text-gray-900 mb-4">Equipment Statistics</h3>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4"> <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="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="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> <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) => ( {timeSheetEntries.map((entry) => (
<div key={entry.id} className="bg-gray-50 p-4 rounded-lg"> <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 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">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">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> <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>
@ -620,7 +635,7 @@ export default function NewReport() {
</div> </div>
</div> </div>
)} )}
{/* Step 4: Stoppages & Notes */} {/* Step 4: Stoppages & Notes */}
{currentStep === 4 && ( {currentStep === 4 && (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
@ -656,7 +671,7 @@ export default function NewReport() {
</div> </div>
</div> </div>
)} )}
{/* Error Message */} {/* Error Message */}
{actionData?.errors?.form && ( {actionData?.errors?.form && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md"> <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md">
<div className="flex"> <div className="flex">
@ -672,18 +687,18 @@ export default function NewReport() {
</div> </div>
{/* Navigation Buttons */} {/* Navigation Buttons */}
<div className="px-6 py-4 bg-gray-50 border-t border-gray-200 flex justify-between"> <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 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'}`}> <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 <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> </button>
<div className="flex space-x-3"> <div className="flex space-x-3">
{currentStep < totalSteps ? ( {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> 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>
) : ( ) : (
<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 ? ( {isSubmitting ? (
<> <>
<svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"> <svg className="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">

View File

@ -45,15 +45,15 @@ export default function SignIn() {
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
return ( 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="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-8"> <div className="max-w-md w-full space-y-6 sm:space-y-8">
<div className="text-center"> <div className="text-center">
<img <img
className="mx-auto h-28 w-auto" className="mx-auto h-20 sm:h-28 w-auto"
src="/clogo-sm.png" src="/clogo-sm.png"
alt="Phosphat Report" 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 Sign in to your account
</h2> </h2>
<p className="mt-2 text-sm text-gray-600"> <p className="mt-2 text-sm text-gray-600">

View File

@ -81,15 +81,15 @@ export default function SignUp() {
const actionData = useActionData<typeof action>(); const actionData = useActionData<typeof action>();
return ( 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="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-8"> <div className="max-w-md w-full space-y-6 sm:space-y-8">
<div className="text-center"> <div className="text-center">
<img <img
className="mx-auto h-24 w-auto" className="mx-auto h-20 sm:h-24 w-auto"
src="/clogo-sm.png" src="/clogo-sm.png"
alt="Phosphat Report" 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 Create your account
</h2> </h2>
<p className="mt-2 text-sm text-gray-600"> <p className="mt-2 text-sm text-gray-600">

View File

@ -43,8 +43,8 @@ export default function TestEmail() {
return ( return (
<DashboardLayout user={user}> <DashboardLayout user={user}>
<div className="max-w-2xl mx-auto p-6"> <div className="max-w-2xl mx-auto p-4 sm:p-6">
<h1 className="text-2xl font-bold mb-6">Test Email</h1> <h1 className="text-xl sm:text-2xl font-bold mb-6">Test Email</h1>
<p className="text-gray-600 mb-6"> <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. Use this form to test your email configuration. Only users with auth level 3 can access this feature.
</p> </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; 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 workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Report'); 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 = [ worksheet.columns = [
{ width: 30 }, // A - Labels/Machine names { width: 25 }, // A - First column (25% width)
{ width: 20 }, // B - Data/Values { width: 25 }, // B - Second column (25% width)
{ width: 20 }, // C - Labels/Data { width: 25 }, // C - Third column (25% width)
{ width: 20 }, // D - Data/Values { width: 25 }, // D - Fourth column (25% width)
{ width: 15 }, // E - Pipeline data
{ width: 15 }, // F - Pipeline data
{ width: 25 } // G - Reason/Notes
]; ];
let currentRow = 1; let currentRow = 1;
// 1. HEADER SECTION - Professional layout matching reference file // Check if this is a ReportSheet (combined day/night) or single Report
// Main header with company info const isReportSheet = 'dayReport' in reportOrSheet || 'nightReport' in reportOrSheet;
worksheet.mergeCells(`A${currentRow}:E${currentRow + 2}`);
const headerCell = worksheet.getCell(`A${currentRow}`); if (isReportSheet) {
headerCell.value = 'Reclamation Work Diary'; await exportReportSheet(worksheet, reportOrSheet as ReportSheet, currentRow);
headerCell.style = { } 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 }, font: { name: 'Arial', size: 16, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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' } } right: { style: 'thick', color: { argb: '000000' } }
} }
}; };
// Logo area // QF code
worksheet.mergeCells(`F${currentRow}:G${currentRow + 2}`); worksheet.mergeCells(`B${currentRow + 1}:C${currentRow + 1}`);
const logoCell = worksheet.getCell(`F${currentRow}`); const qfCell = worksheet.getCell(`B${currentRow + 1}`);
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}`);
qfCell.value = 'QF-3.6.1-08'; qfCell.value = 'QF-3.6.1-08';
qfCell.style = { qfCell.style = {
font: { name: 'Arial', size: 10 }, font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
border: { border: {
top: { style: 'thin', color: { argb: '000000' } }, 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.value = 'Rev. 1.0';
revCell.style = { revCell.style = {
font: { name: 'Arial', size: 10 }, font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
border: { border: {
top: { style: 'thin', color: { argb: '000000' } }, top: { style: 'thin', color: { argb: '000000' } },
@ -110,33 +254,90 @@ export async function exportReportToExcel(report: ReportData) {
} }
}; };
currentRow += 4; // Skip to next section // 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' } }
}
};
// 2. REPORT INFO SECTION - Professional table layout return currentRow + 4;
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 async function addSingleReportHeader(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
['A', 'B', 'C', 'D', 'E', 'F', 'G'].forEach(col => { return await addReportSheetHeader(worksheet, { area: report.area.name } as ReportSheet, currentRow);
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' } }
}
};
});
async function addReportSheetInfo(worksheet: ExcelJS.Worksheet, sheet: ReportSheet, currentRow: number): Promise<number> {
const dateCell = worksheet.getCell(`A${currentRow}`); const dateCell = worksheet.getCell(`A${currentRow}`);
dateCell.value = 'Date:'; dateCell.value = 'Date:';
dateCell.style = { 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' }, alignment: { horizontal: 'left', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F0F0F0' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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}`); const dateValueCell = worksheet.getCell(`B${currentRow}`);
dateValueCell.value = new Date(report.createdDate).toLocaleDateString('en-GB'); dateValueCell.value = new Date(report.createdDate).toLocaleDateString('en-GB');
dateValueCell.style = { dateValueCell.style = {
font: { name: 'Arial', size: 11 }, font: { name: 'Arial', size: 12 },
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 },
alignment: { horizontal: 'left', vertical: 'middle' }, alignment: { horizontal: 'left', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F0F0F0' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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}`); const shiftNoCell = worksheet.getCell(`C${currentRow}`);
reportNoValueCell.value = report.id.toString(); shiftNoCell.value = 'Shift No.';
reportNoValueCell.style = { shiftNoCell.style = {
font: { name: 'Arial', size: 11 }, font: { name: 'Arial', size: 12, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'left', vertical: 'middle' },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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 return currentRow + 2;
worksheet.mergeCells(`A${currentRow}:G${currentRow}`); }
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}`); const dredgerCell = worksheet.getCell(`A${currentRow}`);
dredgerCell.value = `${report.area.name} Dredger`; dredgerCell.value = `${areaName} Dredger`;
dredgerCell.style = { dredgerCell.style = {
font: { name: 'Arial', size: 18, bold: true, underline: true }, font: { name: 'Arial', size: 18, bold: true, underline: true },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' }
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E6F3FF' } }
}; };
currentRow += 2; // Skip empty row return currentRow + 2;
}
// 4. LOCATION DATA SECTION - Professional table with green headers async function addLocationData(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
const locationRows = [ const locationData = [
['Dredger Location', report.dredgerLocation.name, '', 'Dredger Line Length', report.dredgerLineLength.toString()], ['Dredger Location', report.dredgerLocation.name, 'Dredger Line Length', report.dredgerLineLength.toString()],
['Reclamation Location', report.reclamationLocation.name, '', 'Shore Connection', report.shoreConnection.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`, '', '', ''] ['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; const row = currentRow + index;
// Apply styling to all cells in the row first rowData.forEach((cellValue, colIndex) => {
for (let col = 1; col <= 7; col++) { const cell = worksheet.getCell(row, colIndex + 1);
const cell = worksheet.getCell(row, col); cell.value = cellValue;
const isGreenHeader = (colIndex === 0 || colIndex === 2) && cellValue !== '';
cell.style = { 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: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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' } } 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;
}
// 5. PIPELINE LENGTH SECTION - Professional table with green headers async function addPipelineLength(worksheet: ExcelJS.Worksheet, report: ReportData, currentRow: number): Promise<number> {
const pipelineHeaderRow = currentRow; // 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
];
// First row - main header with rowspan // Pipeline header row
const mainHeaderCell = worksheet.getCell(pipelineHeaderRow, 1); const pipelineHeaderCell = worksheet.getCell(`A${currentRow}`);
mainHeaderCell.value = 'Pipeline Length "from Shore Connection"'; pipelineHeaderCell.value = 'Pipeline Length "from Shore Connection"';
mainHeaderCell.style = { pipelineHeaderCell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } }, font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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']; const pipelineSubHeaders = ['Main', 'extension', 'total', 'Reserve', 'extension', 'total'];
pipelineSubHeaders.forEach((header, colIndex) => { pipelineSubHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(pipelineHeaderRow, colIndex + 2); const cell = worksheet.getCell(currentRow, colIndex + 2);
cell.value = header; cell.value = header;
cell.style = { 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' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } }, left: { style: 'thick', color: { argb: '000000' } },
@ -292,9 +479,20 @@ export async function exportReportToExcel(report: ReportData) {
}; };
}); });
// Data row // Pipeline data row
const pipelineDataRow = currentRow + 1; currentRow++;
const pipelineData = ['', 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?.main || 0).toString(),
(report.pipelineLength?.ext1 || 0).toString(), (report.pipelineLength?.ext1 || 0).toString(),
((report.pipelineLength?.main || 0) + (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) => { pipelineData.forEach((data, colIndex) => {
const cell = worksheet.getCell(pipelineDataRow, colIndex + 1); const cell = worksheet.getCell(currentRow, colIndex + 2);
cell.value = data; cell.value = data;
cell.style = { cell.style = {
font: { name: 'Arial', size: 11 }, font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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 return currentRow + 2;
}
// 6. SHIFT HEADER SECTION - Professional full-width header async function addShiftSection(worksheet: ExcelJS.Worksheet, report: ReportData, shiftName: string, currentRow: number): Promise<number> {
// SHIFT HEADER SECTION
worksheet.mergeCells(`A${currentRow}:G${currentRow}`); worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const shiftCell = worksheet.getCell(`A${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 = { shiftCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } }, font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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']; const equipmentHeaders = ['Dozers', 'Exc.', 'Loader', 'Foreman', 'Laborer'];
// Apply borders to all cells in the equipment section // Adjust columns back to 5 for equipment section
for (let col = 1; col <= 7; col++) { worksheet.columns = [
for (let row = currentRow; row <= currentRow + 1; row++) { { width: 20 }, // A - Dozers
const cell = worksheet.getCell(row, col); { width: 20 }, // B - Exc.
cell.style = { { width: 20 }, // C - Loader
border: { { width: 20 }, // D - Foreman
top: { style: 'thick', color: { argb: '000000' } }, { width: 20 } // E - Laborer
left: { style: 'thick', color: { argb: '000000' } }, ];
bottom: { style: 'thick', color: { argb: '000000' } },
right: { style: 'thick', color: { argb: '000000' } }
}
};
}
}
equipmentHeaders.forEach((header, colIndex) => { equipmentHeaders.forEach((header, colIndex) => {
const cell = worksheet.getCell(currentRow, colIndex + 1); const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = header; cell.value = header;
cell.style = { cell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } }, font: { name: 'Arial', size: 11, bold: true },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } }, left: { style: 'thick', color: { argb: '000000' } },
@ -373,6 +565,7 @@ export async function exportReportToExcel(report: ReportData) {
}; };
}); });
currentRow++;
const equipmentData = [ const equipmentData = [
(report.stats?.Dozers || 0).toString(), (report.stats?.Dozers || 0).toString(),
(report.stats?.Exc || 0).toString(), (report.stats?.Exc || 0).toString(),
@ -382,12 +575,11 @@ export async function exportReportToExcel(report: ReportData) {
]; ];
equipmentData.forEach((data, colIndex) => { equipmentData.forEach((data, colIndex) => {
const cell = worksheet.getCell(currentRow + 1, colIndex + 1); const cell = worksheet.getCell(currentRow, colIndex + 1);
cell.value = data; cell.value = data;
cell.style = { cell.style = {
font: { name: 'Arial', size: 11 }, font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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 // TIME SHEET SECTION
const createProfessionalTable = (headers: string[], data: any[][], startRow: number) => { // Expand to 7 columns for time sheet
// Headers worksheet.columns = [
headers.forEach((header, colIndex) => { { width: 20 }, // A - Time Sheet
const cell = worksheet.getCell(startRow, colIndex + 1); { width: 15 }, // B - From
cell.value = header; { 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 = { cell.style = {
font: { name: 'Arial', size: 11, bold: true, color: { argb: 'FFFFFF' } }, font: { name: 'Arial', size: 10, bold: colIndex === 0 },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: colIndex === 0 ? 'left' : 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } }, left: { style: 'thick', color: { argb: '000000' } },
@ -417,47 +642,19 @@ export async function exportReportToExcel(report: ReportData) {
} }
}; };
}); });
currentRow++;
});
// Data rows currentRow += 1;
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; // STOPPAGES SECTION
};
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 = createProfessionalTable(timeSheetHeaders, timeSheetData, currentRow);
currentRow += 2; // Skip empty row
// 9. STOPPAGES SECTION - Professional section with header
worksheet.mergeCells(`A${currentRow}:G${currentRow}`); worksheet.mergeCells(`A${currentRow}:G${currentRow}`);
const stoppagesHeaderCell = worksheet.getCell(`A${currentRow}`); const stoppagesHeaderCell = worksheet.getCell(`A${currentRow}`);
stoppagesHeaderCell.value = 'Dredger Stoppages'; stoppagesHeaderCell.value = 'Dredger Stoppages';
stoppagesHeaderCell.style = { stoppagesHeaderCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } }, font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } }, left: { style: 'thick', color: { argb: '000000' } },
@ -468,23 +665,68 @@ export async function exportReportToExcel(report: ReportData) {
currentRow++; 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 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, '']) ? report.stoppages
: [['No stoppages recorded', '', '', '', '', '', '']]; : [{ 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 // NOTES SECTION
worksheet.mergeCells(`A${currentRow}:G${currentRow}`); worksheet.mergeCells(`A${currentRow}:F${currentRow}`);
const notesHeaderCell = worksheet.getCell(`A${currentRow}`); const notesHeaderCell = worksheet.getCell(`A${currentRow}`);
notesHeaderCell.value = 'Notes & Comments'; notesHeaderCell.value = 'Notes & Comments';
notesHeaderCell.style = { notesHeaderCell.style = {
font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } }, font: { name: 'Arial', size: 14, bold: true, color: { argb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '70AD47' } }, fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: '10B981' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { style: 'thick', color: { argb: '000000' } }, left: { style: 'thick', color: { argb: '000000' } },
@ -495,13 +737,12 @@ export async function exportReportToExcel(report: ReportData) {
currentRow++; currentRow++;
worksheet.mergeCells(`A${currentRow}:G${currentRow + 3}`); worksheet.mergeCells(`A${currentRow}:F${currentRow + 3}`);
const notesContentCell = worksheet.getCell(`A${currentRow}`); const notesContentCell = worksheet.getCell(`A${currentRow}`);
notesContentCell.value = report.notes || 'No additional notes'; notesContentCell.value = report.notes || 'No additional notes';
notesContentCell.style = { notesContentCell.style = {
font: { name: 'Arial', size: 11 }, font: { name: 'Arial', size: 11 },
alignment: { horizontal: 'left', vertical: 'top', wrapText: true }, alignment: { horizontal: 'center', vertical: 'middle', wrapText: true },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFFF' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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 return currentRow + 5;
}
// 11. FOOTER SECTION - Professional footer async function addFooter(worksheet: ExcelJS.Worksheet, currentRow: number): Promise<number> {
worksheet.mergeCells(`A${currentRow}:G${currentRow}`); worksheet.mergeCells(`A${currentRow}:F${currentRow}`);
const footerCell = worksheet.getCell(`A${currentRow}`); const footerCell = worksheet.getCell(`A${currentRow}`);
footerCell.value = 'موقعة لأعمال الصيانة'; footerCell.value = '';
footerCell.style = { footerCell.style = {
font: { name: 'Arial', size: 12, bold: true }, font: { name: 'Arial', size: 12 },
alignment: { horizontal: 'center', vertical: 'middle' }, alignment: { horizontal: 'center', vertical: 'middle' },
fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'E6F3FF' } },
border: { border: {
top: { style: 'thick', color: { argb: '000000' } }, top: { style: 'thick', color: { argb: '000000' } },
left: { 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) => { worksheet.eachRow((row, rowNumber) => {
if (rowNumber <= 3) { row.height = 20;
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
}
}); });
// Set print settings for professional output // Set print settings
worksheet.pageSetup = { worksheet.pageSetup = {
paperSize: 9, // A4 paperSize: 9, // A4
orientation: 'landscape', orientation: 'portrait',
fitToPage: true, fitToPage: true,
fitToWidth: 1, fitToWidth: 1,
fitToHeight: 0, fitToHeight: 0,
@ -559,10 +791,5 @@ export async function exportReportToExcel(report: ReportData) {
} }
}; };
// Generate and save file return currentRow + 1;
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);
} }

View File

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