diff --git a/.kiro/specs/construction-site-management-auth/design.md b/.kiro/specs/construction-site-management-auth/design.md deleted file mode 100644 index e3b32a5..0000000 --- a/.kiro/specs/construction-site-management-auth/design.md +++ /dev/null @@ -1,546 +0,0 @@ -# Design Document: Production-Ready Authentication System - -## Overview - -This document specifies the technical design for a production-ready authentication and user management system for SiteOS Enterprise. The system implements secure user registration, login, password recovery, and account management using localStorage for frontend persistence. - -## Architecture - -### Authentication Flow - -``` -User Registration -├── Sign Up Form -├── Email Verification -├── Account Created -└── Redirect to Dashboard - -User Login -├── Login Form -├── Credential Validation -├── JWT Token Generation -├── Session Storage -└── Redirect to Dashboard - -Password Recovery -├── Forgot Password Form -├── Email Verification -├── Password Reset Form -├── Password Update -└── Redirect to Login -``` - -### Data Storage Architecture - -``` -localStorage -├── user: { -│ id, name, email, phone, role, -│ createdAt, lastLogin, verified -│ } -├── token: JWT token string -├── tokenExpiry: timestamp -├── verificationCode: code for email verification -├── resetToken: token for password reset -└── sessionData: { - rememberMe, loginTime, lastActivity - } -``` - -### Security Architecture - -``` -User Input -├── Client-side Validation -├── XSS Prevention (sanitization) -├── CSRF Protection (token) -├── Password Hashing (bcrypt) -└── Secure Storage (localStorage) -``` - -## Components and Interfaces - -### Authentication Pages - -#### Sign Up Page -**Purpose**: Allow new users to create accounts - -**Components**: -- Header with company branding -- Sign Up form with fields: - - Full Name (text input) - - Email (email input) - - Password (password input with show/hide toggle) - - Confirm Password (password input with show/hide toggle) - - Terms & Conditions checkbox -- Password strength indicator -- Error messages below each field -- Submit button (disabled until valid) -- Link to Login page -- Link to Terms of Service - -**Validation Rules**: -- Full Name: required, 2-50 characters -- Email: required, valid email format -- Password: required, min 8 chars, uppercase, lowercase, number, special char -- Confirm Password: must match Password -- Terms: must be checked - -**Error Handling**: -- Display field-level errors -- Show password strength feedback -- Display generic error for duplicate email -- Show loading state during submission - -#### Email Verification Page -**Purpose**: Verify user's email address - -**Components**: -- Header with verification message -- Verification code input (6-digit code) -- Resend Code button -- Countdown timer (5 minutes) -- Back to Login link -- Error messages - -**Validation Rules**: -- Code: required, 6 digits -- Code: must match generated code -- Code: must not be expired - -**Error Handling**: -- Display invalid code error -- Show expired code message -- Allow resend with new code - -#### Login Page -**Purpose**: Authenticate existing users - -**Components**: -- Header with company branding -- Login form with fields: - - Email (email input) - - Password (password input with show/hide toggle) - - Remember Me checkbox -- Submit button -- Forgot Password link -- Sign Up link -- Error messages -- Loading state - -**Validation Rules**: -- Email: required, valid format -- Password: required, non-empty - -**Error Handling**: -- Display "Invalid email or password" (generic for security) -- Show account locked message after 5 attempts -- Display loading state during verification - -#### Forgot Password Page -**Purpose**: Initiate password reset process - -**Components**: -- Header with reset message -- Email input field -- Submit button -- Back to Login link -- Success message after submission -- Error messages - -**Validation Rules**: -- Email: required, valid format -- Email: must exist in system - -**Error Handling**: -- Display generic message (for security) -- Show success message regardless - -#### Password Reset Page -**Purpose**: Allow user to set new password - -**Components**: -- Header with reset message -- New Password input (with show/hide toggle) -- Confirm Password input (with show/hide toggle) -- Password strength indicator -- Submit button -- Back to Login link -- Error messages - -**Validation Rules**: -- New Password: min 8 chars, uppercase, lowercase, number, special char -- Confirm Password: must match New Password -- Token: must be valid and not expired - -**Error Handling**: -- Display expired token message -- Show password strength feedback -- Display validation errors - -#### Profile Page -**Purpose**: Display and manage user profile - -**Components**: -- User information display: - - Name - - Email - - Role - - Phone - - Account created date - - Last login date -- Edit Profile button -- Change Password link -- Delete Account button -- Logout button - -**Edit Profile Form**: -- Name input -- Phone input -- Submit button -- Cancel button -- Error messages - -#### Change Password Page -**Purpose**: Allow user to change password - -**Components**: -- Current Password input (with show/hide toggle) -- New Password input (with show/hide toggle) -- Confirm Password input (with show/hide toggle) -- Password strength indicator -- Submit button -- Cancel button -- Error messages - -**Validation Rules**: -- Current Password: must match stored password -- New Password: min 8 chars, uppercase, lowercase, number, special char -- Confirm Password: must match New Password - -**Error Handling**: -- Display "Current password incorrect" error -- Show password strength feedback -- Display validation errors - -### UI Components - -#### Form Input Component -**Props**: -```javascript -{ - label: string, - type: 'text' | 'email' | 'password' | 'number', - value: string, - onChange: (value: string) => void, - error: string, - required: boolean, - placeholder: string, - hint: string, - icon: ReactNode, - disabled: boolean, - showToggle: boolean (for password fields) -} -``` - -**Features**: -- Real-time validation -- Error display below field -- Success checkmark for valid fields -- Show/hide toggle for password fields -- Hint text below field -- Icon support -- Disabled state - -#### Password Strength Indicator Component -**Props**: -```javascript -{ - password: string, - showRequirements: boolean -} -``` - -**Features**: -- Visual strength meter (0-4 levels) -- Color coding (red, orange, yellow, green) -- Requirement checklist: - - Minimum 8 characters - - Uppercase letter - - Lowercase letter - - Number - - Special character - -#### Toast Notification Component -**Props**: -```javascript -{ - type: 'success' | 'error' | 'warning' | 'info', - message: string, - duration: number (milliseconds), - onClose: () => void -} -``` - -**Features**: -- Auto-dismiss after duration -- Manual close button -- Color-coded by type -- Slide-in animation -- Multiple notifications queue - -#### Loading Spinner Component -**Props**: -```javascript -{ - size: 'sm' | 'md' | 'lg', - text: string -} -``` - -**Features**: -- Animated spinner -- Optional loading text -- Overlay mode for full-page loading - -### Authentication Service - -#### AuthService -**Methods**: -```javascript -// Registration -signup(name, email, password) -> Promise<{success, message, userId}> -verifyEmail(code) -> Promise<{success, message}> -resendVerificationCode() -> Promise<{success, message}> - -// Login -login(email, password) -> Promise<{success, message, token, user}> -logout() -> Promise<{success, message}> - -// Password Recovery -requestPasswordReset(email) -> Promise<{success, message}> -resetPassword(token, newPassword) -> Promise<{success, message}> - -// Session Management -getStoredToken() -> string | null -getStoredUser() -> object | null -isTokenValid() -> boolean -refreshToken() -> Promise<{success, token}> -clearSession() -> void - -// Profile Management -updateProfile(updates) -> Promise<{success, message, user}> -changePassword(currentPassword, newPassword) -> Promise<{success, message}> -deleteAccount(password) -> Promise<{success, message}> -``` - -### Data Models - -#### User -```javascript -{ - id: string (UUID), - name: string, - email: string, - passwordHash: string (bcrypt hash), - phone: string, - role: 'Admin' | 'Project_Manager' | 'Site_Engineer' | 'Storekeeper', - verified: boolean, - createdAt: ISO 8601 timestamp, - lastLogin: ISO 8601 timestamp, - lastPasswordChange: ISO 8601 timestamp, - accountLocked: boolean, - lockUntil: ISO 8601 timestamp, - failedLoginAttempts: number -} -``` - -#### Session -```javascript -{ - token: string (JWT), - tokenExpiry: ISO 8601 timestamp, - user: User object, - rememberMe: boolean, - loginTime: ISO 8601 timestamp, - lastActivity: ISO 8601 timestamp -} -``` - -#### VerificationCode -```javascript -{ - code: string (6 digits), - email: string, - expiresAt: ISO 8601 timestamp, - attempts: number, - verified: boolean -} -``` - -#### PasswordReset -```javascript -{ - token: string (UUID), - email: string, - expiresAt: ISO 8601 timestamp, - used: boolean -} -``` - -### Validation Rules - -#### Email Validation -- Format: RFC 5322 standard -- Pattern: `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` -- Length: 5-254 characters -- Unique: must not exist in system - -#### Password Validation -- Minimum length: 8 characters -- Must contain: uppercase letter (A-Z) -- Must contain: lowercase letter (a-z) -- Must contain: number (0-9) -- Must contain: special character (!@#$%^&*) -- Maximum length: 128 characters -- Cannot contain: username or email - -#### Name Validation -- Minimum length: 2 characters -- Maximum length: 50 characters -- Allowed characters: letters, spaces, hyphens, apostrophes -- Pattern: `/^[a-zA-Z\s'-]{2,50}$/` - -#### Phone Validation -- Format: International format or local format -- Pattern: `/^[\d\s\-\+\(\)]{10,20}$/` -- Optional field - -### Error Handling - -#### Authentication Errors -- Invalid credentials: "Invalid email or password" -- User not found: "Invalid email or password" (generic) -- Email not verified: "Please verify your email first" -- Account locked: "Account locked. Try again in 30 minutes" -- Token expired: "Session expired. Please log in again" -- Invalid token: "Invalid or expired link" - -#### Validation Errors -- Required field: "This field is required" -- Invalid email: "Please enter a valid email address" -- Weak password: "Password does not meet requirements" -- Password mismatch: "Passwords do not match" -- Duplicate email: "Email already registered" - -#### Server Errors -- Network error: "Unable to connect. Please check your connection" -- Server error: "Something went wrong. Please try again" -- Timeout: "Request timed out. Please try again" - -### Security Measures - -#### Password Security -- Hash passwords using bcrypt with salt rounds: 10 -- Never store plain text passwords -- Never send passwords in URLs or logs -- Implement password strength requirements -- Prevent password reuse (last 5 passwords) - -#### Token Security -- Use JWT with HS256 algorithm -- Token expiration: 24 hours -- Refresh token mechanism for extended sessions -- Store tokens in localStorage (not cookies for CORS) -- Validate token signature on every request - -#### Input Security -- Sanitize all user input to prevent XSS -- Validate input on client and server -- Escape special characters in output -- Use Content Security Policy headers -- Implement CSRF protection with tokens - -#### Session Security -- Implement session timeout after 24 hours -- Track last activity timestamp -- Invalidate session on logout -- Prevent session fixation attacks -- Implement secure session storage - -#### Rate Limiting -- Max 5 login attempts per 15 minutes -- Max 3 password reset requests per hour -- Max 10 verification code requests per hour -- Lock account after 5 failed attempts for 30 minutes - -### Responsive Design - -#### Mobile (< 768px) -- Single column layout -- Full-width form inputs -- Larger touch targets (44px minimum) -- Simplified navigation -- Stacked buttons - -#### Tablet (768px - 1024px) -- Two column layout where appropriate -- Optimized spacing -- Touch-friendly interface - -#### Desktop (> 1024px) -- Multi-column layout -- Centered form containers (max-width: 500px) -- Hover states on interactive elements -- Keyboard navigation support - -### Accessibility - -#### WCAG 2.1 AA Compliance -- Semantic HTML structure -- ARIA labels for form inputs -- Keyboard navigation support -- Color contrast ratio: 4.5:1 for text -- Focus indicators on interactive elements -- Error messages linked to form fields -- Loading states announced to screen readers - -#### Keyboard Navigation -- Tab through form fields -- Enter to submit forms -- Escape to close modals -- Arrow keys for dropdowns -- Space to toggle checkboxes - -## Testing Strategy - -### Unit Tests -- Validation functions -- Password strength checker -- Email format validator -- Token generation and validation -- Error message generation - -### Integration Tests -- Sign up flow -- Email verification flow -- Login flow -- Password reset flow -- Profile update flow -- Session persistence - -### End-to-End Tests -- Complete user registration journey -- Complete login journey -- Complete password recovery journey -- Session timeout and refresh -- Cross-browser compatibility - -### Security Tests -- XSS prevention -- CSRF protection -- Password hashing verification -- Token expiration -- Rate limiting -- Account lockout - diff --git a/.kiro/specs/construction-site-management-auth/requirements.md b/.kiro/specs/construction-site-management-auth/requirements.md deleted file mode 100644 index 0ba3cb6..0000000 --- a/.kiro/specs/construction-site-management-auth/requirements.md +++ /dev/null @@ -1,281 +0,0 @@ -# Requirements Document: Production-Ready Authentication System - -## Introduction - -This document specifies the requirements for a production-ready authentication and user management system for the Construction Site Management System (SiteOS Enterprise). The system provides secure user registration, login, password recovery, and account management with localStorage-based persistence for frontend development. - -## Glossary - -- **User**: A registered person with email and password credentials -- **Session**: An authenticated user's active connection to the system -- **JWT Token**: JSON Web Token stored in localStorage for session management -- **Email Verification**: Process to confirm user's email address ownership -- **Password Reset**: Process to recover account access via email link -- **Role**: User's permission level (Admin, Project_Manager, Site_Engineer, Storekeeper) -- **localStorage**: Browser's local storage for persisting user data and tokens -- **Form Validation**: Client-side validation of user input before submission - -## Requirements - -### Requirement 1: User Registration (Sign Up) - -**User Story:** As a new user, I want to create an account with email and password, so that I can access the system - -#### Acceptance Criteria - -1. THE Sign Up page SHALL display a form with fields: Full Name, Email, Password, Confirm Password -2. THE Sign Up form SHALL validate that all fields are required -3. THE Sign Up form SHALL validate email format (valid email pattern) -4. THE Sign Up form SHALL validate password strength (minimum 8 characters, uppercase, lowercase, number, special character) -5. THE Sign Up form SHALL validate that Password and Confirm Password match -6. WHEN validation fails, THE System SHALL display error messages below each field -7. WHEN validation passes, THE System SHALL create a new user account in localStorage -8. WHEN account creation succeeds, THE System SHALL display a success message -9. WHEN account creation succeeds, THE System SHALL redirect to email verification page -10. THE Sign Up page SHALL include a link to the Login page for existing users -11. THE Sign Up page SHALL display password strength indicator -12. THE Sign Up page SHALL show/hide password toggle buttons - -### Requirement 2: Email Verification - -**User Story:** As a new user, I want to verify my email address, so that I can confirm account ownership - -#### Acceptance Criteria - -1. THE Email Verification page SHALL display a message asking user to verify email -2. THE Email Verification page SHALL display a verification code input field -3. THE Email Verification page SHALL display a "Resend Code" button -4. WHEN user enters verification code, THE System SHALL validate the code -5. WHEN verification code is correct, THE System SHALL mark email as verified in localStorage -6. WHEN verification code is correct, THE System SHALL redirect to Dashboard -7. WHEN verification code is incorrect, THE System SHALL display error message -8. WHEN user clicks "Resend Code", THE System SHALL generate new verification code -9. THE Email Verification page SHALL display countdown timer for code expiration (5 minutes) -10. THE Email Verification page SHALL include a "Back to Login" link - -### Requirement 3: User Login - -**User Story:** As a registered user, I want to log in with email and password, so that I can access my account - -#### Acceptance Criteria - -1. THE Login page SHALL display a form with fields: Email, Password -2. THE Login form SHALL validate that both fields are required -3. THE Login form SHALL validate email format -4. WHEN validation fails, THE System SHALL display error messages -5. WHEN user submits valid credentials, THE System SHALL verify against localStorage users -6. WHEN credentials are correct, THE System SHALL create JWT token and store in localStorage -7. WHEN credentials are correct, THE System SHALL set user session and redirect to Dashboard -8. WHEN credentials are incorrect, THE System SHALL display "Invalid email or password" error -9. THE Login page SHALL include a "Forgot Password?" link -10. THE Login page SHALL include a "Sign Up" link for new users -11. THE Login page SHALL display "Remember Me" checkbox for session persistence -12. THE Login page SHALL show/hide password toggle button -13. THE Login page SHALL display professional branding and company logo - -### Requirement 4: Forgot Password - -**User Story:** As a user who forgot my password, I want to reset it via email, so that I can regain access to my account - -#### Acceptance Criteria - -1. THE Forgot Password page SHALL display an email input field -2. THE Forgot Password form SHALL validate email format -3. WHEN user enters valid email, THE System SHALL check if user exists in localStorage -4. WHEN user exists, THE System SHALL generate password reset token -5. WHEN user exists, THE System SHALL display "Check your email for reset link" message -6. WHEN user does not exist, THE System SHALL display generic message (for security) -7. THE Forgot Password page SHALL include a "Back to Login" link -8. THE Password Reset page SHALL display form with New Password and Confirm Password fields -9. THE Password Reset form SHALL validate password strength -10. WHEN reset token is valid, THE System SHALL update password in localStorage -11. WHEN password is updated, THE System SHALL redirect to Login page with success message -12. WHEN reset token is expired, THE System SHALL display "Link expired, request new reset" message - -### Requirement 5: Session Management - -**User Story:** As a logged-in user, I want my session to persist, so that I don't need to log in every time - -#### Acceptance Criteria - -1. THE System SHALL store JWT token in localStorage upon successful login -2. THE System SHALL store user data in localStorage upon successful login -3. THE System SHALL check for valid token on app initialization -4. WHEN valid token exists, THE System SHALL automatically log in user -5. WHEN token is invalid or expired, THE System SHALL redirect to Login page -6. THE System SHALL provide logout functionality that clears token and user data -7. WHEN user logs out, THE System SHALL redirect to Login page -8. THE System SHALL maintain session across browser tabs -9. THE System SHALL clear session on browser close (if "Remember Me" not checked) -10. THE System SHALL provide session timeout after 24 hours of inactivity - -### Requirement 6: User Profile Management - -**User Story:** As a logged-in user, I want to view and edit my profile, so that I can keep my information current - -#### Acceptance Criteria - -1. THE Profile page SHALL display user's current information (Name, Email, Role, Phone) -2. THE Profile page SHALL display an "Edit Profile" button -3. WHEN user clicks "Edit Profile", THE System SHALL display editable form -4. THE Profile form SHALL validate all fields before submission -5. WHEN profile is updated, THE System SHALL update user data in localStorage -6. WHEN profile is updated, THE System SHALL display success message -7. THE Profile page SHALL display account creation date -8. THE Profile page SHALL display last login date and time -9. THE Profile page SHALL include a "Change Password" link -10. THE Profile page SHALL include a "Delete Account" option with confirmation - -### Requirement 7: Password Change - -**User Story:** As a logged-in user, I want to change my password, so that I can keep my account secure - -#### Acceptance Criteria - -1. THE Change Password page SHALL display form with: Current Password, New Password, Confirm Password -2. THE Change Password form SHALL validate all fields are required -3. THE Change Password form SHALL validate new password strength -4. WHEN user submits form, THE System SHALL verify current password -5. WHEN current password is incorrect, THE System SHALL display error message -6. WHEN current password is correct, THE System SHALL update password in localStorage -7. WHEN password is updated, THE System SHALL display success message -8. WHEN password is updated, THE System SHALL redirect to Profile page -9. THE Change Password page SHALL show/hide password toggle buttons -10. THE Change Password page SHALL display password strength indicator - -### Requirement 8: Form Validation and Error Handling - -**User Story:** As a user, I want clear error messages when I make mistakes, so that I can correct them easily - -#### Acceptance Criteria - -1. THE System SHALL validate form fields in real-time as user types -2. THE System SHALL display error messages below each invalid field -3. THE System SHALL disable submit button until all validations pass -4. THE System SHALL display field-level error icons (red border, error icon) -5. THE System SHALL display success icons for valid fields (green checkmark) -6. THE System SHALL provide helpful error messages (not generic "Error") -7. THE System SHALL validate email format using RFC 5322 standard -8. THE System SHALL validate password strength with specific requirements -9. THE System SHALL prevent form submission with invalid data -10. THE System SHALL clear error messages when user corrects input - -### Requirement 9: Security Best Practices - -**User Story:** As a system administrator, I want secure authentication, so that user accounts are protected - -#### Acceptance Criteria - -1. THE System SHALL NOT store passwords in plain text in localStorage -2. THE System SHALL hash passwords using bcrypt or similar algorithm -3. THE System SHALL use JWT tokens with expiration time -4. THE System SHALL validate all user input to prevent XSS attacks -5. THE System SHALL use HTTPS in production (enforced by backend) -6. THE System SHALL implement CSRF protection (token-based) -7. THE System SHALL NOT display sensitive information in URLs -8. THE System SHALL implement rate limiting on login attempts (max 5 attempts per 15 minutes) -9. THE System SHALL log authentication events for audit trail -10. THE System SHALL implement secure password reset with time-limited tokens - -### Requirement 10: User Interface and UX - -**User Story:** As a user, I want a professional and intuitive authentication interface, so that I can easily manage my account - -#### Acceptance Criteria - -1. THE Authentication pages SHALL follow the design system (dark industrial aesthetic) -2. THE Authentication pages SHALL be fully responsive (mobile, tablet, desktop) -3. THE Authentication pages SHALL display loading states during API calls -4. THE Authentication pages SHALL display success/error toast notifications -5. THE Authentication pages SHALL include company branding and logo -6. THE Authentication pages SHALL display helpful hints and tooltips -7. THE Authentication pages SHALL use consistent typography and spacing -8. THE Authentication pages SHALL include accessibility features (ARIA labels, keyboard navigation) -9. THE Authentication pages SHALL display progress indicators for multi-step flows -10. THE Authentication pages SHALL provide clear call-to-action buttons - -### Requirement 11: Email Notifications - -**User Story:** As a user, I want to receive email notifications for account activities, so that I can monitor my account security - -#### Acceptance Criteria - -1. THE System SHALL send welcome email after successful registration -2. THE System SHALL send email verification code to user's email -3. THE System SHALL send password reset link to user's email -4. THE System SHALL send password change confirmation email -5. THE System SHALL send login notification email (optional) -6. THE System SHALL include unsubscribe link in all emails -7. THE System SHALL use professional email templates -8. THE System SHALL include company branding in emails -9. THE System SHALL send emails within 1 minute of trigger event -10. THE System SHALL handle email delivery failures gracefully - -### Requirement 12: Account Recovery and Security - -**User Story:** As a user, I want to recover my account if compromised, so that I can regain control - -#### Acceptance Criteria - -1. THE System SHALL provide account recovery via email verification -2. THE System SHALL allow password reset via email link -3. THE System SHALL implement security questions for account recovery (optional) -4. THE System SHALL log all login attempts (successful and failed) -5. THE System SHALL alert user of suspicious login activity -6. THE System SHALL allow user to view active sessions -7. THE System SHALL allow user to log out from all devices -8. THE System SHALL implement account lockout after 5 failed login attempts -9. THE System SHALL unlock account after 30 minutes or via email -10. THE System SHALL provide account deletion with data retention policy - -### Requirement 13: Role-Based User Management - -**User Story:** As an admin, I want to manage user roles and permissions, so that I can control system access - -#### Acceptance Criteria - -1. THE System SHALL support four user roles: Admin, Project_Manager, Site_Engineer, Storekeeper -2. THE System SHALL assign role during user registration or by admin -3. THE System SHALL display user's current role in profile -4. THE System SHALL enforce role-based access control on all pages -5. THE System SHALL prevent unauthorized role changes by non-admin users -6. THE System SHALL log all role changes for audit trail -7. THE System SHALL display role-specific features and permissions -8. THE System SHALL provide admin panel for user management (future) -9. THE System SHALL allow role-based email notifications -10. THE System SHALL implement permission inheritance for role hierarchy - -### Requirement 14: Data Persistence and Storage - -**User Story:** As a developer, I want reliable data persistence, so that user data is not lost - -#### Acceptance Criteria - -1. THE System SHALL store user data in localStorage with encryption -2. THE System SHALL store JWT tokens in localStorage with expiration -3. THE System SHALL implement data backup mechanism -4. THE System SHALL validate data integrity on retrieval -5. THE System SHALL handle localStorage quota exceeded errors -6. THE System SHALL provide data export functionality for users -7. THE System SHALL implement data retention policy -8. THE System SHALL allow users to request data deletion -9. THE System SHALL comply with GDPR data protection requirements -10. THE System SHALL implement audit logging for all data changes - -### Requirement 15: Testing and Quality Assurance - -**User Story:** As a developer, I want comprehensive testing, so that the authentication system is reliable - -#### Acceptance Criteria - -1. THE System SHALL include unit tests for all validation functions -2. THE System SHALL include integration tests for authentication flows -3. THE System SHALL include end-to-end tests for user journeys -4. THE System SHALL achieve 80%+ code coverage -5. THE System SHALL test all error scenarios -6. THE System SHALL test security vulnerabilities -7. THE System SHALL test performance and load times -8. THE System SHALL test accessibility compliance (WCAG 2.1 AA) -9. THE System SHALL test cross-browser compatibility -10. THE System SHALL test mobile responsiveness - diff --git a/.kiro/specs/construction-site-management-auth/tasks.md b/.kiro/specs/construction-site-management-auth/tasks.md deleted file mode 100644 index ddaf35a..0000000 --- a/.kiro/specs/construction-site-management-auth/tasks.md +++ /dev/null @@ -1,456 +0,0 @@ -# Implementation Plan: Production-Ready Authentication System - -## Overview - -This plan implements a production-ready authentication system with sign up, email verification, login, forgot password, and account management. The system uses localStorage for data persistence and implements real-world security practices. - -## Tasks - -- [x] 1. Create authentication service layer - - [x] 1.1 Create AuthService with all authentication methods - - Implement signup, login, logout functions - - Implement password reset and verification functions - - Implement session management functions - - Implement localStorage operations with encryption - - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 5.10_ - - - [x] 1.2 Create validation utilities - - Implement email validation function - - Implement password strength validation - - Implement name validation - - Implement phone validation - - Implement form field validation - - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 8.10_ - - - [x] 1.3 Create password hashing utilities - - Implement bcrypt password hashing - - Implement password comparison function - - Implement password strength checker - - _Requirements: 9.1, 9.2, 9.3_ - - - [x] 1.4 Create JWT token utilities - - Implement JWT token generation - - Implement JWT token validation - - Implement token expiration handling - - _Requirements: 9.3, 9.4, 9.5_ - -- [x] 2. Create authentication context and state management - - [x] 2.1 Create AuthContext provider - - Implement authentication state (user, token, loading, error) - - Implement authentication actions (signup, login, logout, etc.) - - Implement session persistence on app load - - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 5.10_ - - - [x] 2.2 Create useAuth custom hook - - Provide easy access to auth state and actions - - Implement auth state selectors - - _Requirements: 5.1, 5.2, 5.3_ - -- [x] 3. Create reusable authentication UI components - - [x] 3.1 Create FormInput component - - Implement text, email, password input types - - Implement real-time validation - - Implement error display - - Implement show/hide toggle for passwords - - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9, 10.10_ - - - [x] 3.2 Create PasswordStrengthIndicator component - - Display strength meter (0-4 levels) - - Display requirement checklist - - Color-code strength levels - - _Requirements: 1.11, 7.11_ - - - [x] 3.3 Create Toast notification component - - Implement success, error, warning, info types - - Implement auto-dismiss - - Implement notification queue - - _Requirements: 10.4_ - - - [x] 3.4 Create LoadingSpinner component - - Implement animated spinner - - Implement loading text - - Implement overlay mode - - _Requirements: 10.3_ - -- [x] 4. Create Sign Up page - - [x] 4.1 Create Sign Up form - - Implement form fields: Name, Email, Password, Confirm Password - - Implement form validation - - Implement password strength indicator - - Implement show/hide password toggles - - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.11, 1.12_ - - - [x] 4.2 Implement sign up submission - - Validate all fields - - Check for duplicate email - - Create user account in localStorage - - Generate verification code - - Display success message - - Redirect to email verification page - - _Requirements: 1.7, 1.8, 1.9_ - - - [x] 4.3 Add links and branding - - Add link to Login page - - Add company branding and logo - - Add Terms & Conditions checkbox - - _Requirements: 1.10, 10.5, 10.6_ - -- [x] 5. Create Email Verification page - - [x] 5.1 Create verification code input - - Implement 6-digit code input - - Implement code validation - - Implement countdown timer (5 minutes) - - _Requirements: 2.2, 2.3, 2.4, 2.9_ - - - [x] 5.2 Implement verification submission - - Validate verification code - - Mark email as verified - - Redirect to Dashboard on success - - Display error on invalid code - - _Requirements: 2.5, 2.6, 2.7_ - - - [x] 5.3 Implement resend code functionality - - Generate new verification code - - Send code to email - - Reset countdown timer - - _Requirements: 2.3, 2.8_ - - - [x] 5.4 Add navigation and messaging - - Add "Back to Login" link - - Display verification message - - Display error messages - - _Requirements: 2.1, 2.10_ - -- [x] 6. Create Login page - - [x] 6.1 Create login form - - Implement form fields: Email, Password - - Implement form validation - - Implement show/hide password toggle - - Implement Remember Me checkbox - - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.11, 3.12_ - - - [x] 6.2 Implement login submission - - Validate credentials - - Verify against localStorage users - - Create JWT token - - Store session data - - Redirect to Dashboard - - _Requirements: 3.5, 3.6, 3.7, 3.8_ - - - [x] 6.3 Implement error handling - - Display "Invalid email or password" error - - Implement account lockout after 5 attempts - - Display locked account message - - _Requirements: 3.8, 3.9_ - - - [x] 6.4 Add links and branding - - Add "Forgot Password?" link - - Add "Sign Up" link - - Add company branding and logo - - _Requirements: 3.9, 3.10, 3.13_ - -- [x] 7. Create Forgot Password page - - [x] 7.1 Create forgot password form - - Implement email input field - - Implement form validation - - _Requirements: 4.1, 4.2_ - - - [x] 7.2 Implement password reset request - - Validate email exists - - Generate password reset token - - Display success message (generic for security) - - _Requirements: 4.3, 4.4, 4.5, 4.6_ - - - [x] 7.3 Add navigation and messaging - - Add "Back to Login" link - - Display reset message - - _Requirements: 4.7_ - -- [x] 8. Create Password Reset page - - [x] 8.1 Create password reset form - - Implement form fields: New Password, Confirm Password - - Implement form validation - - Implement password strength indicator - - Implement show/hide password toggles - - _Requirements: 4.8, 4.9_ - - - [x] 8.2 Implement password reset submission - - Validate reset token - - Check token expiration - - Update password in localStorage - - Redirect to Login page - - _Requirements: 4.10, 4.11, 4.12_ - - - [x] 8.3 Add error handling - - Display expired token message - - Display validation errors - - _Requirements: 4.12_ - -- [ ] 9. Create Profile page - - [ ] 9.1 Create profile display - - Display user information (Name, Email, Role, Phone) - - Display account creation date - - Display last login date - - _Requirements: 6.1, 6.7, 6.8_ - - - [ ] 9.2 Implement edit profile functionality - - Create edit form with Name and Phone fields - - Implement form validation - - Update user data in localStorage - - Display success message - - _Requirements: 6.2, 6.3, 6.4, 6.5, 6.6_ - - - [ ] 9.3 Add account management links - - Add "Change Password" link - - Add "Delete Account" button with confirmation - - _Requirements: 6.9, 6.10_ - -- [ ] 10. Create Change Password page - - [ ] 10.1 Create change password form - - Implement form fields: Current Password, New Password, Confirm Password - - Implement form validation - - Implement password strength indicator - - Implement show/hide password toggles - - _Requirements: 7.1, 7.2, 7.3, 7.4_ - - - [ ] 10.2 Implement password change submission - - Verify current password - - Validate new password strength - - Update password in localStorage - - Display success message - - Redirect to Profile page - - _Requirements: 7.5, 7.6, 7.7, 7.8_ - - - [ ] 10.3 Add error handling - - Display "Current password incorrect" error - - Display validation errors - - _Requirements: 7.5_ - -- [ ] 11. Implement session management - - [ ] 11.1 Create session persistence - - Store JWT token in localStorage - - Store user data in localStorage - - Check for valid token on app initialization - - Auto-login if valid token exists - - _Requirements: 5.1, 5.2, 5.3, 5.4_ - - - [ ] 11.2 Implement logout functionality - - Clear token and user data from localStorage - - Redirect to Login page - - _Requirements: 5.6, 5.7_ - - - [ ] 11.3 Implement session timeout - - Track last activity timestamp - - Implement 24-hour session timeout - - Redirect to Login on timeout - - _Requirements: 5.10_ - - - [ ] 11.4 Implement cross-tab session management - - Maintain session across browser tabs - - Sync logout across tabs - - _Requirements: 5.8, 5.9_ - -- [ ] 12. Implement security features - - [ ] 12.1 Implement password hashing - - Hash passwords using bcrypt - - Never store plain text passwords - - Implement password comparison - - _Requirements: 9.1, 9.2_ - - - [ ] 12.2 Implement input sanitization - - Sanitize all user input to prevent XSS - - Validate input on client side - - Escape special characters in output - - _Requirements: 9.4, 9.5_ - - - [ ] 12.3 Implement rate limiting - - Max 5 login attempts per 15 minutes - - Lock account after 5 failed attempts - - Unlock after 30 minutes - - _Requirements: 9.8, 12.8, 12.9_ - - - [ ] 12.4 Implement CSRF protection - - Generate CSRF tokens - - Validate tokens on form submission - - _Requirements: 9.6_ - -- [ ] 13. Implement error handling and validation - - [ ] 13.1 Create comprehensive error messages - - Display field-level errors - - Display form-level errors - - Display authentication errors - - Display server errors - - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 8.10_ - - - [ ] 13.2 Implement real-time validation - - Validate fields as user types - - Display validation feedback - - Disable submit button until valid - - _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_ - - - [ ] 13.3 Implement error recovery - - Allow users to correct errors - - Clear error messages on correction - - Provide helpful hints - - _Requirements: 8.10_ - -- [ ] 14. Implement responsive design - - [ ] 14.1 Create mobile-responsive layouts - - Single column layout on mobile - - Full-width form inputs - - Larger touch targets (44px minimum) - - Stacked buttons - - _Requirements: 10.2_ - - - [ ] 14.2 Create tablet-responsive layouts - - Two column layout where appropriate - - Optimized spacing - - Touch-friendly interface - - _Requirements: 10.2_ - - - [ ] 14.3 Create desktop layouts - - Multi-column layout - - Centered form containers - - Hover states - - Keyboard navigation - - _Requirements: 10.2_ - -- [ ] 15. Implement accessibility features - - [ ] 15.1 Add semantic HTML - - Use proper HTML structure - - Use semantic elements - - _Requirements: 10.8_ - - - [ ] 15.2 Add ARIA labels - - Label all form inputs - - Add ARIA labels to interactive elements - - _Requirements: 10.8_ - - - [ ] 15.3 Implement keyboard navigation - - Tab through form fields - - Enter to submit forms - - Escape to close modals - - _Requirements: 10.8_ - - - [ ] 15.4 Ensure color contrast - - Verify 4.5:1 contrast ratio for text - - Test with accessibility tools - - _Requirements: 10.8_ - -- [ ] 16. Integrate with existing application - - [ ] 16.1 Update App.jsx routing - - Add routes for all authentication pages - - Implement public routes (Sign Up, Login, Forgot Password) - - Implement protected routes (Profile, Change Password) - - _Requirements: 5.1, 5.2, 5.3_ - - - [ ] 16.2 Update AppContext - - Integrate AuthContext with AppContext - - Merge authentication and application state - - _Requirements: 5.1, 5.2, 5.3_ - - - [ ] 16.3 Update Navbar - - Display user name and role - - Add Profile link - - Add Logout button - - _Requirements: 6.1, 6.2_ - - - [ ] 16.4 Update protected routes - - Check authentication status - - Redirect unauthenticated users to Login - - _Requirements: 5.1, 5.2, 5.3_ - -- [ ] 17. Implement email notifications (mock) - - [ ] 17.1 Create email notification service - - Mock email sending for development - - Log emails to console - - Display email preview in UI - - _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5_ - - - [ ] 17.2 Create email templates - - Welcome email template - - Verification code email template - - Password reset email template - - Password change confirmation email template - - _Requirements: 11.7, 11.8_ - -- [ ] 18. Implement data persistence and storage - - [ ] 18.1 Create localStorage encryption - - Implement simple encryption for sensitive data - - Implement decryption on retrieval - - _Requirements: 14.1, 14.2_ - - - [ ] 18.2 Implement data validation - - Validate data integrity on retrieval - - Handle corrupted data gracefully - - _Requirements: 14.4_ - - - [ ] 18.3 Implement data export - - Allow users to export their data - - Provide data in JSON format - - _Requirements: 14.6_ - -- [ ] 19. Create comprehensive testing - - [ ] 19.1 Create unit tests - - Test validation functions - - Test password strength checker - - Test email validator - - Test token generation - - _Requirements: 15.1, 15.2_ - - - [ ] 19.2 Create integration tests - - Test sign up flow - - Test email verification flow - - Test login flow - - Test password reset flow - - Test profile update flow - - _Requirements: 15.2, 15.3_ - - - [ ] 19.3 Create end-to-end tests - - Test complete user registration journey - - Test complete login journey - - Test complete password recovery journey - - _Requirements: 15.3_ - - - [ ] 19.4 Create security tests - - Test XSS prevention - - Test CSRF protection - - Test password hashing - - Test token expiration - - Test rate limiting - - _Requirements: 15.6_ - -- [ ] 20. Final testing and deployment - - [ ] 20.1 Cross-browser testing - - Test on Chrome, Firefox, Safari, Edge - - Test on mobile browsers - - _Requirements: 15.9_ - - - [ ] 20.2 Performance testing - - Test page load times - - Test form submission times - - Optimize performance - - _Requirements: 15.7_ - - - [ ] 20.3 Accessibility testing - - Test with screen readers - - Test keyboard navigation - - Verify WCAG 2.1 AA compliance - - _Requirements: 15.8, 15.10_ - - - [ ] 20.4 Final verification - - Verify all requirements are met - - Test all user flows - - Verify security measures - - _Requirements: 15.1, 15.2, 15.3, 15.4, 15.5, 15.6, 15.7, 15.8, 15.9, 15.10_ - -## Notes - -- This is a frontend-only implementation using localStorage -- Backend integration will be added later -- Email notifications are mocked for development -- All security measures are implemented on the frontend -- Real backend will need to implement server-side validation and security -- Testing is comprehensive to ensure reliability -- Accessibility is prioritized for inclusive design - diff --git a/.kiro/specs/construction-site-management-system/.config.kiro b/.kiro/specs/construction-site-management-system/.config.kiro deleted file mode 100644 index f706d87..0000000 --- a/.kiro/specs/construction-site-management-system/.config.kiro +++ /dev/null @@ -1 +0,0 @@ -{"specId": "55f38ad3-cfa5-4068-a00c-942365a7fac7", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/construction-site-management-system/design.md b/.kiro/specs/construction-site-management-system/design.md deleted file mode 100644 index 0086328..0000000 --- a/.kiro/specs/construction-site-management-system/design.md +++ /dev/null @@ -1,964 +0,0 @@ -# Design Document: Construction Site Management System - -## Overview - -The Construction Site Management System (SiteOS Enterprise) is a production-quality React 19 frontend application that provides comprehensive construction project management capabilities. The system implements a dark industrial design aesthetic and serves four distinct user roles with role-based access control. - -### System Architecture Philosophy - -This is a frontend-only application that simulates a full-stack experience using React Context API as a mock data layer. The architecture prioritizes: - -- **Component modularity**: Clear separation between layout, UI, and feature components -- **State centralization**: Single source of truth via Context API -- **Role-based security**: Navigation and route protection based on user roles -- **Responsive design**: Mobile-first approach with progressive enhancement -- **Type safety through structure**: Consistent data shapes and prop interfaces - -### Key Technical Decisions - -1. **React 19 with Vite**: Leverages latest React features (concurrent rendering, automatic batching) with fast HMR development experience -2. **Tailwind CSS utility-first**: Enables rapid UI development with consistent design tokens -3. **Context API over Redux**: Simpler state management appropriate for frontend-only scope -4. **Recharts for visualization**: Declarative chart library with good React integration -5. **React Router v6**: Modern routing with data loading and nested route support - -## Architecture - -### High-Level Component Hierarchy - -``` -App -├── AppContext.Provider (Global State) -├── Router - ├── AppLayout - │ ├── Sidebar (Navigation) - │ ├── Navbar (Header) - │ └── Outlet (Page Content) - │ ├── Dashboard (Protected) - │ ├── Projects (Protected: Admin, PM, SE) - │ ├── Tasks (Protected: PM, SE) - │ ├── Workforce (Protected: Admin, SE) - │ ├── Inventory (Protected: Admin, PM, SK) - │ └── Finance (Protected: Admin, PM) - └── Login (Public) -``` - -### Data Flow Architecture - -```mermaid -graph TD - A[User Interaction] --> B[Component Event Handler] - B --> C[Context API Action] - C --> D[State Update] - D --> E[React Re-render] - E --> F[UI Update] - - G[Context Provider] --> H[Mock Data Arrays] - H --> I[CRUD Operations] - I --> G -``` - -### Module Organization - -``` -src/ -├── components/ -│ ├── layout/ # AppLayout, Sidebar, Navbar -│ ├── ui/ # Button, Input, Card, Modal, Badge, Table -│ └── charts/ # BudgetChart, CostDistributionChart -├── pages/ # Dashboard, Projects, Tasks, Workforce, Inventory, Finance -├── context/ # AppContext.jsx (state + actions) -├── data/ # mockData.js (initial seed data) -├── utils/ # helpers.js (date formatting, calculations) -└── App.jsx # Root component with Router -``` - -## Components and Interfaces - -### Core Layout Components - -#### AppLayout -**Purpose**: Provides consistent layout structure across all authenticated pages - -**Props**: None (uses Outlet for nested routes) - -**Structure**: -- Fixed sidebar (left, 256px width on desktop) -- Fixed navbar (top, full width) -- Main content area (scrollable, fills remaining space) -- Responsive: Sidebar collapses to hamburger menu below 768px - -#### Sidebar -**Purpose**: Primary navigation with role-based menu items - -**Props**: -```javascript -{ - currentUser: User, - isCollapsed: boolean, - onToggle: () => void -} -``` - -**Navigation Items**: -- Dashboard (all roles) -- Projects (Admin, Project_Manager, Site_Engineer) -- Tasks (Project_Manager, Site_Engineer) -- Workforce (Admin, Site_Engineer) -- Inventory (Admin, Project_Manager, Storekeeper) -- Finance (Admin, Project_Manager) - -**Visual States**: -- Active route: bg-amber-500 with text-slate-950 -- Inactive route: text-slate-400 with hover:bg-slate-800 -- Icons from Lucide React - -#### Navbar -**Purpose**: Top header with branding, date, notifications, and user menu - -**Props**: -```javascript -{ - currentUser: User, - onLogout: () => void, - onSwitchRole: () => void -} -``` - -**Elements**: -- Title: "SiteOS Enterprise" (text-amber-500, font-bold) -- Current date (text-slate-400) -- Notification bell icon (with badge count) -- User avatar with dropdown (Profile, Switch Role, Logout) - -### Reusable UI Components - -#### Button -**Props**: -```javascript -{ - variant: 'primary' | 'secondary' | 'danger', - size: 'sm' | 'md' | 'lg', - onClick: () => void, - disabled: boolean, - children: ReactNode -} -``` - -**Variants**: -- Primary: bg-amber-500 hover:bg-amber-600 -- Secondary: bg-slate-800 hover:bg-slate-700 -- Danger: bg-rose-600 hover:bg-rose-700 - -#### Input -**Props**: -```javascript -{ - label: string, - type: 'text' | 'number' | 'date' | 'email', - value: string, - onChange: (value: string) => void, - error: string, - required: boolean -} -``` - -**Styling**: bg-slate-900, border-slate-800, focus:border-amber-500 - -#### Select -**Props**: -```javascript -{ - label: string, - options: Array<{value: string, label: string}>, - value: string, - onChange: (value: string) => void -} -``` - -#### Card -**Props**: -```javascript -{ - title: string, - children: ReactNode, - className: string -} -``` - -**Base Styling**: bg-slate-900, rounded-xl, p-6, border border-slate-800 - -#### Badge -**Props**: -```javascript -{ - variant: 'status' | 'success' | 'warning' | 'danger', - children: string -} -``` - -**Variants**: -- Status: bg-blue-500/10 text-blue-500 -- Success: bg-emerald-500/10 text-emerald-500 -- Warning: bg-yellow-500/10 text-yellow-500 -- Danger: bg-rose-500/10 text-rose-500 - -#### Modal -**Props**: -```javascript -{ - isOpen: boolean, - onClose: () => void, - title: string, - children: ReactNode -} -``` - -**Features**: -- Overlay: bg-black/50 backdrop-blur-sm -- Content: bg-slate-900 rounded-xl max-w-2xl -- Close button: top-right corner -- ESC key to close -- Click outside to close - -#### Table -**Props**: -```javascript -{ - columns: Array<{key: string, label: string, render?: (value, row) => ReactNode}>, - data: Array, - onRowClick: (row) => void -} -``` - -**Styling**: -- Header: bg-slate-800 text-slate-400 -- Rows: hover:bg-slate-800/50 -- Borders: border-slate-800 -- Responsive: horizontal scroll on mobile - -### Page Components - -#### Dashboard -**Purpose**: Overview with KPIs, charts, and recent activity - -**Layout**: Bento grid with varied card sizes - -**Sections**: -1. KPI Cards (4 cards in grid) - - Total Projects (count) - - Active Workers (count) - - Low Stock Items (count with warning) - - Total Budget (sum with currency format) - -2. Budget vs Actual Chart (BarChart) - - X-axis: Project names - - Y-axis: Amount - - Two bars per project: Budget (amber), Actual (slate) - -3. Recent Tasks List (Card with table) - - Task name, Project name, Status badge - - Limited to 5 most recent - -#### Projects -**Purpose**: Project management with CRUD operations - -**Features**: -- Search bar (filters by project name or location) -- Filter dropdown (by project type) -- "New Project" button (opens modal) -- Data table with columns: Name, Location, Type, Start Date, Budget, Status - -**New Project Modal Form**: -- Project Name (text, required) -- Location (text, required) -- Project Type (select: Residential, Commercial, Infrastructure) -- Start Date (date, required) -- End Date (date, required) -- Budget (number, required) - -#### Tasks -**Purpose**: Kanban board for task management - -**Layout**: Three columns (Open, In Progress, Completed) - -**Task Card**: -- Task name (text-slate-50) -- Project name (text-slate-400, text-sm) -- Assigned user (text-slate-400, text-sm) -- Draggable with visual feedback - -**Drag & Drop**: -- Uses HTML5 drag and drop API -- onDragStart: store task id -- onDrop: call updateTaskStatus with new status -- Visual states: dragging (opacity-50), drop target (border-amber-500) - -#### Workforce -**Purpose**: Worker management and attendance tracking - -**Features**: -- Worker table with columns: Name, Skill, Contact, Rate Type, Base Rate, Attendance -- Attendance buttons per row: Present, Half Day, Absent -- Button states: active (bg-emerald-500), inactive (bg-slate-800) - -**Attendance Logic**: -- Clicking button calls updateWorkerAttendance(workerId, status, date) -- Updates worker's attendance record in context -- Visual feedback: button color change - -#### Inventory -**Purpose**: Material stock monitoring and reorder management - -**Features**: -- Inventory table with columns: Item Name, Category, Unit Cost, Current Stock, Min Stock, Actions -- Low stock highlighting: row with bg-rose-500/10 when current < min -- Warning icon (AlertTriangle from Lucide) for low stock items -- "Reorder" button for low stock items - -**Stock Status Logic**: -```javascript -const isLowStock = item.current_stock < item.min_stock_qty; -``` - -#### Finance -**Purpose**: Financial analytics and budget tracking - -**Sections**: -1. Cost Distribution Pie Chart - - Labor Costs (sum of Finance_Records where cost_category = 'Labor') - - Material Costs (sum of Finance_Records where cost_category = 'Material') - - Colors: amber-500 (Labor), slate-600 (Material) - -2. Project Budget Table - - Columns: Project, Budget, Total Cost, Remaining Budget - - Total Cost: calculated by summing Finance_Records per project - - Remaining Budget: Budget - Total Cost - - Color coding: text-emerald-500 if remaining > 0, text-rose-500 if < 0 - -### Chart Components - -#### BudgetChart -**Purpose**: Visualize budget vs actual expenses per project - -**Implementation**: -```javascript - - - - - - - - - - - -``` - -#### CostDistributionChart -**Purpose**: Show labor vs material cost distribution - -**Implementation**: -```javascript - - - - {costData.map((entry, index) => ( - - ))} - - - - - -``` - -## Data Models - -### User -```javascript -{ - id: string, - name: string, - role: 'Admin' | 'Project_Manager' | 'Site_Engineer' | 'Storekeeper', - email: string, - phone: string -} -``` - -### Project -```javascript -{ - id: string, - project_name: string, - site_location: string, - project_type: 'Residential' | 'Commercial' | 'Infrastructure', - start_date: string, // ISO 8601 format - end_date: string, // ISO 8601 format - budget: number, - status: 'Planning' | 'Active' | 'Completed' | 'On Hold' -} -``` - -### Task -```javascript -{ - id: string, - task_name: string, - projectId: string, - assigned_to: string, // userId - status: 'Open' | 'In Progress' | 'Completed', - priority: 'Low' | 'Medium' | 'High', - due_date: string -} -``` - -### Worker -```javascript -{ - id: string, - name: string, - skill_type: 'Mason' | 'Carpenter' | 'Electrician' | 'Plumber' | 'Laborer', - contact: string, - rate_type: 'Daily' | 'Hourly', - base_rate: number, - attendance: Array<{date: string, status: 'Present' | 'Half Day' | 'Absent'}> -} -``` - -### Inventory_Item -```javascript -{ - id: string, - item_name: string, - category: 'Cement' | 'Steel' | 'Bricks' | 'Sand' | 'Tools' | 'Other', - uom: string, // Unit of Measurement (kg, bags, pieces, etc.) - unit_cost: number, - min_stock_qty: number, - current_stock: number, - supplier: string -} -``` - -### Finance_Record -```javascript -{ - id: string, - projectId: string, - cost_category: 'Labor' | 'Material' | 'Equipment' | 'Other', - amount: number, - date: string, // ISO 8601 format - description: string, - payment_status: 'Pending' | 'Paid' -} -``` - -### Context State Shape -```javascript -{ - // Authentication - currentUser: User | null, - isAuthenticated: boolean, - - // Data Collections - users: Array, - projects: Array, - tasks: Array, - workers: Array, - inventory: Array, - financeRecords: Array, - - // Actions - login: (userId: string) => void, - logout: () => void, - switchRole: (newRole: string) => void, - - // Project Actions - addProject: (project: Omit) => void, - updateProject: (id: string, updates: Partial) => void, - deleteProject: (id: string) => void, - - // Task Actions - addTask: (task: Omit) => void, - updateTaskStatus: (id: string, status: string) => void, - - // Worker Actions - updateWorkerAttendance: (workerId: string, status: string, date: string) => void, - - // Inventory Actions - issueMaterial: (itemId: string, quantity: number, projectId: string) => void, - addProcurement: (itemId: string, quantity: number, cost: number) => void, - - // Finance Actions - addFinanceRecord: (record: Omit) => void -} -``` - -### Role-Based Access Matrix - -| Page | Admin | Project_Manager | Site_Engineer | Storekeeper | -|-----------|-------|-----------------|---------------|-------------| -| Dashboard | ✓ | ✓ | ✓ | ✓ | -| Projects | ✓ | ✓ | ✓ | ✗ | -| Tasks | ✗ | ✓ | ✓ | ✗ | -| Workforce | ✓ | ✗ | ✓ | ✗ | -| Inventory | ✓ | ✓ | ✗ | ✓ | -| Finance | ✓ | ✓ | ✗ | ✗ | - - -## Correctness Properties - -*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* - -### Property 1: Context Data Structure Integrity - -*For any* data entity (User, Project, Task, Worker, Inventory_Item, Finance_Record) stored in the Context Provider, the entity must contain all required fields with correct types as specified in the data model. - -**Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 4.6** - -### Property 2: Role-Based Navigation Visibility - -*For any* user role and navigation item, the navigation item should be visible if and only if the role has access according to the role-access matrix (Dashboard: all roles; Projects: Admin, Project_Manager, Site_Engineer; Tasks: Project_Manager, Site_Engineer; Workforce: Admin, Site_Engineer; Inventory: Admin, Project_Manager, Storekeeper; Finance: Admin, Project_Manager). - -**Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5, 5.6** - -### Property 3: Unauthorized Route Redirection - -*For any* user attempting to navigate to a route they don't have access to, the system should redirect them to the Dashboard page. - -**Validates: Requirements 5.7** - -### Property 4: Login State Update - -*For any* valid userId, calling the login function should update the currentUser state to the user with that id and set isAuthenticated to true. - -**Validates: Requirements 4.7** - -### Property 5: Context CRUD Operations - -*For any* valid data entity, calling the appropriate add/update function (addProject, addTask, updateTaskStatus, updateWorkerAttendance, issueMaterial, addProcurement) should correctly modify the corresponding array in the context state. - -**Validates: Requirements 4.8** - -### Property 6: Dashboard Accessibility - -*For any* user role, the Dashboard page should be accessible and render without errors. - -**Validates: Requirements 7.5** - -### Property 7: Projects Table Completeness - -*For any* project in the context state, that project should appear as a row in the Projects page table. - -**Validates: Requirements 8.1** - -### Property 8: Project Form Submission - -*For any* valid project data submitted through the project creation form, the addProject function should be called and the new project should appear in the projects array. - -**Validates: Requirements 8.8** - -### Property 9: Task Card Display Completeness - -*For any* task card rendered on the kanban board, the card must display the task name, project name, and assigned user name. - -**Validates: Requirements 9.3** - -### Property 10: Task Status Update on Drag - -*For any* task moved to a different kanban column, the updateTaskStatus function should be called with the task id and the new status corresponding to the target column. - -**Validates: Requirements 9.4** - -### Property 11: Workers Table Completeness - -*For any* worker in the context state, that worker should appear as a row in the Workforce page table. - -**Validates: Requirements 10.1** - -### Property 12: Worker Attendance Controls - -*For any* worker row in the workforce table, the row must include three attendance control buttons: Present, Half Day, and Absent. - -**Validates: Requirements 10.3** - -### Property 13: Attendance Update on Button Click - -*For any* worker and any attendance status button clicked, the updateWorkerAttendance function should be called with the worker id, the selected status, and the current date. - -**Validates: Requirements 10.4** - -### Property 14: Inventory Table Completeness - -*For any* inventory item in the context state, that item should appear as a row in the Inventory page table. - -**Validates: Requirements 11.1** - -### Property 15: Low Stock Indicator Consistency - -*For any* inventory item where current_stock < min_stock_qty, the item's row must display all three low stock indicators: row highlighting, warning icon, and reorder button. - -**Validates: Requirements 11.3, 11.4, 11.5** - -### Property 16: Finance Table Project Mapping - -*For any* project in the context state, there should be exactly one row in the Finance page table corresponding to that project. - -**Validates: Requirements 12.2** - -### Property 17: Finance Calculations Accuracy - -*For any* project, the Total Cost displayed should equal the sum of all finance records for that project, and the Remaining Budget should equal Budget minus Total Cost. - -**Validates: Requirements 12.4, 12.5** - -### Property 18: Chart Labeling Completeness - -*For any* chart component (bar chart, pie chart), the chart must include axis labels (where applicable) and a legend. - -**Validates: Requirements 16.6** - -### Property 19: Active Route Highlighting - -*For any* active route in the application, the corresponding navigation item in the Sidebar should be highlighted with the active state styling. - -**Validates: Requirements 17.3** - -### Property 20: Client-Side Navigation - -*For any* navigation link clicked, the route should change without triggering a full page reload (client-side navigation). - -**Validates: Requirements 17.4** - -### Property 21: Navigation State Persistence - -*For any* route transition, the application state (context data, user authentication) should be maintained without loss. - -**Validates: Requirements 17.6** - -### Property 22: Context State Change Re-rendering - -*For any* state update in the Context Provider, all components that consume that specific state via useContext should re-render to reflect the updated data. - -**Validates: Requirements 18.4** - -### Property 23: Required Field Validation - -*For any* required form field that is empty or invalid, the system should display an error message and prevent form submission. - -**Validates: Requirements 19.2** - -### Property 24: Successful Form Submission Behavior - -*For any* valid form submission, the system should: (1) close the modal, (2) update the context state with the new data, (3) display the new record in the relevant table, and (4) clear all form fields. - -**Validates: Requirements 19.3, 19.4, 19.5, 19.6** - -### Property 25: Button Hover State - -*For any* button component, hovering over it should trigger a color change to the hover state color. - -**Validates: Requirements 20.1** - -### Property 26: Button Click Visual Feedback - -*For any* button component, clicking it should display a visual click effect. - -**Validates: Requirements 20.2** - -### Property 27: Task Card Drag Visual State - -*For any* task card being dragged, the card should display a visual dragging state (e.g., reduced opacity). - -**Validates: Requirements 20.3** - -### Property 28: Immediate UI Update on Data Change - -*For any* data update operation (add, update, delete), the UI should reflect the change immediately without requiring a manual refresh. - -**Validates: Requirements 20.4** - -## Error Handling - -### Form Validation Errors - -**Strategy**: Client-side validation with immediate feedback - -**Implementation**: -- Required field validation: Check for empty strings or null values -- Type validation: Ensure numbers are numeric, dates are valid ISO 8601 format -- Range validation: Budget must be positive, dates must be logical (end_date > start_date) -- Display errors inline below the relevant input field -- Prevent form submission until all errors are resolved - -**Error Messages**: -- "This field is required" -- "Please enter a valid number" -- "End date must be after start date" -- "Budget must be greater than zero" - -### Context API Operation Errors - -**Strategy**: Defensive programming with fallback states - -**Implementation**: -- Check for entity existence before update/delete operations -- Validate foreign key relationships (e.g., projectId exists before adding task) -- Return success/failure status from context actions -- Log errors to console for debugging - -**Error Scenarios**: -- Attempting to update non-existent entity: No-op, log warning -- Invalid foreign key reference: Reject operation, show error toast -- Duplicate ID: Generate new unique ID automatically - -### Navigation Errors - -**Strategy**: Graceful fallback to safe routes - -**Implementation**: -- Protected routes check user role before rendering -- Unauthorized access redirects to Dashboard -- Invalid routes redirect to Dashboard (404 handling) -- Maintain navigation history for back button functionality - -### Data Consistency Errors - -**Strategy**: Validation at state update boundaries - -**Implementation**: -- Validate data shape before adding to context arrays -- Ensure required fields are present -- Type coercion for numeric fields (string to number) -- Default values for optional fields - -### UI Component Errors - -**Strategy**: React Error Boundaries for graceful degradation - -**Implementation**: -- Wrap major page components in Error Boundary -- Display user-friendly error message instead of blank screen -- Log error details to console -- Provide "Reload" button to recover - -**Error Boundary Fallback UI**: -``` -"Something went wrong loading this page. Please try reloading." -[Reload Button] -``` - -## Testing Strategy - -### Dual Testing Approach - -This system requires both unit testing and property-based testing for comprehensive coverage: - -**Unit Tests**: Focus on specific examples, edge cases, and integration points -- Component rendering with specific props -- User interaction flows (click, drag, form submission) -- Edge cases (empty lists, low stock items, negative budgets) -- Integration between components and context - -**Property Tests**: Verify universal properties across all inputs -- Data structure integrity across random data -- Role-based access control across all role combinations -- CRUD operations with randomly generated entities -- Calculation accuracy with varied numeric inputs - -### Property-Based Testing Configuration - -**Library Selection**: -- **fast-check** for JavaScript/React property-based testing -- Integrates well with Jest/Vitest test runners -- Provides generators for common data types and custom generators - -**Test Configuration**: -- Minimum 100 iterations per property test (due to randomization) -- Each property test must reference its design document property -- Tag format: `// Feature: construction-site-management-system, Property {number}: {property_text}` - -**Example Property Test Structure**: -```javascript -import fc from 'fast-check'; - -// Feature: construction-site-management-system, Property 1: Context Data Structure Integrity -test('all users in context have required fields', () => { - fc.assert( - fc.property( - fc.array(userGenerator()), - (users) => { - users.forEach(user => { - expect(user).toHaveProperty('id'); - expect(user).toHaveProperty('name'); - expect(user).toHaveProperty('role'); - expect(user).toHaveProperty('email'); - expect(user).toHaveProperty('phone'); - }); - } - ), - { numRuns: 100 } - ); -}); -``` - -### Unit Testing Strategy - -**Component Testing**: -- Use React Testing Library for component tests -- Test user interactions with fireEvent or userEvent -- Assert on rendered output and DOM structure -- Mock Context Provider for isolated component tests - -**Test Categories**: - -1. **Layout Components** (Sidebar, Navbar, AppLayout) - - Renders without crashing - - Displays correct navigation items based on role - - Responsive behavior at different viewport sizes - - User menu interactions - -2. **UI Components** (Button, Input, Card, Modal, Badge, Table) - - Renders with different prop variants - - Handles user interactions (click, change, submit) - - Displays error states correctly - - Applies correct styling classes - -3. **Page Components** (Dashboard, Projects, Tasks, Workforce, Inventory, Finance) - - Renders with mock context data - - Displays correct data in tables/charts - - Form submission updates context - - Search and filter functionality - - Role-based access enforcement - -4. **Context Provider** - - Initial state is correct - - CRUD operations update state correctly - - Login/logout updates authentication state - - State changes trigger re-renders - -**Example Unit Test**: -```javascript -import { render, screen, fireEvent } from '@testing-library/react'; -import { AppContext } from '../context/AppContext'; -import Projects from '../pages/Projects'; - -test('clicking New Project button opens modal', () => { - const mockContext = { - projects: [], - addProject: jest.fn(), - currentUser: { role: 'Admin' } - }; - - render( - - - - ); - - const newProjectButton = screen.getByText('New Project'); - fireEvent.click(newProjectButton); - - expect(screen.getByText('Create New Project')).toBeInTheDocument(); -}); -``` - -### Integration Testing - -**Focus Areas**: -- End-to-end user flows (login → navigate → create project → view in table) -- Context state updates propagating to multiple components -- Form submission → context update → table re-render -- Drag and drop → status update → UI refresh - -**Tools**: -- React Testing Library for component integration -- Mock Service Worker (MSW) if adding API calls in future -- Testing Library User Event for realistic user interactions - -### Test Coverage Goals - -**Minimum Coverage Targets**: -- Statements: 80% -- Branches: 75% -- Functions: 80% -- Lines: 80% - -**Priority Areas for 100% Coverage**: -- Context Provider CRUD operations -- Role-based access control logic -- Form validation logic -- Calculation functions (finance totals, remaining budget) - -### Custom Generators for Property Tests - -**Data Generators**: -```javascript -// User generator -const userGenerator = () => fc.record({ - id: fc.uuid(), - name: fc.string({ minLength: 1, maxLength: 50 }), - role: fc.constantFrom('Admin', 'Project_Manager', 'Site_Engineer', 'Storekeeper'), - email: fc.emailAddress(), - phone: fc.string({ minLength: 10, maxLength: 15 }) -}); - -// Project generator -const projectGenerator = () => fc.record({ - id: fc.uuid(), - project_name: fc.string({ minLength: 1, maxLength: 100 }), - site_location: fc.string({ minLength: 1, maxLength: 100 }), - project_type: fc.constantFrom('Residential', 'Commercial', 'Infrastructure'), - start_date: fc.date().map(d => d.toISOString()), - end_date: fc.date().map(d => d.toISOString()), - budget: fc.integer({ min: 10000, max: 10000000 }) -}); - -// Task generator -const taskGenerator = (projectIds, userIds) => fc.record({ - id: fc.uuid(), - task_name: fc.string({ minLength: 1, maxLength: 100 }), - projectId: fc.constantFrom(...projectIds), - assigned_to: fc.constantFrom(...userIds), - status: fc.constantFrom('Open', 'In Progress', 'Completed') -}); -``` - -### Testing Best Practices - -1. **Isolation**: Each test should be independent and not rely on other tests -2. **Clarity**: Test names should clearly describe what is being tested -3. **Arrange-Act-Assert**: Structure tests with clear setup, action, and verification -4. **Mock External Dependencies**: Mock Context Provider, Router, and external libraries -5. **Test User Behavior**: Focus on what users see and do, not implementation details -6. **Avoid Testing Implementation**: Don't test internal state or private methods -7. **Use Semantic Queries**: Prefer getByRole, getByLabelText over getByTestId -8. **Accessibility**: Ensure components are accessible (proper ARIA labels, keyboard navigation) - -### Continuous Integration - -**CI Pipeline**: -1. Lint code (ESLint) -2. Type check (if using TypeScript) -3. Run unit tests -4. Run property tests -5. Generate coverage report -6. Build production bundle -7. Run visual regression tests (optional) - -**Quality Gates**: -- All tests must pass -- Coverage must meet minimum thresholds -- No linting errors -- Build must succeed - diff --git a/.kiro/specs/construction-site-management-system/requirements.md b/.kiro/specs/construction-site-management-system/requirements.md deleted file mode 100644 index d67ea98..0000000 --- a/.kiro/specs/construction-site-management-system/requirements.md +++ /dev/null @@ -1,309 +0,0 @@ -# Requirements Document - -## Introduction - -The Construction Site Management System (SiteOS Enterprise) is a production-quality frontend application designed to manage construction projects, workforce, inventory, tasks, and finances. The system provides role-based access control for four user types (Admin, Project Manager, Site Engineer, Storekeeper) and delivers a modern enterprise SaaS experience with a dark industrial aesthetic. - -## Glossary - -- **System**: The Construction Site Management System frontend application -- **User**: Any authenticated person using the system with an assigned role -- **Admin**: User role with full system access including workforce and finance management -- **Project_Manager**: User role with access to projects, tasks, inventory, and finance -- **Site_Engineer**: User role with access to projects, tasks, and workforce -- **Storekeeper**: User role with access to inventory management only -- **Project**: A construction project with location, budget, timeline, and associated tasks -- **Task**: A work item assigned to a user within a project with status tracking -- **Worker**: A construction workforce member with skill type and rate information -- **Inventory_Item**: A material or supply tracked in the system with stock levels -- **Finance_Record**: A financial transaction associated with a project and cost category -- **Context_Provider**: React Context API implementation serving as the frontend data layer -- **Protected_Route**: A route component that enforces role-based access control -- **KPI_Card**: Key Performance Indicator display component showing summary metrics -- **Bento_Grid**: A modern dashboard layout pattern with varied card sizes -- **Modal**: An overlay dialog component for forms and confirmations -- **Badge**: A small status indicator component with color coding - -## Requirements - -### Requirement 1: Technology Stack Compliance - -**User Story:** As a developer, I want the system built with specific modern technologies, so that it meets performance and maintainability standards - -#### Acceptance Criteria - -1. THE System SHALL use React 19 as the UI framework -2. THE System SHALL use Vite as the build tool -3. THE System SHALL use Tailwind CSS for styling -4. THE System SHALL use React Router v6 for navigation -5. THE System SHALL use Recharts for data visualization -6. THE System SHALL use Lucide React for icons -7. THE System SHALL implement all components as functional components with hooks -8. THE System SHALL use Context API for global state management -9. THE System SHALL NOT include Material UI, Ant Design, or other heavy UI frameworks - -### Requirement 2: Design System Implementation - -**User Story:** As a user, I want a consistent dark industrial aesthetic, so that the interface feels professional and cohesive - -#### Acceptance Criteria - -1. THE System SHALL use bg-slate-950 as the main background color -2. THE System SHALL use bg-slate-900 for card surfaces -3. THE System SHALL use border-slate-800 for borders -4. THE System SHALL use bg-amber-500 with hover:bg-amber-600 for primary buttons -5. THE System SHALL use text-emerald-500 for success states -6. THE System SHALL use text-yellow-500 for warning states -7. THE System SHALL use text-rose-500 for danger states -8. THE System SHALL use text-slate-50 for primary text -9. THE System SHALL use text-slate-400 for secondary text -10. THE System SHALL apply rounded-xl to card components -11. THE System SHALL use p-6 spacing for card interiors -12. THE System SHALL include smooth transitions on interactive elements - -### Requirement 3: Project Structure Organization - -**User Story:** As a developer, I want a modular component structure, so that the codebase is maintainable and scalable - -#### Acceptance Criteria - -1. THE System SHALL organize layout components in src/components/layout directory -2. THE System SHALL organize reusable UI components in src/components/ui directory -3. THE System SHALL organize chart components in src/components/charts directory -4. THE System SHALL organize page components in src/pages directory -5. THE System SHALL place context providers in src/context directory -6. THE System SHALL place mock data in src/data directory -7. THE System SHALL include AppLayout, Sidebar, and Navbar in layout components -8. THE System SHALL include Button, Input, Select, Card, Table, Badge, and Modal in UI components - -### Requirement 4: Mock Data Layer - -**User Story:** As a developer, I want a simulated database using Context API, so that the application functions without a backend - -#### Acceptance Criteria - -1. THE Context_Provider SHALL maintain an array of Users with id, name, role, email, and phone fields -2. THE Context_Provider SHALL maintain an array of Projects with id, project_name, site_location, project_type, start_date, end_date, and budget fields -3. THE Context_Provider SHALL maintain an array of Tasks with id, task_name, projectId, assigned_to, and status fields -4. THE Context_Provider SHALL maintain an array of Workers with id, name, skill_type, contact, rate_type, and base_rate fields -5. THE Context_Provider SHALL maintain an array of Inventory_Items with id, item_name, category, uom, unit_cost, min_stock_qty, and current_stock fields -6. THE Context_Provider SHALL maintain an array of Finance_Records with id, projectId, cost_category, amount, and date fields -7. THE Context_Provider SHALL provide a login function accepting userId -8. THE Context_Provider SHALL provide functions for addProject, addTask, updateTaskStatus, updateWorkerAttendance, issueMaterial, and addProcurement - -### Requirement 5: Role-Based Access Control - -**User Story:** As a system administrator, I want role-based navigation restrictions, so that users only access authorized features - -#### Acceptance Criteria - -1. THE System SHALL display Dashboard navigation to all roles -2. THE System SHALL display Projects navigation to Admin, Project_Manager, and Site_Engineer roles only -3. THE System SHALL display Tasks navigation to Project_Manager and Site_Engineer roles only -4. THE System SHALL display Workforce navigation to Admin and Site_Engineer roles only -5. THE System SHALL display Inventory navigation to Admin, Project_Manager, and Storekeeper roles only -6. THE System SHALL display Finance navigation to Admin and Project_Manager roles only -7. WHEN a User attempts to access an unauthorized route, THE System SHALL redirect to the Dashboard page -8. THE System SHALL implement Protected_Route components using React Router v6 - -### Requirement 6: Application Layout Structure - -**User Story:** As a user, I want a consistent layout with navigation and header, so that I can easily navigate the system - -#### Acceptance Criteria - -1. THE System SHALL display a Sidebar on the left side of the viewport -2. THE System SHALL display a Navbar at the top of the viewport -3. THE System SHALL display page content in the main content area -4. THE Navbar SHALL display the title "SiteOS Enterprise" -5. THE Navbar SHALL display the current date -6. THE Navbar SHALL display a notification icon -7. THE Navbar SHALL display a user avatar with dropdown menu -8. THE User dropdown SHALL include Profile, Switch Role, and Logout options -9. WHEN viewport width is below tablet breakpoint, THE System SHALL collapse the Sidebar - -### Requirement 7: Dashboard Page - -**User Story:** As a user, I want an overview dashboard with key metrics, so that I can quickly assess system status - -#### Acceptance Criteria - -1. THE Dashboard SHALL display a Bento_Grid layout -2. THE Dashboard SHALL display four KPI_Cards showing Total Projects, Active Workers, Low Stock Items, and Total Budget -3. THE Dashboard SHALL display a Recharts bar chart comparing Project Budget versus Actual Expenses -4. THE Dashboard SHALL display a list of recent tasks with task name, project name, and status Badge -5. THE Dashboard SHALL be accessible to all roles - -### Requirement 8: Project Management Page - -**User Story:** As a Project_Manager, I want to view and create projects, so that I can manage construction initiatives - -#### Acceptance Criteria - -1. THE Projects page SHALL display a data table of all Projects -2. THE Projects table SHALL include columns for Project Name, Location, Type, Start Date, Budget, and Status -3. THE Projects page SHALL include a search bar for filtering projects -4. THE Projects page SHALL include a filter dropdown -5. THE Projects page SHALL include a "New Project" button -6. WHEN the "New Project" button is clicked, THE System SHALL display a Modal with a form -7. THE Project form SHALL include fields for Project Name, Location, Project Type, Start Date, End Date, and Budget -8. WHEN the Project form is submitted, THE System SHALL call the addProject function from Context_Provider -9. THE Projects page SHALL be accessible to Admin, Project_Manager, and Site_Engineer roles only - -### Requirement 9: Task Management Page - -**User Story:** As a Site_Engineer, I want to manage tasks with a kanban board, so that I can track work progress visually - -#### Acceptance Criteria - -1. THE Tasks page SHALL display a kanban board with three columns: Open, In Progress, and Completed -2. THE Tasks page SHALL display task cards that are draggable between columns -3. EACH task card SHALL display task name, project name, and assigned user name -4. WHEN a task card is moved to a different column, THE System SHALL call updateTaskStatus from Context_Provider -5. THE Tasks page SHALL be accessible to Project_Manager and Site_Engineer roles only - -### Requirement 10: Workforce Management Page - -**User Story:** As a Site_Engineer, I want to manage worker attendance, so that I can track labor availability - -#### Acceptance Criteria - -1. THE Workforce page SHALL display a table of all Workers -2. THE Workers table SHALL include columns for Name, Skill, Contact, Rate Type, and Base Rate -3. EACH worker row SHALL include attendance control buttons for Present, Half Day, and Absent -4. WHEN an attendance button is clicked, THE System SHALL call updateWorkerAttendance from Context_Provider -5. THE Workforce page SHALL be accessible to Admin and Site_Engineer roles only - -### Requirement 11: Inventory Management Page - -**User Story:** As a Storekeeper, I want to monitor material stock levels, so that I can prevent shortages - -#### Acceptance Criteria - -1. THE Inventory page SHALL display a table of all Inventory_Items -2. THE Inventory table SHALL include columns for Item Name, Category, Unit Cost, Current Stock, and Min Stock -3. WHEN an Inventory_Item has current_stock less than min_stock_qty, THE System SHALL highlight the row -4. WHEN an Inventory_Item has current_stock less than min_stock_qty, THE System SHALL display a warning icon -5. WHEN an Inventory_Item has current_stock less than min_stock_qty, THE System SHALL display a "Reorder" button -6. THE Inventory page SHALL be accessible to Admin, Project_Manager, and Storekeeper roles only - -### Requirement 12: Finance Analytics Page - -**User Story:** As an Admin, I want to view financial analytics, so that I can monitor project budgets and costs - -#### Acceptance Criteria - -1. THE Finance page SHALL display a Recharts pie chart showing distribution of Labor Costs versus Material Costs -2. THE Finance page SHALL display a table with one row per Project -3. THE Finance table SHALL include columns for Project, Budget, Total Cost, and Remaining Budget -4. THE Finance page SHALL calculate Total Cost by summing all Finance_Records for each Project -5. THE Finance page SHALL calculate Remaining Budget as Budget minus Total Cost -6. THE Finance page SHALL be accessible to Admin and Project_Manager roles only - -### Requirement 13: Reusable UI Components - -**User Story:** As a developer, I want consistent reusable components, so that the UI is uniform and maintainable - -#### Acceptance Criteria - -1. THE System SHALL provide a Button component with variants for primary, secondary, and danger styles -2. THE System SHALL provide an Input component with label and error state support -3. THE System SHALL provide a Select component with label and options array -4. THE System SHALL provide a Card component with consistent padding and styling -5. THE System SHALL provide a Badge component with color variants for status, success, warning, and danger -6. THE System SHALL provide a Modal component with overlay, close button, and content area -7. THE System SHALL provide a Table component with header and body rendering -8. ALL UI components SHALL follow the design system color palette and spacing standards - -### Requirement 14: Responsive Design - -**User Story:** As a user, I want the application to work on all devices, so that I can access it from desktop, tablet, or mobile - -#### Acceptance Criteria - -1. THE System SHALL display optimally on desktop viewports (1024px and above) -2. THE System SHALL display optimally on tablet viewports (768px to 1023px) -3. THE System SHALL display optimally on mobile viewports (below 768px) -4. WHEN viewport width is below 768px, THE System SHALL collapse the Sidebar into a hamburger menu -5. WHEN viewport width is below 768px, THE System SHALL stack Bento_Grid cards vertically -6. WHEN viewport width is below 768px, THE System SHALL make tables horizontally scrollable - -### Requirement 15: Code Quality Standards - -**User Story:** As a developer, I want clean professional code, so that the application is maintainable and production-ready - -#### Acceptance Criteria - -1. THE System SHALL include correct ES6 module imports for all dependencies -2. THE System SHALL include JSX syntax that is valid and executable -3. THE System SHALL include comments explaining architectural decisions -4. THE System SHALL NOT include placeholder UI or "TODO" comments in production code -5. THE System SHALL follow React best practices for component composition -6. THE System SHALL follow Tailwind CSS utility-first styling patterns -7. THE System SHALL include proper prop validation where appropriate - -### Requirement 16: Chart Visualization - -**User Story:** As a user, I want visual charts for data analysis, so that I can understand trends and distributions quickly - -#### Acceptance Criteria - -1. THE Dashboard SHALL display a bar chart using Recharts BarChart component -2. THE Dashboard bar chart SHALL compare Budget versus Actual Expenses for each Project -3. THE Finance page SHALL display a pie chart using Recharts PieChart component -4. THE Finance pie chart SHALL show the distribution of Labor Costs versus Material Costs -5. ALL charts SHALL use colors consistent with the design system -6. ALL charts SHALL include axis labels and legends where appropriate -7. ALL charts SHALL be responsive and resize with viewport changes - -### Requirement 17: Navigation and Routing - -**User Story:** As a user, I want seamless navigation between pages, so that I can access different features efficiently - -#### Acceptance Criteria - -1. THE System SHALL use React Router v6 for client-side routing -2. THE System SHALL define routes for Dashboard, Projects, Tasks, Workforce, Inventory, and Finance pages -3. THE Sidebar SHALL highlight the active route -4. WHEN a navigation link is clicked, THE System SHALL navigate without page reload -5. WHEN a User navigates to the root path, THE System SHALL display the Dashboard page -6. THE System SHALL maintain navigation state during route transitions - -### Requirement 18: State Management - -**User Story:** As a developer, I want centralized state management, so that data flows predictably through the application - -#### Acceptance Criteria - -1. THE System SHALL use React Context API for global state management -2. THE Context_Provider SHALL wrap the entire application component tree -3. THE Context_Provider SHALL expose state and functions via useContext hook -4. WHEN state is updated in Context_Provider, THE System SHALL re-render dependent components -5. THE System SHALL NOT use prop drilling for global state -6. THE System SHALL use local component state for UI-only state like modal visibility - -### Requirement 19: Form Handling - -**User Story:** As a user, I want intuitive forms for data entry, so that I can create and update records easily - -#### Acceptance Criteria - -1. THE Project creation Modal SHALL include form validation -2. WHEN a required form field is empty, THE System SHALL display an error message -3. WHEN a form is submitted successfully, THE System SHALL close the Modal -4. WHEN a form is submitted successfully, THE System SHALL update the Context_Provider state -5. WHEN a form is submitted successfully, THE System SHALL display the new record in the relevant table -6. THE System SHALL clear form fields after successful submission - -### Requirement 20: Interactive Feedback - -**User Story:** As a user, I want visual feedback for my actions, so that I know the system is responding - -#### Acceptance Criteria - -1. WHEN a User hovers over a button, THE System SHALL display a hover state with color change -2. WHEN a User clicks a button, THE System SHALL display a visual click effect -3. WHEN a User drags a task card, THE System SHALL display a dragging visual state -4. WHEN a User updates data, THE System SHALL reflect the change immediately in the UI -5. THE System SHALL use smooth CSS transitions for state changes -6. THE System SHALL provide visual indicators for loading states where appropriate diff --git a/.kiro/specs/construction-site-management-system/tasks.md b/.kiro/specs/construction-site-management-system/tasks.md deleted file mode 100644 index a0bcbf5..0000000 --- a/.kiro/specs/construction-site-management-system/tasks.md +++ /dev/null @@ -1,505 +0,0 @@ -# Implementation Plan: Construction Site Management System - -## Overview - -This plan implements the frontend-only Construction Site Management System using React 19, Vite, Tailwind CSS, React Router v6, Recharts, and Lucide React. The implementation follows a bottom-up approach: starting with project setup, then building reusable UI components, followed by layout components, Context API state management, page components, and finally integration with routing and testing. - -**Important**: This is frontend-only implementation using mock data and Context API. No backend API integration is included. - -## Tasks - -- [x] 1. Project setup and configuration - - Initialize Vite project with React 19 - - Install dependencies: react-router-dom, tailwindcss, recharts, lucide-react - - Configure Tailwind CSS with custom color palette (slate-950, slate-900, amber-500, etc.) - - Set up project directory structure: src/components/{layout,ui,charts}, src/pages, src/context, src/data, src/utils - - Configure ESLint and prettier for code quality - - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 15.1, 15.2, 15.5, 15.6_ - -- [x] 2. Create reusable UI components - - [x] 2.1 Implement Button component - - Create Button.jsx with variants (primary, secondary, danger) and sizes (sm, md, lg) - - Apply Tailwind classes for design system colors and hover states - - _Requirements: 2.4, 2.12, 13.1, 20.1, 20.2_ - - - [x] 2.2 Implement Input component - - Create Input.jsx with label, error state, and validation support - - Support input types: text, number, date, email - - _Requirements: 2.8, 2.9, 13.2, 19.2_ - - - [x] 2.3 Implement Select component - - Create Select.jsx with label and options array - - Apply consistent styling with Input component - - _Requirements: 13.3_ - - - [x] 2.4 Implement Card component - - Create Card.jsx with title prop and children - - Apply bg-slate-900, rounded-xl, p-6, border-slate-800 - - _Requirements: 2.2, 2.3, 2.10, 2.11, 13.4_ - - - [x] 2.5 Implement Badge component - - Create Badge.jsx with variants (status, success, warning, danger) - - Apply color-coded backgrounds and text colors - - _Requirements: 2.5, 2.6, 2.7, 13.5_ - - - [x] 2.6 Implement Modal component - - Create Modal.jsx with overlay, close button, ESC key handler, and click-outside-to-close - - Apply backdrop-blur-sm and bg-slate-900 for content - - _Requirements: 13.6_ - - - [x] 2.7 Implement Table component - - Create Table.jsx with columns prop and data array - - Support custom render functions for columns - - Apply hover states and responsive horizontal scroll - - _Requirements: 13.7, 14.6_ - -- [x] 3. Create chart components - - [x] 3.1 Implement BudgetChart component - - Create BudgetChart.jsx using Recharts BarChart - - Display Budget vs Actual bars for each project - - Apply design system colors (amber-500 for budget, slate-600 for actual) - - Include CartesianGrid, XAxis, YAxis, Tooltip, and Legend - - _Requirements: 16.1, 16.2, 16.5, 16.6, 16.7_ - - - [x] 3.2 Implement CostDistributionChart component - - Create CostDistributionChart.jsx using Recharts PieChart - - Display Labor vs Material cost distribution - - Apply design system colors and custom label rendering - - _Requirements: 16.3, 16.4, 16.5, 16.6, 16.7_ - -- [x] 4. Implement Context API and mock data layer - - [x] 4.1 Create mock data seed file - - Create src/data/mockData.js with sample data for all entities - - Include 3-5 users with different roles - - Include 4-6 projects with varied types and statuses - - Include 8-10 tasks across different projects - - Include 6-8 workers with different skills - - Include 8-10 inventory items with some below min stock - - Include 10-15 finance records across projects - - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_ - - - [x] 4.2 Implement AppContext provider - - Create src/context/AppContext.jsx with Context and Provider - - Initialize state with mock data arrays - - Implement authentication state (currentUser, isAuthenticated) - - _Requirements: 4.7, 18.1, 18.2, 18.3_ - - - [x] 4.3 Implement Context CRUD actions - - Implement login(userId) and logout() functions - - Implement switchRole(newRole) function - - Implement addProject, updateProject, deleteProject functions - - Implement addTask and updateTaskStatus functions - - Implement updateWorkerAttendance function - - Implement issueMaterial and addProcurement functions - - Implement addFinanceRecord function - - _Requirements: 4.8, 18.4_ - - - [ ]* 4.4 Write property test for Context data structure integrity - - **Property 1: Context Data Structure Integrity** - - **Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 4.6** - - Use fast-check to generate random entities and verify all required fields exist with correct types - -- [x] 5. Create layout components - - [x] 5.1 Implement Sidebar component - - Create Sidebar.jsx with navigation items - - Implement role-based navigation visibility logic - - Apply active route highlighting (bg-amber-500) - - Include Lucide React icons for each menu item - - Implement collapse/expand functionality for mobile - - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 6.1, 14.4, 17.3_ - - - [ ]* 5.2 Write property test for role-based navigation visibility - - **Property 2: Role-Based Navigation Visibility** - - **Validates: Requirements 5.1, 5.2, 5.3, 5.4, 5.5, 5.6** - - Test all role combinations against navigation items to verify access matrix - - - [x] 5.3 Implement Navbar component - - Create Navbar.jsx with title, date, notification icon, and user menu - - Display "SiteOS Enterprise" in text-amber-500 - - Implement user dropdown with Profile, Switch Role, and Logout options - - _Requirements: 6.4, 6.5, 6.6, 6.7, 6.8_ - - - [x] 5.4 Implement AppLayout component - - Create AppLayout.jsx with Sidebar, Navbar, and Outlet - - Apply fixed positioning for sidebar and navbar - - Implement responsive layout with mobile breakpoints - - _Requirements: 6.2, 6.3, 6.9, 14.1, 14.2, 14.3_ - -- [x] 6. Checkpoint - Verify component rendering - - Ensure all UI, chart, and layout components render without errors, ask the user if questions arise. - -- [x] 7. Implement Dashboard page - - [x] 7.1 Create Dashboard page structure - - Create Dashboard.jsx with Bento grid layout - - Implement responsive grid that stacks on mobile - - _Requirements: 7.1, 14.5_ - - - [x] 7.2 Implement KPI cards - - Create four KPI cards: Total Projects, Active Workers, Low Stock Items, Total Budget - - Calculate metrics from Context data - - Apply Card component with appropriate styling - - _Requirements: 7.2_ - - - [x] 7.3 Integrate BudgetChart - - Add BudgetChart component to Dashboard - - Pass project data with budget and calculated actual expenses - - _Requirements: 7.3_ - - - [x] 7.4 Implement recent tasks list - - Display 5 most recent tasks with name, project, and status badge - - Use Table component for consistent styling - - _Requirements: 7.4_ - - - [ ]* 7.5 Write unit tests for Dashboard - - Test KPI calculations with various data sets - - Test chart data transformation - - Test responsive layout behavior - - _Requirements: 7.5_ - -- [x] 8. Implement Projects page - - [x] 8.1 Create Projects page structure - - Create Projects.jsx with table, search bar, filter dropdown, and "New Project" button - - _Requirements: 8.1, 8.3, 8.4, 8.5_ - - - [x] 8.2 Implement projects table - - Display all projects with columns: Name, Location, Type, Start Date, Budget, Status - - Use Table component with custom column renderers - - _Requirements: 8.2_ - - - [x] 8.3 Implement search and filter functionality - - Add search input that filters by project name or location - - Add filter dropdown for project type - - Update table display based on filters - - _Requirements: 8.3, 8.4_ - - - [x] 8.4 Implement new project modal and form - - Create modal with form fields: Project Name, Location, Project Type, Start Date, End Date, Budget - - Implement form validation for required fields - - Wire form submission to Context addProject function - - Close modal and clear form on successful submission - - _Requirements: 8.6, 8.7, 8.8, 19.1, 19.2, 19.3, 19.4, 19.5, 19.6_ - - - [ ]* 8.5 Write property test for project form submission - - **Property 8: Project Form Submission** - - **Validates: Requirements 8.8** - - Generate random valid project data and verify it appears in projects array after submission - - - [ ]* 8.6 Write unit tests for Projects page - - Test search filtering with various queries - - Test project type filtering - - Test modal open/close behavior - - Test form validation errors - -- [x] 9. Implement Tasks page - - [x] 9.1 Create Tasks page with kanban board structure - - Create Tasks.jsx with three columns: Open, In Progress, Completed - - Apply column styling with bg-slate-900 cards - - _Requirements: 9.1_ - - - [x] 9.2 Implement task cards - - Create task card component displaying task name, project name, and assigned user - - Apply Card component styling - - _Requirements: 9.3_ - - - [x] 9.3 Implement drag and drop functionality - - Add HTML5 drag and drop handlers (onDragStart, onDragOver, onDrop) - - Implement visual dragging state (opacity-50) - - Implement drop target highlighting (border-amber-500) - - Wire drop event to Context updateTaskStatus function - - _Requirements: 9.2, 9.4, 20.3_ - - - [ ]* 9.4 Write property test for task status update on drag - - **Property 10: Task Status Update on Drag** - - **Validates: Requirements 9.4** - - Verify updateTaskStatus is called with correct parameters when task is moved - - - [ ]* 9.5 Write unit tests for Tasks page - - Test task card rendering with complete data - - Test drag and drop state changes - - Test status update after drop - -- [x] 10. Implement Workforce page - - [x] 10.1 Create Workforce page structure - - Create Workforce.jsx with workers table - - _Requirements: 10.1_ - - - [x] 10.2 Implement workers table with attendance controls - - Display columns: Name, Skill, Contact, Rate Type, Base Rate, Attendance - - Add three attendance buttons per row: Present, Half Day, Absent - - Apply active state styling (bg-emerald-500) for selected attendance - - Wire button clicks to Context updateWorkerAttendance function - - _Requirements: 10.2, 10.3, 10.4_ - - - [ ]* 10.3 Write property test for attendance update - - **Property 13: Attendance Update on Button Click** - - **Validates: Requirements 10.4** - - Verify updateWorkerAttendance is called with correct workerId, status, and date - - - [ ]* 10.4 Write unit tests for Workforce page - - Test attendance button state changes - - Test attendance data persistence in context - -- [x] 11. Implement Inventory page - - [x] 11.1 Create Inventory page structure - - Create Inventory.jsx with inventory table - - _Requirements: 11.1_ - - - [x] 11.2 Implement inventory table with low stock indicators - - Display columns: Item Name, Category, Unit Cost, Current Stock, Min Stock, Actions - - Implement low stock logic: current_stock < min_stock_qty - - Apply row highlighting (bg-rose-500/10) for low stock items - - Display warning icon (AlertTriangle from Lucide) for low stock - - Display "Reorder" button for low stock items - - _Requirements: 11.2, 11.3, 11.4, 11.5_ - - - [ ]* 11.3 Write property test for low stock indicator consistency - - **Property 15: Low Stock Indicator Consistency** - - **Validates: Requirements 11.3, 11.4, 11.5** - - Verify all three indicators appear when current_stock < min_stock_qty - - - [ ]* 11.4 Write unit tests for Inventory page - - Test low stock highlighting with various stock levels - - Test reorder button visibility - -- [x] 12. Implement Finance page - - [x] 12.1 Create Finance page structure - - Create Finance.jsx with cost distribution chart and budget table - - _Requirements: 12.1_ - - - [x] 12.2 Integrate CostDistributionChart - - Calculate total labor costs from finance records - - Calculate total material costs from finance records - - Pass data to CostDistributionChart component - - _Requirements: 12.1_ - - - [x] 12.3 Implement project budget table - - Display one row per project with columns: Project, Budget, Total Cost, Remaining Budget - - Calculate Total Cost by summing finance records per project - - Calculate Remaining Budget as Budget - Total Cost - - Apply color coding: text-emerald-500 for positive, text-rose-500 for negative - - _Requirements: 12.2, 12.3, 12.4, 12.5_ - - - [ ]* 12.4 Write property test for finance calculations accuracy - - **Property 17: Finance Calculations Accuracy** - - **Validates: Requirements 12.4, 12.5** - - Verify Total Cost equals sum of finance records and Remaining Budget equals Budget - Total Cost - - - [ ]* 12.5 Write unit tests for Finance page - - Test cost distribution calculation with various finance records - - Test budget table calculations with edge cases (zero budget, negative remaining) - -- [x] 13. Checkpoint - Verify all pages render correctly - - Ensure all pages render without errors and display mock data correctly, ask the user if questions arise. - -- [x] 14. Implement routing and navigation - - [x] 14.1 Set up React Router configuration - - Create router in App.jsx with routes for all pages - - Define root route ("/") to redirect to Dashboard - - Wrap routes with AppLayout for authenticated pages - - _Requirements: 17.1, 17.2, 17.5_ - - - [x] 14.2 Implement protected routes with role-based access - - Create ProtectedRoute component that checks user role - - Redirect unauthorized users to Dashboard - - Apply ProtectedRoute to Projects, Tasks, Workforce, Inventory, and Finance routes - - _Requirements: 5.7, 5.8_ - - - [ ]* 14.3 Write property test for unauthorized route redirection - - **Property 3: Unauthorized Route Redirection** - - **Validates: Requirements 5.7** - - Test all role/route combinations to verify proper redirection - - - [x] 14.4 Implement navigation state persistence - - Verify context state persists during route transitions - - Test browser back/forward buttons - - _Requirements: 17.6_ - - - [ ]* 14.5 Write unit tests for routing - - Test route navigation without page reload - - Test protected route access control - - Test active route highlighting in sidebar - -- [x] 15. Implement authentication flow - - [x] 15.1 Create Login page - - Create Login.jsx with user selection (simulate login) - - Display list of available users from mock data - - Wire user selection to Context login function - - Redirect to Dashboard after successful login - - _Requirements: 4.7_ - - - [ ]* 15.2 Write property test for login state update - - **Property 4: Login State Update** - - **Validates: Requirements 4.7** - - Verify login function updates currentUser and isAuthenticated correctly - - - [x] 15.3 Implement logout functionality - - Wire Navbar logout button to Context logout function - - Clear currentUser and redirect to Login page - - _Requirements: 4.7_ - - - [x] 15.4 Implement switch role functionality - - Create role switcher in Navbar dropdown - - Wire to Context switchRole function - - Update navigation visibility based on new role - - _Requirements: 4.7_ - -- [x] 16. Implement responsive design - - [x] 16.1 Add mobile breakpoint styles - - Implement hamburger menu for Sidebar on mobile (<768px) - - Stack Bento grid cards vertically on mobile - - Make tables horizontally scrollable on mobile - - _Requirements: 14.3, 14.4, 14.5, 14.6_ - - - [x] 16.2 Test responsive behavior - - Verify layout at 320px, 768px, 1024px, and 1920px widths - - Test sidebar collapse/expand on mobile - - Test chart responsiveness - - _Requirements: 14.1, 14.2, 14.3, 16.7_ - -- [x] 17. Implement interactive feedback and polish - - [x] 17.1 Add CSS transitions to interactive elements - - Add transition classes to buttons, cards, and badges - - Implement smooth hover and active states - - _Requirements: 2.12, 20.5_ - - - [x] 17.2 Add loading states (optional for mock data) - - Add loading indicators for future API integration points - - _Requirements: 20.6_ - - - [x] 17.3 Verify immediate UI updates - - Test that all CRUD operations update UI without manual refresh - - Verify Context state changes trigger re-renders - - _Requirements: 20.4_ - -- [ ] 18. Testing setup and core property tests - - [ ] 18.1 Set up testing framework - - Install and configure Vitest, React Testing Library, and fast-check - - Create test utilities and custom render functions - - Set up coverage reporting - - _Requirements: 15.1, 15.2_ - - - [ ]* 18.2 Write property test for Context CRUD operations - - **Property 5: Context CRUD Operations** - - **Validates: Requirements 4.8** - - Test all CRUD functions with randomly generated valid entities - - - [ ]* 18.3 Write property test for dashboard accessibility - - **Property 6: Dashboard Accessibility** - - **Validates: Requirements 7.5** - - Verify Dashboard renders for all user roles - - - [ ]* 18.4 Write property test for projects table completeness - - **Property 7: Projects Table Completeness** - - **Validates: Requirements 8.1** - - Verify all projects in context appear in table - - - [ ]* 18.5 Write property test for task card display completeness - - **Property 9: Task Card Display Completeness** - - **Validates: Requirements 9.3** - - Verify task cards display all required fields - - - [ ]* 18.6 Write property test for workers table completeness - - **Property 11: Workers Table Completeness** - - **Validates: Requirements 10.1** - - Verify all workers in context appear in table - - - [ ]* 18.7 Write property test for worker attendance controls - - **Property 12: Worker Attendance Controls** - - **Validates: Requirements 10.3** - - Verify each worker row has three attendance buttons - - - [ ]* 18.8 Write property test for inventory table completeness - - **Property 14: Inventory Table Completeness** - - **Validates: Requirements 11.1** - - Verify all inventory items in context appear in table - - - [ ]* 18.9 Write property test for finance table project mapping - - **Property 16: Finance Table Project Mapping** - - **Validates: Requirements 12.2** - - Verify each project has exactly one row in finance table - - - [ ]* 18.10 Write property test for chart labeling completeness - - **Property 18: Chart Labeling Completeness** - - **Validates: Requirements 16.6** - - Verify all charts include required labels and legends - - - [ ]* 18.11 Write property test for active route highlighting - - **Property 19: Active Route Highlighting** - - **Validates: Requirements 17.3** - - Verify active route is highlighted in sidebar - - - [ ]* 18.12 Write property test for client-side navigation - - **Property 20: Client-Side Navigation** - - **Validates: Requirements 17.4** - - Verify navigation doesn't trigger page reload - - - [ ]* 18.13 Write property test for navigation state persistence - - **Property 21: Navigation State Persistence** - - **Validates: Requirements 17.6** - - Verify context state persists during route transitions - - - [ ]* 18.14 Write property test for context state change re-rendering - - **Property 22: Context State Change Re-rendering** - - **Validates: Requirements 18.4** - - Verify components re-render when consumed context state changes - - - [ ]* 18.15 Write property test for required field validation - - **Property 23: Required Field Validation** - - **Validates: Requirements 19.2** - - Verify forms display errors and prevent submission for empty required fields - - - [ ]* 18.16 Write property test for successful form submission behavior - - **Property 24: Successful Form Submission Behavior** - - **Validates: Requirements 19.3, 19.4, 19.5, 19.6** - - Verify modal closes, context updates, table displays new record, and form clears - - - [ ]* 18.17 Write property test for button hover state - - **Property 25: Button Hover State** - - **Validates: Requirements 20.1** - - Verify buttons display hover state color change - - - [ ]* 18.18 Write property test for button click visual feedback - - **Property 26: Button Click Visual Feedback** - - **Validates: Requirements 20.2** - - Verify buttons display visual click effect - - - [ ]* 18.19 Write property test for task card drag visual state - - **Property 27: Task Card Drag Visual State** - - **Validates: Requirements 20.3** - - Verify task cards display dragging state during drag - - - [ ]* 18.20 Write property test for immediate UI update on data change - - **Property 28: Immediate UI Update on Data Change** - - **Validates: Requirements 20.4** - - Verify UI reflects data changes immediately without manual refresh - -- [x] 19. Final integration and polish - - [x] 19.1 Verify all requirements are met - - Review requirements document and check each acceptance criterion - - Test all user flows end-to-end - - _Requirements: All_ - - - [x] 19.2 Code cleanup and documentation - - Add JSDoc comments to complex functions - - Remove console.logs and debug code - - Ensure consistent code formatting - - _Requirements: 15.3, 15.4, 15.5_ - - - [x] 19.3 Create utility helper functions - - Create src/utils/helpers.js with date formatting and calculation utilities - - Implement currency formatting function - - Implement date formatting function - - _Requirements: 15.5_ - -- [x] 20. Final checkpoint - Complete testing and verification - - Run all tests (unit and property tests), ensure 80%+ coverage, verify all features work correctly, ask the user if questions arise. - -## Notes - -- Tasks marked with `*` are optional testing tasks and can be skipped for faster MVP delivery -- Each task references specific requirements for traceability -- Property tests validate universal correctness properties across all inputs -- Unit tests validate specific examples, edge cases, and user interactions -- This is a frontend-only implementation - all data operations use Context API with mock data -- The implementation follows a bottom-up approach: UI components → Layout → Context → Pages → Integration -- Checkpoints ensure incremental validation at key milestones diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d83a64c --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Construction Site Management System + +Full-stack construction management application with: +- `frontend`: React + Vite + Tailwind +- `backend`: Node.js + Express + PostgreSQL + +## Repository Structure + +``` +construction-site-management/ +├── backend/ +│ ├── config/ +│ ├── controllers/ +│ ├── middleware/ +│ ├── models/ +│ ├── routes/ +│ ├── utils/ +│ ├── server.js +│ └── package.json +├── frontend/ +│ ├── public/ +│ ├── src/ +│ ├── vite.config.js +│ └── package.json +├── .gitignore +├── package.json +└── README.md +``` + +## Prerequisites + +- Node.js 18+ +- npm 9+ +- PostgreSQL 14+ + +## Setup + +1. Install dependencies for both apps: + +```bash +npm run install:all +``` + +2. Configure environment variables: + +- Copy `backend/.env.example` to `backend/.env` +- Copy `frontend/.env.example` to `frontend/.env` (optional) + +3. Run development servers: + +```bash +npm run dev +``` + +Default ports: +- Frontend: `http://localhost:3000` +- Backend: `http://localhost:5000` + +## Root Scripts + +- `npm run dev` - Run frontend and backend together +- `npm run dev:frontend` - Run only frontend +- `npm run dev:backend` - Run only backend +- `npm run start:frontend` - Start frontend +- `npm run start:backend` - Start backend +- `npm run install:all` - Install backend + frontend dependencies + +## Database + +Backend includes SQL helpers and seed scripts: +- `backend/siteos_enterprise_schema.sql` +- `backend/migrate_schema.sql` +- `backend/reset_db.sql` +- `backend/seed_data.js` + +Use them according to your local PostgreSQL setup. + +## GitHub Publishing Checklist + +- `node_modules` removed +- build outputs (`dist`, `coverage`) removed +- `.env` files not committed +- only source, configs, and docs committed + +This repository is now structured for direct GitHub publishing from this folder. diff --git a/Site Management System.pdf b/Site Management System.pdf deleted file mode 100644 index ef5a6c5..0000000 Binary files a/Site Management System.pdf and /dev/null differ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..03fb0f2 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +PORT=5000 +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=your_password_here +DB_NAME=site_management_db diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..d731c91 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,83 @@ +# Construction Site Management Backend + +This is the Node.js/Express backend for the Construction Site Management System. + +## Setup + +1. Navigate to the backend directory: + ```bash + cd backend + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Start the server: + ```bash + npm start + ``` + + For development with auto-restart: + ```bash + npm run dev + ``` + +The server will run on http://localhost:5000 + +## API Endpoints + +- `GET /api/message` - Returns a hello message from backend +- `POST /api/data` - Accepts JSON data and returns success status + +## Integration with Frontend + +The frontend is configured with a proxy in `vite.config.js` to forward `/api` requests to the backend. + +In React components, you can call the APIs using fetch: + +```javascript +// GET request +const response = await fetch('/api/message'); +const data = await response.json(); + +// POST request +const response = await fetch('/api/data', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ yourData: 'value' }), +}); +const data = await response.json(); +``` + +## Running Both Frontend and Backend + +1. Start the backend: + ```bash + cd backend + npm start + ``` + +2. In a new terminal, start the frontend: + ```bash + npm run dev + ``` + +## Common Issues + +1. **CORS Errors**: Make sure CORS is enabled in the backend and the origin is set to `http://localhost:3000` + +2. **Port Conflicts**: Ensure backend runs on port 5000 and frontend on 3000 + +3. **API URLs**: Use relative URLs (`/api/...`) in frontend due to proxy setup, or full URLs (`http://localhost:5000/api/...`) if not using proxy + +4. **Connection Refused**: Make sure both servers are running + +## Error Handling + +- Backend includes try-catch blocks and error middleware +- Frontend includes try-catch for API calls +- Check browser console and server logs for debugging \ No newline at end of file diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 0000000..563e7ae --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,254 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, +}); + +pool.on('connect', () => { + console.log('Connected to PostgreSQL database'); +}); + +pool.on('error', (err) => { + console.error('Unexpected error on idle client', err); + process.exit(-1); +}); + +const ensureSchema = async () => { + try { + // 1. User + await pool.query(` + CREATE TABLE IF NOT EXISTS "User" ( + user_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + role VARCHAR(50) DEFAULT 'Site_Engineer', + email VARCHAR(255) UNIQUE NOT NULL, + phone VARCHAR(20), + password VARCHAR(255), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 2. Project + await pool.query(` + CREATE TABLE IF NOT EXISTS project ( + project_id SERIAL PRIMARY KEY, + project_name VARCHAR(255) NOT NULL, + site_location VARCHAR(255), + project_type VARCHAR(100), + start_date DATE, + end_date DATE, + budget NUMERIC(15, 2), + status VARCHAR(50) DEFAULT 'Active', + created_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + ALTER TABLE project ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'Active'; + `); + + // 3. Task + await pool.query(` + CREATE TABLE IF NOT EXISTS task ( + task_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + task_name VARCHAR(255) NOT NULL, + assigned_to INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + start_date DATE, + end_date DATE, + status VARCHAR(50) DEFAULT 'Open', + priority VARCHAR(50) DEFAULT 'Medium', + due_date DATE, + deadline DATE, + progress INTEGER DEFAULT 0, + workers_assigned JSONB DEFAULT '[]'::jsonb, + materials_used JSONB DEFAULT '[]'::jsonb, + dependencies JSONB DEFAULT '[]'::jsonb, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + ALTER TABLE task ADD COLUMN IF NOT EXISTS priority VARCHAR(50) DEFAULT 'Medium'; + ALTER TABLE task ADD COLUMN IF NOT EXISTS due_date DATE; + ALTER TABLE task ADD COLUMN IF NOT EXISTS deadline DATE; + ALTER TABLE task ADD COLUMN IF NOT EXISTS progress INTEGER DEFAULT 0; + ALTER TABLE task ADD COLUMN IF NOT EXISTS workers_assigned JSONB DEFAULT '[]'::jsonb; + ALTER TABLE task ADD COLUMN IF NOT EXISTS materials_used JSONB DEFAULT '[]'::jsonb; + ALTER TABLE task ADD COLUMN IF NOT EXISTS dependencies JSONB DEFAULT '[]'::jsonb; + `); + + // 4. Worker + await pool.query(` + CREATE TABLE IF NOT EXISTS worker ( + worker_id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + project_id INTEGER REFERENCES project(project_id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + skill_type VARCHAR(100), + contact VARCHAR(50), + rate_type VARCHAR(50), + base_rate NUMERIC(10, 2), + salary NUMERIC(15, 2) DEFAULT 0, + attendance JSONB DEFAULT '[]'::jsonb, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + ALTER TABLE worker ADD COLUMN IF NOT EXISTS user_id INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL; + ALTER TABLE worker ADD COLUMN IF NOT EXISTS project_id INTEGER REFERENCES project(project_id) ON DELETE SET NULL; + ALTER TABLE worker ADD COLUMN IF NOT EXISTS salary NUMERIC(15, 2) DEFAULT 0; + ALTER TABLE worker ADD COLUMN IF NOT EXISTS attendance JSONB DEFAULT '[]'::jsonb; + `); + + // 5. Inventory Items + await pool.query(` + CREATE TABLE IF NOT EXISTS inventory_item ( + item_id SERIAL PRIMARY KEY, + item_name VARCHAR(255) NOT NULL, + category VARCHAR(100), + uom VARCHAR(50), + unit_cost NUMERIC(10, 2), + min_stock_qty INTEGER DEFAULT 0, + current_stock INTEGER DEFAULT 0, + supplier VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 6. Vendor + await pool.query(` + CREATE TABLE IF NOT EXISTS vendor ( + vendor_id SERIAL PRIMARY KEY, + vendor_name VARCHAR(255) NOT NULL, + contact VARCHAR(50), + email VARCHAR(255), + address TEXT, + rating NUMERIC(3, 1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 7. Procurement + await pool.query(` + CREATE TABLE IF NOT EXISTS procurement ( + id SERIAL PRIMARY KEY, + procurement_id VARCHAR(50), + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + vendor_id INTEGER REFERENCES vendor(vendor_id) ON DELETE SET NULL, + item_id INTEGER REFERENCES inventory_item(item_id) ON DELETE SET NULL, + quantity INTEGER NOT NULL, + unit_price NUMERIC(10, 2), + delivery_status VARCHAR(50) DEFAULT 'ordered', + expected_delivery DATE, + delivered_at DATE, + created_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 8. Material Issue + await pool.query(` + CREATE TABLE IF NOT EXISTS material_issue ( + material_issue_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + task_id INTEGER REFERENCES task(task_id) ON DELETE CASCADE, + item_id INTEGER REFERENCES inventory_item(item_id) ON DELETE CASCADE, + quantity INTEGER NOT NULL, + issued_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 9. Attendance + await pool.query(` + CREATE TABLE IF NOT EXISTS attendance ( + attendance_id SERIAL PRIMARY KEY, + worker_id INTEGER REFERENCES worker(worker_id) ON DELETE CASCADE, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + date DATE NOT NULL, + status VARCHAR(50), + hours_worked INTEGER DEFAULT 0, + labor_cost NUMERIC(12, 2) DEFAULT 0, + recorded_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 10. Finance + await pool.query(` + CREATE TABLE IF NOT EXISTS finance ( + finance_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + cost_category VARCHAR(100), + amount NUMERIC(15, 2), + date DATE, + description TEXT, + payment_status VARCHAR(50) DEFAULT 'Pending', + source VARCHAR(50) DEFAULT 'manual', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `); + + // 11. Leave Application + await pool.query(` + CREATE TABLE IF NOT EXISTS leave_application ( + leave_id SERIAL PRIMARY KEY, + worker_id INTEGER REFERENCES worker(worker_id) ON DELETE CASCADE, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + reason TEXT, + status VARCHAR(50) DEFAULT 'Pending', + applied_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + reviewed_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + reviewed_on TIMESTAMP + ); + `); + + // 12. Project Members (assign site engineers to projects) + await pool.query(` + CREATE TABLE IF NOT EXISTS project_members ( + id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE CASCADE, + project_role VARCHAR(50) DEFAULT 'Site_Engineer', + assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(project_id, user_id) + ); + `); + + // Add unique constraint on attendance (worker+date) for upsert + await pool.query(` + DO $$ BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'attendance_worker_date_unique' + ) THEN + ALTER TABLE attendance ADD CONSTRAINT attendance_worker_date_unique UNIQUE (worker_id, date); + END IF; + END $$; + `); + + // 13. Add password_reset_count column to User table for reset-limit tracking + await pool.query(` + ALTER TABLE "User" ADD COLUMN IF NOT EXISTS password_reset_count INTEGER DEFAULT 0; + `); + + console.log('Database schemas verified properly.'); + } catch (err) { + console.error('Failed to ensure database schemas:', err); + } +}; + +pool.query('SELECT NOW()', (err, res) => { + if (err) { + console.error('Database connection failed:', err); + } else { + console.log('Database connection successful'); + ensureSchema().catch((schemaErr) => { + console.error('Schema initialization error:', schemaErr); + }); + } +}); + +module.exports = pool; \ No newline at end of file diff --git a/backend/controllers/AttendanceController.js b/backend/controllers/AttendanceController.js new file mode 100644 index 0000000..1e51979 --- /dev/null +++ b/backend/controllers/AttendanceController.js @@ -0,0 +1,100 @@ +const Attendance = require('../models/Attendance'); + +class AttendanceController { + static async getAllAttendance(req, res) { + try { + const attendance = await Attendance.getAll(); + res.json(attendance); + } catch (error) { + console.error('Error fetching attendance:', error); + res.status(500).json({ error: 'Failed to fetch attendance' }); + } + } + + static async getAttendanceById(req, res) { + try { + const { id } = req.params; + const attendance = await Attendance.getById(id); + if (!attendance) { + return res.status(404).json({ error: 'Attendance record not found' }); + } + res.json(attendance); + } catch (error) { + console.error('Error fetching attendance:', error); + res.status(500).json({ error: 'Failed to fetch attendance' }); + } + } + + static async getAttendanceByProject(req, res) { + try { + const { projectId } = req.params; + const attendance = await Attendance.getByProjectId(projectId); + res.json(attendance); + } catch (error) { + console.error('Error fetching attendance by project:', error); + res.status(500).json({ error: 'Failed to fetch attendance' }); + } + } + + static async getAttendanceByWorker(req, res) { + try { + const { workerId } = req.params; + const attendance = await Attendance.getByWorkerId(workerId); + res.json(attendance); + } catch (error) { + console.error('Error fetching attendance by worker:', error); + res.status(500).json({ error: 'Failed to fetch attendance' }); + } + } + + static async createAttendance(req, res) { + try { + const attendanceData = req.body; + const Attendance = require('../models/Attendance'); + const newAttendance = await Attendance.create(attendanceData); + + // Recalculate Worker Salary based on total attendance + const allAttendance = await Attendance.getByWorkerId(newAttendance.worker_id); + const totalSalary = allAttendance.reduce((sum, att) => sum + Number(att.labor_cost || 0), 0); + + const Worker = require('../models/Worker'); + await Worker.update(newAttendance.worker_id, { salary: totalSalary }); + + res.status(201).json(newAttendance); + } catch (error) { + console.error('Error creating attendance:', error); + res.status(500).json({ error: 'Failed to create/update attendance' }); + } + } + + static async updateAttendance(req, res) { + try { + const { id } = req.params; + const attendanceData = req.body; + const updatedAttendance = await Attendance.update(id, attendanceData); + if (!updatedAttendance) { + return res.status(404).json({ error: 'Attendance record not found' }); + } + res.json(updatedAttendance); + } catch (error) { + console.error('Error updating attendance:', error); + res.status(500).json({ error: 'Failed to update attendance' }); + } + } + + static async deleteAttendance(req, res) { + try { + const { id } = req.params; + const deletedAttendance = await Attendance.delete(id); + if (!deletedAttendance) { + return res.status(404).json({ error: 'Attendance record not found' }); + } + res.json({ message: 'Attendance record deleted successfully' }); + } catch (error) { + console.error('Error deleting attendance:', error); + res.status(500).json({ error: 'Failed to delete attendance' }); + } + } +} + +module.exports = AttendanceController; \ No newline at end of file diff --git a/backend/controllers/FinanceController.js b/backend/controllers/FinanceController.js new file mode 100644 index 0000000..78e0bfb --- /dev/null +++ b/backend/controllers/FinanceController.js @@ -0,0 +1,80 @@ +const Finance = require('../models/Finance'); + +class FinanceController { + static async getAllFinance(req, res) { + try { + const finance = await Finance.getAll(); + res.json(finance); + } catch (error) { + console.error('Error fetching finance records:', error); + res.status(500).json({ error: 'Failed to fetch finance records' }); + } + } + + static async getFinanceById(req, res) { + try { + const { id } = req.params; + const finance = await Finance.getById(id); + if (!finance) { + return res.status(404).json({ error: 'Finance record not found' }); + } + res.json(finance); + } catch (error) { + console.error('Error fetching finance record:', error); + res.status(500).json({ error: 'Failed to fetch finance record' }); + } + } + + static async getFinanceByProject(req, res) { + try { + const { projectId } = req.params; + const finance = await Finance.getByProjectId(projectId); + res.json(finance); + } catch (error) { + console.error('Error fetching finance by project:', error); + res.status(500).json({ error: 'Failed to fetch finance records' }); + } + } + + static async createFinance(req, res) { + try { + const financeData = req.body; + const newFinance = await Finance.create(financeData); + res.status(201).json(newFinance); + } catch (error) { + console.error('Error creating finance record:', error); + res.status(500).json({ error: 'Failed to create finance record' }); + } + } + + static async updateFinance(req, res) { + try { + const { id } = req.params; + const financeData = req.body; + const updatedFinance = await Finance.update(id, financeData); + if (!updatedFinance) { + return res.status(404).json({ error: 'Finance record not found' }); + } + res.json(updatedFinance); + } catch (error) { + console.error('Error updating finance record:', error); + res.status(500).json({ error: 'Failed to update finance record' }); + } + } + + static async deleteFinance(req, res) { + try { + const { id } = req.params; + const deletedFinance = await Finance.delete(id); + if (!deletedFinance) { + return res.status(404).json({ error: 'Finance record not found' }); + } + res.json({ message: 'Finance record deleted successfully' }); + } catch (error) { + console.error('Error deleting finance record:', error); + res.status(500).json({ error: 'Failed to delete finance record' }); + } + } +} + +module.exports = FinanceController; \ No newline at end of file diff --git a/backend/controllers/InventoryController.js b/backend/controllers/InventoryController.js new file mode 100644 index 0000000..e627b66 --- /dev/null +++ b/backend/controllers/InventoryController.js @@ -0,0 +1,69 @@ +const InventoryItem = require('../models/InventoryItem'); + +class InventoryController { + static async getAllItems(req, res) { + try { + const items = await InventoryItem.getAll(); + res.json(items); + } catch (error) { + console.error('Error fetching inventory items:', error); + res.status(500).json({ error: 'Failed to fetch inventory items' }); + } + } + + static async getItemById(req, res) { + try { + const { id } = req.params; + const item = await InventoryItem.getById(id); + if (!item) { + return res.status(404).json({ error: 'Inventory item not found' }); + } + res.json(item); + } catch (error) { + console.error('Error fetching inventory item:', error); + res.status(500).json({ error: 'Failed to fetch inventory item' }); + } + } + + static async createItem(req, res) { + try { + const itemData = req.body; + const newItem = await InventoryItem.create(itemData); + res.status(201).json(newItem); + } catch (error) { + console.error('Error creating inventory item:', error); + res.status(500).json({ error: 'Failed to create inventory item' }); + } + } + + static async updateItem(req, res) { + try { + const { id } = req.params; + const itemData = req.body; + const updatedItem = await InventoryItem.update(id, itemData); + if (!updatedItem) { + return res.status(404).json({ error: 'Inventory item not found' }); + } + res.json(updatedItem); + } catch (error) { + console.error('Error updating inventory item:', error); + res.status(500).json({ error: 'Failed to update inventory item' }); + } + } + + static async deleteItem(req, res) { + try { + const { id } = req.params; + const deletedItem = await InventoryItem.delete(id); + if (!deletedItem) { + return res.status(404).json({ error: 'Inventory item not found' }); + } + res.json({ message: 'Inventory item deleted successfully' }); + } catch (error) { + console.error('Error deleting inventory item:', error); + res.status(500).json({ error: 'Failed to delete inventory item' }); + } + } +} + +module.exports = InventoryController; \ No newline at end of file diff --git a/backend/controllers/LeaveController.js b/backend/controllers/LeaveController.js new file mode 100644 index 0000000..7785763 --- /dev/null +++ b/backend/controllers/LeaveController.js @@ -0,0 +1,52 @@ +const Leave = require('../models/Leave'); + +exports.getAllLeaves = async (req, res) => { + try { + const leaves = await Leave.getAll(); + res.json(leaves); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch leave applications', details: error.message }); + } +}; + +exports.getLeavesByWorker = async (req, res) => { + try { + const leaves = await Leave.getByWorkerId(req.params.workerId); + res.json(leaves); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch worker leaves', details: error.message }); + } +}; + +exports.createLeave = async (req, res) => { + try { + const leave = await Leave.create(req.body); + res.status(201).json(leave); + } catch (error) { + res.status(500).json({ error: 'Failed to apply for leave', details: error.message }); + } +}; + +exports.approveLeave = async (req, res) => { + try { + const leave = await Leave.updateStatus(req.params.id, 'Approved', req.body.reviewerId); + if (!leave) { + return res.status(404).json({ error: 'Leave application not found' }); + } + res.json(leave); + } catch (error) { + res.status(500).json({ error: 'Failed to approve leave', details: error.message }); + } +}; + +exports.rejectLeave = async (req, res) => { + try { + const leave = await Leave.updateStatus(req.params.id, 'Rejected', req.body.reviewerId, req.body.reason); + if (!leave) { + return res.status(404).json({ error: 'Leave application not found' }); + } + res.json(leave); + } catch (error) { + res.status(500).json({ error: 'Failed to reject leave', details: error.message }); + } +}; diff --git a/backend/controllers/MaterialIssueController.js b/backend/controllers/MaterialIssueController.js new file mode 100644 index 0000000..3697226 --- /dev/null +++ b/backend/controllers/MaterialIssueController.js @@ -0,0 +1,80 @@ +const MaterialIssue = require('../models/MaterialIssue'); + +class MaterialIssueController { + static async getAllIssues(req, res) { + try { + const issues = await MaterialIssue.getAll(); + res.json(issues); + } catch (error) { + console.error('Error fetching material issues:', error); + res.status(500).json({ error: 'Failed to fetch material issues' }); + } + } + + static async getIssueById(req, res) { + try { + const { id } = req.params; + const issue = await MaterialIssue.getById(id); + if (!issue) { + return res.status(404).json({ error: 'Material issue not found' }); + } + res.json(issue); + } catch (error) { + console.error('Error fetching material issue:', error); + res.status(500).json({ error: 'Failed to fetch material issue' }); + } + } + + static async getIssuesByProject(req, res) { + try { + const { projectId } = req.params; + const issues = await MaterialIssue.getByProjectId(projectId); + res.json(issues); + } catch (error) { + console.error('Error fetching material issues by project:', error); + res.status(500).json({ error: 'Failed to fetch material issues' }); + } + } + + static async createIssue(req, res) { + try { + const issueData = req.body; + const newIssue = await MaterialIssue.create(issueData); + res.status(201).json(newIssue); + } catch (error) { + console.error('Error creating material issue:', error); + res.status(500).json({ error: 'Failed to create material issue' }); + } + } + + static async updateIssue(req, res) { + try { + const { id } = req.params; + const issueData = req.body; + const updatedIssue = await MaterialIssue.update(id, issueData); + if (!updatedIssue) { + return res.status(404).json({ error: 'Material issue not found' }); + } + res.json(updatedIssue); + } catch (error) { + console.error('Error updating material issue:', error); + res.status(500).json({ error: 'Failed to update material issue' }); + } + } + + static async deleteIssue(req, res) { + try { + const { id } = req.params; + const deletedIssue = await MaterialIssue.delete(id); + if (!deletedIssue) { + return res.status(404).json({ error: 'Material issue not found' }); + } + res.json({ message: 'Material issue deleted successfully' }); + } catch (error) { + console.error('Error deleting material issue:', error); + res.status(500).json({ error: 'Failed to delete material issue' }); + } + } +} + +module.exports = MaterialIssueController; \ No newline at end of file diff --git a/backend/controllers/NotificationController.js b/backend/controllers/NotificationController.js new file mode 100644 index 0000000..8541ca3 --- /dev/null +++ b/backend/controllers/NotificationController.js @@ -0,0 +1,76 @@ +const Notification = require('../models/Notification'); + +class NotificationController { + static async getNotifications(req, res) { + try { + const { userId } = req.params; + const notifications = await Notification.getByUserId(userId); + res.json(notifications); + } catch (error) { + console.error('Error fetching notifications:', error); + res.status(500).json({ error: 'Failed to fetch notifications' }); + } + } + + static async getAllNotifications(req, res) { + try { + const notifications = await Notification.getAll(); + res.json(notifications); + } catch (error) { + console.error('Error fetching all notifications:', error); + res.status(500).json({ error: 'Failed to fetch notifications' }); + } + } + + static async createNotification(req, res) { + try { + const notificationData = req.body; + const newNotification = await Notification.create(notificationData); + res.status(201).json(newNotification); + } catch (error) { + console.error('Error creating notification:', error); + res.status(500).json({ error: 'Failed to create notification' }); + } + } + + static async markRead(req, res) { + try { + const { id } = req.params; + const updated = await Notification.markRead(id); + if (!updated) { + return res.status(404).json({ error: 'Notification not found' }); + } + res.json(updated); + } catch (error) { + console.error('Error marking notification read:', error); + res.status(500).json({ error: 'Failed to update notification' }); + } + } + + static async markAllRead(req, res) { + try { + const { userId } = req.params; + const updated = await Notification.markAllRead(userId); + res.json({ updated: updated.length }); + } catch (error) { + console.error('Error marking all notifications read:', error); + res.status(500).json({ error: 'Failed to update notifications' }); + } + } + + static async deleteNotification(req, res) { + try { + const { id } = req.params; + const deleted = await Notification.delete(id); + if (!deleted) { + return res.status(404).json({ error: 'Notification not found' }); + } + res.json({ message: 'Notification deleted successfully' }); + } catch (error) { + console.error('Error deleting notification:', error); + res.status(500).json({ error: 'Failed to delete notification' }); + } + } +} + +module.exports = NotificationController; diff --git a/backend/controllers/ProcurementController.js b/backend/controllers/ProcurementController.js new file mode 100644 index 0000000..9a008b7 --- /dev/null +++ b/backend/controllers/ProcurementController.js @@ -0,0 +1,80 @@ +const Procurement = require('../models/Procurement'); + +class ProcurementController { + static async getAllProcurements(req, res) { + try { + const procurements = await Procurement.getAll(); + res.json(procurements); + } catch (error) { + console.error('Error fetching procurements:', error); + res.status(500).json({ error: 'Failed to fetch procurements' }); + } + } + + static async getProcurementById(req, res) { + try { + const { id } = req.params; + const procurement = await Procurement.getById(id); + if (!procurement) { + return res.status(404).json({ error: 'Procurement not found' }); + } + res.json(procurement); + } catch (error) { + console.error('Error fetching procurement:', error); + res.status(500).json({ error: 'Failed to fetch procurement' }); + } + } + + static async getProcurementsByProject(req, res) { + try { + const { projectId } = req.params; + const procurements = await Procurement.getByProjectId(projectId); + res.json(procurements); + } catch (error) { + console.error('Error fetching procurements by project:', error); + res.status(500).json({ error: 'Failed to fetch procurements' }); + } + } + + static async createProcurement(req, res) { + try { + const procurementData = req.body; + const newProcurement = await Procurement.create(procurementData); + res.status(201).json(newProcurement); + } catch (error) { + console.error('Error creating procurement:', error); + res.status(500).json({ error: 'Failed to create procurement' }); + } + } + + static async updateProcurement(req, res) { + try { + const { id } = req.params; + const procurementData = req.body; + const updatedProcurement = await Procurement.update(id, procurementData); + if (!updatedProcurement) { + return res.status(404).json({ error: 'Procurement not found' }); + } + res.json(updatedProcurement); + } catch (error) { + console.error('Error updating procurement:', error); + res.status(500).json({ error: 'Failed to update procurement: ' + error.message }); + } + } + + static async deleteProcurement(req, res) { + try { + const { id } = req.params; + const deletedProcurement = await Procurement.delete(id); + if (!deletedProcurement) { + return res.status(404).json({ error: 'Procurement not found' }); + } + res.json({ message: 'Procurement deleted successfully' }); + } catch (error) { + console.error('Error deleting procurement:', error); + res.status(500).json({ error: 'Failed to delete procurement' }); + } + } +} + +module.exports = ProcurementController; \ No newline at end of file diff --git a/backend/controllers/ProjectController.js b/backend/controllers/ProjectController.js new file mode 100644 index 0000000..c1829fe --- /dev/null +++ b/backend/controllers/ProjectController.js @@ -0,0 +1,69 @@ +const Project = require('../models/Project'); + +class ProjectController { + static async getAllProjects(req, res) { + try { + const projects = await Project.getAll(); + res.json(projects); + } catch (error) { + console.error('Error fetching projects:', error); + res.status(500).json({ error: 'Failed to fetch projects' }); + } + } + + static async getProjectById(req, res) { + try { + const { id } = req.params; + const project = await Project.getById(id); + if (!project) { + return res.status(404).json({ error: 'Project not found' }); + } + res.json(project); + } catch (error) { + console.error('Error fetching project:', error); + res.status(500).json({ error: 'Failed to fetch project' }); + } + } + + static async createProject(req, res) { + try { + const projectData = req.body; + const newProject = await Project.create(projectData); + res.status(201).json(newProject); + } catch (error) { + console.error('Error creating project:', error); + res.status(500).json({ error: 'Failed to create project' }); + } + } + + static async updateProject(req, res) { + try { + const { id } = req.params; + const projectData = req.body; + const updatedProject = await Project.update(id, projectData); + if (!updatedProject) { + return res.status(404).json({ error: 'Project not found' }); + } + res.json(updatedProject); + } catch (error) { + console.error('Error updating project:', error); + res.status(500).json({ error: 'Failed to update project' }); + } + } + + static async deleteProject(req, res) { + try { + const { id } = req.params; + const deletedProject = await Project.delete(id); + if (!deletedProject) { + return res.status(404).json({ error: 'Project not found' }); + } + res.json({ message: 'Project deleted successfully' }); + } catch (error) { + console.error('Error deleting project:', error); + res.status(500).json({ error: 'Failed to delete project' }); + } + } +} + +module.exports = ProjectController; \ No newline at end of file diff --git a/backend/controllers/ProjectMemberController.js b/backend/controllers/ProjectMemberController.js new file mode 100644 index 0000000..84f9eb6 --- /dev/null +++ b/backend/controllers/ProjectMemberController.js @@ -0,0 +1,69 @@ +const ProjectMember = require('../models/ProjectMember'); +const User = require('../models/User'); + +class ProjectMemberController { + static async getAllMembers(req, res) { + try { + const members = await ProjectMember.getAll(); + res.json(members); + } catch (error) { + console.error('Error fetching project members:', error); + res.status(500).json({ error: 'Failed to fetch project members' }); + } + } + + static async getMembersByProject(req, res) { + try { + const { projectId } = req.params; + const members = await ProjectMember.getByProjectId(projectId); + res.json(members); + } catch (error) { + console.error('Error fetching project members by project:', error); + res.status(500).json({ error: 'Failed to fetch project members' }); + } + } + + static async createMember(req, res) { + try { + const { project_id, user_id, member_role, from_date, to_date } = req.body; + + if (!project_id || !user_id) { + return res.status(400).json({ error: 'project_id and user_id are required' }); + } + + // Validate that user exists and has Site_Engineer role (or allow Admin override) + const user = await User.getById(user_id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + if (user.role !== 'Site_Engineer' && user.role !== 'Admin' && user.role !== 'Project_Manager') { + return res.status(400).json({ error: 'Only Site Engineers can be assigned to projects' }); + } + + const newMember = await ProjectMember.create({ project_id, user_id, member_role: member_role || 'Site_Engineer', from_date, to_date }); + res.status(201).json(newMember); + } catch (error) { + console.error('Error creating project member:', error); + if (error.code === '23505') { + return res.status(409).json({ error: 'This user is already assigned to the selected project' }); + } + res.status(500).json({ error: 'Failed to create project member' }); + } + } + + static async deleteMember(req, res) { + try { + const { id } = req.params; + const deleted = await ProjectMember.delete(id); + if (!deleted) { + return res.status(404).json({ error: 'Project member not found' }); + } + res.json({ message: 'Project member removed successfully' }); + } catch (error) { + console.error('Error deleting project member:', error); + res.status(500).json({ error: 'Failed to delete project member' }); + } + } +} + +module.exports = ProjectMemberController; diff --git a/backend/controllers/TaskController.js b/backend/controllers/TaskController.js new file mode 100644 index 0000000..fad1625 --- /dev/null +++ b/backend/controllers/TaskController.js @@ -0,0 +1,80 @@ +const Task = require('../models/Task'); + +class TaskController { + static async getAllTasks(req, res) { + try { + const tasks = await Task.getAll(); + res.json(tasks); + } catch (error) { + console.error('Error fetching tasks:', error); + res.status(500).json({ error: 'Failed to fetch tasks' }); + } + } + + static async getTaskById(req, res) { + try { + const { id } = req.params; + const task = await Task.getById(id); + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + res.json(task); + } catch (error) { + console.error('Error fetching task:', error); + res.status(500).json({ error: 'Failed to fetch task' }); + } + } + + static async getTasksByProject(req, res) { + try { + const { projectId } = req.params; + const tasks = await Task.getByProjectId(projectId); + res.json(tasks); + } catch (error) { + console.error('Error fetching tasks by project:', error); + res.status(500).json({ error: 'Failed to fetch tasks' }); + } + } + + static async createTask(req, res) { + try { + const taskData = req.body; + const newTask = await Task.create(taskData); + res.status(201).json(newTask); + } catch (error) { + console.error('Error creating task:', error); + res.status(500).json({ error: 'Failed to create task' }); + } + } + + static async updateTask(req, res) { + try { + const { id } = req.params; + const taskData = req.body; + const updatedTask = await Task.update(id, taskData); + if (!updatedTask) { + return res.status(404).json({ error: 'Task not found' }); + } + res.json(updatedTask); + } catch (error) { + console.error('Error updating task:', error); + res.status(500).json({ error: 'Failed to update task' }); + } + } + + static async deleteTask(req, res) { + try { + const { id } = req.params; + const deletedTask = await Task.delete(id); + if (!deletedTask) { + return res.status(404).json({ error: 'Task not found' }); + } + res.json({ message: 'Task deleted successfully' }); + } catch (error) { + console.error('Error deleting task:', error); + res.status(500).json({ error: 'Failed to delete task' }); + } + } +} + +module.exports = TaskController; \ No newline at end of file diff --git a/backend/controllers/UserController.js b/backend/controllers/UserController.js new file mode 100644 index 0000000..f81d289 --- /dev/null +++ b/backend/controllers/UserController.js @@ -0,0 +1,69 @@ +const User = require('../models/User'); + +class UserController { + static async getAllUsers(req, res) { + try { + const users = await User.getAll(); + res.json(users); + } catch (error) { + console.error('Error fetching users:', error); + res.status(500).json({ error: 'Failed to fetch users' }); + } + } + + static async getUserById(req, res) { + try { + const { id } = req.params; + const user = await User.getById(id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(user); + } catch (error) { + console.error('Error fetching user:', error); + res.status(500).json({ error: 'Failed to fetch user' }); + } + } + + static async createUser(req, res) { + try { + const userData = req.body; + const newUser = await User.create(userData); + res.status(201).json(newUser); + } catch (error) { + console.error('Error creating user:', error); + res.status(500).json({ error: 'Failed to create user' }); + } + } + + static async updateUser(req, res) { + try { + const { id } = req.params; + const userData = req.body; + const updatedUser = await User.update(id, userData); + if (!updatedUser) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(updatedUser); + } catch (error) { + console.error('Error updating user:', error); + res.status(500).json({ error: 'Failed to update user' }); + } + } + + static async deleteUser(req, res) { + try { + const { id } = req.params; + const deletedUser = await User.delete(id); + if (!deletedUser) { + return res.status(404).json({ error: 'User not found' }); + } + res.json({ message: 'User deleted successfully' }); + } catch (error) { + console.error('Error deleting user:', error); + res.status(500).json({ error: 'Failed to delete user' }); + } + } +} + +module.exports = UserController; \ No newline at end of file diff --git a/backend/controllers/VendorController.js b/backend/controllers/VendorController.js new file mode 100644 index 0000000..b233022 --- /dev/null +++ b/backend/controllers/VendorController.js @@ -0,0 +1,69 @@ +const Vendor = require('../models/Vendor'); + +class VendorController { + static async getAllVendors(req, res) { + try { + const vendors = await Vendor.getAll(); + res.json(vendors); + } catch (error) { + console.error('Error fetching vendors:', error); + res.status(500).json({ error: 'Failed to fetch vendors' }); + } + } + + static async getVendorById(req, res) { + try { + const { id } = req.params; + const vendor = await Vendor.getById(id); + if (!vendor) { + return res.status(404).json({ error: 'Vendor not found' }); + } + res.json(vendor); + } catch (error) { + console.error('Error fetching vendor:', error); + res.status(500).json({ error: 'Failed to fetch vendor' }); + } + } + + static async createVendor(req, res) { + try { + const vendorData = req.body; + const newVendor = await Vendor.create(vendorData); + res.status(201).json(newVendor); + } catch (error) { + console.error('Error creating vendor:', error); + res.status(500).json({ error: 'Failed to create vendor' }); + } + } + + static async updateVendor(req, res) { + try { + const { id } = req.params; + const vendorData = req.body; + const updatedVendor = await Vendor.update(id, vendorData); + if (!updatedVendor) { + return res.status(404).json({ error: 'Vendor not found' }); + } + res.json(updatedVendor); + } catch (error) { + console.error('Error updating vendor:', error); + res.status(500).json({ error: 'Failed to update vendor' }); + } + } + + static async deleteVendor(req, res) { + try { + const { id } = req.params; + const deletedVendor = await Vendor.delete(id); + if (!deletedVendor) { + return res.status(404).json({ error: 'Vendor not found' }); + } + res.json({ message: 'Vendor deleted successfully' }); + } catch (error) { + console.error('Error deleting vendor:', error); + res.status(500).json({ error: 'Failed to delete vendor' }); + } + } +} + +module.exports = VendorController; \ No newline at end of file diff --git a/backend/controllers/WorkerAssignmentController.js b/backend/controllers/WorkerAssignmentController.js new file mode 100644 index 0000000..ed1b317 --- /dev/null +++ b/backend/controllers/WorkerAssignmentController.js @@ -0,0 +1,68 @@ +const WorkerAssignment = require('../models/WorkerAssignment'); + +class WorkerAssignmentController { + static async getAllAssignments(req, res) { + try { + const assignments = await WorkerAssignment.getAll(); + res.json(assignments); + } catch (error) { + console.error('Error fetching worker assignments:', error); + res.status(500).json({ error: 'Failed to fetch worker assignments' }); + } + } + + static async getAssignmentById(req, res) { + try { + const { id } = req.params; + const assignment = await WorkerAssignment.getById(id); + if (!assignment) { + return res.status(404).json({ error: 'Assignment not found' }); + } + res.json(assignment); + } catch (error) { + console.error('Error fetching assignment:', error); + res.status(500).json({ error: 'Failed to fetch assignment' }); + } + } + + static async getAssignmentsByTask(req, res) { + try { + const { taskId } = req.params; + const assignments = await WorkerAssignment.getByTaskId(taskId); + res.json(assignments); + } catch (error) { + console.error('Error fetching assignments by task:', error); + res.status(500).json({ error: 'Failed to fetch assignments' }); + } + } + + static async createAssignment(req, res) { + try { + const { worker_id, task_id, from_date, to_date } = req.body; + if (!worker_id || !task_id) { + return res.status(400).json({ error: 'worker_id and task_id are required' }); + } + const newAssignment = await WorkerAssignment.create({ task_id, worker_id, from_date, to_date }); + res.status(201).json(newAssignment); + } catch (error) { + console.error('Error creating worker assignment:', error); + res.status(500).json({ error: 'Failed to create worker assignment' }); + } + } + + static async deleteAssignment(req, res) { + try { + const { id } = req.params; + const deleted = await WorkerAssignment.delete(id); + if (!deleted) { + return res.status(404).json({ error: 'Assignment not found' }); + } + res.json({ message: 'Assignment deleted successfully' }); + } catch (error) { + console.error('Error deleting assignment:', error); + res.status(500).json({ error: 'Failed to delete assignment' }); + } + } +} + +module.exports = WorkerAssignmentController; diff --git a/backend/controllers/WorkerController.js b/backend/controllers/WorkerController.js new file mode 100644 index 0000000..de88b09 --- /dev/null +++ b/backend/controllers/WorkerController.js @@ -0,0 +1,69 @@ +const Worker = require('../models/Worker'); + +class WorkerController { + static async getAllWorkers(req, res) { + try { + const workers = await Worker.getAll(); + res.json(workers); + } catch (error) { + console.error('Error fetching workers:', error); + res.status(500).json({ error: 'Failed to fetch workers' }); + } + } + + static async getWorkerById(req, res) { + try { + const { id } = req.params; + const worker = await Worker.getById(id); + if (!worker) { + return res.status(404).json({ error: 'Worker not found' }); + } + res.json(worker); + } catch (error) { + console.error('Error fetching worker:', error); + res.status(500).json({ error: 'Failed to fetch worker' }); + } + } + + static async createWorker(req, res) { + try { + const workerData = req.body; + const newWorker = await Worker.create(workerData); + res.status(201).json(newWorker); + } catch (error) { + console.error('Error creating worker:', error); + res.status(500).json({ error: 'Failed to create worker' }); + } + } + + static async updateWorker(req, res) { + try { + const { id } = req.params; + const workerData = req.body; + const updatedWorker = await Worker.update(id, workerData); + if (!updatedWorker) { + return res.status(404).json({ error: 'Worker not found' }); + } + res.json(updatedWorker); + } catch (error) { + console.error('Error updating worker:', error); + res.status(500).json({ error: 'Failed to update worker' }); + } + } + + static async deleteWorker(req, res) { + try { + const { id } = req.params; + const deletedWorker = await Worker.delete(id); + if (!deletedWorker) { + return res.status(404).json({ error: 'Worker not found' }); + } + res.json({ message: 'Worker deleted successfully' }); + } catch (error) { + console.error('Error deleting worker:', error); + res.status(500).json({ error: 'Failed to delete worker' }); + } + } +} + +module.exports = WorkerController; \ No newline at end of file diff --git a/backend/migrate_schema.sql b/backend/migrate_schema.sql new file mode 100644 index 0000000..47b7b94 --- /dev/null +++ b/backend/migrate_schema.sql @@ -0,0 +1,51 @@ +-- SiteOS Enterprise — Migration SQL +-- Run this on an EXISTING database to apply all schema fixes WITHOUT dropping data. +-- Safe to run multiple times (uses IF NOT EXISTS / DO NOTHING). + +-- 1. Fix "User" role CHECK — remove Site_Manager, keep only valid roles +-- PostgreSQL requires dropping and recreating the constraint +ALTER TABLE "User" DROP CONSTRAINT IF EXISTS "User_role_check"; +ALTER TABLE "User" ADD CONSTRAINT "User_role_check" + CHECK (role IN ('Admin', 'Project_Manager', 'Site_Engineer', 'Worker')); + +-- 2. Fix attendance status CHECK — add 'Half Day' (space) to allowed values +ALTER TABLE attendance DROP CONSTRAINT IF EXISTS attendance_status_check; +ALTER TABLE attendance ADD CONSTRAINT attendance_status_check + CHECK (status IN ('Present', 'Absent', 'Half Day', 'Half_Day', 'Leave', 'Holiday')); + +-- 3. Fix hours_worked column type (should be NUMERIC not just INTEGER for half-days) +ALTER TABLE attendance ALTER COLUMN hours_worked TYPE NUMERIC(5,2); + +-- 4. Add UNIQUE(worker_id, date) to attendance so ON CONFLICT works correctly +ALTER TABLE attendance DROP CONSTRAINT IF EXISTS attendance_worker_id_date_key; +ALTER TABLE attendance ADD CONSTRAINT attendance_worker_id_date_key UNIQUE (worker_id, date); + +-- 5. Create project_members table (was missing from original schema) +CREATE TABLE IF NOT EXISTS project_members ( + project_member_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE CASCADE, + member_role VARCHAR(50) DEFAULT 'Site_Engineer' CHECK (member_role IN ('Site_Engineer', 'Project_Manager', 'Admin')), + from_date DATE, + to_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (project_id, user_id) +); + +-- 6. Create notifications table (was missing — notifications were in-memory only) +CREATE TABLE IF NOT EXISTS notifications ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE CASCADE, + title VARCHAR(255), + message TEXT NOT NULL, + type VARCHAR(100) DEFAULT 'general', + severity VARCHAR(50) DEFAULT 'medium' CHECK (severity IN ('low', 'medium', 'high')), + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 7. Update any existing 'Site_Manager' users to 'Site_Engineer' if they exist +UPDATE "User" SET role = 'Site_Engineer' WHERE role = 'Site_Manager'; + +-- Done! +SELECT 'Migration applied successfully' AS result; diff --git a/backend/models/Attendance.js b/backend/models/Attendance.js new file mode 100644 index 0000000..3b87e30 --- /dev/null +++ b/backend/models/Attendance.js @@ -0,0 +1,72 @@ +const pool = require('../config/db'); + +class Attendance { + static async getAll() { + const query = 'SELECT * FROM attendance ORDER BY attendance_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM attendance WHERE attendance_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByProjectId(projectId) { + const query = 'SELECT * FROM attendance WHERE project_id = $1'; + const result = await pool.query(query, [projectId]); + return result.rows; + } + + static async getByWorkerId(workerId) { + const query = 'SELECT * FROM attendance WHERE worker_id = $1'; + const result = await pool.query(query, [workerId]); + return result.rows; + } + + static async create(attendanceData) { + const { worker_id, project_id, date, status, hours_worked, labor_cost, recorded_by } = attendanceData; + // Only pass recorded_by if it is a valid integer user id + const safeRecordedBy = recorded_by && !isNaN(Number(recorded_by)) ? Number(recorded_by) : null; + const query = ` + INSERT INTO attendance (worker_id, project_id, date, status, hours_worked, labor_cost, recorded_by) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (worker_id, date) + DO UPDATE SET status = $4, hours_worked = $5, labor_cost = $6, recorded_by = $7, project_id = $2 + RETURNING * + `; + const result = await pool.query(query, [worker_id, project_id, date || new Date().toISOString().split('T')[0], status || 'Present', hours_worked || 0, labor_cost || 0, safeRecordedBy]); + return result.rows[0]; + } + + static async update(id, attendanceData) { + const att = await Attendance.getById(id); + if (!att) return null; + + const worker_id = attendanceData.worker_id !== undefined ? attendanceData.worker_id : att.worker_id; + const project_id = attendanceData.project_id !== undefined ? attendanceData.project_id : att.project_id; + const date = attendanceData.date !== undefined ? attendanceData.date : att.date; + const status = attendanceData.status !== undefined ? attendanceData.status : att.status; + const hours_worked = attendanceData.hours_worked !== undefined ? attendanceData.hours_worked : att.hours_worked; + const labor_cost = attendanceData.labor_cost !== undefined ? attendanceData.labor_cost : att.labor_cost; + const recorded_by = attendanceData.recorded_by !== undefined ? attendanceData.recorded_by : att.recorded_by; + + const query = ` + UPDATE attendance + SET worker_id = $1, project_id = $2, date = $3, status = $4, hours_worked = $5, labor_cost = $6, recorded_by = $7 + WHERE attendance_id = $8 + RETURNING * + `; + const result = await pool.query(query, [worker_id, project_id, date, status, hours_worked, labor_cost, recorded_by, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM attendance WHERE attendance_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Attendance; \ No newline at end of file diff --git a/backend/models/Finance.js b/backend/models/Finance.js new file mode 100644 index 0000000..b160c8d --- /dev/null +++ b/backend/models/Finance.js @@ -0,0 +1,62 @@ +const pool = require('../config/db'); + +class Finance { + static async getAll() { + const query = 'SELECT * FROM finance ORDER BY finance_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM finance WHERE finance_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByProjectId(projectId) { + const query = 'SELECT * FROM finance WHERE project_id = $1'; + const result = await pool.query(query, [projectId]); + return result.rows; + } + + static async create(financeData) { + const { project_id, cost_category, amount, date, description, payment_status, source } = financeData; + const query = ` + INSERT INTO finance (project_id, cost_category, amount, date, description, payment_status, source) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `; + const result = await pool.query(query, [project_id, cost_category, amount, date || new Date().toISOString().split('T')[0], description, payment_status || 'Pending', source || 'manual']); + return result.rows[0]; + } + + static async update(id, financeData) { + const fin = await Finance.getById(id); + if (!fin) return null; + + const project_id = financeData.project_id !== undefined ? financeData.project_id : fin.project_id; + const cost_category = financeData.cost_category !== undefined ? financeData.cost_category : fin.cost_category; + const amount = financeData.amount !== undefined ? financeData.amount : fin.amount; + const date = financeData.date !== undefined ? financeData.date : fin.date; + const description = financeData.description !== undefined ? financeData.description : fin.description; + const payment_status = financeData.payment_status !== undefined ? financeData.payment_status : fin.payment_status; + const source = financeData.source !== undefined ? financeData.source : fin.source; + + const query = ` + UPDATE finance + SET project_id = $1, cost_category = $2, amount = $3, date = $4, description = $5, payment_status = $6, source = $7 + WHERE finance_id = $8 + RETURNING * + `; + const result = await pool.query(query, [project_id, cost_category, amount, date, description, payment_status, source, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM finance WHERE finance_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Finance; \ No newline at end of file diff --git a/backend/models/InventoryItem.js b/backend/models/InventoryItem.js new file mode 100644 index 0000000..fb6c80a --- /dev/null +++ b/backend/models/InventoryItem.js @@ -0,0 +1,56 @@ +const pool = require('../config/db'); + +class InventoryItem { + static async getAll() { + const query = 'SELECT * FROM inventory_item ORDER BY item_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM inventory_item WHERE item_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async create(itemData) { + const { item_name, category, uom, unit_cost, min_stock_qty, current_stock, supplier } = itemData; + const query = ` + INSERT INTO inventory_item (item_name, category, uom, unit_cost, min_stock_qty, current_stock, supplier) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + `; + const result = await pool.query(query, [item_name, category, uom, unit_cost, min_stock_qty || 0, current_stock || 0, supplier]); + return result.rows[0]; + } + + static async update(id, itemData) { + const item = await InventoryItem.getById(id); + if (!item) return null; + + const item_name = itemData.item_name !== undefined ? itemData.item_name : item.item_name; + const category = itemData.category !== undefined ? itemData.category : item.category; + const uom = itemData.uom !== undefined ? itemData.uom : item.uom; + const unit_cost = itemData.unit_cost !== undefined ? itemData.unit_cost : item.unit_cost; + const min_stock_qty = itemData.min_stock_qty !== undefined ? itemData.min_stock_qty : item.min_stock_qty; + const current_stock = itemData.current_stock !== undefined ? itemData.current_stock : item.current_stock; + const supplier = itemData.supplier !== undefined ? itemData.supplier : item.supplier; + + const query = ` + UPDATE inventory_item + SET item_name = $1, category = $2, uom = $3, unit_cost = $4, min_stock_qty = $5, current_stock = $6, supplier = $7 + WHERE item_id = $8 + RETURNING * + `; + const result = await pool.query(query, [item_name, category, uom, unit_cost, min_stock_qty, current_stock, supplier, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM inventory_item WHERE item_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = InventoryItem; \ No newline at end of file diff --git a/backend/models/Leave.js b/backend/models/Leave.js new file mode 100644 index 0000000..95e4826 --- /dev/null +++ b/backend/models/Leave.js @@ -0,0 +1,45 @@ +const pool = require('../config/db'); + +class Leave { + static async getAll() { + const query = 'SELECT * FROM leave_application ORDER BY applied_on DESC'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM leave_application WHERE leave_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByWorkerId(workerId) { + const query = 'SELECT * FROM leave_application WHERE worker_id = $1 ORDER BY applied_on DESC'; + const result = await pool.query(query, [workerId]); + return result.rows; + } + + static async create(leaveData) { + const { worker_id, start_date, end_date, reason } = leaveData; + const query = ` + INSERT INTO leave_application (worker_id, start_date, end_date, reason, status) + VALUES ($1, $2, $3, $4, 'Pending') + RETURNING * + `; + const result = await pool.query(query, [worker_id, start_date, end_date, reason]); + return result.rows[0]; + } + + static async updateStatus(id, status, reviewerId, rejectionReason = '') { + const query = ` + UPDATE leave_application + SET status = $1, reviewed_by = $2, reviewed_on = CURRENT_TIMESTAMP + WHERE leave_id = $3 + RETURNING * + `; + const result = await pool.query(query, [status, reviewerId, id]); + return result.rows[0]; + } +} + +module.exports = Leave; diff --git a/backend/models/MaterialIssue.js b/backend/models/MaterialIssue.js new file mode 100644 index 0000000..cf3059d --- /dev/null +++ b/backend/models/MaterialIssue.js @@ -0,0 +1,60 @@ +const pool = require('../config/db'); + +class MaterialIssue { + static async getAll() { + const query = 'SELECT * FROM material_issue ORDER BY material_issue_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM material_issue WHERE material_issue_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByProjectId(projectId) { + const query = 'SELECT * FROM material_issue WHERE project_id = $1'; + const result = await pool.query(query, [projectId]); + return result.rows; + } + + static async create(issueData) { + const { project_id, task_id, item_id, quantity, issued_by } = issueData; + const query = ` + INSERT INTO material_issue (project_id, task_id, item_id, quantity, issued_by) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + `; + const result = await pool.query(query, [project_id, task_id, item_id, quantity, issued_by]); + return result.rows[0]; + } + + static async update(id, issueData) { + const issue = await MaterialIssue.getById(id); + if (!issue) return null; + + const project_id = issueData.project_id !== undefined ? issueData.project_id : issue.project_id; + const task_id = issueData.task_id !== undefined ? issueData.task_id : issue.task_id; + const item_id = issueData.item_id !== undefined ? issueData.item_id : issue.item_id; + const quantity = issueData.quantity !== undefined ? issueData.quantity : issue.quantity; + const issued_by = issueData.issued_by !== undefined ? issueData.issued_by : issue.issued_by; + + const query = ` + UPDATE material_issue + SET project_id = $1, task_id = $2, item_id = $3, quantity = $4, issued_by = $5 + WHERE material_issue_id = $6 + RETURNING * + `; + const result = await pool.query(query, [project_id, task_id, item_id, quantity, issued_by, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM material_issue WHERE material_issue_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = MaterialIssue; \ No newline at end of file diff --git a/backend/models/Notification.js b/backend/models/Notification.js new file mode 100644 index 0000000..ad1111b --- /dev/null +++ b/backend/models/Notification.js @@ -0,0 +1,63 @@ +const pool = require('../config/db'); + +class Notification { + static async getByUserId(userId) { + const query = ` + SELECT * FROM notifications + WHERE user_id = $1 OR user_id IS NULL + ORDER BY created_at DESC + LIMIT 50 + `; + const result = await pool.query(query, [userId]); + return result.rows; + } + + static async getAll() { + const query = 'SELECT * FROM notifications ORDER BY created_at DESC LIMIT 100'; + const result = await pool.query(query); + return result.rows; + } + + static async create(notificationData) { + const { user_id, message, title, type, severity } = notificationData; + const query = ` + INSERT INTO notifications (user_id, message, title, type, severity, is_read) + VALUES ($1, $2, $3, $4, $5, false) + RETURNING * + `; + const result = await pool.query(query, [ + user_id || null, + message, + title || message, + type || 'general', + severity || 'medium', + ]); + return result.rows[0]; + } + + static async markRead(id) { + const query = ` + UPDATE notifications SET is_read = true WHERE id = $1 RETURNING * + `; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async markAllRead(userId) { + const query = ` + UPDATE notifications SET is_read = true + WHERE user_id = $1 OR user_id IS NULL + RETURNING * + `; + const result = await pool.query(query, [userId]); + return result.rows; + } + + static async delete(id) { + const query = 'DELETE FROM notifications WHERE id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Notification; diff --git a/backend/models/Procurement.js b/backend/models/Procurement.js new file mode 100644 index 0000000..3babbc7 --- /dev/null +++ b/backend/models/Procurement.js @@ -0,0 +1,81 @@ +const pool = require('../config/db'); + +class Procurement { + static async getAll() { + const query = 'SELECT * FROM procurement ORDER BY id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + // Try by integer id first, then by procurement_id string + let result; + if (!isNaN(Number(id))) { + result = await pool.query('SELECT * FROM procurement WHERE id = $1', [Number(id)]); + } + if (!result || result.rows.length === 0) { + result = await pool.query('SELECT * FROM procurement WHERE procurement_id = $1', [String(id)]); + } + return result.rows[0]; + } + + static async getByProjectId(projectId) { + const query = 'SELECT * FROM procurement WHERE project_id = $1'; + const result = await pool.query(query, [projectId]); + return result.rows; + } + + static async create(procurementData) { + const { procurement_id, project_id, vendor_id, item_id, quantity, unit_price, delivery_status, expected_delivery, created_by } = procurementData; + const query = ` + INSERT INTO procurement (procurement_id, project_id, vendor_id, item_id, quantity, unit_price, delivery_status, expected_delivery, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING * + `; + const result = await pool.query(query, [ + procurement_id || `PO-${Date.now()}`, + project_id, vendor_id, item_id, quantity, unit_price, delivery_status || 'ordered', expected_delivery, created_by + ]); + return result.rows[0]; + } + + static async update(id, procurementData) { + const proc = await Procurement.getById(id); + if (!proc) return null; + + // Always use the actual integer id from the found record + const dbId = proc.id; + + const project_id = procurementData.project_id !== undefined ? procurementData.project_id : proc.project_id; + const vendor_id = procurementData.vendor_id !== undefined ? procurementData.vendor_id : proc.vendor_id; + const item_id = procurementData.item_id !== undefined ? procurementData.item_id : proc.item_id; + const quantity = procurementData.quantity !== undefined ? procurementData.quantity : proc.quantity; + const unit_price = procurementData.unit_price !== undefined ? procurementData.unit_price : proc.unit_price; + const delivery_status = procurementData.delivery_status !== undefined ? procurementData.delivery_status : proc.delivery_status; + const expected_delivery = procurementData.expected_delivery !== undefined ? procurementData.expected_delivery : proc.expected_delivery; + const delivered_at = procurementData.delivered_at !== undefined ? procurementData.delivered_at : proc.delivered_at; + + const query = ` + UPDATE procurement + SET project_id = $1, vendor_id = $2, item_id = $3, quantity = $4, unit_price = $5, delivery_status = $6, expected_delivery = $7, delivered_at = $8 + WHERE id = $9 + RETURNING * + `; + const result = await pool.query(query, [project_id, vendor_id, item_id, quantity, unit_price, delivery_status, expected_delivery, delivered_at, dbId]); + return result.rows[0]; + } + + static async delete(id) { + // Try by integer id first, then by procurement_id string + let result; + if (!isNaN(Number(id))) { + result = await pool.query('DELETE FROM procurement WHERE id = $1 RETURNING *', [Number(id)]); + } + if (!result || result.rows.length === 0) { + result = await pool.query('DELETE FROM procurement WHERE procurement_id = $1 RETURNING *', [String(id)]); + } + return result.rows[0]; + } +} + +module.exports = Procurement; \ No newline at end of file diff --git a/backend/models/Project.js b/backend/models/Project.js new file mode 100644 index 0000000..7e5944d --- /dev/null +++ b/backend/models/Project.js @@ -0,0 +1,46 @@ +const pool = require('../config/db'); + +class Project { + static async getAll() { + const query = 'SELECT * FROM project ORDER BY project_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM project WHERE project_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async create(projectData) { + const { project_name, site_location, project_type, start_date, end_date, budget, status, created_by } = projectData; + const query = ` + INSERT INTO project (project_name, site_location, project_type, start_date, end_date, budget, status, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * + `; + const result = await pool.query(query, [project_name, site_location, project_type, start_date, end_date, budget, status || 'Active', created_by]); + return result.rows[0]; + } + + static async update(id, projectData) { + const { project_name, site_location, project_type, start_date, end_date, budget, status } = projectData; + const query = ` + UPDATE project + SET project_name = $1, site_location = $2, project_type = $3, start_date = $4, end_date = $5, budget = $6, status = $7, updated_at = CURRENT_TIMESTAMP + WHERE project_id = $8 + RETURNING * + `; + const result = await pool.query(query, [project_name, site_location, project_type, start_date, end_date, budget, status, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM project WHERE project_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Project; \ No newline at end of file diff --git a/backend/models/ProjectMember.js b/backend/models/ProjectMember.js new file mode 100644 index 0000000..64e148d --- /dev/null +++ b/backend/models/ProjectMember.js @@ -0,0 +1,59 @@ +const pool = require('../config/db'); + +class ProjectMember { + static async getAll() { + const query = 'SELECT * FROM project_members ORDER BY project_member_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM project_members WHERE project_member_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByProjectId(projectId) { + const query = 'SELECT * FROM project_members WHERE project_id = $1'; + const result = await pool.query(query, [projectId]); + return result.rows; + } + + static async create(memberData) { + const { project_id, user_id, member_role, from_date, to_date } = memberData; + + const checkQuery = 'SELECT * FROM project_members WHERE project_id = $1 AND user_id = $2'; + const existing = await pool.query(checkQuery, [project_id, user_id]); + if (existing.rows.length > 0) { + throw new Error('User is already a member of this project'); + } + + const query = ` + INSERT INTO project_members (project_id, user_id, member_role, from_date, to_date) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + `; + const result = await pool.query(query, [project_id, user_id, member_role, from_date, to_date]); + return result.rows[0]; + } + + static async update(id, memberData) { + const { project_id, user_id, member_role, from_date, to_date } = memberData; + const query = ` + UPDATE project_members + SET project_id = $1, user_id = $2, member_role = $3, from_date = $4, to_date = $5 + WHERE project_member_id = $6 + RETURNING * + `; + const result = await pool.query(query, [project_id, user_id, member_role, from_date, to_date, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM project_members WHERE project_member_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = ProjectMember; \ No newline at end of file diff --git a/backend/models/Task.js b/backend/models/Task.js new file mode 100644 index 0000000..6b13a6e --- /dev/null +++ b/backend/models/Task.js @@ -0,0 +1,91 @@ +const pool = require('../config/db'); + +class Task { + static async getAll() { + const query = 'SELECT * FROM task ORDER BY task_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM task WHERE task_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByProjectId(projectId) { + const query = 'SELECT * FROM task WHERE project_id = $1'; + const result = await pool.query(query, [projectId]); + return result.rows; + } + + static async create(taskData) { + const { + project_id, task_name, assigned_to, start_date, end_date, status, + priority, due_date, deadline, progress, workers_assigned, materials_used, dependencies + } = taskData; + const query = ` + INSERT INTO task (project_id, task_name, assigned_to, start_date, end_date, status, priority, due_date, deadline, progress, workers_assigned, materials_used, dependencies) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + RETURNING * + `; + const result = await pool.query(query, [ + project_id, + task_name, + assigned_to, + start_date, + end_date, + status || 'Open', + priority || 'Medium', + due_date, + deadline, + progress || 0, + workers_assigned ? JSON.stringify(workers_assigned) : '[]', + materials_used ? JSON.stringify(materials_used) : '[]', + dependencies ? JSON.stringify(dependencies) : '[]' + ]); + return result.rows[0]; + } + + static async update(id, taskData) { + // Allows partial updates for any fields provided + const task = await Task.getById(id); + if (!task) return null; + + const project_id = taskData.project_id !== undefined ? taskData.project_id : task.project_id; + const task_name = taskData.task_name !== undefined ? taskData.task_name : task.task_name; + const assigned_to = taskData.assigned_to !== undefined ? taskData.assigned_to : task.assigned_to; + const start_date = taskData.start_date !== undefined ? taskData.start_date : task.start_date; + const end_date = taskData.end_date !== undefined ? taskData.end_date : task.end_date; + const status = taskData.status !== undefined ? taskData.status : task.status; + const priority = taskData.priority !== undefined ? taskData.priority : task.priority; + const due_date = taskData.due_date !== undefined ? taskData.due_date : task.due_date; + const deadline = taskData.deadline !== undefined ? taskData.deadline : task.deadline; + const progress = taskData.progress !== undefined ? taskData.progress : task.progress; + const workers_assigned = taskData.workers_assigned !== undefined ? JSON.stringify(taskData.workers_assigned) : JSON.stringify(task.workers_assigned); + const materials_used = taskData.materials_used !== undefined ? JSON.stringify(taskData.materials_used) : JSON.stringify(task.materials_used); + const dependencies = taskData.dependencies !== undefined ? JSON.stringify(taskData.dependencies) : JSON.stringify(task.dependencies); + + const query = ` + UPDATE task + SET project_id = $1, task_name = $2, assigned_to = $3, start_date = $4, end_date = $5, status = $6, + priority = $7, due_date = $8, deadline = $9, progress = $10, workers_assigned = $11, materials_used = $12, dependencies = $13 + WHERE task_id = $14 + RETURNING * + `; + const result = await pool.query(query, [ + project_id, task_name, assigned_to, start_date, end_date, status, + priority, due_date, deadline, progress, workers_assigned, materials_used, dependencies, + id + ]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM task WHERE task_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Task; \ No newline at end of file diff --git a/backend/models/User.js b/backend/models/User.js new file mode 100644 index 0000000..c8364e6 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,110 @@ +const pool = require('../config/db'); +const crypto = require('crypto'); + +class User { + static hashPassword(password) { + const salt = crypto.randomBytes(16).toString('hex'); + const derived = crypto.scryptSync(password, salt, 64).toString('hex'); + return `${salt}:${derived}`; + } + + static verifyPassword(password, storedHash) { + if (!storedHash) return false; + const [salt, key] = storedHash.split(':'); + if (!salt || !key) return false; + const derived = crypto.scryptSync(password, salt, 64).toString('hex'); + return crypto.timingSafeEqual(Buffer.from(key, 'hex'), Buffer.from(derived, 'hex')); + } + + static async getAll() { + const query = 'SELECT * FROM "User" ORDER BY user_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM "User" WHERE user_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByEmail(email) { + const query = 'SELECT * FROM "User" WHERE email = $1'; + const result = await pool.query(query, [email]); + return result.rows[0]; + } + + static async create(userData) { + const { name, role, email, phone, password, is_active } = userData; + const passwordHash = password ? User.hashPassword(password) : null; + const query = ` + INSERT INTO "User" (name, role, email, phone, password, is_active) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING user_id, name, role, email, phone, is_active, created_at, updated_at + `; + const result = await pool.query(query, [name, role, email, phone, passwordHash, is_active ?? true]); + return result.rows[0]; + } + + static async update(id, userData) { + const { name, role, email, phone, password, is_active } = userData; + if (password) { + const passwordHash = User.hashPassword(password); + const query = ` + UPDATE "User" + SET name = $1, role = $2, email = $3, phone = $4, password = $5, is_active = $6, updated_at = CURRENT_TIMESTAMP + WHERE user_id = $7 + RETURNING user_id, name, role, email, phone, is_active, created_at, updated_at + `; + const result = await pool.query(query, [name, role, email, phone, passwordHash, is_active, id]); + return result.rows[0]; + } + + const query = ` + UPDATE "User" + SET name = $1, role = $2, email = $3, phone = $4, is_active = $5, updated_at = CURRENT_TIMESTAMP + WHERE user_id = $6 + RETURNING user_id, name, role, email, phone, is_active, created_at, updated_at + `; + const result = await pool.query(query, [name, role, email, phone, is_active, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM "User" WHERE user_id = $1 RETURNING user_id, name, role, email'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getResetCount(email) { + const query = 'SELECT password_reset_count FROM "User" WHERE email = $1'; + const result = await pool.query(query, [email]); + if (result.rows.length === 0) return null; + return result.rows[0].password_reset_count || 0; + } + + static async incrementResetCount(email) { + const query = ` + UPDATE "User" + SET password_reset_count = COALESCE(password_reset_count, 0) + 1, updated_at = CURRENT_TIMESTAMP + WHERE email = $1 + RETURNING password_reset_count + `; + const result = await pool.query(query, [email]); + return result.rows[0]?.password_reset_count; + } + + static async resetPassword(email, newPassword) { + const passwordHash = User.hashPassword(newPassword); + const query = ` + UPDATE "User" + SET password = $1, updated_at = CURRENT_TIMESTAMP + WHERE email = $2 + RETURNING user_id, name, role, email + `; + const result = await pool.query(query, [passwordHash, email]); + return result.rows[0]; + } +} + +module.exports = User; \ No newline at end of file diff --git a/backend/models/Vendor.js b/backend/models/Vendor.js new file mode 100644 index 0000000..63b1f74 --- /dev/null +++ b/backend/models/Vendor.js @@ -0,0 +1,46 @@ +const pool = require('../config/db'); + +class Vendor { + static async getAll() { + const query = 'SELECT * FROM vendor ORDER BY vendor_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM vendor WHERE vendor_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async create(vendorData) { + const { vendor_name, contact, email, address, rating } = vendorData; + const query = ` + INSERT INTO vendor (vendor_name, contact, email, address, rating) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + `; + const result = await pool.query(query, [vendor_name, contact, email, address, rating || 0]); + return result.rows[0]; + } + + static async update(id, vendorData) { + const { vendor_name, contact, email, address, rating } = vendorData; + const query = ` + UPDATE vendor + SET vendor_name = $1, contact = $2, email = $3, address = $4, rating = $5 + WHERE vendor_id = $6 + RETURNING * + `; + const result = await pool.query(query, [vendor_name, contact, email, address, rating, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM vendor WHERE vendor_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Vendor; \ No newline at end of file diff --git a/backend/models/Worker.js b/backend/models/Worker.js new file mode 100644 index 0000000..0126934 --- /dev/null +++ b/backend/models/Worker.js @@ -0,0 +1,70 @@ +const pool = require('../config/db'); + +class Worker { + static async getAll() { + const query = 'SELECT * FROM worker ORDER BY worker_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM worker WHERE worker_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async create(workerData) { + const { user_id, project_id, name, skill_type, contact, rate_type, base_rate, salary, attendance } = workerData; + const query = ` + INSERT INTO worker (user_id, project_id, name, skill_type, contact, rate_type, base_rate, salary, attendance) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING * + `; + const result = await pool.query(query, [ + user_id || null, + project_id || null, + name, + skill_type, + contact, + rate_type, + base_rate, + salary || 0, + attendance ? JSON.stringify(attendance) : '[]' + ]); + return result.rows[0]; + } + + static async update(id, workerData) { + const worker = await Worker.getById(id); + if (!worker) return null; + + const user_id = workerData.user_id !== undefined ? workerData.user_id : worker.user_id; + const project_id = workerData.project_id !== undefined ? workerData.project_id : worker.project_id; + const name = workerData.name !== undefined ? workerData.name : worker.name; + const skill_type = workerData.skill_type !== undefined ? workerData.skill_type : worker.skill_type; + const contact = workerData.contact !== undefined ? workerData.contact : worker.contact; + const rate_type = workerData.rate_type !== undefined ? workerData.rate_type : worker.rate_type; + const base_rate = workerData.base_rate !== undefined ? workerData.base_rate : worker.base_rate; + const salary = workerData.salary !== undefined ? workerData.salary : worker.salary; + const attendance = workerData.attendance !== undefined ? JSON.stringify(workerData.attendance) : JSON.stringify(worker.attendance); + + const query = ` + UPDATE worker + SET user_id = $1, project_id = $2, name = $3, skill_type = $4, contact = $5, rate_type = $6, base_rate = $7, salary = $8, attendance = $9 + WHERE worker_id = $10 + RETURNING * + `; + const result = await pool.query(query, [ + user_id, project_id, name, skill_type, contact, rate_type, base_rate, salary, attendance, id + ]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM worker WHERE worker_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = Worker; \ No newline at end of file diff --git a/backend/models/WorkerAssignment.js b/backend/models/WorkerAssignment.js new file mode 100644 index 0000000..64bb0e3 --- /dev/null +++ b/backend/models/WorkerAssignment.js @@ -0,0 +1,52 @@ +const pool = require('../config/db'); + +class WorkerAssignment { + static async getAll() { + const query = 'SELECT * FROM workerassignment ORDER BY assignment_id'; + const result = await pool.query(query); + return result.rows; + } + + static async getById(id) { + const query = 'SELECT * FROM workerassignment WHERE assignment_id = $1'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } + + static async getByTaskId(taskId) { + const query = 'SELECT * FROM workerassignment WHERE task_id = $1'; + const result = await pool.query(query, [taskId]); + return result.rows; + } + + static async create(assignmentData) { + const { task_id, worker_id, from_date, to_date } = assignmentData; + const query = ` + INSERT INTO workerassignment (task_id, worker_id, from_date, to_date) + VALUES ($1, $2, $3, $4) + RETURNING * + `; + const result = await pool.query(query, [task_id, worker_id, from_date, to_date]); + return result.rows[0]; + } + + static async update(id, assignmentData) { + const { task_id, worker_id, from_date, to_date } = assignmentData; + const query = ` + UPDATE workerassignment + SET task_id = $1, worker_id = $2, from_date = $3, to_date = $4 + WHERE assignment_id = $5 + RETURNING * + `; + const result = await pool.query(query, [task_id, worker_id, from_date, to_date, id]); + return result.rows[0]; + } + + static async delete(id) { + const query = 'DELETE FROM workerassignment WHERE assignment_id = $1 RETURNING *'; + const result = await pool.query(query, [id]); + return result.rows[0]; + } +} + +module.exports = WorkerAssignment; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..801dc5a --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1392 @@ +{ + "name": "construction-site-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "construction-site-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.1", + "express": "^4.18.2", + "pg": "^8.20.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz", + "integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..b0ba883 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,19 @@ +{ + "name": "construction-site-backend", + "version": "1.0.0", + "description": "Backend for Construction Site Management System", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.1", + "express": "^4.18.2", + "pg": "^8.20.0" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } +} diff --git a/backend/reset_db.sql b/backend/reset_db.sql new file mode 100644 index 0000000..8d33974 --- /dev/null +++ b/backend/reset_db.sql @@ -0,0 +1,24 @@ +-- Run this SQL script in your PostgreSQL database (e.g. using pgAdmin or psql) +-- This will DROP all existing tables, immediately deleting all previously inserted mock data. +-- +-- WARNING: This deletes ALL data from these tables. +-- +-- After running this, re-run siteos_enterprise_schema.sql to recreate all tables. + +DROP TABLE IF EXISTS notifications CASCADE; +DROP TABLE IF EXISTS leave_application CASCADE; +DROP TABLE IF EXISTS attendance CASCADE; +DROP TABLE IF EXISTS material_issue CASCADE; +DROP TABLE IF EXISTS materialissue CASCADE; +DROP TABLE IF EXISTS finance CASCADE; +DROP TABLE IF EXISTS procurement CASCADE; +DROP TABLE IF EXISTS vendor CASCADE; +DROP TABLE IF EXISTS inventory_item CASCADE; +DROP TABLE IF EXISTS item CASCADE; +DROP TABLE IF EXISTS worker CASCADE; +DROP TABLE IF EXISTS project_members CASCADE; +DROP TABLE IF EXISTS task CASCADE; +DROP TABLE IF EXISTS project CASCADE; +DROP TABLE IF EXISTS "User" CASCADE; + +-- Schema is now clean. Run siteos_enterprise_schema.sql to recreate all tables. diff --git a/backend/routes/api.js b/backend/routes/api.js new file mode 100644 index 0000000..405e350 --- /dev/null +++ b/backend/routes/api.js @@ -0,0 +1,237 @@ +const express = require('express'); +const router = express.Router(); + +// Import controllers +const UserController = require('../controllers/UserController'); +const ProjectController = require('../controllers/ProjectController'); +const TaskController = require('../controllers/TaskController'); +const WorkerController = require('../controllers/WorkerController'); +const VendorController = require('../controllers/VendorController'); +const ProcurementController = require('../controllers/ProcurementController'); +const InventoryController = require('../controllers/InventoryController'); +const MaterialIssueController = require('../controllers/MaterialIssueController'); +const AttendanceController = require('../controllers/AttendanceController'); +const FinanceController = require('../controllers/FinanceController'); +const NotificationController = require('../controllers/NotificationController'); +const ProjectMemberController = require('../controllers/ProjectMemberController'); +const LeaveController = require('../controllers/LeaveController'); +const WorkerAssignmentController = require('../controllers/WorkerAssignmentController'); +const User = require('../models/User'); + +// Auth routes +const ALLOWED_ROLES = ['Admin', 'Project_Manager', 'Site_Engineer', 'Worker']; + +router.post('/auth/signup', async (req, res) => { + try { + const { name, email, password, role, phone } = req.body; + + if (!name || !email || !password || !role) { + return res.status(400).json({ error: 'Name, email, password, and role are required' }); + } + + if (!ALLOWED_ROLES.includes(role)) { + return res.status(400).json({ error: 'Invalid role selected' }); + } + + const existingUser = await User.getByEmail(email); + if (existingUser) { + return res.status(409).json({ error: 'Email is already registered' }); + } + + const newUser = await User.create({ name, email, password, role, phone }); + const { password: _, ...userData } = newUser; + + res.status(201).json({ + message: 'Account created successfully', + user: userData, + }); + } catch (error) { + console.error('Error in signup:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.post('/auth/login', async (req, res) => { + try { + const { email, password, role } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password are required' }); + } + + const user = await User.getByEmail(email); + if (!user || user.role !== role || !User.verifyPassword(password, user.password)) { + if (user && user.role !== role) { + return res.status(401).json({ error: 'Invalid role for this user' }); + } + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const { password: _, ...userData } = user; + res.json({ + message: 'Login successful', + user: userData, + }); + } catch (error) { + console.error('Error in login:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Password Reset route — allows max 2 self-resets per user +router.post('/auth/reset-password', async (req, res) => { + try { + const { email, newPassword } = req.body; + + if (!email || !newPassword) { + return res.status(400).json({ error: 'Email and new password are required' }); + } + + // Check if user exists + const user = await User.getByEmail(email); + if (!user) { + return res.status(404).json({ error: 'No account found with this email address' }); + } + + // Check reset count (max 2 allowed) + const resetCount = await User.getResetCount(email); + if (resetCount >= 2) { + return res.status(403).json({ + error: 'Password reset limit reached (2 resets allowed). Please contact the Admin to change your password.', + limitReached: true, + }); + } + + // Reset the password and increment count + const updatedUser = await User.resetPassword(email, newPassword); + if (!updatedUser) { + return res.status(500).json({ error: 'Failed to reset password' }); + } + + await User.incrementResetCount(email); + const newCount = (resetCount || 0) + 1; + + res.json({ + message: `Password reset successful! You have ${2 - newCount} reset(s) remaining.`, + resetsRemaining: 2 - newCount, + }); + } catch (error) { + console.error('Error in password reset:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// User routes +router.get('/users', UserController.getAllUsers); +router.get('/users/:id', UserController.getUserById); +router.post('/users', UserController.createUser); +router.put('/users/:id', UserController.updateUser); +router.delete('/users/:id', UserController.deleteUser); + +// Project routes +router.get('/projects', ProjectController.getAllProjects); +router.get('/projects/:id', ProjectController.getProjectById); +router.post('/projects', ProjectController.createProject); +router.put('/projects/:id', ProjectController.updateProject); +router.delete('/projects/:id', ProjectController.deleteProject); + +// Task routes +router.get('/tasks', TaskController.getAllTasks); +router.get('/tasks/:id', TaskController.getTaskById); +router.get('/projects/:projectId/tasks', TaskController.getTasksByProject); +router.post('/tasks', TaskController.createTask); +router.put('/tasks/:id', TaskController.updateTask); +router.delete('/tasks/:id', TaskController.deleteTask); + +// Worker routes +router.get('/workers', WorkerController.getAllWorkers); +router.get('/workers/:id', WorkerController.getWorkerById); +router.post('/workers', WorkerController.createWorker); +router.put('/workers/:id', WorkerController.updateWorker); +router.delete('/workers/:id', WorkerController.deleteWorker); + +// Vendor routes +router.get('/vendors', VendorController.getAllVendors); +router.get('/vendors/:id', VendorController.getVendorById); +router.post('/vendors', VendorController.createVendor); +router.put('/vendors/:id', VendorController.updateVendor); +router.delete('/vendors/:id', VendorController.deleteVendor); + +// Procurement routes +router.get('/procurement', ProcurementController.getAllProcurements); +router.get('/procurement/:id', ProcurementController.getProcurementById); +router.get('/projects/:projectId/procurement', ProcurementController.getProcurementsByProject); +router.post('/procurement', ProcurementController.createProcurement); +router.put('/procurement/:id', ProcurementController.updateProcurement); +router.delete('/procurement/:id', ProcurementController.deleteProcurement); + +// Inventory routes +router.get('/inventory', InventoryController.getAllItems); +router.get('/inventory/:id', InventoryController.getItemById); +router.post('/inventory', InventoryController.createItem); +router.put('/inventory/:id', InventoryController.updateItem); +router.delete('/inventory/:id', InventoryController.deleteItem); + +// Material Issue routes +router.get('/material-issue', MaterialIssueController.getAllIssues); +router.get('/material-issue/:id', MaterialIssueController.getIssueById); +router.get('/projects/:projectId/material-issue', MaterialIssueController.getIssuesByProject); +router.post('/material-issue', MaterialIssueController.createIssue); +router.put('/material-issue/:id', MaterialIssueController.updateIssue); +router.delete('/material-issue/:id', MaterialIssueController.deleteIssue); + +// Attendance routes +router.get('/attendance', AttendanceController.getAllAttendance); +router.get('/attendance/:id', AttendanceController.getAttendanceById); +router.get('/projects/:projectId/attendance', AttendanceController.getAttendanceByProject); +router.get('/workers/:workerId/attendance', AttendanceController.getAttendanceByWorker); +router.post('/attendance', AttendanceController.createAttendance); +router.put('/attendance/:id', AttendanceController.updateAttendance); +router.delete('/attendance/:id', AttendanceController.deleteAttendance); + +// Finance routes +router.get('/finance', FinanceController.getAllFinance); +router.get('/finance/:id', FinanceController.getFinanceById); +router.get('/projects/:projectId/finance', FinanceController.getFinanceByProject); +router.post('/finance', FinanceController.createFinance); +router.put('/finance/:id', FinanceController.updateFinance); +router.delete('/finance/:id', FinanceController.deleteFinance); + +// Notification routes +router.get('/notifications', NotificationController.getAllNotifications); +router.get('/notifications/user/:userId', NotificationController.getNotifications); +router.post('/notifications', NotificationController.createNotification); +router.put('/notifications/:id/read', NotificationController.markRead); +router.put('/notifications/read-all/:userId', NotificationController.markAllRead); +router.delete('/notifications/:id', NotificationController.deleteNotification); + +// Project Member routes (Site Engineer assignment to projects) +router.get('/project-members', ProjectMemberController.getAllMembers); +router.get('/project-members/project/:projectId', ProjectMemberController.getMembersByProject); +router.post('/project-members', ProjectMemberController.createMember); +router.delete('/project-members/:id', ProjectMemberController.deleteMember); + +// Leave routes +router.get('/leave', LeaveController.getAllLeaves); +router.get('/workers/:workerId/leave', LeaveController.getLeavesByWorker); +router.post('/leave', LeaveController.createLeave); +router.put('/leave/:id/approve', LeaveController.approveLeave); +router.put('/leave/:id/reject', LeaveController.rejectLeave); + +// Worker Assignment routes +router.get('/worker-assignments', WorkerAssignmentController.getAllAssignments); +router.get('/worker-assignments/:id', WorkerAssignmentController.getAssignmentById); +router.get('/tasks/:taskId/assignments', WorkerAssignmentController.getAssignmentsByTask); +router.post('/worker-assignments', WorkerAssignmentController.createAssignment); +router.delete('/worker-assignments/:id', WorkerAssignmentController.deleteAssignment); + +// Legacy routes for compatibility +router.get('/message', (req, res) => { + res.json({ message: "Hello from backend" }); +}); + +router.post('/data', (req, res) => { + res.json({ status: "success" }); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/seed_data.js b/backend/seed_data.js new file mode 100644 index 0000000..f9ff826 --- /dev/null +++ b/backend/seed_data.js @@ -0,0 +1,314 @@ +/** + * Seed Script for SiteOS Enterprise + * Run: node seed_data.js + * + * This inserts 10+ rows into every table with realistic Indian construction data. + * Users are created via the model so passwords are properly hashed. + * All other tables use direct SQL inserts. + * + * DEFAULT PASSWORD for all users: Test@1234 + */ + +require('dotenv').config(); +const pool = require('./config/db'); +const User = require('./models/User'); + +const PASSWORD = 'Test@1234'; + +async function seed() { + console.log('🌱 Starting database seed...\n'); + + // ────────────────────────────────────────── + // 1. USERS (12 users across all roles) + // ────────────────────────────────────────── + console.log('👤 Creating users...'); + const users = [ + { name: 'Rajesh Kumar', email: 'admin@siteos.in', role: 'Admin', phone: '98765-10001' }, + { name: 'Sunita Sharma', email: 'admin2@siteos.in', role: 'Admin', phone: '98765-10002' }, + { name: 'Vikram Mehta', email: 'pm1@siteos.in', role: 'Project_Manager', phone: '98765-20001' }, + { name: 'Priya Patel', email: 'pm2@siteos.in', role: 'Project_Manager', phone: '98765-20002' }, + { name: 'Amit Joshi', email: 'se1@siteos.in', role: 'Site_Engineer', phone: '98765-30001' }, + { name: 'Neha Verma', email: 'se2@siteos.in', role: 'Site_Engineer', phone: '98765-30002' }, + { name: 'Suresh Yadav', email: 'se3@siteos.in', role: 'Site_Engineer', phone: '98765-30003' }, + { name: 'Rakesh Patel', email: 'worker1@siteos.in', role: 'Worker', phone: '98980-11111' }, + { name: 'Jignesh Chauhan', email: 'worker2@siteos.in', role: 'Worker', phone: '98980-22222' }, + { name: 'Mehul Shah', email: 'worker3@siteos.in', role: 'Worker', phone: '98980-33333' }, + { name: 'Amit Solanki', email: 'worker4@siteos.in', role: 'Worker', phone: '98980-44444' }, + { name: 'Bhavesh Desai', email: 'worker5@siteos.in', role: 'Worker', phone: '98980-66666' }, + ]; + + const createdUsers = []; + for (const u of users) { + try { + const created = await User.create({ ...u, password: PASSWORD }); + createdUsers.push(created); + console.log(` ✓ ${created.name} (${created.role})`); + } catch (err) { + if (err.code === '23505') { + console.log(` ⏭ ${u.name} already exists, skipping`); + const existing = await User.getByEmail(u.email); + createdUsers.push(existing); + } else { + throw err; + } + } + } + + // ────────────────────────────────────────── + // 2. PROJECTS (10 projects) + // ────────────────────────────────────────── + console.log('\n🏗️ Creating projects...'); + await pool.query(` + INSERT INTO project (project_name, site_location, project_type, start_date, end_date, budget, status, created_by) VALUES + ('Gota Housing Block A', 'Gota, Ahmedabad', 'Residential', '2025-01-10', '2026-06-30', 12500000.00, 'Active', ${createdUsers[2].user_id}), + ('Metro Depot Shed', 'Sachin, Surat', 'Infrastructure', '2025-02-01', '2026-12-31', 6500000.00, 'Active', ${createdUsers[2].user_id}), + ('Switchgear Panel Upgrade', 'Makarpura, Vadodara', 'Commercial', '2025-03-05', '2025-12-31', 2800000.00, 'Active', ${createdUsers[3].user_id}), + ('Smart Housing Complex', 'Naranpura, Ahmedabad', 'Residential', '2025-04-01', '2027-03-31', 32000000.00, 'Active', ${createdUsers[3].user_id}), + ('Highway Bridge Rehab', 'Anand–Nadiad Highway', 'Infrastructure', '2025-05-15', '2026-05-14', 18500000.00, 'Planning', ${createdUsers[2].user_id}), + ('Corporate Tower Phase 2', 'SG Highway, Ahmedabad', 'Commercial', '2025-06-01', '2027-06-01', 45000000.00, 'Planning', ${createdUsers[3].user_id}), + ('Village School Renovation', 'Dholka, Ahmedabad', 'Residential', '2025-01-20', '2025-07-31', 1200000.00, 'Completed', ${createdUsers[2].user_id}), + ('Water Tank Construction', 'Gandhinagar', 'Infrastructure', '2025-03-01', '2025-09-30', 3500000.00, 'Active', ${createdUsers[2].user_id}), + ('Mall Interior Fitout', 'CG Road, Ahmedabad', 'Commercial', '2025-07-01', '2026-01-31', 8000000.00, 'Active', ${createdUsers[3].user_id}), + ('Solar Farm Mounting', 'Charanka, Patan', 'Infrastructure', '2025-08-01', '2026-03-31', 5600000.00, 'Planning', ${createdUsers[2].user_id}) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 10 projects inserted'); + + // ────────────────────────────────────────── + // 3. WORKERS (12 workers — linked to users 8–12 + extras) + // ────────────────────────────────────────── + console.log('\n👷 Creating workers...'); + await pool.query(` + INSERT INTO worker (user_id, project_id, name, skill_type, contact, rate_type, base_rate, salary) VALUES + (${createdUsers[7].user_id}, 1, 'Rakesh Patel', 'Mason', '98980-11111', 'Daily', 900.00, 27000), + (${createdUsers[8].user_id}, 1, 'Jignesh Chauhan', 'Helper', '98980-22222', 'Daily', 600.00, 18000), + (${createdUsers[9].user_id}, 2, 'Mehul Shah', 'Welder', '98980-33333', 'Hourly', 120.00, 19200), + (${createdUsers[10].user_id}, 2, 'Amit Solanki', 'Electrician', '98980-44444', 'Hourly', 150.00, 24000), + (${createdUsers[11].user_id}, 3, 'Bhavesh Desai', 'Electrician', '98980-66666', 'Hourly', 140.00, 22400), + (NULL, 1, 'Kiran Rathod', 'Plumber', '98980-55555', 'Daily', 850.00, 25500), + (NULL, 3, 'Dinesh Parmar', 'Carpenter', '98980-77777', 'Daily', 800.00, 24000), + (NULL, 4, 'Sanjay Thakor', 'Mason', '98980-88888', 'Daily', 950.00, 28500), + (NULL, 4, 'Ramesh Bharwad', 'Painter', '98980-99999', 'Daily', 700.00, 21000), + (NULL, 2, 'Gopal Vankar', 'Helper', '98980-10101', 'Hourly', 80.00, 12800), + (NULL, 5, 'Prakash Solanki', 'Steel Fixer', '98980-20202', 'Daily', 1000.00, 30000), + (NULL, 1, 'Harish Makwana', 'Tiler', '98980-30303', 'Daily', 750.00, 22500) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 12 workers inserted'); + + // ────────────────────────────────────────── + // 4. TASKS (15 tasks across projects) + // ────────────────────────────────────────── + console.log('\n📋 Creating tasks...'); + await pool.query(` + INSERT INTO task (project_id, task_name, assigned_to, start_date, end_date, status, priority, due_date, progress) VALUES + (1, 'Foundation Excavation', ${createdUsers[4].user_id}, '2025-01-15', '2025-02-15', 'Completed', 'High', '2025-02-15', 100), + (1, 'RCC Column Casting', ${createdUsers[4].user_id}, '2025-02-20', '2025-04-20', 'In_Progress', 'High', '2025-04-20', 65), + (1, 'Brick Wall Construction', ${createdUsers[4].user_id}, '2025-04-25', '2025-07-25', 'Open', 'Medium', '2025-07-25', 0), + (1, 'Plumbing Rough-In', ${createdUsers[5].user_id}, '2025-05-01', '2025-06-30', 'Open', 'Medium', '2025-06-30', 0), + (2, 'Steel Structure Erection', ${createdUsers[5].user_id}, '2025-02-10', '2025-05-10', 'In_Progress', 'Critical', '2025-05-10', 40), + (2, 'Roofing Sheet Installation', ${createdUsers[5].user_id}, '2025-05-15', '2025-07-15', 'Open', 'High', '2025-07-15', 0), + (3, 'Panel Wiring Phase 1', ${createdUsers[6].user_id}, '2025-03-10', '2025-06-10', 'In_Progress', 'High', '2025-06-10', 55), + (3, 'Panel Testing & QC', ${createdUsers[6].user_id}, '2025-06-15', '2025-08-15', 'Open', 'Medium', '2025-08-15', 0), + (4, 'Site Clearing & Leveling', ${createdUsers[4].user_id}, '2025-04-05', '2025-05-05', 'Completed', 'High', '2025-05-05', 100), + (4, 'Pile Foundation', ${createdUsers[4].user_id}, '2025-05-10', '2025-08-10', 'In_Progress', 'Critical', '2025-08-10', 30), + (5, 'Bridge Pier Inspection', ${createdUsers[5].user_id}, '2025-06-01', '2025-07-01', 'Open', 'High', '2025-07-01', 0), + (7, 'Roof Waterproofing', ${createdUsers[6].user_id}, '2025-02-01', '2025-03-15', 'Completed', 'Medium', '2025-03-15', 100), + (7, 'Classroom Painting', ${createdUsers[6].user_id}, '2025-03-20', '2025-05-20', 'Completed', 'Low', '2025-05-20', 100), + (8, 'Tank Foundation', ${createdUsers[5].user_id}, '2025-03-10', '2025-05-10', 'In_Progress', 'High', '2025-05-10', 70), + (9, 'False Ceiling Installation', ${createdUsers[4].user_id}, '2025-07-10', '2025-09-10', 'Open', 'Medium', '2025-09-10', 0) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 15 tasks inserted'); + + // ────────────────────────────────────────── + // 5. INVENTORY ITEMS (12 items) + // ────────────────────────────────────────── + console.log('\n📦 Creating inventory items...'); + await pool.query(` + INSERT INTO inventory_item (item_name, category, uom, unit_cost, min_stock_qty, current_stock, supplier) VALUES + ('OPC 53 Cement', 'Materials', 'Bags', 380.00, 100, 520, 'UltraTech Cement'), + ('TMT Steel 12mm', 'Materials', 'Tons', 62000.00, 5, 18, 'Tata Tiscon'), + ('TMT Steel 8mm', 'Materials', 'Tons', 60000.00, 3, 8, 'Tata Tiscon'), + ('River Sand', 'Materials', 'Cu.m', 1800.00, 20, 45, 'Local Supplier'), + ('20mm Aggregate', 'Materials', 'Cu.m', 1500.00, 15, 35, 'Ambuja Quarry'), + ('Red Clay Bricks', 'Materials', 'Pcs', 7.50, 5000, 12000, 'Morbi Bricks'), + ('AAC Blocks 600x200x150', 'Materials', 'Pcs', 52.00, 1000, 3500, 'Magicrete'), + ('PVC Pipe 4 inch', 'Plumbing', 'Meters', 180.00, 50, 120, 'Astral Pipes'), + ('Electrical Cable 2.5mm', 'Electrical','Meters', 22.00, 200, 450, 'Havells'), + ('GI Binding Wire', 'Materials', 'Kg', 85.00, 50, 90, 'Local Supplier'), + ('Waterproof Membrane', 'Chemicals', 'Sq.m', 120.00, 100, 40, 'Dr. Fixit'), + ('Ready Mix Concrete M25', 'Materials', 'Cu.m', 5500.00, 10, 25, 'ACC RMX') + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 12 inventory items inserted'); + + // ────────────────────────────────────────── + // 6. VENDORS (10 vendors) + // ────────────────────────────────────────── + console.log('\n🏪 Creating vendors...'); + await pool.query(` + INSERT INTO vendor (vendor_name, contact, email, address, rating) VALUES + ('UltraTech Cement Ltd', '079-2345-6789', 'sales@ultratech.in', 'GIDC Sanand, Ahmedabad', 4.5), + ('Tata Tiscon (Tata Steel)', '079-6789-0123', 'orders@tatasteel.com', 'Narol Industrial Area, Ahmedabad', 4.8), + ('Ambuja Cements Ltd', '079-3456-7890', 'supply@ambuja.com', 'Chandkheda, Ahmedabad', 4.2), + ('Astral Pipes Ltd', '079-4567-8901', 'b2b@astralpipes.com', 'Santej, Gandhinagar', 4.6), + ('Havells India Ltd', '079-5678-9012', 'dealer@havells.com', 'Vatva GIDC, Ahmedabad', 4.4), + ('Magicrete Building', '022-6789-0123', 'info@magicrete.in', 'Kalol, North Gujarat', 4.0), + ('Morbi Bricks Traders', '98251-34567', 'morbibricks@gmail.com', 'Morbi, Rajkot', 3.8), + ('ACC Ready Mix', '079-7890-1234', 'rmx@acclimited.com', 'Navrangpura, Ahmedabad', 4.3), + ('Dr. Fixit (Pidilite)', '079-8901-2345', 'waterproof@pidilite.com','Odhav, Ahmedabad', 4.7), + ('Gujarat Electrical Co.', '079-9012-3456', 'info@gujelectrical.in', 'Relief Road, Ahmedabad', 3.9) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 10 vendors inserted'); + + // ────────────────────────────────────────── + // 7. PROCUREMENT (12 purchase orders) + // ────────────────────────────────────────── + console.log('\n🛒 Creating procurement orders...'); + await pool.query(` + INSERT INTO procurement (procurement_id, project_id, vendor_id, item_id, quantity, unit_price, delivery_status, expected_delivery, delivered_at, created_by) VALUES + ('PO-2025-001', 1, 1, 1, 200, 380.00, 'delivered', '2025-01-20', '2025-01-19', ${createdUsers[2].user_id}), + ('PO-2025-002', 1, 2, 2, 5, 62000.00, 'delivered', '2025-01-25', '2025-01-24', ${createdUsers[2].user_id}), + ('PO-2025-003', 1, 6, 6, 8000,7.50, 'delivered', '2025-02-05', '2025-02-04', ${createdUsers[2].user_id}), + ('PO-2025-004', 2, 2, 3, 10, 60000.00, 'delivered', '2025-02-15', '2025-02-16', ${createdUsers[3].user_id}), + ('PO-2025-005', 2, 1, 1, 150, 380.00, 'shipped', '2025-04-10', NULL, ${createdUsers[3].user_id}), + ('PO-2025-006', 3, 5, 9, 300, 22.00, 'delivered', '2025-03-20', '2025-03-19', ${createdUsers[3].user_id}), + ('PO-2025-007', 3, 10,9, 200, 22.00, 'ordered', '2025-06-20', NULL, ${createdUsers[3].user_id}), + ('PO-2025-008', 4, 1, 1, 500, 380.00, 'ordered', '2025-05-20', NULL, ${createdUsers[2].user_id}), + ('PO-2025-009', 4, 3, 5, 30, 1500.00, 'ordered', '2025-05-25', NULL, ${createdUsers[2].user_id}), + ('PO-2025-010', 8, 8, 12, 15, 5500.00, 'delivered', '2025-03-20', '2025-03-21', ${createdUsers[2].user_id}), + ('PO-2025-011', 1, 4, 8, 80, 180.00, 'shipped', '2025-05-01', NULL, ${createdUsers[2].user_id}), + ('PO-2025-012', 9, 7, 7, 2000,52.00, 'ordered', '2025-07-20', NULL, ${createdUsers[3].user_id}) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 12 procurement orders inserted'); + + // ────────────────────────────────────────── + // 8. MATERIAL ISSUE (12 issues) + // ────────────────────────────────────────── + console.log('\n📤 Creating material issues...'); + await pool.query(` + INSERT INTO material_issue (project_id, task_id, item_id, quantity, issued_by) VALUES + (1, 1, 1, 80, ${createdUsers[4].user_id}), + (1, 1, 2, 2, ${createdUsers[4].user_id}), + (1, 2, 1, 60, ${createdUsers[4].user_id}), + (1, 2, 2, 3, ${createdUsers[4].user_id}), + (1, 3, 6, 3000,${createdUsers[4].user_id}), + (2, 5, 3, 4, ${createdUsers[5].user_id}), + (2, 5, 10, 20, ${createdUsers[5].user_id}), + (3, 7, 9, 150, ${createdUsers[6].user_id}), + (4, 9, 1, 100, ${createdUsers[4].user_id}), + (4, 10,2, 5, ${createdUsers[4].user_id}), + (7, 12,11, 30, ${createdUsers[6].user_id}), + (8, 14,12, 10, ${createdUsers[5].user_id}) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 12 material issues inserted'); + + // ────────────────────────────────────────── + // 9. ATTENDANCE (30 records — multiple workers, dates) + // ────────────────────────────────────────── + console.log('\n📅 Creating attendance records...'); + await pool.query(` + INSERT INTO attendance (worker_id, project_id, date, status, hours_worked, labor_cost, recorded_by) VALUES + (1, 1, '2025-04-01', 'Present', 8, 900.00, ${createdUsers[4].user_id}), + (1, 1, '2025-04-02', 'Present', 8, 900.00, ${createdUsers[4].user_id}), + (1, 1, '2025-04-03', 'Half_Day', 4, 450.00, ${createdUsers[4].user_id}), + (1, 1, '2025-04-04', 'Present', 8, 900.00, ${createdUsers[4].user_id}), + (1, 1, '2025-04-05', 'Absent', 0, 0.00, ${createdUsers[4].user_id}), + (2, 1, '2025-04-01', 'Present', 8, 600.00, ${createdUsers[4].user_id}), + (2, 1, '2025-04-02', 'Present', 8, 600.00, ${createdUsers[4].user_id}), + (2, 1, '2025-04-03', 'Present', 8, 600.00, ${createdUsers[4].user_id}), + (2, 1, '2025-04-04', 'Absent', 0, 0.00, ${createdUsers[4].user_id}), + (2, 1, '2025-04-05', 'Present', 8, 600.00, ${createdUsers[4].user_id}), + (3, 2, '2025-04-01', 'Present', 8, 960.00, ${createdUsers[5].user_id}), + (3, 2, '2025-04-02', 'Present', 6, 720.00, ${createdUsers[5].user_id}), + (3, 2, '2025-04-03', 'Present', 8, 960.00, ${createdUsers[5].user_id}), + (4, 2, '2025-04-01', 'Present', 8, 1200.00, ${createdUsers[5].user_id}), + (4, 2, '2025-04-02', 'Half_Day', 4, 600.00, ${createdUsers[5].user_id}), + (4, 2, '2025-04-03', 'Present', 8, 1200.00, ${createdUsers[5].user_id}), + (5, 3, '2025-04-01', 'Present', 8, 1120.00, ${createdUsers[6].user_id}), + (5, 3, '2025-04-02', 'Present', 8, 1120.00, ${createdUsers[6].user_id}), + (5, 3, '2025-04-03', 'Absent', 0, 0.00, ${createdUsers[6].user_id}), + (6, 1, '2025-04-01', 'Present', 8, 850.00, ${createdUsers[4].user_id}), + (6, 1, '2025-04-02', 'Present', 8, 850.00, ${createdUsers[4].user_id}), + (7, 3, '2025-04-01', 'Present', 8, 800.00, ${createdUsers[6].user_id}), + (7, 3, '2025-04-02', 'Present', 8, 800.00, ${createdUsers[6].user_id}), + (8, 4, '2025-04-01', 'Present', 8, 950.00, ${createdUsers[4].user_id}), + (8, 4, '2025-04-02', 'Present', 8, 950.00, ${createdUsers[4].user_id}), + (8, 4, '2025-04-03', 'Present', 8, 950.00, ${createdUsers[4].user_id}), + (9, 4, '2025-04-01', 'Present', 8, 700.00, ${createdUsers[4].user_id}), + (9, 4, '2025-04-02', 'Absent', 0, 0.00, ${createdUsers[4].user_id}), + (10,2, '2025-04-01', 'Present', 8, 640.00, ${createdUsers[5].user_id}), + (10,2, '2025-04-02', 'Present', 8, 640.00, ${createdUsers[5].user_id}) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 30 attendance records inserted'); + + // ────────────────────────────────────────── + // 10. FINANCE (15 records across projects) + // ────────────────────────────────────────── + console.log('\n💰 Creating finance records...'); + await pool.query(` + INSERT INTO finance (project_id, cost_category, amount, date, description, payment_status, source) VALUES + (1, 'Material', 76000.00, '2025-01-20', 'Cement purchase PO-001', 'Paid', 'procurement'), + (1, 'Material', 310000.00, '2025-01-25', 'TMT Steel 12mm purchase PO-002', 'Paid', 'procurement'), + (1, 'Material', 60000.00, '2025-02-05', 'Bricks purchase PO-003', 'Paid', 'procurement'), + (1, 'Labor', 45000.00, '2025-04-05', 'Weekly labor payout — Week 14', 'Paid', 'salary'), + (1, 'Equipment', 18000.00, '2025-03-01', 'Excavator rental — 3 days', 'Paid', 'manual'), + (2, 'Material', 600000.00, '2025-02-16', 'Steel 8mm PO-004', 'Paid', 'procurement'), + (2, 'Labor', 28800.00, '2025-04-05', 'Weekly labor payout — Week 14', 'Paid', 'salary'), + (2, 'Equipment', 35000.00, '2025-03-15', 'Crane rental — 5 days', 'Paid', 'manual'), + (3, 'Material', 6600.00, '2025-03-20', 'Electrical cable PO-006', 'Paid', 'procurement'), + (3, 'Labor', 19200.00, '2025-04-05', 'Weekly labor payout — Week 14', 'Pending', 'salary'), + (4, 'Material', 190000.00, '2025-05-01', 'Cement advance for pile foundation', 'Pending', 'procurement'), + (4, 'Labor', 38500.00, '2025-04-05', 'Weekly labor payout — Week 14', 'Paid', 'salary'), + (7, 'Material', 3600.00, '2025-03-01', 'Waterproofing membrane', 'Paid', 'manual'), + (7, 'Labor', 12000.00, '2025-05-25', 'Final labor settlement', 'Paid', 'salary'), + (8, 'Material', 82500.00, '2025-03-22', 'RMX Concrete PO-010', 'Paid', 'procurement') + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 15 finance records inserted'); + + // ────────────────────────────────────────── + // 11. LEAVE APPLICATIONS (10 records) + // ────────────────────────────────────────── + console.log('\n🏖️ Creating leave applications...'); + await pool.query(` + INSERT INTO leave_application (worker_id, start_date, end_date, reason, status, reviewed_by) VALUES + (1, '2025-04-10', '2025-04-12', 'Family wedding in village', 'Approved', ${createdUsers[4].user_id}), + (2, '2025-04-15', '2025-04-15', 'Medical appointment', 'Approved', ${createdUsers[4].user_id}), + (3, '2025-04-20', '2025-04-22', 'Personal work — home town visit', 'Pending', NULL), + (4, '2025-05-01', '2025-05-03', 'Holi festival travel', 'Approved', ${createdUsers[5].user_id}), + (5, '2025-04-18', '2025-04-18', 'Not feeling well', 'Rejected', ${createdUsers[6].user_id}), + (6, '2025-05-05', '2025-05-07', 'Daughter school admission', 'Pending', NULL), + (7, '2025-04-25', '2025-04-26', 'Government office work', 'Approved', ${createdUsers[6].user_id}), + (8, '2025-05-10', '2025-05-14', 'Village farming season', 'Pending', NULL), + (9, '2025-04-28', '2025-04-28', 'Doctor visit for back pain', 'Approved', ${createdUsers[4].user_id}), + (10, '2025-05-02', '2025-05-02', 'Child vaccination', 'Pending', NULL) + ON CONFLICT DO NOTHING; + `); + console.log(' ✓ 10 leave applications inserted'); + + // ────────────────────────────────────────── + console.log('\n✅ Seed complete!\n'); + console.log('═══════════════════════════════════════════'); + console.log(' LOGIN CREDENTIALS (all same password):'); + console.log(' Password: Test@1234'); + console.log(''); + console.log(' Admin: admin@siteos.in'); + console.log(' Project Manager: pm1@siteos.in'); + console.log(' Site Engineer: se1@siteos.in'); + console.log(' Worker: worker1@siteos.in'); + console.log('═══════════════════════════════════════════\n'); + + await pool.end(); + process.exit(0); +} + +seed().catch((err) => { + console.error('❌ Seed failed:', err); + process.exit(1); +}); diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..cb1ed9d --- /dev/null +++ b/backend/server.js @@ -0,0 +1,51 @@ +const express = require('express'); +const cors = require('cors'); +const apiRoutes = require('./routes/api'); +require('dotenv').config(); + +const app = express(); +let PORT = Number(process.env.PORT) || 5000; + +// Enable CORS for frontend +app.use(cors({ + origin: function(origin, callback) { + // Allow requests from any localhost port, or no origin (like curl) + if (!origin || /^http:\/\/localhost(:\d+)?$/.test(origin)) { + callback(null, true); + } else { + callback(null, true); // Allow all in dev + } + }, + credentials: true, +})); + +// Middleware to parse JSON +app.use(express.json()); + +// API routes +app.use('/api', apiRoutes); + +// Error handling middleware +app.use((err, req, res, next) => { + console.error('Unhandled error:', err); + res.status(500).json({ error: 'Something went wrong!' }); +}); + +const startServer = () => { + const server = app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + }); + + server.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.warn(`Port ${PORT} is already in use. Trying port ${PORT + 1}...`); + PORT += 1; + setTimeout(startServer, 100); + } else { + console.error('Server error:', err); + process.exit(1); + } + }); +}; + +startServer(); \ No newline at end of file diff --git a/backend/siteos_enterprise_schema.sql b/backend/siteos_enterprise_schema.sql new file mode 100644 index 0000000..368586f --- /dev/null +++ b/backend/siteos_enterprise_schema.sql @@ -0,0 +1,219 @@ +-- SiteOS Enterprise Complete PostgreSQL Schema +-- Fixed: Site_Manager removed from role check, Half Day added to attendance status, +-- UNIQUE constraint on attendance, notifications table added, project_members table added. +-- Run directly in pgAdmin or psql. + +-------------------------------------------------- +-- 1. CLEANUP (Drop tables in correct order to avoid FK errors) +-------------------------------------------------- +DROP TABLE IF EXISTS notifications CASCADE; +DROP TABLE IF EXISTS leave_application CASCADE; +DROP TABLE IF EXISTS finance CASCADE; +DROP TABLE IF EXISTS attendance CASCADE; +DROP TABLE IF EXISTS material_issue CASCADE; +DROP TABLE IF EXISTS procurement CASCADE; +DROP TABLE IF EXISTS vendor CASCADE; +DROP TABLE IF EXISTS inventory_item CASCADE; +DROP TABLE IF EXISTS worker CASCADE; +DROP TABLE IF EXISTS project_members CASCADE; +DROP TABLE IF EXISTS task CASCADE; +DROP TABLE IF EXISTS project CASCADE; +DROP TABLE IF EXISTS "User" CASCADE; + +-------------------------------------------------- +-- 2. CREATE TABLES +-------------------------------------------------- + +-- Users Table (Handles role-based authentication) +-- FIX: Removed 'Site_Manager' from role CHECK — only Site_Engineer is valid +CREATE TABLE "User" ( + user_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL CHECK (role IN ('Admin', 'Project_Manager', 'Site_Engineer', 'Worker')), + email VARCHAR(255) UNIQUE NOT NULL, + phone VARCHAR(20), + password VARCHAR(255) NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Projects Table (Core Project Management) +CREATE TABLE project ( + project_id SERIAL PRIMARY KEY, + project_name VARCHAR(255) NOT NULL, + site_location VARCHAR(255), + project_type VARCHAR(100), + start_date DATE, + end_date DATE, + budget NUMERIC(15, 2) DEFAULT 0.00, + status VARCHAR(50) DEFAULT 'Active' CHECK (status IN ('Active', 'Completed', 'On_Hold', 'Planning', 'Cancelled')), + created_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Project Members Table (Site Engineer → Project assignment) +-- FIX: This table was missing from original schema, causing runtime crashes +CREATE TABLE project_members ( + project_member_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE CASCADE, + member_role VARCHAR(50) DEFAULT 'Site_Engineer' CHECK (member_role IN ('Site_Engineer', 'Project_Manager', 'Admin')), + from_date DATE, + to_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (project_id, user_id) +); + +-- Tasks Table (Task Management & Assignment) +CREATE TABLE task ( + task_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + task_name VARCHAR(255) NOT NULL, + assigned_to INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + start_date DATE, + end_date DATE, + status VARCHAR(50) DEFAULT 'Open' CHECK (status IN ('Open', 'In Progress', 'In_Progress', 'Completed', 'Blocked', 'Review')), + priority VARCHAR(50) DEFAULT 'Medium' CHECK (priority IN ('Low', 'Medium', 'High', 'Critical')), + due_date DATE, + deadline DATE, + progress INTEGER DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), + workers_assigned JSONB DEFAULT '[]'::jsonb, + materials_used JSONB DEFAULT '[]'::jsonb, + dependencies JSONB DEFAULT '[]'::jsonb, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Workers Table (Workforce & Salary Management) +CREATE TABLE worker ( + worker_id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + project_id INTEGER REFERENCES project(project_id) ON DELETE SET NULL, + name VARCHAR(255) NOT NULL, + skill_type VARCHAR(100), + contact VARCHAR(50), + rate_type VARCHAR(50) CHECK (rate_type IN ('Daily', 'Hourly', 'Monthly')), + base_rate NUMERIC(15, 2) DEFAULT 0.00, + salary NUMERIC(15, 2) DEFAULT 0.00, + attendance JSONB DEFAULT '[]'::jsonb, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Inventory Table (Item Catalog & Stock) +CREATE TABLE inventory_item ( + item_id SERIAL PRIMARY KEY, + item_name VARCHAR(255) NOT NULL, + category VARCHAR(100), + uom VARCHAR(50), + unit_cost NUMERIC(15, 2) DEFAULT 0.00, + min_stock_qty INTEGER DEFAULT 0, + current_stock INTEGER DEFAULT 0, + supplier VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Vendors Table (Supplier details) +CREATE TABLE vendor ( + vendor_id SERIAL PRIMARY KEY, + vendor_name VARCHAR(255) NOT NULL, + contact VARCHAR(50), + email VARCHAR(255), + address TEXT, + rating NUMERIC(3, 1) DEFAULT 0 CHECK (rating >= 0 AND rating <= 5), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Procurement Table (Purchase Orders mapping to Inventory & Vendor) +CREATE TABLE procurement ( + id SERIAL PRIMARY KEY, + procurement_id VARCHAR(50) UNIQUE, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + vendor_id INTEGER REFERENCES vendor(vendor_id) ON DELETE SET NULL, + item_id INTEGER REFERENCES inventory_item(item_id) ON DELETE SET NULL, + quantity INTEGER NOT NULL, + unit_price NUMERIC(15, 2) DEFAULT 0.00, + delivery_status VARCHAR(50) DEFAULT 'ordered' CHECK (delivery_status IN ('ordered', 'shipped', 'delivered', 'cancelled')), + expected_delivery DATE, + delivered_at DATE, + created_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Material Issue Table (Tracking material usage on site/tasks) +CREATE TABLE material_issue ( + material_issue_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + task_id INTEGER REFERENCES task(task_id) ON DELETE CASCADE, + item_id INTEGER REFERENCES inventory_item(item_id) ON DELETE CASCADE, + quantity INTEGER NOT NULL, + issued_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Attendance Table (Daily per-worker attendance) +-- FIX: Added UNIQUE(worker_id, date) so ON CONFLICT works correctly +-- FIX: Added 'Half Day' (with space) to status CHECK — frontend sends "Half Day" not "Half_Day" +CREATE TABLE attendance ( + attendance_id SERIAL PRIMARY KEY, + worker_id INTEGER REFERENCES worker(worker_id) ON DELETE CASCADE, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + date DATE NOT NULL, + status VARCHAR(50) DEFAULT 'Present' CHECK (status IN ('Present', 'Absent', 'Half Day', 'Half_Day', 'Leave', 'Holiday')), + hours_worked NUMERIC(5, 2) DEFAULT 0, + labor_cost NUMERIC(15, 2) DEFAULT 0.00, + recorded_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE (worker_id, date) +); + +-- Finance Table (Expenses, Revenue, Salary Payouts) +CREATE TABLE finance ( + finance_id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES project(project_id) ON DELETE CASCADE, + cost_category VARCHAR(100), + amount NUMERIC(15, 2) NOT NULL, + date DATE NOT NULL, + description TEXT, + payment_status VARCHAR(50) DEFAULT 'Pending' CHECK (payment_status IN ('Pending', 'Paid', 'Overdue', 'Cancelled')), + source VARCHAR(50) DEFAULT 'manual', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Worker Leave System +CREATE TABLE leave_application ( + leave_id SERIAL PRIMARY KEY, + worker_id INTEGER REFERENCES worker(worker_id) ON DELETE CASCADE, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + reason TEXT, + status VARCHAR(50) DEFAULT 'Pending' CHECK (status IN ('Pending', 'Approved', 'Rejected')), + applied_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + reviewed_by INTEGER REFERENCES "User"(user_id) ON DELETE SET NULL, + reviewed_on TIMESTAMP +); + +-- Notifications Table (DB-backed notification system) +-- FIX: Previously notifications were only in-memory — this makes them persistent +CREATE TABLE notifications ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES "User"(user_id) ON DELETE CASCADE, + title VARCHAR(255), + message TEXT NOT NULL, + type VARCHAR(100) DEFAULT 'general', + severity VARCHAR(50) DEFAULT 'medium' CHECK (severity IN ('low', 'medium', 'high')), + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-------------------------------------------------- +-- 3. SAMPLE DATA +-------------------------------------------------- + +INSERT INTO project (project_name, site_location, project_type, start_date, budget, status) +VALUES ('Downtown Commercial Tower', '1200 NY Ave', 'Commercial', CURRENT_DATE, 5000000.00, 'Active'); + +INSERT INTO inventory_item (item_name, category, uom, unit_cost, min_stock_qty, current_stock, supplier) +VALUES +('Portland Cement', 'Materials', 'Bags', 450.00, 100, 500, 'Ambuja Cements'), +('TMT Steel Bars', 'Materials', 'Tons', 65000.00, 5, 20, 'Tata Steel'); diff --git a/construction-site-management/.gitignore b/construction-site-management/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/construction-site-management/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/construction-site-management/FINAL_SUMMARY.md b/construction-site-management/FINAL_SUMMARY.md deleted file mode 100644 index af38a0b..0000000 --- a/construction-site-management/FINAL_SUMMARY.md +++ /dev/null @@ -1,259 +0,0 @@ -# Final Summary - Role-Based Construction Site Management System - -## ✅ Implementation Complete - -A comprehensive, production-ready construction site management system with complete role-based access control has been successfully implemented. - -## 📋 What Was Delivered - -### 1. Core System Features -- ✅ Secure authentication with email/password login -- ✅ Role selection during login (4 roles) -- ✅ Session management with localStorage -- ✅ Protected routes with AuthContext -- ✅ Role-based access control on all pages - -### 2. Six Feature Pages (All Role-Based) -- ✅ **Dashboard**: Role-specific KPIs and metrics -- ✅ **Projects**: Full CRUD with role-based permissions -- ✅ **Tasks**: Kanban board with drag-and-drop (role-filtered) -- ✅ **Workforce**: Worker management (Admin/Engineer only) -- ✅ **Inventory**: Stock management (Admin/PM/Storekeeper) -- ✅ **Finance**: Budget analytics (Admin/PM only) - -### 3. Four User Roles with Distinct Permissions -1. **Admin** - Full system access -2. **Project Manager** - Project and financial oversight -3. **Site Engineer** - On-site task and workforce management -4. **Storekeeper** - Inventory specialist - -### 4. Real-World Examples (English Data) -- **Projects**: Mumbai Office Complex, Delhi Metro Station, Bangalore Residential Project -- **Workers**: Raj Kumar (Mason), Priya Sharma (Electrician), Amit Patel (Labor) -- **Inventory**: Cement, Steel Rods, Bricks, Sand -- **Budget**: ₹5,00,00,000 with cost breakdown - -### 5. Comprehensive Documentation -- ✅ **GETTING_STARTED.md** - Setup and overview -- ✅ **ROLE_BASED_FEATURES.md** - Detailed feature documentation -- ✅ **ROLE_MATRIX.md** - Complete access matrix -- ✅ **TESTING_GUIDE.md** - Test scenarios and checklist -- ✅ **QUICK_REFERENCE.md** - Quick reference card -- ✅ **IMPLEMENTATION_SUMMARY.md** - Technical details -- ✅ **README_HINDI.md** - Hindi/English documentation - -## 🎯 Key Features Implemented - -### Authentication System -``` -Login → Role Selection → AuthContext Storage → Dashboard -``` - -### Role-Based Access Pattern -```javascript -const canManage = ['Admin', 'Project_Manager'].includes(user?.role); -{canManage && } -{!canManage && } -``` - -### Data Filtering by Role -- Admin: All data -- Project Manager: All projects/tasks -- Site Engineer: Only assigned tasks -- Storekeeper: Inventory only - -### Dynamic Navigation -- Sidebar automatically filters menu items by role -- Only accessible pages appear in navigation -- Role displayed in user info - -## 📊 Feature Access Matrix - -| Feature | Admin | PM | Engineer | Storekeeper | -|---------|:-----:|:--:|:--------:|:-----------:| -| Dashboard | ✅ | ✅ | ✅ | ✅ | -| Projects | ✅ | ✅ | 👁️ | ❌ | -| Tasks | ✅ | ✅ | 👁️ | ❌ | -| Workforce | ✅ | ❌ | ✅ | ❌ | -| Inventory | ✅ | ✅ | ❌ | ✅ | -| Finance | ✅ | ✅ | ❌ | ❌ | - -## 🧪 Testing - -### Demo Credentials -``` -Email: Any email (e.g., admin@siteos.in) -Password: Any password (e.g., password123) -Role: Select from 4 options -``` - -### Test Each Role -1. Login with any credentials -2. Select each role -3. Verify sidebar menu changes -4. Check dashboard KPIs -5. Test feature access -6. Verify access denied messages - -## 📁 Files Modified/Created - -### New Documentation Files -- `ROLE_BASED_FEATURES.md` - Feature documentation -- `ROLE_MATRIX.md` - Access matrix -- `TESTING_GUIDE.md` - Testing guide -- `IMPLEMENTATION_SUMMARY.md` - Technical details -- `QUICK_REFERENCE.md` - Quick reference -- `README_HINDI.md` - Hindi/English docs -- `FINAL_SUMMARY.md` - This file - -### Modified Pages -- `src/pages/Dashboard.jsx` - Role-specific KPIs -- `src/pages/Projects.jsx` - Role-based CRUD -- `src/pages/Tasks.jsx` - Role-filtered tasks -- `src/pages/Workforce.jsx` - Access control -- `src/pages/Inventory.jsx` - Access control -- `src/pages/Finance.jsx` - Access control - -### Existing Files (Already Implemented) -- `src/context/AuthContext.jsx` - Auth state with role -- `src/hooks/useAuth.js` - Auth hook -- `src/pages/AuthLogin.jsx` - Login with role selection -- `src/components/layout/Sidebar.jsx` - Role-based navigation -- `src/App.jsx` - Routing setup - -## 🚀 How to Use - -### Quick Start -```bash -cd construction-site-management -npm install -npm run dev -``` - -### Login -1. Open http://localhost:5173 -2. Use any email/password -3. Select a role -4. Explore features - -### Test Different Roles -- **Admin**: Full access to everything -- **Project Manager**: Projects, Tasks, Inventory, Finance -- **Site Engineer**: Projects (view), Tasks (assigned), Workforce -- **Storekeeper**: Inventory only - -## 💡 Real-World Examples - -### Projects -- Mumbai Office Complex - ₹5,00,00,000 -- Delhi Metro Station - ₹10,00,00,000 -- Bangalore Residential Project - ₹3,50,00,000 - -### Workers -- Raj Kumar - Mason - ₹500/day -- Priya Sharma - Electrician - ₹600/day -- Amit Patel - Labor - ₹400/day - -### Inventory -- Cement: 500 bags -- Steel Rods: 2 tons -- Bricks: 10,000 - -### Budget Breakdown -- Total: ₹5,00,00,000 -- Labor: ₹1,50,00,000 (30%) -- Material: ₹2,00,00,000 (40%) -- Equipment: ₹1,00,00,000 (20%) -- Other: ₹50,00,000 (10%) - -## ✨ Code Quality - -- ✅ No syntax errors -- ✅ Clean, maintainable code -- ✅ Proper React hooks usage -- ✅ Efficient data filtering with useMemo -- ✅ Consistent styling with Tailwind CSS -- ✅ Responsive design -- ✅ Comprehensive documentation - -## 🔒 Security Notes - -**Current**: Client-side role checking (demo) - -**For Production**: -1. Validate roles on backend -2. Use JWT tokens with role claims -3. Implement server-side permission checks -4. Add audit logging -5. Use HTTPS -6. Implement rate limiting -7. Add CSRF protection - -## 📈 Performance Optimizations - -- ✅ useMemo for data filtering -- ✅ Conditional rendering -- ✅ Client-side role checking (no API calls) -- ✅ Efficient sidebar filtering - -## 🎓 Learning Resources - -### Documentation Files -1. **GETTING_STARTED.md** - Start here -2. **ROLE_MATRIX.md** - Understand permissions -3. **TESTING_GUIDE.md** - Test scenarios -4. **QUICK_REFERENCE.md** - Quick lookup -5. **README_HINDI.md** - Hindi/English guide - -### Code Examples -- Role-based access pattern in all pages -- Data filtering by role in Dashboard -- Conditional rendering in Projects -- Access denied messages in Inventory/Finance - -## 🚀 Next Steps - -### For Development -1. Explore features with different roles -2. Review code patterns -3. Understand role-based access -4. Customize for your needs - -### For Production -1. Connect to real backend -2. Implement server-side validation -3. Set up database -4. Configure environment variables -5. Add error tracking -6. Set up monitoring - -## 📞 Support - -### Documentation -- Check GETTING_STARTED.md for setup -- Review ROLE_MATRIX.md for permissions -- See TESTING_GUIDE.md for test cases -- Read QUICK_REFERENCE.md for quick lookup - -### Troubleshooting -- Can't login? Use any email/password -- Can't see page? Check your role -- Can't create item? Only certain roles can -- Sidebar empty? Refresh page - -## 🎉 Conclusion - -The Construction Site Management System is now: -- ✅ Fully functional with role-based access -- ✅ Production-ready for backend integration -- ✅ Comprehensively documented -- ✅ Ready for deployment -- ✅ Scalable and maintainable - -All examples use English data with Indian currency (₹) for real-world context. - ---- - -**System Status**: ✅ COMPLETE AND READY FOR USE - -**Happy Building! 🏗️** diff --git a/construction-site-management/GETTING_STARTED.md b/construction-site-management/GETTING_STARTED.md deleted file mode 100644 index 1fbe5c8..0000000 --- a/construction-site-management/GETTING_STARTED.md +++ /dev/null @@ -1,365 +0,0 @@ -# Getting Started - Construction Site Management System - -## Overview - -This is a production-ready construction site management system with complete authentication, role-based access control, and real-world features for managing projects, tasks, workforce, inventory, and finances. - -## Quick Start - -### 1. Installation -```bash -cd construction-site-management -npm install -``` - -### 2. Start Development Server -```bash -npm run dev -``` - -The application will open at `http://localhost:5173` - -### 3. Login -- **Email**: Any email (e.g., admin@siteos.in) -- **Password**: Any password (e.g., password123) -- **Role**: Select from 4 options: - - Admin (प्रशासक) - - Project Manager (परियोजना प्रबंधक) - - Site Engineer (साइट इंजीनियर) - - Storekeeper (गोदाम प्रभारी) - -## Features Overview - -### Authentication -- ✅ Email/Password login -- ✅ Sign up with email verification -- ✅ Password reset functionality -- ✅ Role selection during login -- ✅ Session management - -### Role-Based Access Control -- ✅ 4 distinct user roles -- ✅ Role-specific dashboards -- ✅ Feature gating by role -- ✅ Dynamic navigation -- ✅ Access denied messages - -### Core Features - -#### Dashboard -- Role-specific KPIs -- Financial charts (Admin/PM) -- Recent activity -- Inventory overview (Storekeeper) - -#### Projects -- Create, read, update, delete projects -- Search and filter -- Budget tracking -- Status management -- Role-based access - -#### Tasks -- Kanban board with drag-and-drop -- Task assignment -- Priority levels -- Status tracking -- Role-based visibility - -#### Workforce -- Worker management -- Attendance tracking -- Skill categorization -- Rate management -- Admin/Engineer access only - -#### Inventory -- Stock level tracking -- Low stock alerts -- Reorder management -- Category filtering -- Admin/PM/Storekeeper access - -#### Finance -- Budget vs actual tracking -- Cost distribution analysis -- Project financial summary -- Financial charts -- Admin/PM access only - -## User Roles - -### Admin (प्रशासक) -**Full system access** -- Manage all projects, tasks, workers, inventory, and finances -- View all dashboards and analytics -- System-wide oversight - -### Project Manager (परियोजना प्रबंधक) -**Project and financial oversight** -- Manage projects and tasks -- Assign work to engineers -- Track budgets and expenses -- Manage inventory - -### Site Engineer (साइट इंजीनियर) -**On-site execution** -- View projects (read-only) -- Manage assigned tasks -- Manage workforce and attendance -- Report progress - -### Storekeeper (गोदाम प्रभारी) -**Inventory specialist** -- Manage inventory -- Track stock levels -- Process reorders -- Maintain inventory records - -## File Structure - -``` -construction-site-management/ -├── src/ -│ ├── components/ -│ │ ├── auth/ # Authentication components -│ │ ├── charts/ # Chart components -│ │ ├── layout/ # Layout components (Sidebar, Navbar) -│ │ └── ui/ # Reusable UI components -│ ├── context/ -│ │ ├── AppContext.jsx # App state management -│ │ └── AuthContext.jsx # Authentication state -│ ├── hooks/ -│ │ └── useAuth.js # Auth hook -│ ├── pages/ -│ │ ├── Dashboard.jsx # Role-based dashboard -│ │ ├── Projects.jsx # Project management -│ │ ├── Tasks.jsx # Task management -│ │ ├── Workforce.jsx # Worker management -│ │ ├── Inventory.jsx # Inventory management -│ │ ├── Finance.jsx # Financial analytics -│ │ └── Auth*.jsx # Auth pages -│ ├── services/ -│ │ └── authService.js # Auth logic -│ ├── utils/ -│ │ ├── validation.js # Form validation -│ │ └── crypto.js # Encryption utilities -│ ├── data/ -│ │ └── mockData.js # Demo data -│ └── App.jsx # Main app component -├── ROLE_BASED_FEATURES.md # Feature documentation -├── ROLE_MATRIX.md # Access matrix -├── TESTING_GUIDE.md # Testing scenarios -└── IMPLEMENTATION_SUMMARY.md # Implementation details -``` - -## Key Technologies - -- **React 18**: UI framework -- **React Router**: Navigation -- **Tailwind CSS**: Styling -- **Lucide React**: Icons -- **Recharts**: Charts and graphs -- **Vite**: Build tool - -## Authentication Flow - -``` -1. User visits login page -2. Enters email and password -3. Selects role from 4 options -4. AuthContext stores user and role -5. Redirected to dashboard -6. Role-based features displayed -``` - -## Role-Based Access Pattern - -```javascript -// In any page component -import { useAuth } from '../hooks/useAuth'; - -function MyPage() { - const { user } = useAuth(); - - // Check if user can access feature - const canManage = ['Admin', 'Project_Manager'].includes(user?.role); - - return ( - <> - {canManage && } - {!canManage && } - - ); -} -``` - -## Testing - -### Test Each Role -1. Login with any credentials -2. Select each role -3. Verify sidebar menu changes -4. Check dashboard KPIs -5. Test feature access -6. Verify access denied messages - -### Test Scenarios -See `TESTING_GUIDE.md` for comprehensive test cases - -### Access Matrix -See `ROLE_MATRIX.md` for complete access matrix - -## Common Tasks - -### Create a Project -1. Login as Admin or Project Manager -2. Go to Projects page -3. Click "New Project" -4. Fill in project details -5. Click "Create Project" - -**Example:** -- Name: "Mumbai Office Complex" -- Location: "Mumbai, Maharashtra" -- Type: "Commercial" -- Budget: ₹5,00,00,000 - -### Assign a Task -1. Login as Admin or Project Manager -2. Go to Tasks page -3. Click "New Task" -4. Select project and worker -5. Set priority -6. Click "Create Task" - -**Example:** -- Task: "Foundation Excavation" -- Assign to: "Raj Kumar" -- Priority: "High" - -### Mark Attendance -1. Login as Admin or Site Engineer -2. Go to Workforce page -3. Select date -4. Click Present/Half Day/Absent for each worker -5. Attendance is saved - -**Example:** -- Date: March 15, 2024 -- Raj Kumar: Present -- Priya Sharma: Half Day -- Amit Patel: Absent - -### Manage Inventory -1. Login as Admin, PM, or Storekeeper -2. Go to Inventory page -3. Search or filter items -4. View low stock alerts -5. Click "Reorder" for low stock items - -**Example:** -- Cement: 500 bags (minimum: 200) -- Steel Rods: 2 tons (minimum: 1 ton) -- Bricks: 10,000 (minimum: 5,000) - -### View Finance -1. Login as Admin or Project Manager -2. Go to Finance page -3. View budget vs actual -4. Check cost distribution -5. Review project financials - -**Example:** -- Total Budget: ₹5,00,00,000 -- Spent: ₹2,50,00,000 -- Remaining: ₹2,50,00,000 - -## Troubleshooting - -### Issue: Can't login -**Solution**: Use any email and password (demo mode) - -### Issue: Can't see certain pages -**Solution**: Check your role - some pages are restricted by role - -### Issue: Can't create projects -**Solution**: Only Admin and Project Manager can create projects - -### Issue: Can't see all tasks -**Solution**: Site Engineer only sees assigned tasks - -### Issue: Sidebar menu is empty -**Solution**: Refresh page or check your role selection - -## Development - -### Add New Feature -1. Create component in `src/components/` -2. Add role check using `useAuth()` -3. Conditionally render based on role -4. Update documentation - -### Modify Role Permissions -1. Edit role check in component -2. Update `ROLE_MATRIX.md` -3. Update `ROLE_BASED_FEATURES.md` -4. Test all roles - -### Add New Role -1. Update `AuthLogin.jsx` with new role -2. Add role to all permission checks -3. Update sidebar filtering -4. Update documentation - -## Production Deployment - -### Before Going Live -1. ✅ Connect to real backend -2. ✅ Implement server-side permission validation -3. ✅ Add HTTPS -4. ✅ Implement audit logging -5. ✅ Add rate limiting -6. ✅ Set up database -7. ✅ Configure environment variables -8. ✅ Add error tracking -9. ✅ Set up monitoring -10. ✅ Create backup strategy - -### Environment Variables -``` -VITE_API_URL=https://api.example.com -VITE_AUTH_TOKEN_KEY=auth_token -VITE_APP_NAME=SiteOS -``` - -## Documentation - -- **ROLE_BASED_FEATURES.md**: Complete feature documentation -- **ROLE_MATRIX.md**: Access matrix and permissions -- **TESTING_GUIDE.md**: Testing scenarios and checklist -- **IMPLEMENTATION_SUMMARY.md**: Implementation details - -## Support - -For issues or questions: -1. Check documentation files -2. Review test scenarios -3. Check role permissions -4. Verify user role selection - -## Next Steps - -1. **Explore Features**: Login with different roles -2. **Test Workflows**: Follow user workflows -3. **Review Code**: Understand role-based patterns -4. **Customize**: Modify for your needs -5. **Deploy**: Set up production environment - -## License - -This project is provided as-is for construction site management. - ---- - -**Happy Building! 🏗️** diff --git a/construction-site-management/IMPLEMENTATION_SUMMARY.md b/construction-site-management/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index c1e4326..0000000 --- a/construction-site-management/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,227 +0,0 @@ -# Role-Based Features Implementation Summary - -## What Was Implemented - -A complete real-world construction site management system with role-based access control and customized features for 4 different user roles. - -## Key Features - -### 1. Authentication System ✅ -- Email/password login with role selection -- Email verification (currently skipped for demo) -- Password reset functionality -- Session management with localStorage -- Protected routes with AuthContext - -### 2. Role-Based Access Control ✅ -- **4 User Roles**: Admin, Project Manager, Site Engineer, Storekeeper -- **Role Selection**: Users choose role during login -- **Dynamic Navigation**: Sidebar filters menu items by role -- **Feature Gating**: Pages show/hide features based on role -- **Access Denied Messages**: Clear notifications for restricted access - -### 3. Dashboard (Role-Customized) ✅ -- **Admin**: All KPIs + Financial Charts -- **Project Manager**: Projects, Tasks, Budget, Inventory -- **Site Engineer**: Projects, Tasks, Workers -- **Storekeeper**: Inventory Overview - -### 4. Projects Management ✅ -- **Admin & PM**: Full CRUD operations -- **Site Engineer**: View-only with lock notification -- **Storekeeper**: No access -- Search, filter, and table display - -### 5. Tasks Management ✅ -- **Admin & PM**: Create and manage all tasks -- **Site Engineer**: Only see assigned tasks -- **Drag-and-drop**: Update task status -- **Kanban board**: Open → In Progress → Completed - -### 6. Workforce Management ✅ -- **Admin & Site Engineer**: Full access -- **Others**: Access denied -- Attendance tracking by date -- Worker management (add/edit/delete) - -### 7. Inventory Management ✅ -- **Admin, PM, Storekeeper**: Full access -- **Site Engineer**: Access denied -- Low stock alerts -- Reorder functionality -- Search and category filter - -### 8. Finance Analytics ✅ -- **Admin & PM**: Full access -- **Others**: Access denied -- Budget vs actual expenses -- Cost distribution charts -- Project-wise financial tracking - -## Files Modified/Created - -### New Files -- `ROLE_BASED_FEATURES.md` - Comprehensive feature documentation -- `TESTING_GUIDE.md` - Testing scenarios and checklist -- `IMPLEMENTATION_SUMMARY.md` - This file - -### Modified Pages -- `src/pages/Dashboard.jsx` - Role-specific KPIs and charts -- `src/pages/Projects.jsx` - Role-based CRUD and view-only mode -- `src/pages/Tasks.jsx` - Role-based task visibility and management -- `src/pages/Workforce.jsx` - Access control for Admin/Site Engineer -- `src/pages/Inventory.jsx` - Access control for managers/storekeeper -- `src/pages/Finance.jsx` - Access control for Admin/PM - -### Existing Files (Already Implemented) -- `src/context/AuthContext.jsx` - Authentication state with role support -- `src/hooks/useAuth.js` - Hook for accessing auth context -- `src/pages/AuthLogin.jsx` - Login with role selection -- `src/components/layout/Sidebar.jsx` - Role-based navigation -- `src/components/layout/Navbar.jsx` - User info and logout - -## How It Works - -### 1. User Login Flow -``` -User enters email/password → Selects role → AuthContext stores role → Redirected to Dashboard -``` - -### 2. Role-Based Access -``` -Page loads → useAuth() gets user role → Conditional rendering based on role → Show/hide features -``` - -### 3. Data Filtering -``` -Component mounts → useMemo filters data by role → Only relevant data displayed -``` - -## Real-World Use Cases - -### Admin -- Oversees entire project -- Manages budgets and finances -- Monitors all workers and tasks -- Tracks inventory levels -- Full system control - -### Project Manager -- Manages project timeline and budget -- Assigns tasks to engineers -- Monitors project progress -- Manages inventory for projects -- Tracks financial performance - -### Site Engineer -- Executes assigned tasks -- Manages on-site workforce -- Updates task status -- Tracks worker attendance -- Reports progress - -### Storekeeper -- Manages material inventory -- Tracks stock levels -- Processes reorders -- Maintains inventory records -- Ensures material availability - -## Technical Implementation - -### Access Control Pattern -```javascript -// Check permissions -const canManageProjects = ['Admin', 'Project_Manager'].includes(user?.role); - -// Conditional rendering -{canManageProjects && } -{!canManageProjects && } -``` - -### Data Filtering Pattern -```javascript -// Filter based on role -const visibleData = useMemo(() => { - if (user?.role === 'Site_Engineer') { - return data.filter(item => item.assigned_to === user?.id); - } - return data; -}, [data, user?.role, user?.id]); -``` - -## Demo Credentials - -All roles use the same login credentials (demo mode): -- **Email**: Any email (e.g., admin@example.com) -- **Password**: Any password (e.g., password123) -- **Role**: Select from 4 options during login - -## Testing - -### Quick Test -1. Run `npm run dev` -2. Login with any credentials -3. Select each role and verify features -4. Check sidebar menu changes -5. Verify access denied messages - -### Comprehensive Testing -See `TESTING_GUIDE.md` for detailed test scenarios and checklist - -## Performance Optimizations - -- ✅ useMemo for data filtering (prevents unnecessary re-renders) -- ✅ Conditional rendering (only renders visible components) -- ✅ Client-side role checking (no API calls) -- ✅ Efficient sidebar filtering - -## Security Notes - -⚠️ **Current Implementation**: Client-side role checking (demo only) - -### For Production: -1. Validate roles on backend -2. Use JWT tokens with role claims -3. Implement server-side permission checks -4. Add audit logging -5. Use HTTPS for all communications -6. Implement rate limiting -7. Add CSRF protection - -## Future Enhancements - -1. **Backend Integration** - - Connect to real database - - Server-side permission validation - - Persistent user data - -2. **Advanced Features** - - Custom role creation - - Fine-grained permissions - - Time-based access control - - Department-based filtering - -3. **Audit & Compliance** - - Action logging - - User activity tracking - - Compliance reporting - - Data export - -4. **User Management** - - Admin panel for user management - - Role assignment interface - - Permission management UI - - User activity dashboard - -## Conclusion - -The Construction Site Management System now provides a complete, real-world solution with: -- ✅ Secure authentication with role selection -- ✅ Role-based access control on all pages -- ✅ Customized dashboards for each role -- ✅ Real-world use cases for each position -- ✅ Clean, intuitive user interface -- ✅ Comprehensive documentation - -The system is ready for further development with backend integration and additional features as needed. diff --git a/construction-site-management/INDIAN_EXAMPLES.md b/construction-site-management/INDIAN_EXAMPLES.md deleted file mode 100644 index b9d4e5b..0000000 --- a/construction-site-management/INDIAN_EXAMPLES.md +++ /dev/null @@ -1,169 +0,0 @@ -# भारतीय उदाहरण (Indian Examples) - -## भारतीय शहर और परियोजनाएं (Indian Cities & Projects) - -### मुंबई (Mumbai) -**परियोजनाएं:** -- बांद्रा ऑफिस कॉम्प्लेक्स - ₹5,00,00,000 -- दादर आवासीय परियोजना - ₹3,50,00,000 -- अंधेरी शॉपिंग मॉल - ₹4,00,00,000 - -### दिल्ली (Delhi) -**परियोजनाएं:** -- नई दिल्ली मेट्रो स्टेशन - ₹10,00,00,000 -- गुड़गांव कॉर्पोरेट पार्क - ₹8,50,00,000 -- नोएडा औद्योगिक क्षेत्र - ₹6,00,00,000 - -### बेंगलुरु (Bangalore) -**परियोजनाएं:** -- व्हाइटफील्ड आवासीय परियोजना - ₹3,50,00,000 -- कोरमंगला ऑफिस स्पेस - ₹4,50,00,000 -- इंदिरानगर शॉपिंग कॉम्प्लेक्स - ₹2,50,00,000 - -### हैदराबाद (Hyderabad) -**परियोजनाएं:** -- हिटेक सिटी ऑफिस बिल्डिंग - ₹5,50,00,000 -- बंजारा हिल्स आवासीय - ₹3,00,00,000 -- कचीगुडा औद्योगिक परियोजना - ₹4,00,00,000 - -### चेन्नई (Chennai) -**परियोजनाएं:** -- अन्नानगर शॉपिंग मॉल - ₹2,00,00,000 -- अडयार आवासीय परियोजना - ₹2,80,00,000 -- पोर्टो नोवो ऑफिस स्पेस - ₹3,50,00,000 - -### कोलकाता (Kolkata) -**परियोजनाएं:** -- साल्टलेक सिटी ऑफिस - ₹2,50,00,000 -- बल्लीगंज आवासीय परियोजना - ₹2,00,00,000 -- न्यू टाउन औद्योगिक क्षेत्र - ₹3,50,00,000 - -## भारतीय निर्माण कंपनियां (Indian Construction Companies) - -### बड़ी कंपनियां (Large Companies) -- **Larsen & Toubro (L&T)** - मुंबई -- **Reliance Infrastructure** - मुंबई -- **Tata Projects** - मुंबई -- **Godrej Properties** - मुंबई -- **DLF Limited** - दिल्ली -- **Oberoi Realty** - मुंबई - -### मध्यम कंपनियां (Medium Companies) -- **Prestige Constructions** - बेंगलुरु -- **Brigade Group** - बेंगलुरु -- **Puravankara** - बेंगलुरु -- **Mahindra Lifespace** - मुंबई -- **Shapoorji Pallonji** - मुंबई - -## भारतीय कर्मचारी और कौशल (Indian Workers & Skills) - -### कौशल प्रकार (Skill Types) -- **मेसन (Mason)** - ईंट बिछाना, प्लास्टरिंग -- **इलेक्ट्रीशियन (Electrician)** - विद्युत कार्य -- **प्लंबर (Plumber)** - पाइपिंग कार्य -- **लेबर (Labor)** - सामान्य निर्माण कार्य -- **कारपेंटर (Carpenter)** - लकड़ी का काम -- **पेंटर (Painter)** - पेंटिंग कार्य -- **वेल्डर (Welder)** - स्टील कार्य - -### दैनिक दर (Daily Rates) -- **मेसन**: ₹400-600/दिन -- **इलेक्ट्रीशियन**: ₹500-700/दिन -- **प्लंबर**: ₹450-650/दिन -- **लेबर**: ₹300-400/दिन -- **कारपेंटर**: ₹400-600/दिन -- **पेंटर**: ₹350-500/दिन -- **वेल्डर**: ₹500-750/दिन - -### घंटे की दर (Hourly Rates) -- **मेसन**: ₹50-75/घंटा -- **इलेक्ट्रीशियन**: ₹60-85/घंटा -- **प्लंबर**: ₹55-80/घंटा -- **लेबर**: ₹35-50/घंटा - -## भारतीय निर्माण सामग्री (Indian Construction Materials) - -### सीमेंट और कंक्रीट (Cement & Concrete) -- **सीमेंट**: ₹350-400/बैग (50 किग्रा) -- **रेत**: ₹1,500-2,000/ट्रक -- **कंक्रीट**: ₹4,500-5,500/घन मीटर - -### स्टील (Steel) -- **स्टील रॉड**: ₹40,000-50,000/टन -- **स्टील बीम**: ₹45,000-55,000/टन -- **स्टील प्लेट**: ₹50,000-60,000/टन - -### ईंटें और ब्लॉक (Bricks & Blocks) -- **ईंटें**: ₹4-6/ईंट -- **कंक्रीट ब्लॉक**: ₹15-20/ब्लॉक -- **AAC ब्लॉक**: ₹40-50/ब्लॉक - -### विद्युत सामग्री (Electrical Materials) -- **तार**: ₹50-100/मीटर -- **स्विच**: ₹50-150/पीस -- **बल्ब**: ₹30-100/पीस - -### पाइपिंग सामग्री (Plumbing Materials) -- **PVC पाइप**: ₹20-50/मीटर -- **कॉपर पाइप**: ₹200-300/मीटर -- **फिटिंग**: ₹10-100/पीस - -## भारतीय परियोजना बजट उदाहरण (Indian Project Budget Examples) - -### छोटी परियोजना (Small Project) - ₹50,00,000 -``` -कुल बजट: ₹50,00,000 - -विभाजन: -- श्रम लागत: ₹15,00,000 (30%) -- सामग्री लागत: ₹20,00,000 (40%) -- उपकरण लागत: ₹10,00,000 (20%) -- अन्य लागत: ₹5,00,000 (10%) -``` - -### मध्यम परियोजना (Medium Project) - ₹2,50,00,000 -``` -कुल बजट: ₹2,50,00,000 - -विभाजन: -- श्रम लागत: ₹75,00,000 (30%) -- सामग्री लागत: ₹1,00,00,000 (40%) -- उपकरण लागत: ₹50,00,000 (20%) -- अन्य लागत: ₹25,00,000 (10%) -``` - -### बड़ी परियोजना (Large Project) - ₹10,00,00,000 -``` -कुल बजट: ₹10,00,00,000 - -विभाजन: -- श्रम लागत: ₹3,00,00,000 (30%) -- सामग्री लागत: ₹4,00,00,000 (40%) -- उपकरण लागत: ₹2,00,00,000 (20%) -- अन्य लागत: ₹1,00,00,000 (10%) -``` - -## भारतीय निर्माण समय सारणी (Indian Construction Timeline) - -### आवासीय परियोजना (Residential Project) -- **योजना चरण**: 2-3 महीने -- **नींव**: 2-3 महीने -- **संरचना**: 4-6 महीने -- **समाप्ति**: 2-3 महीने -- **कुल समय**: 10-15 महीने - -### वाणिज्यिक परियोजना (Commercial Project) -- **योजना चरण**: 3-4 महीने -- **नींव**: 3-4 महीने -- **संरचना**: 6-8 महीने -- **समाप्ति**: 3-4 महीने -- **कुल समय**: 15-20 महीने - -### औद्योगिक परियोजना (Industrial Project) -- **योजना चरण**: 2-3 महीने -- **नींव**: 2-3 महीने -- **संरचना**: 3-4 महीने -- **समाप्ति**: 1-2 महीने -- **कुल समय**: 8-12 महीने - -## भारतीय निर्माण नियम (Indian Construct \ No newline at end of file diff --git a/construction-site-management/QUICK_REFERENCE.md b/construction-site-management/QUICK_REFERENCE.md deleted file mode 100644 index 43e213f..0000000 --- a/construction-site-management/QUICK_REFERENCE.md +++ /dev/null @@ -1,231 +0,0 @@ -# त्वरित संदर्भ कार्ड (Quick Reference Card) - -## लॉगिन क्रेडेंशियल्स (Demo Mode) -``` -ईमेल: कोई भी ईमेल (उदाहरण: admin@siteos.in) -पासवर्ड: कोई भी पासवर्ड (उदाहरण: password123) -भूमिका: 4 विकल्पों में से चुनें -``` - -## भूमिका त्वरित गाइड - -### 👨‍💼 Admin (प्रशासक) -- **एक्सेस**: सब कुछ -- **डैशबोर्ड**: सभी KPI + चार्ट -- **कर सकते हैं**: परियोजनाएं, कार्य, कर्मचारी, इन्वेंटरी, वित्त बनाएं/संपादित/हटाएं -- **सर्वश्रेष्ठ**: सिस्टम निरीक्षण - -### 📊 Project Manager (परियोजना प्रबंधक) -- **एक्सेस**: परियोजनाएं, कार्य, इन्वेंटरी, वित्त -- **डैशबोर्ड**: परियोजनाएं, कार्य, बजट, कम स्टॉक -- **कर सकते हैं**: परियोजनाओं का प्रबंधन, कार्य असाइन करें, बजट ट्रैक करें -- **सर्वश्रेष्ठ**: परियोजना निरीक्षण - -### 🔨 Site Engineer (साइट इंजीनियर) -- **एक्सेस**: परियोजनाएं (देखें), कार्य (असाइन किए गए), कार्यबल -- **डैशबोर्ड**: परियोजनाएं, कार्य, कर्मचारी -- **कर सकते हैं**: असाइन किए गए कार्यों को अपडेट करें, कर्मचारियों का प्रबंधन करें, उपस्थिति चिह्नित करें -- **सर्वश्रेष्ठ**: साइट पर निष्पादन - -### 📦 Storekeeper (गोदाम प्रभारी) -- **एक्सेस**: केवल इन्वेंटरी -- **डैशबोर्ड**: इन्वेंटरी अवलोकन -- **कर सकते हैं**: स्टॉक प्रबंधित करें, पुनः ऑर्डर प्रक्रिया करें -- **सर्वश्रेष्ठ**: इन्वेंटरी प्रबंधन - -## सुविधा एक्सेस मैट्रिक्स (Feature Access Matrix) - -| सुविधा | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| डैशबोर्ड | ✅ | ✅ | ✅ | ✅ | -| परियोजनाएं | ✅ | ✅ | 👁️ | ❌ | -| कार्य | ✅ | ✅ | 👁️ | ❌ | -| कार्यबल | ✅ | ❌ | ✅ | ❌ | -| इन्वेंटरी | ✅ | ✅ | ❌ | ✅ | -| वित्त | ✅ | ✅ | ❌ | ❌ | - -**किंवदंती**: ✅ पूर्ण एक्सेस | 👁️ देखें/सीमित | ❌ कोई एक्सेस नहीं - -## सामान्य कार्य (Common Actions) - -### परियोजना बनाएं (Create Project) -1. Admin या PM के रूप में लॉगिन करें -2. परियोजनाएं → नई परियोजना -3. विवरण भरें → बनाएं - -**उदाहरण:** -- नाम: "दिल्ली मेट्रो स्टेशन" -- स्थान: "नई दिल्ली" -- बजट: ₹10,00,00,000 - -### कार्य असाइन करें (Assign Task) -1. Admin या PM के रूप में लॉगिन करें -2. कार्य → नया कार्य -3. कर्मचारी चुनें → बनाएं - -**उदाहरण:** -- कार्य: "कंक्रीट डालना" -- असाइन करें: "राज कुमार" -- प्राथमिकता: "उच्च" - -### उपस्थिति चिह्नित करें (Mark Attendance) -1. Admin या Engineer के रूप में लॉगिन करें -2. कार्यबल → तारीख चुनें -3. उपस्थित/आधा दिन/अनुपस्थित पर क्लिक करें - -### इन्वेंटरी प्रबंधित करें (Manage Inventory) -1. Admin, PM, या Storekeeper के रूप में लॉगिन करें -2. इन्वेंटरी → खोजें/फ़िल्टर करें -3. कम स्टॉक आइटम के लिए पुनः ऑर्डर करें - -**उदाहरण:** -- सीमेंट: 500 बैग -- स्टील: 2 टन -- ईंटें: 10,000 - -### वित्त देखें (View Finance) -1. Admin या PM के रूप में लॉगिन करें -2. वित्त → चार्ट देखें -3. बजट बनाम वास्तविक जांचें - -**उदाहरण:** -- कुल बजट: ₹5,00,00,000 -- खर्च: ₹2,50,00,000 -- शेष: ₹2,50,00,000 - -## कीबोर्ड शॉर्टकट (Keyboard Shortcuts) - -| कार्य | शॉर्टकट | -|--------|---------| -| लॉगआउट | उपयोगकर्ता मेनू पर क्लिक करें → लॉगआउट | -| खोज | प्रत्येक पेज पर खोज बॉक्स का उपयोग करें | -| फ़िल्टर | फ़िल्टर ड्रॉपडाउन का उपयोग करें | -| कार्य खींचें | कार्य कार्ड को क्लिक और खींचें | -| हटाएं | हटाएं आइकन पर क्लिक करें (पुष्टि करें) | - -## साइडबार नेविगेशन (Sidebar Navigation) - -### Admin (प्रशासक) -``` -डैशबोर्ड -├── परियोजनाएं -├── कार्य -├── कार्यबल -├── इन्वेंटरी -└── वित्त -``` - -### Project Manager (परियोजना प्रबंधक) -``` -डैशबोर्ड -├── परियोजनाएं -├── कार्य -├── इन्वेंटरी -└── वित्त -``` - -### Site Engineer (साइट इंजीनियर) -``` -डैशबोर्ड -├── परियोजनाएं -├── कार्य -└── कार्यबल -``` - -### Storekeeper (गोदाम प्रभारी) -``` -डैशबोर्ड -└── इन्वेंटरी -``` - -## कार्य स्थिति प्रवाह (Task Status Flow) - -``` -खुला → प्रगति में → पूर्ण -(अपडेट करने के लिए ड्रैग और ड्रॉप करें) -``` - -## उपस्थिति विकल्प (Attendance Options) - -``` -उपस्थित (हरा) -आधा दिन (पीला) -अनुपस्थित (लाल) -``` - -## प्राथमिकता स्तर (Priority Levels) - -``` -कम (नीला) -मध्यम (पीला) -उच्च (लाल) -``` - -## परियोजना स्थिति (Project Status) - -``` -योजना -सक्रिय -पूर्ण -रोक दिया गया -``` - -## सामान्य समस्याएं और समाधान (Common Issues & Fixes) - -| समस्या | समाधान | -|--------|--------| -| पेज नहीं दिख रहा | अपनी भूमिका जांचें - पेज प्रतिबंधित हो सकता है | -| आइटम नहीं बना सकते | केवल कुछ भूमिकाएं बना सकती हैं - भूमिका जांचें | -| परियोजना संपादित नहीं कर सकते | केवल Admin/PM संपादित कर सकते हैं - भूमिका जांचें | -| सभी कार्य नहीं दिख रहे | Site Engineer केवल असाइन किए गए कार्य देखता है | -| साइडबार खाली है | पेज रीफ्रेश करें या भूमिका चयन जांचें | - -## दस्तावेज़ फ़ाइलें (Documentation Files) - -| फ़ाइल | उद्देश्य | -|--------|---------| -| GETTING_STARTED.md | सेटअप और अवलोकन | -| ROLE_BASED_FEATURES.md | विस्तृत सुविधा दस्तावेज़ | -| ROLE_MATRIX.md | एक्सेस मैट्रिक्स | -| TESTING_GUIDE.md | परीक्षण परिदृश्य | -| IMPLEMENTATION_SUMMARY.md | तकनीकी विवरण | -| QUICK_REFERENCE.md | यह फ़ाइल | - -## टिप्स और ट्रिक्स (Tips & Tricks) - -1. **Demo Mode**: कोई भी ईमेल/पासवर्ड संयोजन का उपयोग करें -2. **भूमिका परीक्षण**: विभिन्न सुविधाएं देखने के लिए प्रत्येक भूमिका आजमाएं -3. **ड्रैग & ड्रॉप**: कार्यों को स्तंभों के बीच खींचा जा सकता है -4. **खोज**: आइटम खोजने के लिए खोज बॉक्स का उपयोग करें -5. **फ़िल्टर**: परिणामों को कम करने के लिए फ़िल्टर का उपयोग करें -6. **उपस्थिति**: तारीख के अनुसार उपस्थिति चिह्नित करें -7. **कम स्टॉक**: स्टॉक कम होने पर आइटम पुनः ऑर्डर करें -8. **बजट**: वित्त में परियोजना बजट ट्रैक करें - -## प्रदर्शन टिप्स (Performance Tips) - -- डेटा कम करने के लिए खोज/फ़िल्टर का उपयोग करें -- अप्रयुक्त मोडल बंद करें -- यदि धीमा हो तो पेज रीफ्रेश करें -- यदि आवश्यक हो तो ब्राउज़र कैश साफ़ करें - -## समर्थन संसाधन (Support Resources) - -1. सेटअप के लिए GETTING_STARTED.md जांचें -2. अनुमतियों के लिए ROLE_MATRIX.md की समीक्षा करें -3. परीक्षण मामलों के लिए TESTING_GUIDE.md देखें -4. विवरण के लिए ROLE_BASED_FEATURES.md पढ़ें - -## त्वरित प्रारंभ कमांड (Quick Start Command) - -```bash -cd construction-site-management -npm install -npm run dev -``` - -फिर http://localhost:5173 खोलें और लॉगिन करें! - ---- - -**सहायता चाहिए?** दस्तावेज़ फ़ाइलों की जांच करें या भूमिका मैट्रिक्स की समीक्षा करें अनुमतियों को समझने के लिए। diff --git a/construction-site-management/README.md b/construction-site-management/README.md deleted file mode 100644 index 18bc70e..0000000 --- a/construction-site-management/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# React + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/construction-site-management/README_HINDI.md b/construction-site-management/README_HINDI.md deleted file mode 100644 index d371365..0000000 --- a/construction-site-management/README_HINDI.md +++ /dev/null @@ -1,282 +0,0 @@ -# SiteOS - निर्माण साइट प्रबंधन प्रणाली -# Construction Site Management System - -## विवरण (Overview) - -SiteOS एक पूर्ण, उत्पादन-तैयार निर्माण साइट प्रबंधन प्रणाली है जिसमें: -- ✅ सुरक्षित प्रमाणीकरण और भूमिका चयन -- ✅ 4 विभिन्न उपयोगकर्ता भूमिकाएं -- ✅ भूमिका-आधारित एक्सेस नियंत्रण -- ✅ परियोजना, कार्य, कार्यबल, इन्वेंटरी और वित्त प्रबंधन -- ✅ वास्तविक दुनिया के उपयोग के मामले - -## त्वरित प्रारंभ (Quick Start) - -```bash -# स्थापना (Installation) -cd construction-site-management -npm install - -# विकास सर्वर शुरू करें (Start Development Server) -npm run dev -``` - -फिर http://localhost:5173 खोलें - -## लॉगिन विवरण (Login Details) - -**Demo Mode - कोई भी ईमेल/पासवर्ड काम करता है:** -``` -ईमेल: admin@siteos.in -पासवर्ड: password123 -भूमिका: 4 विकल्पों में से चुनें -``` - -## भूमिकाएं (Roles) - -### 1. Admin (प्रशासक) 👨‍💼 -**पूर्ण सिस्टम एक्सेस** -- सभी परियोजनाएं, कार्य, कर्मचारी, इन्वेंटरी, वित्त -- सभी डैशबोर्ड और विश्लेषण -- सिस्टम-व्यापी निरीक्षण - -### 2. Project Manager (परियोजना प्रबंधक) 📊 -**परियोजना और वित्तीय निरीक्षण** -- परियोजनाएं और कार्य प्रबंधित करें -- इंजीनियरों को काम सौंपें -- बजट और खर्च ट्रैक करें -- इन्वेंटरी प्रबंधित करें - -### 3. Site Engineer (साइट इंजीनियर) 🔨 -**साइट पर निष्पादन** -- असाइन किए गए कार्य प्रबंधित करें -- कार्यबल और उपस्थिति प्रबंधित करें -- परियोजनाएं देखें (केवल पढ़ने के लिए) -- प्रगति की रिपोर्ट करें - -### 4. Storekeeper (गोदाम प्रभारी) 📦 -**इन्वेंटरी विशेषज्ञ** -- इन्वेंटरी प्रबंधित करें -- स्टॉक स्तर ट्रैक करें -- पुनः ऑर्डर प्रक्रिया करें -- इन्वेंटरी रिकॉर्ड बनाए रखें - -## मुख्य सुविधाएं (Key Features) - -### डैशबोर्ड (Dashboard) -- भूमिका-विशिष्ट KPI -- वित्तीय चार्ट (Admin/PM) -- हाल की गतिविधि -- इन्वेंटरी अवलोकन (Storekeeper) - -### परियोजनाएं (Projects) -- परियोजना CRUD संचालन -- खोज और फ़िल्टर -- बजट ट्रैकिंग -- स्थिति प्रबंधन - -**Example:** -- Mumbai Office Complex - ₹5,00,00,000 -- Delhi Metro Station - ₹10,00,00,000 - -### कार्य (Tasks) -- कानबन बोर्ड ड्रैग-एंड-ड्रॉप के साथ -- कार्य असाइनमेंट -- प्राथमिकता स्तर -- स्थिति ट्रैकिंग - -**Example:** -- Foundation Excavation - Raj Kumar - High -- Concrete Pouring - Priya Sharma - Medium - -### कार्यबल (Workforce) -- कर्मचारी प्रबंधन -- उपस्थिति ट्रैकिंग -- कौशल वर्गीकरण -- दर प्रबंधन - -**Example:** -- Raj Kumar - Mason - ₹500/day -- Priya Sharma - Electrician - ₹600/day - -### इन्वेंटरी (Inventory) -- स्टॉक स्तर ट्रैकिंग -- कम स्टॉक अलर्ट -- पुनः ऑर्डर प्रबंधन -- श्रेणी फ़िल्टरिंग - -**Example:** -- Cement: 500 bags (minimum: 200) -- Steel Rods: 2 tons (minimum: 1 ton) -- Bricks: 10,000 (minimum: 5,000) - -### वित्त (Finance) -- बजट बनाम वास्तविक ट्रैकिंग -- लागत वितरण विश्लेषण -- परियोजना वित्तीय सारांश -- वित्तीय चार्ट - -**Example:** -- Total Budget: ₹5,00,00,000 -- Spent: ₹2,50,00,000 -- Remaining: ₹2,50,00,000 - -## सामान्य कार्य (Common Tasks) - -### परियोजना बनाएं (Create Project) -1. Admin या PM के रूप में लॉगिन करें -2. परियोजनाएं → नई परियोजना -3. विवरण भरें → बनाएं - -### कार्य असाइन करें (Assign Task) -1. Admin या PM के रूप में लॉगिन करें -2. कार्य → नया कार्य -3. कर्मचारी चुनें → बनाएं - -### उपस्थिति चिह्नित करें (Mark Attendance) -1. Admin या Engineer के रूप में लॉगिन करें -2. कार्यबल → तारीख चुनें -3. उपस्थित/आधा दिन/अनुपस्थित पर क्लिक करें - -### इन्वेंटरी प्रबंधित करें (Manage Inventory) -1. Admin, PM, या Storekeeper के रूप में लॉगिन करें -2. इन्वेंटरी → खोजें/फ़िल्टर करें -3. कम स्टॉक आइटम के लिए पुनः ऑर्डर करें - -### वित्त देखें (View Finance) -1. Admin या PM के रूप में लॉगिन करें -2. वित्त → चार्ट देखें -3. बजट बनाम वास्तविक जांचें - -## दस्तावेज़ (Documentation) - -| फ़ाइल | विवरण | -|--------|--------| -| GETTING_STARTED.md | सेटअप और अवलोकन | -| ROLE_BASED_FEATURES.md | विस्तृत सुविधा दस्तावेज़ | -| ROLE_MATRIX.md | एक्सेस मैट्रिक्स | -| TESTING_GUIDE.md | परीक्षण परिदृश्य | -| QUICK_REFERENCE.md | त्वरित संदर्भ कार्ड | -| IMPLEMENTATION_SUMMARY.md | तकनीकी विवरण | - -## तकनीकी स्टैक (Technical Stack) - -- **React 18** - UI फ्रेमवर्क -- **React Router** - नेविगेशन -- **Tailwind CSS** - स्टाइलिंग -- **Lucide React** - आइकन -- **Recharts** - चार्ट और ग्राफ -- **Vite** - बिल्ड टूल - -## प्रमाणीकरण प्रवाह (Authentication Flow) - -``` -1. उपयोगकर्ता लॉगिन पेज पर जाता है -2. ईमेल और पासवर्ड दर्ज करता है -3. 4 विकल्पों में से भूमिका चुनता है -4. AuthContext उपयोगकर्ता और भूमिका संग्रहीत करता है -5. डैशबोर्ड पर पुनः निर्देशित किया जाता है -6. भूमिका-आधारित सुविधाएं प्रदर्शित होती हैं -``` - -## भूमिका-आधारित एक्सेस पैटर्न (Role-Based Access Pattern) - -```javascript -// किसी भी पेज घटक में -import { useAuth } from '../hooks/useAuth'; - -function MyPage() { - const { user } = useAuth(); - - // जांचें कि क्या उपयोगकर्ता सुविधा को एक्सेस कर सकता है - const canManage = ['Admin', 'Project_Manager'].includes(user?.role); - - return ( - <> - {canManage && } - {!canManage && } - - ); -} -``` - -## परीक्षण (Testing) - -### प्रत्येक भूमिका का परीक्षण करें -1. किसी भी क्रेडेंशियल के साथ लॉगिन करें -2. प्रत्येक भूमिका चुनें -3. साइडबार मेनू परिवर्तन सत्यापित करें -4. डैशबोर्ड KPI जांचें -5. सुविधा एक्सेस परीक्षण करें -6. एक्सेस अस्वीकृत संदेश सत्यापित करें - -विस्तृत परीक्षण मामलों के लिए `TESTING_GUIDE.md` देखें। - -## उत्पादन तैनाती (Production Deployment) - -### लाइव जाने से पहले -1. ✅ वास्तविक बैकएंड से कनेक्ट करें -2. ✅ सर्वर-साइड अनुमति सत्यापन लागू करें -3. ✅ HTTPS जोड़ें -4. ✅ ऑडिट लॉगिंग लागू करें -5. ✅ दर सीमा जोड़ें -6. ✅ डेटाबेस सेट अप करें -7. ✅ पर्यावरण चर कॉन्फ़िगर करें -8. ✅ त्रुटि ट्रैकिंग जोड़ें -9. ✅ निगरानी सेट अप करें -10. ✅ बैकअप रणनीति बनाएं - -## समर्थन (Support) - -समस्याओं या प्रश्नों के लिए: -1. दस्तावेज़ फ़ाइलें जांचें -2. परीक्षण परिदृश्य समीक्षा करें -3. भूमिका अनुमतियां जांचें -4. उपयोगकर्ता भूमिका चयन सत्यापित करें - -## अगले कदम (Next Steps) - -1. **सुविधाओं का अन्वेषण करें**: विभिन्न भूमिकाओं के साथ लॉगिन करें -2. **वर्कफ़्लो का परीक्षण करें**: उपयोगकर्ता वर्कफ़्लो का पालन करें -3. **कोड की समीक्षा करें**: भूमिका-आधारित पैटर्न समझें -4. **अनुकूलित करें**: अपनी आवश्यकताओं के लिए संशोधित करें -5. **तैनात करें**: उत्पादन पर्यावरण सेट अप करें - -## लाइसेंस (License) - -यह परियोजना निर्माण साइट प्रबंधन के लिए प्रदान की जाती है। - ---- - -## भारतीय निर्माण उदाहरण (Construction Examples) - -### परियोजनाएं (Projects) -- **Mumbai Office Complex** - Mumbai, Maharashtra - ₹5,00,00,000 -- **Delhi Metro Station** - New Delhi - ₹10,00,00,000 -- **Bangalore Residential Project** - Whitefield, Bangalore - ₹3,50,00,000 -- **Chennai Shopping Mall** - Annanagar, Chennai - ₹2,00,00,000 - -### कर्मचारी (Workers) -- **Raj Kumar** - Mason - ₹500/day -- **Priya Sharma** - Electrician - ₹600/day -- **Amit Patel** - Labor - ₹400/day -- **Vijay Singh** - Plumber - ₹550/day - -### इन्वेंटरी (Inventory) -- **Cement** - 500 bags - ₹350/bag -- **Steel Rods** - 2 tons - ₹45,000/ton -- **Bricks** - 10,000 - ₹5/brick -- **Sand** - 50 trucks - ₹1,500/truck - -### बजट उदाहरण (Budget Examples) -- **Total Budget**: ₹5,00,00,000 -- **Labor Cost**: ₹1,50,00,000 (30%) -- **Material Cost**: ₹2,00,00,000 (40%) -- **Equipment Cost**: ₹1,00,00,000 (20%) -- **Other Cost**: ₹50,00,000 (10%) - ---- - -**खुश निर्माण! 🏗️** - -Happy Building! diff --git a/construction-site-management/ROLE_BASED_FEATURES.md b/construction-site-management/ROLE_BASED_FEATURES.md deleted file mode 100644 index 16c161b..0000000 --- a/construction-site-management/ROLE_BASED_FEATURES.md +++ /dev/null @@ -1,184 +0,0 @@ -# Role-Based Features Implementation - -## Overview -The Construction Site Management System now includes comprehensive role-based access control and customized features for each user role. Users select their role during login and see only the features and data relevant to their position. - -## Roles & Permissions - -### 1. Admin -**Full system access with all features** - -- **Dashboard**: All KPIs (Projects, Active Tasks, Workers, Low Stock, Budget) -- **Projects**: Full CRUD operations (Create, Read, Update, Delete) -- **Tasks**: Full task management with drag-and-drop -- **Workforce**: Full worker management and attendance tracking -- **Inventory**: Full inventory management with reorder capabilities -- **Finance**: Complete financial analytics and budget tracking - -### 2. Project Manager -**Project and financial oversight** - -- **Dashboard**: Projects, Active Tasks, Low Stock Items, Total Budget -- **Projects**: Full CRUD operations (Create, Read, Update, Delete) -- **Tasks**: Full task management and assignment -- **Workforce**: No access (view-only message) -- **Inventory**: View and manage inventory, reorder items -- **Finance**: Complete financial analytics and budget tracking - -### 3. Site Engineer -**On-site task and workforce management** - -- **Dashboard**: Projects, Active Tasks, Active Workers -- **Projects**: View-only access (cannot create/edit/delete) -- **Tasks**: Only see and manage their assigned tasks -- **Workforce**: Full worker management and attendance tracking -- **Inventory**: No access (view-only message) -- **Finance**: No access (view-only message) - -### 4. Storekeeper -**Inventory management specialist** - -- **Dashboard**: Inventory overview (Total Items, Low Stock, Total Value) -- **Projects**: No access (view-only message) -- **Tasks**: No access (view-only message) -- **Workforce**: No access (view-only message) -- **Inventory**: Full inventory management with reorder capabilities -- **Finance**: No access (view-only message) - -## Feature Details by Page - -### Dashboard -- **Role-specific KPI cards**: Each role sees only relevant metrics -- **Admin**: All KPIs displayed -- **Project Manager**: Projects, Active Tasks, Low Stock, Budget -- **Site Engineer**: Projects, Active Tasks, Workers -- **Storekeeper**: Inventory overview with total value calculation -- **Charts**: Only Admin and Project Manager see financial charts -- **Recent Tasks**: All roles except Storekeeper see task updates - -### Projects -- **Admin & Project Manager**: Full CRUD with create/edit/delete buttons -- **Site Engineer**: View-only with lock icon notification -- **Storekeeper**: No access -- **Search & Filter**: Available for all roles with access -- **Action Buttons**: Only visible to users with management permissions - -### Tasks -- **Admin & Project Manager**: Create new tasks, manage all tasks -- **Site Engineer**: Only see and update their assigned tasks -- **Drag-and-Drop**: Enabled only for users who can edit tasks -- **Task Creation**: Modal only visible to Project Manager and Admin -- **Storekeeper**: No access - -### Workforce -- **Admin & Site Engineer**: Full access to worker management and attendance -- **Project Manager**: Access denied with notification -- **Storekeeper**: Access denied with notification -- **Attendance Tracking**: Date-based attendance marking -- **Worker Management**: Add, edit, delete workers - -### Inventory -- **Admin, Project Manager, Storekeeper**: Full access -- **Site Engineer**: Access denied with notification -- **Storekeeper**: Primary user for this section -- **Low Stock Alerts**: Automatic reorder notifications -- **Search & Filter**: By item name and category - -### Finance -- **Admin & Project Manager**: Full access to all financial data -- **Site Engineer**: Access denied with notification -- **Storekeeper**: Access denied with notification -- **Budget Tracking**: Project-wise budget vs actual expenses -- **Cost Distribution**: Labor, Material, Equipment, Other costs -- **Financial Charts**: Pie charts and budget analysis - -## Implementation Details - -### Authentication Flow -1. User logs in with email and password -2. User selects their role from 4 options -3. Role is stored in AuthContext -4. useAuth hook provides role information to all pages - -### Access Control Pattern -```javascript -// Check if user can access feature -const canManageProjects = ['Admin', 'Project_Manager'].includes(user?.role); - -// Conditionally render UI -{canManageProjects && ( - -)} - -// Show access denied message -{!canManageProjects && ( - -

You don't have access to this section

-
-)} -``` - -### Data Filtering Pattern -```javascript -// Filter data based on role -const visibleTasks = useMemo(() => { - if (user?.role === 'Site_Engineer') { - return tasks.filter(t => t.assigned_to === user?.id); - } - return tasks; -}, [tasks, user?.role, user?.id]); -``` - -## User Experience Enhancements - -### Visual Indicators -- Lock icons show restricted access -- Role-specific descriptions in headers -- Color-coded access denied messages -- Conditional button visibility - -### Navigation -- Sidebar automatically filters menu items by role -- Only accessible pages appear in navigation -- Navbar shows current user role - -### Feedback -- Clear messages when access is denied -- Helpful instructions for limited access users -- Contextual help text for role-specific features - -## Testing the Roles - -### Admin Login -- Email: any email -- Password: any password -- Role: Admin -- Expected: Full access to all features - -### Project Manager Login -- Email: any email -- Password: any password -- Role: Project Manager -- Expected: Projects, Tasks, Inventory, Finance access - -### Site Engineer Login -- Email: any email -- Password: any password -- Role: Site Engineer -- Expected: Projects (view-only), Tasks (assigned only), Workforce access - -### Storekeeper Login -- Email: any email -- Password: any password -- Role: Storekeeper -- Expected: Inventory access only - -## Future Enhancements - -1. **Backend Integration**: Connect to real database for persistent role-based data -2. **Permissions API**: Server-side permission validation -3. **Audit Logging**: Track actions by role and user -4. **Custom Roles**: Allow creation of custom role definitions -5. **Time-based Access**: Restrict access by time periods -6. **Department-based Filtering**: Filter data by department/project assignment -7. **Approval Workflows**: Role-based approval chains for critical actions diff --git a/construction-site-management/ROLE_MATRIX.md b/construction-site-management/ROLE_MATRIX.md deleted file mode 100644 index 8754d88..0000000 --- a/construction-site-management/ROLE_MATRIX.md +++ /dev/null @@ -1,257 +0,0 @@ -# भूमिका-आधारित एक्सेस मैट्रिक्स (Role-Based Access Matrix) - -## भूमिका के अनुसार सुविधा एक्सेस (Feature Access by Role) - -| सुविधा | Admin | Project Manager | Site Engineer | Storekeeper | -|--------|:-----:|:---------------:|:-------------:|:-----------:| -| **डैशबोर्ड** | ✅ पूर्ण | ✅ पूर्ण | ✅ सीमित | ✅ सीमित | -| **परियोजनाएं** | ✅ CRUD | ✅ CRUD | 👁️ देखें | ❌ नहीं | -| **कार्य** | ✅ CRUD | ✅ CRUD | 👁️ असाइन किए गए | ❌ नहीं | -| **कार्यबल** | ✅ पूर्ण | ❌ नहीं | ✅ पूर्ण | ❌ नहीं | -| **इन्वेंटरी** | ✅ पूर्ण | ✅ पूर्ण | ❌ नहीं | ✅ पूर्ण | -| **वित्त** | ✅ पूर्ण | ✅ पूर्ण | ❌ नहीं | ❌ नहीं | - -**किंवदंती:** -- ✅ पूर्ण = बनाएं/पढ़ें/अपडेट/हटाएं के साथ पूर्ण एक्सेस -- ✅ CRUD = बनाएं, पढ़ें, अपडेट, हटाएं संचालन -- 👁️ देखें = केवल पढ़ने के लिए एक्सेस -- 👁️ असाइन किए गए = केवल असाइन किए गए आइटम देखें -- ❌ नहीं = कोई एक्सेस नहीं (एक्सेस अस्वीकृत संदेश) - -## डैशबोर्ड KPI भूमिका के अनुसार (Dashboard KPIs by Role) - -| KPI | Admin | PM | Engineer | Storekeeper | -|-----|:-----:|:--:|:--------:|:-----------:| -| कुल परियोजनाएं | ✅ | ✅ | ✅ | ❌ | -| सक्रिय कार्य | ✅ | ✅ | ✅ | ❌ | -| सक्रिय कर्मचारी | ✅ | ❌ | ✅ | ❌ | -| कम स्टॉक आइटम | ✅ | ✅ | ❌ | ✅ | -| कुल बजट | ✅ | ✅ | ❌ | ❌ | -| इन्वेंटरी अवलोकन | ❌ | ❌ | ❌ | ✅ | -| वित्तीय चार्ट | ✅ | ✅ | ❌ | ❌ | -| हाल के कार्य | ✅ | ✅ | ✅ | ❌ | - -## Sidebar Navigation by Role - -### Admin -``` -Dashboard -├── Projects -├── Tasks -├── Workforce -├── Inventory -└── Finance -``` - -### Project Manager -``` -Dashboard -├── Projects -├── Tasks -├── Inventory -└── Finance -``` - -### Site Engineer -``` -Dashboard -├── Projects -├── Tasks -└── Workforce -``` - -### Storekeeper -``` -Dashboard -└── Inventory -``` - -## Action Permissions by Role - -### Projects Page - -| Action | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| View Projects | ✅ | ✅ | ✅ | ❌ | -| Create Project | ✅ | ✅ | ❌ | ❌ | -| Edit Project | ✅ | ✅ | ❌ | ❌ | -| Delete Project | ✅ | ✅ | ❌ | ❌ | -| Search/Filter | ✅ | ✅ | ✅ | ❌ | - -### Tasks Page - -| Action | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| View All Tasks | ✅ | ✅ | ❌ | ❌ | -| View Assigned Tasks | ✅ | ✅ | ✅ | ❌ | -| Create Task | ✅ | ✅ | ❌ | ❌ | -| Update Task Status | ✅ | ✅ | ✅* | ❌ | -| Delete Task | ✅ | ✅ | ❌ | ❌ | -| Drag & Drop | ✅ | ✅ | ✅* | ❌ | - -*Only for assigned tasks - -### Workforce Page - -| Action | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| View Workers | ✅ | ❌ | ✅ | ❌ | -| Add Worker | ✅ | ❌ | ✅ | ❌ | -| Edit Worker | ✅ | ❌ | ✅ | ❌ | -| Delete Worker | ✅ | ❌ | ✅ | ❌ | -| Mark Attendance | ✅ | ❌ | ✅ | ❌ | - -### Inventory Page - -| Action | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| View Inventory | ✅ | ✅ | ❌ | ✅ | -| Search/Filter | ✅ | ✅ | ❌ | ✅ | -| View Low Stock | ✅ | ✅ | ❌ | ✅ | -| Reorder Items | ✅ | ✅ | ❌ | ✅ | -| Edit Stock | ✅ | ✅ | ❌ | ✅ | - -### Finance Page - -| Action | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| View Analytics | ✅ | ✅ | ❌ | ❌ | -| View Budget | ✅ | ✅ | ❌ | ❌ | -| View Charts | ✅ | ✅ | ❌ | ❌ | -| Export Reports | ✅ | ✅ | ❌ | ❌ | - -## Data Visibility by Role - -## परियोजना पेज (Projects Page) - -| कार्य | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| परियोजनाएं देखें | ✅ | ✅ | ✅ | ❌ | -| परियोजना बनाएं | ✅ | ✅ | ❌ | ❌ | -| परियोजना संपादित करें | ✅ | ✅ | ❌ | ❌ | -| परियोजना हटाएं | ✅ | ✅ | ❌ | ❌ | -| खोज/फ़िल्टर | ✅ | ✅ | ✅ | ❌ | - -### उदाहरण परियोजनाएं: -- **मुंबई ऑफिस कॉम्प्लेक्स** - ₹5,00,00,000 -- **दिल्ली मेट्रो स्टेशन** - ₹10,00,00,000 -- **बेंगलुरु आवासीय परियोजना** - ₹3,50,00,000 - -## कार्य पेज (Tasks Page) - -| कार्य | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| सभी कार्य देखें | ✅ | ✅ | ❌ | ❌ | -| असाइन किए गए कार्य देखें | ✅ | ✅ | ✅ | ❌ | -| कार्य बनाएं | ✅ | ✅ | ❌ | ❌ | -| कार्य स्थिति अपडेट करें | ✅ | ✅ | ✅* | ❌ | -| कार्य हटाएं | ✅ | ✅ | ❌ | ❌ | -| ड्रैग & ड्रॉप | ✅ | ✅ | ✅* | ❌ | - -*केवल असाइन किए गए कार्यों के लिए - -### उदाहरण कार्य: -- **नींव की खुदाई** - राज कुमार - उच्च प्राथमिकता -- **कंक्रीट डालना** - प्रिया शर्मा - मध्यम प्राथमिकता -- **ईंट बिछाना** - अमित पटेल - कम प्राथमिकता - -## कार्यबल पेज (Workforce Page) - -| कार्य | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| कर्मचारी देखें | ✅ | ❌ | ✅ | ❌ | -| कर्मचारी जोड़ें | ✅ | ❌ | ✅ | ❌ | -| कर्मचारी संपादित करें | ✅ | ❌ | ✅ | ❌ | -| कर्मचारी हटाएं | ✅ | ❌ | ✅ | ❌ | -| उपस्थिति चिह्नित करें | ✅ | ❌ | ✅ | ❌ | - -### उदाहरण कर्मचारी: -- **राज कुमार** - मेसन - ₹500/दिन -- **प्रिया शर्मा** - इलेक्ट्रीशियन - ₹600/दिन -- **अमित पटेल** - लेबर - ₹400/दिन - -## इन्वेंटरी पेज (Inventory Page) - -| कार्य | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| इन्वेंटरी देखें | ✅ | ✅ | ❌ | ✅ | -| खोज/फ़िल्टर | ✅ | ✅ | ❌ | ✅ | -| कम स्टॉक देखें | ✅ | ✅ | ❌ | ✅ | -| आइटम पुनः ऑर्डर करें | ✅ | ✅ | ❌ | ✅ | -| स्टॉक संपादित करें | ✅ | ✅ | ❌ | ✅ | - -### उदाहरण इन्वेंटरी: -- **सीमेंट** - 500 बैग (न्यूनतम: 200) -- **स्टील रॉड** - 2 टन (न्यूनतम: 1 टन) -- **ईंटें** - 10,000 (न्यूनतम: 5,000) - -## वित्त पेज (Finance Page) - -| कार्य | Admin | PM | Engineer | Storekeeper | -|--------|:-----:|:--:|:--------:|:-----------:| -| विश्लेषण देखें | ✅ | ✅ | ❌ | ❌ | -| बजट देखें | ✅ | ✅ | ❌ | ❌ | -| चार्ट देखें | ✅ | ✅ | ❌ | ❌ | -| रिपोर्ट निर्यात करें | ✅ | ✅ | ❌ | ❌ | - -### उदाहरण वित्त: -- **कुल बजट**: ₹5,00,00,000 -- **खर्च किया गया**: ₹2,50,00,000 -- **शेष बजट**: ₹2,50,00,000 - -## विशिष्ट उपयोगकर्ता वर्कफ़्लो (Typical User Workflows) - -### Admin वर्कफ़्लो (Admin Workflow) -1. लॉगिन → Admin चुनें -2. डैशबोर्ड देखें (सभी KPI) -3. परियोजनाएं प्रबंधित करें (CRUD) -4. कार्य प्रबंधित करें (CRUD) -5. कर्मचारी प्रबंधित करें (CRUD) -6. इन्वेंटरी प्रबंधित करें (CRUD) -7. वित्त विश्लेषण देखें - -### Project Manager वर्कफ़्लो (Project Manager Workflow) -1. लॉगिन → Project Manager चुनें -2. डैशबोर्ड देखें (परियोजनाएं, कार्य, बजट) -3. परियोजनाएं बनाएं/संपादित करें -4. इंजीनियरों को कार्य असाइन करें -5. कार्य प्रगति की निगरानी करें -6. इन्वेंटरी प्रबंधित करें -7. बजट और खर्च ट्रैक करें - -### Site Engineer वर्कफ़्लो (Site Engineer Workflow) -1. लॉगिन → Site Engineer चुनें -2. डैशबोर्ड देखें (परियोजनाएं, कार्य, कर्मचारी) -3. असाइन की गई परियोजनाएं देखें -4. कार्य स्थिति अपडेट करें (ड्रैग & ड्रॉप) -5. कर्मचारी प्रबंधित करें -6. उपस्थिति चिह्नित करें -7. प्रगति की रिपोर्ट करें - -### Storekeeper वर्कफ़्लो (Storekeeper Workflow) -1. लॉगिन → Storekeeper चुनें -2. डैशबोर्ड देखें (इन्वेंटरी अवलोकन) -3. इन्वेंटरी आइटम देखें -4. कम स्टॉक आइटम जांचें -5. पुनः ऑर्डर प्रक्रिया करें -6. स्टॉक स्तर अपडेट करें -7. इन्वेंटरी रिपोर्ट जेनरेट करें - -## एक्सेस अस्वीकृत परिदृश्य (Access Denied Scenarios) - -| परिदृश्य | संदेश | -|----------|--------| -| Site Engineer परियोजनाओं को एक्सेस करता है | "आपके पास परियोजनाओं के लिए केवल-पढ़ने के लिए एक्सेस है। परिवर्तन करने के लिए अपने Project Manager से संपर्क करें।" | -| Site Engineer कार्यों को एक्सेस करता है | "आप केवल अपने असाइन किए गए कार्य देख और अपडेट कर सकते हैं। नए कार्य बनाने के लिए अपने Project Manager से संपर्क करें।" | -| Project Manager कार्यबल को एक्सेस करता है | "आपके पास कार्यबल प्रबंधन तक पहुंच नहीं है। केवल Admin और Site Engineers इस अनुभाग को देख सकते हैं।" | -| Site Engineer इन्वेंटरी को एक्सेस करता है | "आपके पास इन्वेंटरी प्रबंधन तक पहुंच नहीं है। केवल Admin, Project Managers, और Storekeepers इस अनुभाग को देख सकते हैं।" | -| Storekeeper वित्त को एक्सेस करता है | "आपके पास वित्त विश्लेषण तक पहुंच नहीं है। केवल Admin और Project Managers इस अनुभाग को देख सकते हैं।" | - -## कार्यान्वयन नोट्स (Implementation Notes) - -- सभी भूमिका जांचें क्लाइंट-साइड `useAuth()` हुक का उपयोग करके की जाती हैं -- भूमिका लॉगिन के दौरान चुनी जाती है और AuthContext में संग्रहीत होती है -- साइडबार स्वचालित रूप से भूमिका के आधार पर मेनू आइटम फ़िल्टर करता है -- प्रत्येक पेज भूमिका के आधार पर सुविधाओं को सशर्त रूप से प्रस्तुत करता है -- एक्सेस अस्वीकृत संदेश सुसंगत स्टाइलिंग के साथ लॉक आइकन का उपयोग करते हैं -- डेटा फ़िल्टरिंग प्रदर्शन अनुकूलन के लिए useMemo का उपयोग करता है diff --git a/construction-site-management/TESTING_GUIDE.md b/construction-site-management/TESTING_GUIDE.md deleted file mode 100644 index a33aa19..0000000 --- a/construction-site-management/TESTING_GUIDE.md +++ /dev/null @@ -1,158 +0,0 @@ -# Testing Guide - Role-Based Features - -## Quick Start - -1. **Start the application** - ```bash - npm run dev - ``` - -2. **Navigate to login page** (should be default) - -3. **Test each role** by logging in and selecting the role - -## Test Scenarios - -### Scenario 1: Admin User -**Login Details:** -- Email: admin@example.com -- Password: password123 -- Role: Admin - -**Expected Behavior:** -- ✅ Dashboard shows all KPI cards (Projects, Tasks, Workers, Low Stock, Budget) -- ✅ Can create, edit, delete projects -- ✅ Can create, edit, delete tasks -- ✅ Can manage workers and attendance -- ✅ Can view and manage inventory -- ✅ Can view financial analytics -- ✅ All sidebar menu items visible - -### Scenario 2: Project Manager -**Login Details:** -- Email: pm@example.com -- Password: password123 -- Role: Project Manager - -**Expected Behavior:** -- ✅ Dashboard shows Projects, Tasks, Low Stock, Budget (no Workers) -- ✅ Can create, edit, delete projects -- ✅ Can create, edit, delete tasks -- ✅ Cannot access Workforce (access denied message) -- ✅ Can view and manage inventory -- ✅ Can view financial analytics -- ✅ Sidebar shows: Dashboard, Projects, Tasks, Inventory, Finance - -### Scenario 3: Site Engineer -**Login Details:** -- Email: engineer@example.com -- Password: password123 -- Role: Site Engineer - -**Expected Behavior:** -- ✅ Dashboard shows Projects, Tasks, Workers (no Budget/Finance) -- ✅ Projects page shows view-only access with lock icon -- ✅ Cannot create new projects -- ✅ Tasks page shows only assigned tasks -- ✅ Can drag-and-drop own tasks to update status -- ✅ Can manage workers and attendance -- ✅ Cannot access Inventory (access denied message) -- ✅ Cannot access Finance (access denied message) -- ✅ Sidebar shows: Dashboard, Projects, Tasks, Workforce - -### Scenario 4: Storekeeper -**Login Details:** -- Email: storekeeper@example.com -- Password: password123 -- Role: Storekeeper - -**Expected Behavior:** -- ✅ Dashboard shows Inventory Overview (Total Items, Low Stock, Total Value) -- ✅ Cannot access Projects (access denied message) -- ✅ Cannot access Tasks (access denied message) -- ✅ Cannot access Workforce (access denied message) -- ✅ Can view and manage inventory -- ✅ Cannot access Finance (access denied message) -- ✅ Sidebar shows: Dashboard, Inventory only - -## Feature Testing Checklist - -### Dashboard -- [ ] Role-specific KPI cards display correctly -- [ ] Charts only show for Admin and Project Manager -- [ ] Recent tasks visible for all except Storekeeper -- [ ] Storekeeper sees inventory overview -- [ ] User name and role display in header - -### Projects -- [ ] Admin can create/edit/delete projects -- [ ] Project Manager can create/edit/delete projects -- [ ] Site Engineer sees view-only message -- [ ] Search and filter work correctly -- [ ] New Project button only visible to managers - -### Tasks -- [ ] Admin sees all tasks -- [ ] Project Manager sees all tasks -- [ ] Site Engineer sees only assigned tasks -- [ ] Drag-and-drop works for authorized users -- [ ] New Task button only visible to managers -- [ ] Task creation modal has correct fields - -### Workforce -- [ ] Admin can access workforce -- [ ] Site Engineer can access workforce -- [ ] Project Manager sees access denied -- [ ] Storekeeper sees access denied -- [ ] Attendance tracking works -- [ ] Date selector functions properly - -### Inventory -- [ ] Admin can access inventory -- [ ] Project Manager can access inventory -- [ ] Storekeeper can access inventory -- [ ] Site Engineer sees access denied -- [ ] Search and filter work -- [ ] Low stock alerts display -- [ ] Reorder buttons visible for low stock items - -### Finance -- [ ] Admin can access finance -- [ ] Project Manager can access finance -- [ ] Site Engineer sees access denied -- [ ] Storekeeper sees access denied -- [ ] Charts display correctly -- [ ] Budget calculations accurate -- [ ] Cost distribution shows all categories - -## Common Issues & Solutions - -### Issue: All roles see all features -**Solution:** Check that useAuth hook is properly imported and user role is being read from AuthContext - -### Issue: Sidebar shows wrong menu items -**Solution:** Verify Sidebar.jsx is using useAuth and filtering items by user.role - -### Issue: Access denied message not showing -**Solution:** Ensure Lock icon is imported from lucide-react and conditional rendering is correct - -### Issue: Tasks not filtering by Site Engineer -**Solution:** Check that task.assigned_to matches user.id (may need to verify data structure) - -## Performance Notes - -- Role-based filtering uses useMemo to prevent unnecessary re-renders -- Access control checks are lightweight (simple array includes) -- No additional API calls for role validation (client-side only) - -## Demo Data - -The application uses mock data from `src/data/mockData.js`. All roles can use the same demo credentials with any email/password combination. - -## Next Steps - -1. **Backend Integration**: Connect to real authentication system -2. **Database**: Store user roles and permissions in database -3. **API Validation**: Validate permissions on backend for security -4. **Audit Logging**: Log all actions by role and user -5. **Custom Permissions**: Allow fine-grained permission control diff --git a/construction-site-management/src/components/layout/Navbar.jsx b/construction-site-management/src/components/layout/Navbar.jsx deleted file mode 100644 index 22641c0..0000000 --- a/construction-site-management/src/components/layout/Navbar.jsx +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Navbar Component - * Top header with branding, date, notifications, and user menu - */ - -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../../hooks/useAuth'; -import { useContext, useMemo } from 'react'; -import { AppContext } from '../../context/AppContext'; -import { Bell, LogOut, Search } from 'lucide-react'; - -const Navbar = () => { - const { user, logout } = useAuth(); - const { projects, tasks, workers, inventory, vendors, unreadNotificationCount } = useContext(AppContext); - const navigate = useNavigate(); - const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - - const handleLogout = async () => { - await logout(); - navigate('/login'); - }; - - const today = new Date().toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }); - - const globalResults = useMemo(() => { - if (!searchQuery.trim()) { - return []; - } - - const query = searchQuery.toLowerCase(); - const pool = [ - ...projects.map((item) => ({ - id: item.id, - type: 'Project', - label: item.project_name, - path: '/projects', - })), - ...tasks.map((item) => ({ - id: item.id, - type: 'Task', - label: item.task_name, - path: '/tasks', - })), - ...workers.map((item) => ({ - id: item.id, - type: 'Worker', - label: item.name, - path: '/workforce', - })), - ...inventory.map((item) => ({ - id: item.id, - type: 'Inventory', - label: item.item_name, - path: '/inventory', - })), - ...vendors.map((item) => ({ - id: item.id, - type: 'Vendor', - label: item.vendor_name, - path: '/vendors', - })), - ]; - - return pool.filter((entry) => entry.label.toLowerCase().includes(query)).slice(0, 6); - }, [inventory, projects, searchQuery, tasks, vendors, workers]); - - return ( - - ); -}; - -export default Navbar; diff --git a/construction-site-management/src/components/ui/Badge.jsx b/construction-site-management/src/components/ui/Badge.jsx deleted file mode 100644 index 7ebdf94..0000000 --- a/construction-site-management/src/components/ui/Badge.jsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Badge Component - * Status indicator with color variants - * Variants: status, success, warning, danger - */ - -const Badge = ({ - variant = 'status', - children, - className = '', - ...props -}) => { - const variantStyles = { - status: 'bg-blue-500/10 text-blue-500', - success: 'bg-emerald-500/10 text-emerald-500', - warning: 'bg-yellow-500/10 text-yellow-500', - danger: 'bg-rose-500/10 text-rose-500', - }; - - return ( - - {children} - - ); -}; - -export default Badge; diff --git a/construction-site-management/src/components/ui/Button.jsx b/construction-site-management/src/components/ui/Button.jsx deleted file mode 100644 index ad1f0e2..0000000 --- a/construction-site-management/src/components/ui/Button.jsx +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Button Component - * Reusable button with multiple variants and sizes - * Variants: primary, secondary, danger - * Sizes: sm, md, lg - */ - -const Button = ({ - variant = 'primary', - size = 'md', - onClick, - disabled = false, - children, - className = '', - type = 'button', - ...props -}) => { - const baseStyles = 'font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed'; - - const variantStyles = { - primary: 'bg-amber-500 hover:bg-amber-600 text-slate-950 focus:ring-amber-500', - secondary: 'bg-slate-800 hover:bg-slate-700 text-slate-50 focus:ring-slate-600', - danger: 'bg-rose-600 hover:bg-rose-700 text-slate-50 focus:ring-rose-500', - }; - - const sizeStyles = { - sm: 'px-3 py-1.5 text-sm', - md: 'px-4 py-2 text-base', - lg: 'px-6 py-3 text-lg', - }; - - const combinedClassName = `${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`; - - return ( - - ); -}; - -export default Button; diff --git a/construction-site-management/src/components/ui/Card.jsx b/construction-site-management/src/components/ui/Card.jsx deleted file mode 100644 index 9aa1073..0000000 --- a/construction-site-management/src/components/ui/Card.jsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Card Component - * Reusable card container with consistent styling - * Applies: bg-slate-900, rounded-xl, p-6, border-slate-800 - */ - -const Card = ({ - title, - children, - className = '', - headerClassName = '', - bodyClassName = '', - ...props -}) => { - return ( -
- {title && ( -
-

{title}

-
- )} -
{children}
-
- ); -}; - -export default Card; diff --git a/construction-site-management/src/components/ui/Input.jsx b/construction-site-management/src/components/ui/Input.jsx deleted file mode 100644 index d908ef6..0000000 --- a/construction-site-management/src/components/ui/Input.jsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Input Component - * Reusable input field with label and error state support - * Supports: text, number, date, email types - */ - -const Input = ({ - label, - type = 'text', - value, - onChange, - error, - required = false, - placeholder, - disabled = false, - className = '', - ...props -}) => { - return ( -
- {label && ( - - )} - - {error &&

{error}

} -
- ); -}; - -export default Input; diff --git a/construction-site-management/src/components/ui/Modal.jsx b/construction-site-management/src/components/ui/Modal.jsx deleted file mode 100644 index f41cba1..0000000 --- a/construction-site-management/src/components/ui/Modal.jsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Modal Component - * Overlay dialog with close button and ESC key handler - * Features: backdrop-blur, click-outside-to-close, ESC key support - */ - -import { useEffect } from 'react'; -import { X } from 'lucide-react'; - -const Modal = ({ - isOpen, - onClose, - title, - children, - className = '', - size = 'md', - ...props -}) => { - useEffect(() => { - const handleEscape = (e) => { - if (e.key === 'Escape' && isOpen) { - onClose(); - } - }; - - if (isOpen) { - document.addEventListener('keydown', handleEscape); - document.body.style.overflow = 'hidden'; - } - - return () => { - document.removeEventListener('keydown', handleEscape); - document.body.style.overflow = 'unset'; - }; - }, [isOpen, onClose]); - - if (!isOpen) return null; - - const sizeStyles = { - sm: 'max-w-sm', - md: 'max-w-md', - lg: 'max-w-lg', - xl: 'max-w-xl', - '2xl': 'max-w-2xl', - }; - - return ( -
- {/* Backdrop */} -
- - {/* Modal Content */} -
e.stopPropagation()} - > - {/* Header */} -
-

{title}

- -
- - {/* Body */} -
{children}
-
-
- ); -}; - -export default Modal; diff --git a/construction-site-management/src/components/ui/Select.jsx b/construction-site-management/src/components/ui/Select.jsx deleted file mode 100644 index 80e1086..0000000 --- a/construction-site-management/src/components/ui/Select.jsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Select Component - * Reusable select dropdown with label and options - */ - -const Select = ({ - label, - options = [], - value, - onChange, - required = false, - disabled = false, - className = '', - ...props -}) => { - return ( -
- {label && ( - - )} - -
- ); -}; - -export default Select; diff --git a/construction-site-management/src/components/ui/Table.jsx b/construction-site-management/src/components/ui/Table.jsx deleted file mode 100644 index d0b36ef..0000000 --- a/construction-site-management/src/components/ui/Table.jsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Table Component - * Reusable data table with custom column rendering - * Features: hover states, responsive horizontal scroll - */ - -const Table = ({ - columns = [], - data = [], - onRowClick, - className = '', - ...props -}) => { - return ( -
- - - - {columns.map((column) => ( - - ))} - - - - {data.length === 0 ? ( - - - - ) : ( - data.map((row, rowIndex) => ( - onRowClick && onRowClick(row)} - className="border-b border-slate-800 hover:bg-slate-800/50 transition-colors cursor-pointer" - > - {columns.map((column) => ( - - ))} - - )) - )} - -
- {column.label} -
- No data available -
- {column.render - ? column.render(row[column.key], row) - : row[column.key]} -
-
- ); -}; - -export default Table; diff --git a/construction-site-management/src/context/AppContext.jsx b/construction-site-management/src/context/AppContext.jsx deleted file mode 100644 index 0d5d309..0000000 --- a/construction-site-management/src/context/AppContext.jsx +++ /dev/null @@ -1,742 +0,0 @@ -/** - * AppContext - * Global state and frontend-only business logic for SiteOS Enterprise SaaS simulation - */ - -import { createContext, useState, useCallback, useMemo } from 'react'; -import { - mockUsers, - mockProjects, - mockTasks, - mockWorkers, - mockInventory, - mockFinance, - mockVendors, - mockPurchaseOrders, - mockMaterialIssues, - mockWorkerAssignments, - mockAttendance, - mockProjectMembers, - mockNotifications, - mockLeaveApplications, -} from '../data/mockData'; - -export const AppContext = createContext(); - -const makeId = (prefix) => `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`; - -const sameDay = (dateA, dateB) => { - return new Date(dateA).toISOString().slice(0, 10) === new Date(dateB).toISOString().slice(0, 10); -}; - -export const AppProvider = ({ children }) => { - const [currentUser, setCurrentUser] = useState(null); - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const [users] = useState(mockUsers); - const [projects, setProjects] = useState(mockProjects); - const [tasks, setTasks] = useState(mockTasks); - const [workers, setWorkers] = useState(mockWorkers); - const [inventory, setInventory] = useState(mockInventory); - const [financeRecords, setFinanceRecords] = useState(mockFinance); - - const [vendors, setVendors] = useState(mockVendors); - const [purchaseOrders, setPurchaseOrders] = useState(mockPurchaseOrders); - const [materialIssues, setMaterialIssues] = useState(mockMaterialIssues); - const [workerAssignments, setWorkerAssignments] = useState(mockWorkerAssignments); - const [attendanceRecords, setAttendanceRecords] = useState(mockAttendance); - const [projectMembers, setProjectMembers] = useState(mockProjectMembers); - const [notifications, setNotifications] = useState(mockNotifications); - const [leaveApplications, setLeaveApplications] = useState(mockLeaveApplications); - - const pushNotification = useCallback((notification) => { - setNotifications((prev) => [ - { - id: makeId('note'), - read: false, - createdAt: new Date().toISOString(), - severity: 'medium', - ...notification, - }, - ...prev, - ]); - }, []); - - // Authentication actions - const login = useCallback((userId) => { - const user = users.find((u) => u.id === userId); - if (user) { - setCurrentUser(user); - setIsAuthenticated(true); - } - }, [users]); - - const logout = useCallback(() => { - setCurrentUser(null); - setIsAuthenticated(false); - }, []); - - const switchRole = useCallback((newRole) => { - if (currentUser) { - setCurrentUser({ ...currentUser, role: newRole }); - } - }, [currentUser]); - - // Project actions - const addProject = useCallback((project) => { - const newProject = { - id: makeId('proj'), - ...project, - status: project.status || 'Planning', - }; - setProjects((prev) => [...prev, newProject]); - return newProject; - }, []); - - const updateProject = useCallback((id, updates) => { - setProjects((prev) => prev.map((p) => (p.id === id ? { ...p, ...updates } : p))); - }, []); - - const deleteProject = useCallback((id) => { - setProjects((prev) => prev.filter((p) => p.id !== id)); - }, []); - - // Task actions - const addTask = useCallback((task) => { - const newTask = { - id: makeId('task'), - ...task, - status: task.status || 'Open', - priority: task.priority || 'Medium', - workers_assigned: task.workers_assigned || [], - materials_used: task.materials_used || [], - deadline: task.deadline || task.due_date, - }; - setTasks((prev) => [...prev, newTask]); - return newTask; - }, []); - - const checkDependencies = useCallback((taskId, tasksList = null) => { - const currentTasks = tasksList || tasks; - const task = currentTasks.find((t) => t.id === taskId); - if (!task || !task.dependencies || task.dependencies.length === 0) { - return true; - } - return task.dependencies.every((depId) => { - const depTask = currentTasks.find((t) => t.id === depId); - return depTask && depTask.status === 'Completed'; - }); - }, [tasks]); - - const updateTaskStatus = useCallback((id, status, skipValidation = false) => { - if (status === 'In Progress' && !skipValidation) { - const depsOk = checkDependencies(id); - if (!depsOk) { - pushNotification({ - type: 'error', - title: 'Blocked Task', - message: 'Cannot start: incomplete dependencies', - }); - return false; - } - } - setTasks((prev) => prev.map((t) => (t.id === id ? { ...t, status } : t))); - return true; - }, [checkDependencies, pushNotification]); - - const addTaskDependency = useCallback((taskId, depId) => { - setTasks((prev) => - prev.map((t) => { - if (t.id === taskId && !t.dependencies?.includes(depId)) { - return { ...t, dependencies: [...(t.dependencies || []), depId] }; - } - return t; - }) - ); - }, []); - - const removeTaskDependency = useCallback((taskId, depId) => { - setTasks((prev) => - prev.map((t) => (t.id === taskId ? { ...t, dependencies: (t.dependencies || []).filter(d => d !== depId) } : t)) - ); - }, []); - - const updateTaskProgress = useCallback((taskId, progress) => { - const p = Math.max(0, Math.min(100, Math.round(progress))); - setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, progress: p } : t))); - }, []); - - const updateTask = useCallback((taskId, updates) => { - setTasks((prev) => prev.map((t) => (t.id === taskId ? { ...t, ...updates } : t))); - }, []); - - // Vendor management - const addVendor = useCallback((vendor) => { - const newVendor = { id: makeId('vendor'), ...vendor, rating: Number(vendor.rating || 0) }; - setVendors((prev) => [...prev, newVendor]); - return newVendor; - }, []); - - const updateVendor = useCallback((vendorId, updates) => { - setVendors((prev) => prev.map((v) => (v.id === vendorId ? { ...v, ...updates } : v))); - }, []); - - const deleteVendor = useCallback((vendorId) => { - setVendors((prev) => prev.filter((v) => v.id !== vendorId)); - }, []); - - // Finance helper - const addFinanceRecord = useCallback((record) => { - const newRecord = { - id: makeId('fin'), - payment_status: 'Pending', - date: new Date().toISOString().slice(0, 10), - source: 'automation', - ...record, - }; - setFinanceRecords((prev) => [...prev, newRecord]); - return newRecord; - }, []); - - // Procurement - const createPurchaseOrder = useCallback((payload, actorId) => { - const po = { - id: makeId('po'), - ...payload, - quantity: Number(payload.quantity), - unit_price: Number(payload.unit_price), - createdAt: new Date().toISOString().slice(0, 10), - delivery_status: payload.delivery_status || 'ordered', - created_by: actorId, - }; - - setPurchaseOrders((prev) => [...prev, po]); - - addFinanceRecord({ - projectId: po.projectId, - cost_category: 'Material', - amount: po.quantity * po.unit_price, - description: `PO ${po.id} created for ${po.quantity} units`, - }); - - pushNotification({ - type: 'procurement_delivery', - severity: 'medium', - title: `PO created: ${po.id}`, - message: `Purchase order for project ${po.projectId} has been created.`, - }); - - if (po.delivery_status === 'delivered') { - setInventory((prev) => - prev.map((item) => - item.id === po.itemId - ? { ...item, current_stock: item.current_stock + po.quantity } - : item - ) - ); - } - - return po; - }, [addFinanceRecord, pushNotification]); - - const updatePurchaseDeliveryStatus = useCallback((poId, status) => { - setPurchaseOrders((prev) => { - const target = prev.find((po) => po.id === poId); - if (!target) { - return prev; - } - - const wasDelivered = target.delivery_status === 'delivered'; - const nowDelivered = status === 'delivered'; - - if (!wasDelivered && nowDelivered) { - setInventory((inventoryPrev) => - inventoryPrev.map((item) => - item.id === target.itemId - ? { ...item, current_stock: item.current_stock + target.quantity } - : item - ) - ); - - pushNotification({ - type: 'procurement_delivery', - severity: 'low', - title: `PO delivered: ${target.id}`, - message: `Delivery received for purchase order ${target.id}.`, - }); - } - - return prev.map((po) => - po.id === poId - ? { - ...po, - delivery_status: status, - deliveredAt: nowDelivered ? new Date().toISOString().slice(0, 10) : po.deliveredAt, - } - : po - ); - }); - }, [pushNotification]); - - // Material issue and inventory workflow - const issueMaterial = useCallback((payload, actorId) => { - const { itemId, quantity, projectId, taskId } = payload; - const qty = Number(quantity); - - setInventory((prev) => - prev.map((item) => - item.id === itemId - ? { ...item, current_stock: Math.max(0, item.current_stock - qty) } - : item - ) - ); - - const issue = { - id: makeId('mi'), - itemId, - projectId, - taskId, - quantity: qty, - issued_by: actorId, - issuedAt: new Date().toISOString().slice(0, 10), - }; - setMaterialIssues((prev) => [...prev, issue]); - - const item = inventory.find((i) => i.id === itemId); - if (item) { - addFinanceRecord({ - projectId, - cost_category: 'Material', - amount: qty * item.unit_cost, - description: `Material issue ${item.item_name} (${qty} ${item.uom})`, - }); - } - - if (taskId) { - setTasks((prev) => - prev.map((task) => { - if (task.id !== taskId) { - return task; - } - - const existing = Array.isArray(task.materials_used) ? task.materials_used : []; - return { - ...task, - materials_used: [...existing, { itemId, quantity: qty }], - }; - }) - ); - } - - return issue; - }, [addFinanceRecord, inventory]); - - const addProcurement = useCallback((itemId, quantity, cost) => { - const item = inventory.find((inv) => inv.id === itemId); - if (!item) { - return; - } - - createPurchaseOrder( - { - projectId: projects[0]?.id, - vendorId: vendors[0]?.id, - itemId, - quantity, - unit_price: cost || item.unit_cost, - delivery_status: 'delivered', - }, - currentUser?.id || 'system' - ); - }, [createPurchaseOrder, currentUser?.id, inventory, projects, vendors]); - - // Worker assignment and attendance - const assignWorkerToTask = useCallback((payload) => { - const assignment = { - id: makeId('wa'), - ...payload, - }; - - setWorkerAssignments((prev) => [...prev, assignment]); - - setTasks((prev) => - prev.map((task) => { - if (task.id !== payload.taskId) { - return task; - } - - const existing = Array.isArray(task.workers_assigned) ? task.workers_assigned : []; - const workersAssigned = existing.includes(payload.workerId) - ? existing - : [...existing, payload.workerId]; - - return { ...task, workers_assigned: workersAssigned }; - }) - ); - - return assignment; - }, []); - - const calculateLaborCost = useCallback((worker, status, hoursWorked) => { - if (!worker || status === 'Absent') { - return 0; - } - - if (worker.rate_type === 'Hourly') { - return worker.base_rate * Number(hoursWorked || 0); - } - - if (status === 'Half Day') { - return worker.base_rate * 0.5; - } - - return worker.base_rate; - }, []); - - const recordAttendance = useCallback((payload, actorId) => { - const worker = workers.find((w) => w.id === payload.workerId); - const laborCost = calculateLaborCost(worker, payload.status, payload.hours_worked); - - const attendance = { - id: makeId('att'), - ...payload, - labor_cost: laborCost, - recorded_by: actorId, - }; - - setAttendanceRecords((prev) => { - const exists = prev.find((entry) => - entry.workerId === payload.workerId && sameDay(entry.date, payload.date) - ); - - if (exists) { - return prev.map((entry) => - entry.id === exists.id - ? { ...entry, ...attendance, id: exists.id } - : entry - ); - } - - return [...prev, attendance]; - }); - - setWorkers((prev) => - prev.map((w) => { - if (w.id !== payload.workerId) { - return w; - } - - const exists = (w.attendance || []).find((entry) => sameDay(entry.date, payload.date)); - const nextAttendance = exists - ? w.attendance.map((entry) => - sameDay(entry.date, payload.date) - ? { date: payload.date, status: payload.status, hours_worked: payload.hours_worked } - : entry - ) - : [...(w.attendance || []), { date: payload.date, status: payload.status, hours_worked: payload.hours_worked }]; - - return { ...w, attendance: nextAttendance }; - }) - ); - - if (laborCost > 0) { - addFinanceRecord({ - projectId: payload.projectId, - cost_category: 'Labor', - amount: laborCost, - description: `Attendance labor cost for ${worker?.name || payload.workerId}`, - }); - } - - if (payload.status === 'Absent') { - pushNotification({ - type: 'worker_absence', - severity: 'high', - title: 'Worker absence logged', - message: `${worker?.name || 'Worker'} marked absent on ${payload.date}.`, - }); - } - - return attendance; - }, [addFinanceRecord, calculateLaborCost, pushNotification, workers]); - - const updateWorkerAttendance = useCallback((workerId, status, date) => { - const worker = workers.find((w) => w.id === workerId); - const defaultHours = status === 'Present' ? 8 : status === 'Half Day' ? 4 : 0; - const projectId = projects[0]?.id; - - recordAttendance( - { - workerId, - status, - date, - hours_worked: defaultHours, - projectId, - }, - currentUser?.id || 'system' - ); - - return worker; - }, [currentUser?.id, projects, recordAttendance, workers]); - - // Project team management - const assignProjectMember = useCallback((payload) => { - const alreadyAssigned = projectMembers.some( - (pm) => pm.projectId === payload.projectId && pm.userId === payload.userId - ); - - if (alreadyAssigned) { - return null; - } - - const member = { - id: makeId('pm'), - ...payload, - }; - - setProjectMembers((prev) => [...prev, member]); - return member; - }, [projectMembers]); - - const removeProjectMember = useCallback((memberId) => { - setProjectMembers((prev) => prev.filter((member) => member.id !== memberId)); - }, []); - - // Worker management - const addWorker = useCallback((worker) => { - const newWorker = { id: makeId('worker'), attendance: [], salary: 0, ...worker }; - setWorkers((prev) => [...prev, newWorker]); - return newWorker; - }, []); - - const updateWorker = useCallback((workerId, updates) => { - setWorkers((prev) => prev.map((w) => (w.id === workerId ? { ...w, ...updates } : w))); - }, []); - - const deleteWorker = useCallback((workerId) => { - setWorkers((prev) => prev.filter((w) => w.id !== workerId)); - }, []); - - // Inventory stock actions - const addInventoryItem = useCallback((item) => { - const newItem = { id: makeId('inv'), current_stock: 0, min_stock_qty: 0, ...item }; - setInventory((prev) => [...prev, newItem]); - return newItem; - }, []); - - const addInventoryStock = useCallback((itemId, quantity) => { - setInventory((prev) => - prev.map((item) => - item.id === itemId ? { ...item, current_stock: item.current_stock + Number(quantity) } : item - ) - ); - }, []); - - // Leave application actions - const applyLeave = useCallback((payload) => { - const application = { - id: makeId('leave'), - status: 'Pending', - applied_at: new Date().toISOString().slice(0, 10), - reviewed_by: null, - reviewed_at: null, - ...payload, - }; - setLeaveApplications((prev) => [...prev, application]); - return application; - }, []); - - const approveLeave = useCallback((leaveId, reviewerId) => { - setLeaveApplications((prev) => - prev.map((leave) => - leave.id === leaveId - ? { ...leave, status: 'Approved', reviewed_by: reviewerId, reviewed_at: new Date().toISOString().slice(0, 10) } - : leave - ) - ); - }, []); - - const rejectLeave = useCallback((leaveId, reviewerId, rejection_reason = '') => { - setLeaveApplications((prev) => - prev.map((leave) => - leave.id === leaveId - ? { ...leave, status: 'Rejected', reviewed_by: reviewerId, reviewed_at: new Date().toISOString().slice(0, 10), rejection_reason } - : leave - ) - ); - }, []); - - // Salary calculation helper - const calculateSalary = useCallback((workerId, fromDate, toDate) => { - const records = attendanceRecords.filter((entry) => { - if (entry.workerId !== workerId) return false; - if (fromDate && entry.date < fromDate) return false; - if (toDate && entry.date > toDate) return false; - return true; - }); - - const totalDaysWorked = records.filter((r) => r.status === 'Present').length; - const halfDays = records.filter((r) => r.status === 'Half Day').length; - const totalHours = records.reduce((sum, r) => sum + Number(r.hours_worked || 0), 0); - const totalSalary = records.reduce((sum, r) => sum + Number(r.labor_cost || 0), 0); - const absentDays = records.filter((r) => r.status === 'Absent').length; - - const worker = workers.find((w) => w.id === workerId); - const absenceDeduction = worker - ? absentDays * (worker.rate_type === 'Daily' ? worker.base_rate : worker.base_rate * 8) - : 0; - - return { - totalDaysWorked, - halfDays, - totalHours, - totalSalary, - absentDays, - absenceDeduction, - netSalary: totalSalary - absenceDeduction, - }; - }, [attendanceRecords, workers]); - - // Notification actions - const markNotificationRead = useCallback((id) => { - setNotifications((prev) => prev.map((note) => (note.id === id ? { ...note, read: true } : note))); - }, []); - - const markAllNotificationsRead = useCallback(() => { - setNotifications((prev) => prev.map((note) => ({ ...note, read: true }))); - }, []); - - const systemNotifications = useMemo(() => { - const lowStock = inventory - .filter((item) => item.current_stock < item.min_stock_qty) - .map((item) => ({ - id: `sys-low-stock-${item.id}`, - type: 'low_stock', - severity: 'high', - title: `Low stock: ${item.item_name}`, - message: `${item.current_stock} ${item.uom} left (min ${item.min_stock_qty}).`, - createdAt: new Date().toISOString(), - read: false, - })); - - const overdue = tasks - .filter((task) => task.status !== 'Completed' && new Date(task.deadline || task.due_date) < new Date()) - .map((task) => ({ - id: `sys-overdue-${task.id}`, - type: 'overdue_tasks', - severity: 'high', - title: `Overdue task: ${task.task_name}`, - message: `Task deadline ${task.deadline || task.due_date} has passed.`, - createdAt: new Date().toISOString(), - read: false, - })); - - const budgetExceed = projects - .filter((project) => { - const spent = financeRecords - .filter((record) => record.projectId === project.id) - .reduce((sum, record) => sum + record.amount, 0); - return spent > project.budget; - }) - .map((project) => ({ - id: `sys-budget-${project.id}`, - type: 'budget_exceed', - severity: 'high', - title: `Budget exceeded: ${project.project_name}`, - message: `Project spending has exceeded planned budget.`, - createdAt: new Date().toISOString(), - read: false, - })); - - return [...lowStock, ...overdue, ...budgetExceed]; - }, [financeRecords, inventory, projects, tasks]); - - const allNotifications = useMemo(() => { - const map = new Map(); - [...notifications, ...systemNotifications].forEach((note) => { - if (!map.has(note.id)) { - map.set(note.id, note); - } - }); - return Array.from(map.values()).sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - }, [notifications, systemNotifications]); - - const unreadNotificationCount = useMemo(() => { - return allNotifications.filter((note) => !note.read).length; - }, [allNotifications]); - - const value = { - currentUser, - isAuthenticated, - login, - logout, - switchRole, - - users, - projects, - tasks, - workers, - inventory, - financeRecords, - vendors, - purchaseOrders, - materialIssues, - workerAssignments, - attendanceRecords, - projectMembers, - leaveApplications, - notifications: allNotifications, - unreadNotificationCount, - - addProject, - updateProject, - deleteProject, - - addTask, - updateTask, - updateTaskStatus, - checkDependencies, - addTaskDependency, - removeTaskDependency, - updateTaskProgress, - - addVendor, - updateVendor, - deleteVendor, - - createPurchaseOrder, - updatePurchaseDeliveryStatus, - - issueMaterial, - addProcurement, - - addWorker, - updateWorker, - deleteWorker, - - assignWorkerToTask, - recordAttendance, - updateWorkerAttendance, - - addInventoryItem, - addInventoryStock, - - assignProjectMember, - removeProjectMember, - - applyLeave, - approveLeave, - rejectLeave, - calculateSalary, - - addFinanceRecord, - - markNotificationRead, - markAllNotificationsRead, - pushNotification, - }; - - return {children}; -}; diff --git a/construction-site-management/src/context/AuthContext.jsx b/construction-site-management/src/context/AuthContext.jsx deleted file mode 100644 index ef36fdc..0000000 --- a/construction-site-management/src/context/AuthContext.jsx +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Authentication Context - * Manages authentication state and actions - */ - -import { createContext, useState, useEffect, useCallback } from 'react'; -import authService from '../services/authService'; - -export const AuthContext = createContext(); - -const normalizeRole = (role) => { - if (!role || typeof role !== 'string') return 'Site_Engineer'; - return role.trim().replace(/\s+/g, '_'); -}; - -export function AuthProvider({ children }) { - const [user, setUser] = useState(null); - const [token, setToken] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [isAuthenticated, setIsAuthenticated] = useState(false); - - // Initialize auth state from localStorage - useEffect(() => { - const session = authService.getSession(); - if (session && session.token && session.user) { - const normalizedUser = { - ...session.user, - role: normalizeRole(session.user.role), - }; - setUser(normalizedUser); - setToken(session.token); - setIsAuthenticated(true); - } - setLoading(false); - }, []); - - // Sign up - const signup = useCallback(async (name, email, password) => { - setLoading(true); - setError(null); - try { - const result = authService.signup(name, email, password); - if (result.success) { - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during signup'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Verify email - const verifyEmail = useCallback(async (email, code) => { - setLoading(true); - setError(null); - try { - const result = authService.verifyEmail(email, code); - if (result.success) { - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during verification'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Resend verification code - const resendVerificationCode = useCallback(async (email) => { - setLoading(true); - setError(null); - try { - const result = authService.resendVerificationCode(email); - if (result.success) { - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Login with role - const login = useCallback(async (email, password, rememberMe = false, role = 'Site_Engineer') => { - setLoading(true); - setError(null); - try { - const result = authService.login(email, password, rememberMe); - if (result.success) { - // Assign selected role to user - const userWithRole = { ...result.user, role: normalizeRole(role) }; - setUser(userWithRole); - setToken(result.token); - setIsAuthenticated(true); - return { ...result, user: userWithRole }; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during login'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Logout - const logout = useCallback(async () => { - setLoading(true); - try { - authService.logout(); - setUser(null); - setToken(null); - setIsAuthenticated(false); - setError(null); - return { success: true, message: 'Logged out successfully' }; - } catch (err) { - const message = 'An error occurred during logout'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Request password reset - const requestPasswordReset = useCallback(async (email) => { - setLoading(true); - setError(null); - try { - const result = authService.requestPasswordReset(email); - return result; - } catch (err) { - const message = 'An error occurred'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Reset password - const resetPassword = useCallback(async (resetToken, newPassword) => { - setLoading(true); - setError(null); - try { - const result = authService.resetPassword(resetToken, newPassword); - if (result.success) { - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during password reset'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, []); - - // Update profile - const updateProfile = useCallback(async (updates) => { - setLoading(true); - setError(null); - try { - const result = authService.updateProfile(user.id, updates); - if (result.success) { - setUser(result.user); - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during profile update'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, [user]); - - // Change password - const changePassword = useCallback(async (currentPassword, newPassword) => { - setLoading(true); - setError(null); - try { - const result = authService.changePassword(user.id, currentPassword, newPassword); - if (result.success) { - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during password change'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, [user]); - - // Delete account - const deleteAccount = useCallback(async (password) => { - setLoading(true); - setError(null); - try { - const result = authService.deleteAccount(user.id, password); - if (result.success) { - setUser(null); - setToken(null); - setIsAuthenticated(false); - return result; - } else { - setError(result.message); - return result; - } - } catch (err) { - const message = 'An error occurred during account deletion'; - setError(message); - return { success: false, message }; - } finally { - setLoading(false); - } - }, [user]); - - const value = { - user, - token, - loading, - error, - isAuthenticated, - signup, - verifyEmail, - resendVerificationCode, - login, - logout, - requestPasswordReset, - resetPassword, - updateProfile, - changePassword, - deleteAccount, - }; - - return {children}; -} diff --git a/construction-site-management/src/data/mockData.js b/construction-site-management/src/data/mockData.js deleted file mode 100644 index fb80abd..0000000 --- a/construction-site-management/src/data/mockData.js +++ /dev/null @@ -1,713 +0,0 @@ -/** - * Mock Data Layer - * Frontend-only simulated database for SiteOS Enterprise SaaS modules - */ - -export const mockUsers = [ - { - id: 'user-1', - name: 'Arjun Agrawal', - role: 'Admin', - email: 'arjun.admin@siteos.in', - phone: '+91-98001-11001', - }, - { - id: 'user-2', - name: 'Priya Sharma', - role: 'Project_Manager', - email: 'priya.manager@siteos.in', - phone: '+91-98001-11002', - }, - { - id: 'user-3', - name: 'Mohan Patil', - role: 'Site_Engineer', - email: 'mohan.engineer@siteos.in', - phone: '+91-98001-11003', - }, - { - id: 'user-4', - name: 'Raj Mehta', - role: 'Site_Engineer', - email: 'raj.engineer@siteos.in', - phone: '+91-98001-11004', - }, - { - id: 'user-5', - name: 'Ramesh Kumar', - role: 'Worker', - email: 'ramesh.worker@siteos.in', - phone: '+91-98001-11005', - workerId: 'worker-1', - }, - { - id: 'user-6', - name: 'Suresh Yadav', - role: 'Worker', - email: 'suresh.worker@siteos.in', - phone: '+91-98001-11006', - workerId: 'worker-2', - }, - { - id: 'user-7', - name: 'Vinod Singh', - role: 'Worker', - email: 'vinod.worker@siteos.in', - phone: '+91-98001-11007', - workerId: 'worker-3', - }, -]; - -export const mockProjects = [ - { - id: 'proj-1', - project_name: 'Connaught Place Office Tower', - site_location: 'New Delhi, Delhi', - project_type: 'Commercial', - start_date: '2024-01-15', - end_date: '2025-06-30', - budget: 5000000, - status: 'Active', - }, - { - id: 'proj-2', - project_name: 'Andheri Residential Complex', - site_location: 'Mumbai, Maharashtra', - project_type: 'Residential', - start_date: '2024-03-01', - end_date: '2025-12-31', - budget: 3500000, - status: 'Active', - }, - { - id: 'proj-3', - project_name: 'NH-48 Highway Expansion', - site_location: 'Pune, Maharashtra', - project_type: 'Infrastructure', - start_date: '2023-06-01', - end_date: '2025-03-31', - budget: 8000000, - status: 'Active', - }, - { - id: 'proj-4', - project_name: 'Lulu Mall Bengaluru Renovation', - site_location: 'Bengaluru, Karnataka', - project_type: 'Commercial', - start_date: '2023-01-10', - end_date: '2024-12-15', - budget: 2500000, - status: 'Completed', - }, - { - id: 'proj-5', - project_name: 'IIT Hyderabad Campus Expansion', - site_location: 'Hyderabad, Telangana', - project_type: 'Commercial', - start_date: '2024-09-01', - end_date: '2026-08-31', - budget: 6000000, - status: 'Planning', - }, -]; - -export const mockTasks = [ - { - id: 'task-1', - task_name: 'Foundation Excavation', - projectId: 'proj-1', - assigned_to: 'user-3', - status: 'In Progress', - priority: 'High', - due_date: '2026-03-05', - workers_assigned: ['worker-1', 'worker-5'], - materials_used: [{ itemId: 'inv-1', quantity: 120 }], - deadline: '2026-03-12', - dependencies: [], - progress: 65, - }, - { - id: 'task-2', - task_name: 'Steel Frame Installation', - projectId: 'proj-1', - assigned_to: 'user-3', - status: 'Open', - priority: 'High', - due_date: '2026-03-18', - workers_assigned: ['worker-2'], - materials_used: [{ itemId: 'inv-2', quantity: 8 }], - deadline: '2026-03-18', - dependencies: ['task-1'], - progress: 0, - }, - { - id: 'task-3', - task_name: 'Electrical Wiring', - projectId: 'proj-1', - assigned_to: 'user-3', - status: 'Open', - priority: 'Medium', - due_date: '2026-03-22', - workers_assigned: ['worker-3'], - materials_used: [{ itemId: 'inv-5', quantity: 2 }], - deadline: '2026-03-22', - dependencies: ['task-1', 'task-2'], - progress: 0, - }, - { - id: 'task-4', - task_name: 'Site Preparation', - projectId: 'proj-2', - assigned_to: 'user-3', - status: 'Completed', - priority: 'High', - due_date: '2026-02-15', - workers_assigned: ['worker-6', 'worker-8'], - materials_used: [{ itemId: 'inv-4', quantity: 25 }], - deadline: '2026-02-15', - dependencies: [], - progress: 100, - }, - { - id: 'task-5', - task_name: 'Concrete Pouring', - projectId: 'proj-2', - assigned_to: 'user-3', - status: 'In Progress', - priority: 'High', - due_date: '2026-03-10', - workers_assigned: ['worker-1', 'worker-7'], - materials_used: [{ itemId: 'inv-7', quantity: 90 }], - deadline: '2026-03-10', - dependencies: ['task-4'], - progress: 45, - }, - { - id: 'task-6', - task_name: 'Road Resurfacing', - projectId: 'proj-3', - assigned_to: 'user-3', - status: 'In Progress', - priority: 'Medium', - due_date: '2026-03-28', - workers_assigned: ['worker-5', 'worker-8'], - materials_used: [{ itemId: 'inv-3', quantity: 1500 }], - deadline: '2026-03-28', - dependencies: ['task-7'], - progress: 35, - }, - { - id: 'task-7', - task_name: 'Traffic Management Setup', - projectId: 'proj-3', - assigned_to: 'user-3', - status: 'Open', - priority: 'High', - due_date: '2026-03-01', - workers_assigned: ['worker-4'], - materials_used: [{ itemId: 'inv-6', quantity: 10 }], - deadline: '2026-03-01', - dependencies: [], - progress: 20, - }, - { - id: 'task-8', - task_name: 'Interior Painting', - projectId: 'proj-4', - assigned_to: 'user-3', - status: 'Completed', - priority: 'Low', - due_date: '2026-01-30', - workers_assigned: ['worker-2'], - materials_used: [{ itemId: 'inv-9', quantity: 40 }], - deadline: '2026-01-30', - dependencies: [], - progress: 100, - }, -]; - -export const mockWorkers = [ - { - id: 'worker-1', - name: 'Ramesh Kumar', - skill_type: 'Mason', - contact: '+91-98001-11005', - rate_type: 'Daily', - base_rate: 650, - attendance: [], - salary: 16900, - userId: 'user-5', - projectId: 'proj-1', - }, - { - id: 'worker-2', - name: 'Suresh Yadav', - skill_type: 'Carpenter', - contact: '+91-98001-11006', - rate_type: 'Daily', - base_rate: 700, - attendance: [], - salary: 18200, - userId: 'user-6', - projectId: 'proj-1', - }, - { - id: 'worker-3', - name: 'Vinod Singh', - skill_type: 'Electrician', - contact: '+91-98001-11007', - rate_type: 'Hourly', - base_rate: 85, - attendance: [], - salary: 17680, - userId: 'user-7', - projectId: 'proj-1', - }, - { - id: 'worker-4', - name: 'Manoj Dubey', - skill_type: 'Plumber', - contact: '+91-98001-22001', - rate_type: 'Hourly', - base_rate: 90, - attendance: [], - salary: 18720, - projectId: 'proj-2', - }, - { - id: 'worker-5', - name: 'Rakesh Gupta', - skill_type: 'Laborer', - contact: '+91-98001-22002', - rate_type: 'Daily', - base_rate: 450, - attendance: [], - salary: 11700, - projectId: 'proj-2', - }, - { - id: 'worker-6', - name: 'Santosh Patel', - skill_type: 'Mason', - contact: '+91-98001-22003', - rate_type: 'Daily', - base_rate: 660, - attendance: [], - salary: 17160, - projectId: 'proj-2', - }, - { - id: 'worker-7', - name: 'Anil Verma', - skill_type: 'Carpenter', - contact: '+91-98001-22004', - rate_type: 'Daily', - base_rate: 720, - attendance: [], - salary: 18720, - projectId: 'proj-3', - }, - { - id: 'worker-8', - name: 'Dinesh Chauhan', - skill_type: 'Laborer', - contact: '+91-98001-22005', - rate_type: 'Daily', - base_rate: 460, - attendance: [], - salary: 11960, - projectId: 'proj-3', - }, -]; - -export const mockInventory = [ - { - id: 'inv-1', - item_name: 'Portland Cement', - category: 'Cement', - uom: 'bags', - unit_cost: 8.5, - min_stock_qty: 500, - current_stock: 450, - supplier: 'BuildCo Supplies', - }, - { - id: 'inv-2', - item_name: 'Steel Rebar', - category: 'Steel', - uom: 'tons', - unit_cost: 650, - min_stock_qty: 50, - current_stock: 75, - supplier: 'Steel Industries', - }, - { - id: 'inv-3', - item_name: 'Red Bricks', - category: 'Bricks', - uom: 'pieces', - unit_cost: 0.5, - min_stock_qty: 10000, - current_stock: 8500, - supplier: 'Brick Factory', - }, - { - id: 'inv-4', - item_name: 'Sand', - category: 'Sand', - uom: 'cubic meters', - unit_cost: 45, - min_stock_qty: 100, - current_stock: 120, - supplier: 'Sand Quarry', - }, - { - id: 'inv-5', - item_name: 'Power Drill', - category: 'Tools', - uom: 'pieces', - unit_cost: 120, - min_stock_qty: 10, - current_stock: 8, - supplier: 'Tool Depot', - }, - { - id: 'inv-6', - item_name: 'Safety Helmets', - category: 'Safety', - uom: 'pieces', - unit_cost: 25, - min_stock_qty: 50, - current_stock: 45, - supplier: 'Safety First', - }, - { - id: 'inv-7', - item_name: 'Concrete Mix', - category: 'Cement', - uom: 'bags', - unit_cost: 12, - min_stock_qty: 300, - current_stock: 350, - supplier: 'BuildCo Supplies', - }, - { - id: 'inv-8', - item_name: 'Wooden Planks', - category: 'Wood', - uom: 'cubic meters', - unit_cost: 200, - min_stock_qty: 20, - current_stock: 25, - supplier: 'Lumber Mill', - }, - { - id: 'inv-9', - item_name: 'Paint (Gallon)', - category: 'Finishing', - uom: 'gallons', - unit_cost: 35, - min_stock_qty: 50, - current_stock: 40, - supplier: 'Paint Co', - }, - { - id: 'inv-10', - item_name: 'Nails and Screws', - category: 'Hardware', - uom: 'kg', - unit_cost: 15, - min_stock_qty: 100, - current_stock: 150, - supplier: 'Hardware Store', - }, -]; - -export const mockFinance = [ - { - id: 'fin-1', - projectId: 'proj-1', - cost_category: 'Labor', - amount: 250000, - date: '2026-02-15', - description: 'Foundation work labor costs', - payment_status: 'Paid', - source: 'manual', - }, - { - id: 'fin-2', - projectId: 'proj-1', - cost_category: 'Material', - amount: 180000, - date: '2026-02-20', - description: 'Cement and steel materials', - payment_status: 'Paid', - source: 'manual', - }, - { - id: 'fin-3', - projectId: 'proj-1', - cost_category: 'Equipment', - amount: 75000, - date: '2026-03-01', - description: 'Crane and excavator rental', - payment_status: 'Pending', - source: 'manual', - }, - { - id: 'fin-4', - projectId: 'proj-2', - cost_category: 'Labor', - amount: 180000, - date: '2026-03-02', - description: 'Site preparation labor', - payment_status: 'Paid', - source: 'manual', - }, - { - id: 'fin-5', - projectId: 'proj-2', - cost_category: 'Material', - amount: 120000, - date: '2026-03-03', - description: 'Concrete and sand materials', - payment_status: 'Paid', - source: 'manual', - }, - { - id: 'fin-6', - projectId: 'proj-3', - cost_category: 'Labor', - amount: 320000, - date: '2026-02-01', - description: 'Road construction labor', - payment_status: 'Paid', - source: 'manual', - }, - { - id: 'fin-7', - projectId: 'proj-3', - cost_category: 'Material', - amount: 450000, - date: '2026-02-10', - description: 'Asphalt and road materials', - payment_status: 'Paid', - source: 'manual', - }, - { - id: 'fin-8', - projectId: 'proj-4', - cost_category: 'Labor', - amount: 150000, - date: '2026-01-15', - description: 'Interior work labor', - payment_status: 'Paid', - source: 'manual', - }, -]; - -export const mockVendors = [ - { - id: 'vendor-1', - vendor_name: 'Birla Building Supplies', - contact: '+91-11-23456789', - email: 'sales@birlasupplies.in', - address: 'Plot 45, Industrial Area, Noida, UP', - rating: 4.5, - }, - { - id: 'vendor-2', - vendor_name: 'Tata Steel Distributors', - contact: '+91-22-23456789', - email: 'orders@tatasteel.in', - address: '12 MIDC Road, Pune, Maharashtra', - rating: 4.2, - }, - { - id: 'vendor-3', - vendor_name: 'SafeGuard Equipments', - contact: '+91-40-23456789', - email: 'support@safeguard.in', - address: '80 Kukatpally Industrial Park, Hyderabad, Telangana', - rating: 4.8, - }, -]; - -export const mockPurchaseOrders = [ - { - id: 'po-1', - projectId: 'proj-1', - vendorId: 'vendor-1', - itemId: 'inv-1', - quantity: 300, - unit_price: 8.25, - delivery_status: 'ordered', - createdAt: '2026-03-09', - expectedDelivery: '2026-03-15', - }, - { - id: 'po-2', - projectId: 'proj-2', - vendorId: 'vendor-2', - itemId: 'inv-2', - quantity: 15, - unit_price: 640, - delivery_status: 'delivered', - createdAt: '2026-03-01', - expectedDelivery: '2026-03-05', - deliveredAt: '2026-03-04', - }, -]; - -export const mockMaterialIssues = [ - { - id: 'mi-1', - projectId: 'proj-1', - taskId: 'task-1', - itemId: 'inv-1', - quantity: 120, - issued_by: 'user-4', - issuedAt: '2026-03-10', - }, - { - id: 'mi-2', - projectId: 'proj-2', - taskId: 'task-5', - itemId: 'inv-7', - quantity: 90, - issued_by: 'user-4', - issuedAt: '2026-03-08', - }, -]; - -export const mockWorkerAssignments = [ - { - id: 'wa-1', - workerId: 'worker-1', - taskId: 'task-1', - from_date: '2026-03-01', - to_date: '2026-03-12', - }, - { - id: 'wa-2', - workerId: 'worker-3', - taskId: 'task-3', - from_date: '2026-03-11', - to_date: '2026-03-22', - }, -]; - -export const mockAttendance = [ - { - id: 'att-1', - workerId: 'worker-1', - date: '2026-03-10', - status: 'Present', - hours_worked: 8, - labor_cost: 650, - projectId: 'proj-1', - recorded_by: 'user-3', - }, - { - id: 'att-2', - workerId: 'worker-2', - date: '2026-03-10', - status: 'Half Day', - hours_worked: 4, - labor_cost: 350, - projectId: 'proj-1', - recorded_by: 'user-3', - }, - { - id: 'att-3', - workerId: 'worker-5', - date: '2026-03-10', - status: 'Absent', - hours_worked: 0, - labor_cost: 0, - projectId: 'proj-2', - recorded_by: 'user-3', - }, -]; - -export const mockProjectMembers = [ - { - id: 'pm-1', - projectId: 'proj-1', - userId: 'user-3', - project_role: 'Site_Engineer', - }, - { - id: 'pm-2', - projectId: 'proj-2', - userId: 'user-3', - project_role: 'Site_Engineer', - }, - { - id: 'pm-3', - projectId: 'proj-3', - userId: 'user-4', - project_role: 'Site_Engineer', - }, -]; - -export const mockNotifications = [ - { - id: 'note-1', - type: 'low_stock', - severity: 'high', - title: 'Low stock: Portland Cement', - message: 'Stock dropped below minimum level for Portland Cement.', - createdAt: '2026-03-11T07:00:00.000Z', - read: false, - }, - { - id: 'note-2', - type: 'procurement_delivery', - severity: 'medium', - title: 'PO Delivered', - message: 'Purchase order po-2 has been delivered.', - createdAt: '2026-03-10T14:00:00.000Z', - read: false, - }, -]; - -export const mockLeaveApplications = [ - { - id: 'leave-1', - workerId: 'worker-1', - start_date: '2026-03-20', - end_date: '2026-03-22', - reason: 'Family function', - leave_type: 'Personal', - status: 'Pending', - applied_at: '2026-03-11', - reviewed_by: null, - reviewed_at: null, - }, - { - id: 'leave-2', - workerId: 'worker-2', - start_date: '2026-03-25', - end_date: '2026-03-26', - reason: 'Medical appointment', - leave_type: 'Medical', - status: 'Approved', - applied_at: '2026-03-10', - reviewed_by: 'user-3', - reviewed_at: '2026-03-10', - }, - { - id: 'leave-3', - workerId: 'worker-3', - start_date: '2026-04-01', - end_date: '2026-04-03', - reason: 'Personal work', - leave_type: 'Personal', - status: 'Rejected', - applied_at: '2026-03-09', - reviewed_by: 'user-3', - reviewed_at: '2026-03-09', - rejection_reason: 'Critical project phase — cannot be spared', - }, -]; diff --git a/construction-site-management/src/pages/AuthLogin.jsx b/construction-site-management/src/pages/AuthLogin.jsx deleted file mode 100644 index d9ce871..0000000 --- a/construction-site-management/src/pages/AuthLogin.jsx +++ /dev/null @@ -1,260 +0,0 @@ -/** - * Login Page - * User authentication with email, password, and role selection - */ - -import { useState } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; -import FormInput from '../components/auth/FormInput'; -import Toast from '../components/auth/Toast'; -import { validateFormField } from '../utils/validation'; -import { Loader, Shield, Users, Hammer, HardHat } from 'lucide-react'; - -const ROLES = [ - { - id: 'Admin', - label: 'Admin', - description: 'Full system access', - icon: Shield, - color: 'bg-rose-500/10 border-rose-500', - }, - { - id: 'Project_Manager', - label: 'Project Manager', - description: 'Manage projects & finance', - icon: Users, - color: 'bg-blue-500/10 border-blue-500', - }, - { - id: 'Site_Engineer', - label: 'Site Manager', - description: 'Manage workers, tasks & inventory', - icon: Hammer, - color: 'bg-amber-500/10 border-amber-500', - }, - { - id: 'Worker', - label: 'Worker', - description: 'View attendance & salary', - icon: HardHat, - color: 'bg-emerald-500/10 border-emerald-500', - }, -]; - -export default function AuthLogin() { - const navigate = useNavigate(); - const { login, loading } = useAuth(); - - const [formData, setFormData] = useState({ - email: '', - password: '', - role: 'Site_Engineer', - rememberMe: false, - }); - - const [errors, setErrors] = useState({}); - const [toast, setToast] = useState(null); - - const handleInputChange = (field, value) => { - setFormData(prev => ({ ...prev, [field]: value })); - - if (errors[field]) { - setErrors(prev => ({ ...prev, [field]: '' })); - } - }; - - const handleBlur = (field) => { - const error = validateFormField(field, formData[field]); - if (error) { - setErrors(prev => ({ ...prev, [field]: error })); - } - }; - - const validateForm = () => { - const newErrors = {}; - - const emailError = validateFormField('email', formData.email); - if (emailError) newErrors.email = emailError; - - const passwordError = validateFormField('password', formData.password); - if (passwordError) newErrors.password = passwordError; - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validateForm()) { - setToast({ - type: 'error', - message: 'Please fix the errors above', - }); - return; - } - - const result = await login(formData.email, formData.password, formData.rememberMe, formData.role); - - if (result.success) { - setToast({ - type: 'success', - message: `Login successful as ${formData.role.replace('_', ' ')}!`, - }); - - setTimeout(() => { - navigate(formData.role === 'Worker' ? '/worker' : '/'); - }, 1000); - } else { - setToast({ - type: 'error', - message: result.message, - }); - } - }; - - return ( -
-
- {/* Header */} -
-

SiteOS

-

Construction Site Management

-
- - {/* Toast */} - {toast && ( -
- setToast(null)} - /> -
- )} - - {/* Form */} -
- {/* Credentials Section */} -
-

Login Credentials

- - handleInputChange('email', value)} - onBlur={() => handleBlur('email')} - error={errors.email} - required - placeholder="you@example.com" - /> - - handleInputChange('password', value)} - onBlur={() => handleBlur('password')} - error={errors.password} - required - placeholder="••••••••" - showToggle - /> - - {/* Remember Me */} -
- handleInputChange('rememberMe', e.target.checked)} - className="w-4 h-4 rounded border-slate-800 bg-slate-900 text-amber-500 focus:ring-amber-500 cursor-pointer" - /> - -
-
- - {/* Role Selection Section */} -
-

Select Your Role

-
- {ROLES.map((roleOption) => { - const Icon = roleOption.icon; - const isSelected = formData.role === roleOption.id; - - return ( - - ); - })} -
-
- - {/* Submit Button */} - -
- - {/* Links */} -
- - Forgot Password? - -

- Don't have an account?{' '} - - Sign Up - -

-
- - {/* Demo Info */} -
-

- Demo Mode: Use any email and password to login -

-

- Select a role to see features available for that role -

-
-
-
- ); -} diff --git a/construction-site-management/src/pages/ForgotPassword.jsx b/construction-site-management/src/pages/ForgotPassword.jsx deleted file mode 100644 index d813924..0000000 --- a/construction-site-management/src/pages/ForgotPassword.jsx +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Forgot Password Page - * Request password reset via email - */ - -import { useState } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; -import FormInput from '../components/auth/FormInput'; -import Toast from '../components/auth/Toast'; -import { validateFormField } from '../utils/validation'; -import { Loader, Mail } from 'lucide-react'; - -export default function ForgotPassword() { - const navigate = useNavigate(); - const { requestPasswordReset, loading } = useAuth(); - - const [email, setEmail] = useState(''); - const [error, setError] = useState(''); - const [toast, setToast] = useState(null); - const [submitted, setSubmitted] = useState(false); - - const handleBlur = () => { - const emailError = validateFormField('email', email); - if (emailError) { - setError(emailError); - } - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - const emailError = validateFormField('email', email); - if (emailError) { - setError(emailError); - return; - } - - const result = await requestPasswordReset(email); - - if (result.success) { - setToast({ - type: 'success', - message: 'If email exists, reset link will be sent', - }); - setSubmitted(true); - - // Store reset token for demo purposes - if (result.resetToken) { - sessionStorage.setItem('resetToken', result.resetToken); - } - - setTimeout(() => { - navigate('/reset-password'); - }, 2000); - } else { - setToast({ - type: 'error', - message: result.message, - }); - } - }; - - return ( -
-
- {/* Header */} -
-
- -
-

Reset Password

-

- Enter your email address and we'll send you a link to reset your password -

-
- - {/* Toast */} - {toast && ( -
- setToast(null)} - /> -
- )} - - {/* Form */} - {!submitted ? ( -
- - - {/* Submit Button */} - - - ) : ( -
-
- -
-

Check your email

-

- We've sent a password reset link to {email} -

-
- )} - - {/* Footer */} -
- - Back to Login - -
-
-
- ); -} diff --git a/construction-site-management/src/pages/Login.jsx b/construction-site-management/src/pages/Login.jsx deleted file mode 100644 index cf1f20b..0000000 --- a/construction-site-management/src/pages/Login.jsx +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Login Page - * User selection for demo purposes - * Simulates login by selecting a user from available users - */ - -import { useContext, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { AppContext } from '../context/AppContext'; -import { Button, Card } from '../components/ui'; -import { LogIn } from 'lucide-react'; - -const Login = () => { - const { users, login } = useContext(AppContext); - const navigate = useNavigate(); - const [selectedUserId, setSelectedUserId] = useState(''); - - const handleLogin = () => { - if (selectedUserId) { - login(selectedUserId); - navigate('/'); - } - }; - - return ( -
- -
-

- SiteOS Enterprise -

-

- Construction Site Management System -

-
- -
-
- - -
- - -
- -
-

- Demo Mode: Select any user to login -

-
-
-
- ); -}; - -export default Login; diff --git a/construction-site-management/src/pages/ResetPassword.jsx b/construction-site-management/src/pages/ResetPassword.jsx deleted file mode 100644 index 9439af1..0000000 --- a/construction-site-management/src/pages/ResetPassword.jsx +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Reset Password Page - * Set new password with reset token - */ - -import { useState, useEffect } from 'react'; -import { useNavigate, useSearchParams, Link } from 'react-router-dom'; -import { useAuth } from '../hooks/useAuth'; -import FormInput from '../components/auth/FormInput'; -import PasswordStrengthIndicator from '../components/auth/PasswordStrengthIndicator'; -import Toast from '../components/auth/Toast'; -import { validateFormField, validatePasswordsMatch } from '../utils/validation'; -import { Loader } from 'lucide-react'; - -export default function ResetPassword() { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const { resetPassword, loading } = useAuth(); - - const [resetToken, setResetToken] = useState(''); - const [formData, setFormData] = useState({ - newPassword: '', - confirmPassword: '', - }); - - const [errors, setErrors] = useState({}); - const [toast, setToast] = useState(null); - - useEffect(() => { - // Get reset token from URL or session storage - const tokenFromUrl = searchParams.get('token'); - const tokenFromSession = sessionStorage.getItem('resetToken'); - const token = tokenFromUrl || tokenFromSession; - - if (!token) { - setToast({ - type: 'error', - message: 'Invalid or expired reset link', - }); - setTimeout(() => { - navigate('/forgot-password'); - }, 2000); - } else { - setResetToken(token); - } - }, [searchParams, navigate]); - - const handleInputChange = (field, value) => { - setFormData(prev => ({ ...prev, [field]: value })); - - if (errors[field]) { - setErrors(prev => ({ ...prev, [field]: '' })); - } - }; - - const handleBlur = (field) => { - const error = validateFormField(field, formData[field]); - if (error) { - setErrors(prev => ({ ...prev, [field]: error })); - } - }; - - const validateForm = () => { - const newErrors = {}; - - const passwordError = validateFormField('password', formData.newPassword); - if (passwordError) newErrors.newPassword = passwordError; - - const matchError = validatePasswordsMatch(formData.newPassword, formData.confirmPassword); - if (matchError) newErrors.confirmPassword = matchError; - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e) => { - e.preventDefault(); - - if (!validateForm()) { - setToast({ - type: 'error', - message: 'Please fix the errors above', - }); - return; - } - - const result = await resetPassword(resetToken, formData.newPassword); - - if (result.success) { - setToast({ - type: 'success', - message: 'Password reset successfully!', - }); - - setTimeout(() => { - sessionStorage.removeItem('resetToken'); - navigate('/login'); - }, 1500); - } else { - setToast({ - type: 'error', - message: result.message, - }); - } - }; - - if (!resetToken) { - return ( -
-
- {toast && ( - setToast(null)} - /> - )} -
-
- ); - } - - return ( -
-
- {/* Header */} -
-

Set New Password

-

Enter your new password below

-
- - {/* Toast */} - {toast && ( -
- setToast(null)} - /> -
- )} - - {/* Form */} -
-
- handleInputChange('newPassword', value)} - onBlur={() => handleBlur('newPassword')} - error={errors.newPassword} - required - placeholder="••••••••" - showToggle - /> - {formData.newPassword && ( -
- -
- )} -
- - handleInputChange('confirmPassword', value)} - onBlur={() => handleBlur('confirmPassword')} - error={errors.confirmPassword} - required - placeholder="••••••••" - showToggle - /> - - {/* Submit Button */} - - - - {/* Footer */} -
- - Back to Login - -
-
-
- ); -} diff --git a/construction-site-management/src/pages/Workforce.jsx b/construction-site-management/src/pages/Workforce.jsx deleted file mode 100644 index 5b56758..0000000 --- a/construction-site-management/src/pages/Workforce.jsx +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Workforce Page - * Worker management with attendance tracking - * Features: worker table, attendance buttons, add/delete workers - * Role-based visibility - Admin and Site Engineer only - */ - -import { useContext, useState } from 'react'; -import { AppContext } from '../context/AppContext'; -import { useAuth } from '../hooks/useAuth'; -import { Card, Button, Input, Select, Modal, Table, Badge } from '../components/ui'; -import { Plus, Trash2, Edit2, Lock } from 'lucide-react'; - -const Workforce = () => { - const { workers, updateWorkerAttendance } = useContext(AppContext); - const { user } = useAuth(); - - const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedDate, setSelectedDate] = useState( - new Date().toISOString().split('T')[0] - ); - const [formData, setFormData] = useState({ - name: '', - skill_type: 'Mason', - contact: '', - rate_type: 'Daily', - base_rate: '', - }); - - // Check if user can manage workforce (Admin and Site Engineer only) - const canManageWorkforce = ['Admin', 'Project_Manager', 'Site_Engineer'].includes(user?.role); - - // Render access denied for other roles - if (!canManageWorkforce) { - return ( -
-
-

Workforce

-

Manage workers and attendance

-
- -
- -

- You don't have access to workforce management. Only Admin and Site Engineers can view this section. -

-
-
-
- ); - } - - // Handle attendance update - const handleAttendance = (workerId, status) => { - updateWorkerAttendance(workerId, status, selectedDate); - }; - - // Get attendance for selected date - const getAttendanceStatus = (worker) => { - const attendance = worker.attendance.find((a) => a.date === selectedDate); - return attendance?.status || null; - }; - - // Attendance button component - const AttendanceButtons = ({ worker }) => { - const currentStatus = getAttendanceStatus(worker); - - return ( -
- - - -
- ); - }; - - // Table columns - const columns = [ - { key: 'name', label: 'Name' }, - { key: 'skill_type', label: 'Skill' }, - { key: 'contact', label: 'Contact' }, - { key: 'rate_type', label: 'Rate Type' }, - { - key: 'base_rate', - label: 'Base Rate', - render: (value, row) => - `₹${value}/${row.rate_type === 'Daily' ? 'day' : 'hr'}`, - }, - { - key: 'id', - label: 'Attendance', - render: (value, row) => , - }, - ]; - - return ( -
-
-
-

Workforce

-

Manage workers and attendance

-
- -
- - {/* Date Selector */} - -
- - setSelectedDate(e.target.value)} - className="px-4 py-2 bg-slate-900 border border-slate-800 rounded-lg text-slate-50 focus:outline-none focus:border-amber-500" - /> -
-
- - {/* Workers Table */} - -
- - - - {columns.map((col) => ( - - ))} - - - - {workers.length === 0 ? ( - - - - ) : ( - workers.map((worker) => ( - - {columns.map((col) => ( - - ))} - - )) - )} - -
- {col.label} -
- No workers available -
- {col.render - ? col.render(worker[col.key], worker) - : worker[col.key]} -
-
-
- - {/* Attendance Summary */} - -
-
-

Present

-

- {workers.filter( - (w) => getAttendanceStatus(w) === 'Present' - ).length} -

-
-
-

Half Day

-

- {workers.filter( - (w) => getAttendanceStatus(w) === 'Half Day' - ).length} -

-
-
-

Absent

-

- {workers.filter( - (w) => getAttendanceStatus(w) === 'Absent' - ).length} -

-
-
-
-
- ); -}; - -export default Workforce; diff --git a/construction-site-management/src/services/authService.js b/construction-site-management/src/services/authService.js deleted file mode 100644 index 1d867dc..0000000 --- a/construction-site-management/src/services/authService.js +++ /dev/null @@ -1,501 +0,0 @@ -/** - * Authentication Service - * Handles user registration, login, password reset, and session management - */ - -import { - hashPassword, - comparePassword, - generateToken, - validateToken, - decodeToken, - generateVerificationCode, - generateResetToken, - encryptData, - decryptData, - generateId, -} from '../utils/crypto'; -import { validateEmail, sanitizeInput } from '../utils/validation'; - -const STORAGE_KEY = 'siteos_users'; -const SESSION_KEY = 'siteos_session'; -const VERIFICATION_KEY = 'siteos_verification'; -const RESET_KEY = 'siteos_reset'; - -class AuthService { - // Get all users from localStorage - getAllUsers() { - try { - const encrypted = localStorage.getItem(STORAGE_KEY); - if (!encrypted) return []; - return decryptData(encrypted) || []; - } catch (error) { - console.error('Error getting users:', error); - return []; - } - } - - // Save users to localStorage - saveUsers(users) { - try { - const encrypted = encryptData(users); - localStorage.setItem(STORAGE_KEY, encrypted); - return true; - } catch (error) { - console.error('Error saving users:', error); - return false; - } - } - - // Get user by email - getUserByEmail(email) { - const users = this.getAllUsers(); - return users.find(u => u.email.toLowerCase() === email.toLowerCase()); - } - - // Get user by ID - getUserById(userId) { - const users = this.getAllUsers(); - return users.find(u => u.id === userId); - } - - // Sign up new user - signup(name, email, password) { - try { - // Validate inputs - if (!name || !email || !password) { - return { success: false, message: 'All fields are required' }; - } - - // Check if email already exists - if (this.getUserByEmail(email)) { - return { success: false, message: 'Email already registered' }; - } - - // Create new user - const newUser = { - id: generateId(), - name: sanitizeInput(name), - email: email.toLowerCase(), - passwordHash: hashPassword(password), - phone: '', - role: 'Site_Engineer', // Default role - verified: false, - createdAt: new Date().toISOString(), - lastLogin: null, - lastPasswordChange: new Date().toISOString(), - accountLocked: false, - lockUntil: null, - failedLoginAttempts: 0, - }; - - // Add user to storage - const users = this.getAllUsers(); - users.push(newUser); - this.saveUsers(users); - - // Generate verification code - const verificationCode = generateVerificationCode(); - this.storeVerificationCode(email, verificationCode); - - return { - success: true, - message: 'Account created successfully. Please verify your email.', - userId: newUser.id, - verificationCode, // For demo purposes - }; - } catch (error) { - console.error('Signup error:', error); - return { success: false, message: 'An error occurred during signup' }; - } - } - - // Store verification code - storeVerificationCode(email, code) { - try { - const verifications = this.getVerifications(); - verifications[email] = { - code, - expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(), // 5 minutes - attempts: 0, - verified: false, - }; - const encrypted = encryptData(verifications); - localStorage.setItem(VERIFICATION_KEY, encrypted); - } catch (error) { - console.error('Error storing verification code:', error); - } - } - - // Get verifications - getVerifications() { - try { - const encrypted = localStorage.getItem(VERIFICATION_KEY); - if (!encrypted) return {}; - return decryptData(encrypted) || {}; - } catch (error) { - console.error('Error getting verifications:', error); - return {}; - } - } - - // Verify email - verifyEmail(email, code) { - try { - const verifications = this.getVerifications(); - const verification = verifications[email]; - - if (!verification) { - return { success: false, message: 'Verification code not found' }; - } - - if (new Date() > new Date(verification.expiresAt)) { - return { success: false, message: 'Verification code expired' }; - } - - if (verification.code !== code) { - verification.attempts++; - if (verification.attempts >= 3) { - delete verifications[email]; - } - const encrypted = encryptData(verifications); - localStorage.setItem(VERIFICATION_KEY, encrypted); - return { success: false, message: 'Invalid verification code' }; - } - - // Mark user as verified - const users = this.getAllUsers(); - const user = this.getUserByEmail(email); - if (user) { - user.verified = true; - this.saveUsers(users); - } - - // Remove verification code - delete verifications[email]; - const encrypted = encryptData(verifications); - localStorage.setItem(VERIFICATION_KEY, encrypted); - - return { success: true, message: 'Email verified successfully' }; - } catch (error) { - console.error('Email verification error:', error); - return { success: false, message: 'An error occurred during verification' }; - } - } - - // Resend verification code - resendVerificationCode(email) { - try { - const user = this.getUserByEmail(email); - if (!user) { - return { success: false, message: 'User not found' }; - } - - const verificationCode = generateVerificationCode(); - this.storeVerificationCode(email, verificationCode); - - return { - success: true, - message: 'Verification code sent to your email', - verificationCode, // For demo purposes - }; - } catch (error) { - console.error('Resend verification error:', error); - return { success: false, message: 'An error occurred' }; - } - } - - // Login user - login(email, password, rememberMe = false) { - try { - const user = this.getUserByEmail(email); - - if (!user) { - return { success: false, message: 'Invalid email or password' }; - } - - if (user.accountLocked && new Date() < new Date(user.lockUntil)) { - return { success: false, message: 'Account locked. Try again later.' }; - } - - // Skip email verification for now - allow login without verification - // if (!user.verified) { - // return { success: false, message: 'Please verify your email first' }; - // } - - if (!comparePassword(password, user.passwordHash)) { - user.failedLoginAttempts++; - if (user.failedLoginAttempts >= 5) { - user.accountLocked = true; - user.lockUntil = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // 30 minutes - } - this.saveUsers(this.getAllUsers()); - return { success: false, message: 'Invalid email or password' }; - } - - // Reset failed attempts - user.failedLoginAttempts = 0; - user.accountLocked = false; - user.lastLogin = new Date().toISOString(); - this.saveUsers(this.getAllUsers()); - - // Generate token - const token = generateToken(user.id); - - // Store session - const session = { - token, - user: { - id: user.id, - name: user.name, - email: user.email, - role: user.role, - phone: user.phone, - verified: user.verified, - createdAt: user.createdAt, - lastLogin: user.lastLogin, - }, - rememberMe, - loginTime: new Date().toISOString(), - lastActivity: new Date().toISOString(), - }; - - const encrypted = encryptData(session); - localStorage.setItem(SESSION_KEY, encrypted); - - return { - success: true, - message: 'Login successful', - token, - user: session.user, - }; - } catch (error) { - console.error('Login error:', error); - return { success: false, message: 'An error occurred during login' }; - } - } - - // Logout user - logout() { - try { - localStorage.removeItem(SESSION_KEY); - return { success: true, message: 'Logged out successfully' }; - } catch (error) { - console.error('Logout error:', error); - return { success: false, message: 'An error occurred during logout' }; - } - } - - // Get current session - getSession() { - try { - const encrypted = localStorage.getItem(SESSION_KEY); - if (!encrypted) return null; - - const session = decryptData(encrypted); - if (!session) return null; - - // Check token validity - if (!validateToken(session.token)) { - this.logout(); - return null; - } - - // Check session timeout (24 hours) - const lastActivity = new Date(session.lastActivity); - const now = new Date(); - if (now - lastActivity > 24 * 60 * 60 * 1000) { - this.logout(); - return null; - } - - // Update last activity - session.lastActivity = new Date().toISOString(); - const newEncrypted = encryptData(session); - localStorage.setItem(SESSION_KEY, newEncrypted); - - return session; - } catch (error) { - console.error('Error getting session:', error); - return null; - } - } - - // Request password reset - requestPasswordReset(email) { - try { - const user = this.getUserByEmail(email); - - if (!user) { - // Return generic message for security - return { success: true, message: 'If email exists, reset link will be sent' }; - } - - const resetToken = generateResetToken(); - const resets = this.getResets(); - resets[resetToken] = { - email, - expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour - used: false, - }; - - const encrypted = encryptData(resets); - localStorage.setItem(RESET_KEY, encrypted); - - return { - success: true, - message: 'If email exists, reset link will be sent', - resetToken, // For demo purposes - }; - } catch (error) { - console.error('Password reset request error:', error); - return { success: false, message: 'An error occurred' }; - } - } - - // Get resets - getResets() { - try { - const encrypted = localStorage.getItem(RESET_KEY); - if (!encrypted) return {}; - return decryptData(encrypted) || {}; - } catch (error) { - console.error('Error getting resets:', error); - return {}; - } - } - - // Reset password - resetPassword(resetToken, newPassword) { - try { - const resets = this.getResets(); - const reset = resets[resetToken]; - - if (!reset) { - return { success: false, message: 'Invalid or expired reset link' }; - } - - if (new Date() > new Date(reset.expiresAt)) { - delete resets[resetToken]; - const encrypted = encryptData(resets); - localStorage.setItem(RESET_KEY, encrypted); - return { success: false, message: 'Reset link expired' }; - } - - if (reset.used) { - return { success: false, message: 'Reset link already used' }; - } - - // Update user password - const users = this.getAllUsers(); - const user = this.getUserByEmail(reset.email); - - if (!user) { - return { success: false, message: 'User not found' }; - } - - user.passwordHash = hashPassword(newPassword); - user.lastPasswordChange = new Date().toISOString(); - this.saveUsers(users); - - // Mark reset as used - reset.used = true; - const newEncrypted = encryptData(resets); - localStorage.setItem(RESET_KEY, newEncrypted); - - return { success: true, message: 'Password reset successfully' }; - } catch (error) { - console.error('Password reset error:', error); - return { success: false, message: 'An error occurred during password reset' }; - } - } - - // Update profile - updateProfile(userId, updates) { - try { - const users = this.getAllUsers(); - const user = this.getUserById(userId); - - if (!user) { - return { success: false, message: 'User not found' }; - } - - if (updates.name) user.name = sanitizeInput(updates.name); - if (updates.phone) user.phone = sanitizeInput(updates.phone); - - this.saveUsers(users); - - // Update session - const session = this.getSession(); - if (session) { - session.user = { - ...session.user, - name: user.name, - phone: user.phone, - }; - const encrypted = encryptData(session); - localStorage.setItem(SESSION_KEY, encrypted); - } - - return { success: true, message: 'Profile updated successfully', user }; - } catch (error) { - console.error('Profile update error:', error); - return { success: false, message: 'An error occurred during profile update' }; - } - } - - // Change password - changePassword(userId, currentPassword, newPassword) { - try { - const user = this.getUserById(userId); - - if (!user) { - return { success: false, message: 'User not found' }; - } - - if (!comparePassword(currentPassword, user.passwordHash)) { - return { success: false, message: 'Current password is incorrect' }; - } - - user.passwordHash = hashPassword(newPassword); - user.lastPasswordChange = new Date().toISOString(); - this.saveUsers(this.getAllUsers()); - - return { success: true, message: 'Password changed successfully' }; - } catch (error) { - console.error('Change password error:', error); - return { success: false, message: 'An error occurred during password change' }; - } - } - - // Delete account - deleteAccount(userId, password) { - try { - const user = this.getUserById(userId); - - if (!user) { - return { success: false, message: 'User not found' }; - } - - if (!comparePassword(password, user.passwordHash)) { - return { success: false, message: 'Password is incorrect' }; - } - - const users = this.getAllUsers(); - const index = users.findIndex(u => u.id === userId); - if (index > -1) { - users.splice(index, 1); - this.saveUsers(users); - } - - this.logout(); - - return { success: true, message: 'Account deleted successfully' }; - } catch (error) { - console.error('Delete account error:', error); - return { success: false, message: 'An error occurred during account deletion' }; - } - } -} - -export default new AuthService(); diff --git a/construction-site-management/src/utils/.gitkeep b/construction-site-management/src/utils/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..14ea4ad --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=/api diff --git a/construction-site-management/eslint.config.js b/frontend/eslint.config.js similarity index 100% rename from construction-site-management/eslint.config.js rename to frontend/eslint.config.js diff --git a/construction-site-management/index.html b/frontend/index.html similarity index 100% rename from construction-site-management/index.html rename to frontend/index.html diff --git a/construction-site-management/package-lock.json b/frontend/package-lock.json similarity index 100% rename from construction-site-management/package-lock.json rename to frontend/package-lock.json diff --git a/construction-site-management/package.json b/frontend/package.json similarity index 100% rename from construction-site-management/package.json rename to frontend/package.json diff --git a/construction-site-management/postcss.config.js b/frontend/postcss.config.js similarity index 100% rename from construction-site-management/postcss.config.js rename to frontend/postcss.config.js diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..656bf20 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/construction-site-management/public/vite.svg b/frontend/public/vite.svg similarity index 100% rename from construction-site-management/public/vite.svg rename to frontend/public/vite.svg diff --git a/construction-site-management/src/App.css b/frontend/src/App.css similarity index 100% rename from construction-site-management/src/App.css rename to frontend/src/App.css diff --git a/construction-site-management/src/App.jsx b/frontend/src/App.jsx similarity index 98% rename from construction-site-management/src/App.jsx rename to frontend/src/App.jsx index b7832a3..3ffb2f6 100644 --- a/construction-site-management/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,8 +6,7 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { AuthProvider } from './context/AuthContext'; -import { AppProvider, AppContext } from './context/AppContext'; -import { useContext } from 'react'; +import { AppProvider } from './context/AppContext'; import { useAuth } from './hooks/useAuth'; import ProtectedRoute from './components/ProtectedRoute'; import { AppLayout } from './components/layout'; diff --git a/construction-site-management/src/assets/react.svg b/frontend/src/assets/react.svg similarity index 100% rename from construction-site-management/src/assets/react.svg rename to frontend/src/assets/react.svg diff --git a/construction-site-management/src/components/ProtectedRoute.jsx b/frontend/src/components/ProtectedRoute.jsx similarity index 92% rename from construction-site-management/src/components/ProtectedRoute.jsx rename to frontend/src/components/ProtectedRoute.jsx index dde7dc5..00dc1dd 100644 --- a/construction-site-management/src/components/ProtectedRoute.jsx +++ b/frontend/src/components/ProtectedRoute.jsx @@ -12,7 +12,7 @@ const ProtectedRoute = ({ children, allowedRoles = [] }) => { if (loading) { return ( -
+
); diff --git a/construction-site-management/src/components/auth/FormInput.jsx b/frontend/src/components/auth/FormInput.jsx similarity index 94% rename from construction-site-management/src/components/auth/FormInput.jsx rename to frontend/src/components/auth/FormInput.jsx index e4e4949..07f304e 100644 --- a/construction-site-management/src/components/auth/FormInput.jsx +++ b/frontend/src/components/auth/FormInput.jsx @@ -43,7 +43,7 @@ export default function FormInput({ onBlur={onBlur} placeholder={placeholder} disabled={disabled} - className={`w-full px-4 py-2 bg-slate-900 border rounded-lg text-slate-50 placeholder-slate-500 focus:outline-none transition-colors ${ + className={`w-full px-4 py-2 bg-slate-900 border rounded-lg text-slate-100 placeholder-slate-500 focus:outline-none transition-colors ${ hasError ? 'border-rose-500 focus:border-rose-500' : isValid @@ -57,7 +57,7 @@ export default function FormInput({
diff --git a/construction-site-management/src/components/auth/Toast.jsx b/frontend/src/components/auth/Toast.jsx similarity index 100% rename from construction-site-management/src/components/auth/Toast.jsx rename to frontend/src/components/auth/Toast.jsx diff --git a/construction-site-management/src/components/charts/.gitkeep b/frontend/src/components/charts/.gitkeep similarity index 100% rename from construction-site-management/src/components/charts/.gitkeep rename to frontend/src/components/charts/.gitkeep diff --git a/construction-site-management/src/components/charts/BudgetChart.jsx b/frontend/src/components/charts/BudgetChart.jsx similarity index 88% rename from construction-site-management/src/components/charts/BudgetChart.jsx rename to frontend/src/components/charts/BudgetChart.jsx index 653256a..400524d 100644 --- a/construction-site-management/src/components/charts/BudgetChart.jsx +++ b/frontend/src/components/charts/BudgetChart.jsx @@ -22,10 +22,10 @@ const BudgetChart = ({ data = [] }) => { - + { cursor={{ fill: 'rgba(245, 158, 11, 0.1)' }} /> diff --git a/construction-site-management/src/components/charts/CostDistributionChart.jsx b/frontend/src/components/charts/CostDistributionChart.jsx similarity index 79% rename from construction-site-management/src/components/charts/CostDistributionChart.jsx rename to frontend/src/components/charts/CostDistributionChart.jsx index 1374afb..0b8c136 100644 --- a/construction-site-management/src/components/charts/CostDistributionChart.jsx +++ b/frontend/src/components/charts/CostDistributionChart.jsx @@ -15,8 +15,12 @@ import { const COLORS = ['#f59e0b', '#475569']; -const renderCustomLabel = ({ name, value, percent }) => { - return `${name}: ${(percent * 100).toFixed(0)}%`; +const renderCustomLabel = ({ x, y, name, value, percent }) => { + return ( + 0 ? "start" : "end"} dominantBaseline="central"> + {`${name}: ${(percent * 100).toFixed(0)}%`} + + ); }; const CostDistributionChart = ({ data = [] }) => { @@ -47,7 +51,7 @@ const CostDistributionChart = ({ data = [] }) => { formatter={(value) => `$${value.toLocaleString()}`} /> diff --git a/construction-site-management/src/components/charts/index.js b/frontend/src/components/charts/index.js similarity index 100% rename from construction-site-management/src/components/charts/index.js rename to frontend/src/components/charts/index.js diff --git a/frontend/src/components/forms/FormInput.jsx b/frontend/src/components/forms/FormInput.jsx new file mode 100644 index 0000000..38b3fdf --- /dev/null +++ b/frontend/src/components/forms/FormInput.jsx @@ -0,0 +1,96 @@ +/** + * FormInput Component + * Reusable form input with validation and error display + */ + +import { useState } from 'react'; +import { Eye, EyeOff, CheckCircle, AlertCircle } from 'lucide-react'; + +export default function FormInput({ + label, + type = 'text', + value, + onChange, + error, + required = false, + placeholder, + hint, + disabled = false, + showToggle = false, + onBlur, +}) { + const [showPassword, setShowPassword] = useState(false); + + const isPassword = type === 'password'; + const inputType = isPassword && showPassword ? 'text' : type; + const hasError = !!error; + const isValid = value && !error && type !== 'password'; + + return ( +
+ {label && ( + + )} + +
+ onChange(e.target.value)} + onBlur={onBlur} + placeholder={placeholder} + disabled={disabled} + className={`w-full px-4 py-2.5 bg-slate-900 border rounded-lg text-slate-100 placeholder-slate-500 text-sm focus:outline-none focus:ring-2 transition-all duration-200 ${ + hasError + ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20' + : isValid + ? 'border-emerald-500 focus:border-emerald-500 focus:ring-emerald-500/20' + : 'border-slate-800 focus:border-amber-500 focus:ring-amber-500/20' + } ${disabled ? 'opacity-50 cursor-not-allowed bg-slate-900' : ''}`} + /> + + {/* Show/Hide Password Toggle */} + {isPassword && showToggle && ( + + )} + + {/* Success Icon */} + {isValid && ( +
+ +
+ )} + + {/* Error Icon */} + {hasError && ( +
+ +
+ )} +
+ + {/* Error Message */} + {hasError && ( +

+ + {error} +

+ )} + + {/* Hint Text */} + {hint && !hasError && ( +

{hint}

+ )} +
+ ); +} diff --git a/frontend/src/components/forms/PasswordStrengthIndicator.jsx b/frontend/src/components/forms/PasswordStrengthIndicator.jsx new file mode 100644 index 0000000..a7ddd0a --- /dev/null +++ b/frontend/src/components/forms/PasswordStrengthIndicator.jsx @@ -0,0 +1,96 @@ +/** + * PasswordStrengthIndicator Component + * Displays password strength meter and requirements + */ + +import { getPasswordStrength, getPasswordStrengthLabel, getPasswordRequirements } from '../../utils/validation'; +import { Check, X } from 'lucide-react'; + +export default function PasswordStrengthIndicator({ password, showRequirements = true }) { + const strength = getPasswordStrength(password); + const label = getPasswordStrengthLabel(strength); + const requirements = getPasswordRequirements(password); + + const strengthColors = [ + 'bg-red-500', + 'bg-primary-100 text-primary-600', + 'bg-yellow-500', + 'bg-blue-500', + 'bg-emerald-500', + ]; + + return ( +
+ {/* Strength Meter */} +
+
+ Password Strength + + {label} + +
+ +
+ {[0, 1, 2, 3, 4].map((i) => ( +
+ ))} +
+
+ + {/* Requirements Checklist */} + {showRequirements && ( +
+

Requirements:

+
+ + + + + +
+
+ )} +
+ ); +} + +function RequirementItem({ met, label }) { + return ( +
+ {met ? ( + + ) : ( + + )} + + {label} + +
+ ); +} diff --git a/frontend/src/components/forms/Toast.jsx b/frontend/src/components/forms/Toast.jsx new file mode 100644 index 0000000..c6d7306 --- /dev/null +++ b/frontend/src/components/forms/Toast.jsx @@ -0,0 +1,61 @@ +/** + * Toast Component + * Notification component for success, error, warning, info messages + */ + +import { useEffect } from 'react'; +import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react'; + +export default function Toast({ type = 'info', message, duration = 5000, onClose }) { + useEffect(() => { + if (duration > 0) { + const timer = setTimeout(onClose, duration); + return () => clearTimeout(timer); + } + }, [duration, onClose]); + + const typeConfig = { + success: { + bg: 'bg-emerald-50', + border: 'border-emerald-200', + text: 'text-emerald-800', + icon: CheckCircle, + }, + error: { + bg: 'bg-red-50', + border: 'border-red-200', + text: 'text-red-800', + icon: AlertCircle, + }, + warning: { + bg: 'bg-amber-50', + border: 'border-amber-200', + text: 'text-amber-800', + icon: AlertTriangle, + }, + info: { + bg: 'bg-blue-50', + border: 'border-blue-200', + text: 'text-blue-800', + icon: Info, + }, + }; + + const config = typeConfig[type] || typeConfig.info; + const Icon = config.icon; + + return ( +
+ +

{message}

+ +
+ ); +} diff --git a/frontend/src/components/forms/index.js b/frontend/src/components/forms/index.js new file mode 100644 index 0000000..45ec805 --- /dev/null +++ b/frontend/src/components/forms/index.js @@ -0,0 +1,3 @@ +export { default as FormInput } from './FormInput'; +export { default as PasswordStrengthIndicator } from './PasswordStrengthIndicator'; +export { default as Toast } from './Toast'; diff --git a/construction-site-management/src/components/layout/.gitkeep b/frontend/src/components/layout/.gitkeep similarity index 100% rename from construction-site-management/src/components/layout/.gitkeep rename to frontend/src/components/layout/.gitkeep diff --git a/construction-site-management/src/components/layout/AppLayout.jsx b/frontend/src/components/layout/AppLayout.jsx similarity index 91% rename from construction-site-management/src/components/layout/AppLayout.jsx rename to frontend/src/components/layout/AppLayout.jsx index 428d480..062ae42 100644 --- a/construction-site-management/src/components/layout/AppLayout.jsx +++ b/frontend/src/components/layout/AppLayout.jsx @@ -10,7 +10,7 @@ import Navbar from './Navbar'; const AppLayout = () => { return ( -
+
diff --git a/frontend/src/components/layout/Navbar.jsx b/frontend/src/components/layout/Navbar.jsx new file mode 100644 index 0000000..46d7f75 --- /dev/null +++ b/frontend/src/components/layout/Navbar.jsx @@ -0,0 +1,203 @@ +/** + * Navbar Component + * Dark Theme Header + */ + +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../hooks/useAuth'; +import { useContext, useMemo } from 'react'; +import { AppContext } from '../../context/AppContext'; +import { Bell, LogOut, Search, Check } from 'lucide-react'; + +const Navbar = () => { + const { user, logout } = useAuth(); + const { projects, tasks, workers, inventory, vendors, notifications, unreadNotificationCount, markNotificationRead, markAllNotificationsRead } = useContext(AppContext); + const navigate = useNavigate(); + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const [isNotifOpen, setIsNotifOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const handleLogout = async () => { + await logout(); + navigate('/login'); + }; + + const today = new Date().toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + const globalResults = useMemo(() => { + if (!searchQuery.trim()) return []; + const query = searchQuery.toLowerCase(); + const pool = [ + ...projects.map(item => ({ id: item.id, type: 'Project', label: item.project_name, path: '/projects' })), + ...tasks.map(item => ({ id: item.id, type: 'Task', label: item.task_name, path: '/tasks' })), + ...workers.map(item => ({ id: item.id, type: 'Worker', label: item.name, path: '/workforce' })), + ...inventory.map(item => ({ id: item.id, type: 'Inventory', label: item.item_name, path: '/inventory' })), + ...vendors.map(item => ({ id: item.id, type: 'Vendor', label: item.vendor_name, path: '/vendors' })), + ]; + return pool.filter(entry => entry.label.toLowerCase().includes(query)).slice(0, 6); + }, [inventory, projects, searchQuery, tasks, vendors, workers]); + + const recentNotifications = useMemo(() => notifications.slice(0, 8), [notifications]); + + return ( + + ); +}; + +export default Navbar; diff --git a/construction-site-management/src/components/layout/Sidebar.jsx b/frontend/src/components/layout/Sidebar.jsx similarity index 77% rename from construction-site-management/src/components/layout/Sidebar.jsx rename to frontend/src/components/layout/Sidebar.jsx index e3a133f..6259140 100644 --- a/construction-site-management/src/components/layout/Sidebar.jsx +++ b/frontend/src/components/layout/Sidebar.jsx @@ -17,7 +17,7 @@ import { FileSpreadsheet, Menu, X, - DollarSign, + IndianRupee, FileText, HardHat, } from 'lucide-react'; @@ -35,7 +35,6 @@ const Sidebar = () => { // Role-based navigation visibility const navigationItems = [ - // Admin & Project Manager { label: 'Dashboard', path: '/', @@ -149,7 +148,7 @@ const Sidebar = () => { { label: 'Salary', path: '/worker/salary', - icon: DollarSign, + icon: IndianRupee, roles: ['Worker'], }, ]; @@ -169,21 +168,26 @@ const Sidebar = () => { {/* Mobile menu button */} {/* Sidebar */} @@ -220,7 +224,7 @@ const Sidebar = () => { {/* Mobile overlay */} {isOpen && (
setIsOpen(false)} /> )} diff --git a/construction-site-management/src/components/layout/index.js b/frontend/src/components/layout/index.js similarity index 100% rename from construction-site-management/src/components/layout/index.js rename to frontend/src/components/layout/index.js diff --git a/construction-site-management/src/components/ui/.gitkeep b/frontend/src/components/ui/.gitkeep similarity index 100% rename from construction-site-management/src/components/ui/.gitkeep rename to frontend/src/components/ui/.gitkeep diff --git a/frontend/src/components/ui/Badge.jsx b/frontend/src/components/ui/Badge.jsx new file mode 100644 index 0000000..10905f7 --- /dev/null +++ b/frontend/src/components/ui/Badge.jsx @@ -0,0 +1,25 @@ +/** + * Badge Component + * Professional Dark Theme Badges + */ + +const Badge = ({ children, variant = 'default', className = '' }) => { + const variants = { + default: 'bg-slate-800 text-slate-300 border-slate-700', + success: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.1)]', + warning: 'bg-amber-500/10 text-amber-400 border-amber-500/20 shadow-[0_0_10px_rgba(245,158,11,0.1)]', + danger: 'bg-rose-500/10 text-rose-400 border-rose-500/20 shadow-[0_0_10px_rgba(225,29,72,0.1)]', + status: 'bg-primary-500/10 text-primary-400 border-primary-500/20 shadow-[0_0_10px_rgba(79,70,229,0.1)]', + info: 'bg-sky-500/10 text-sky-400 border-sky-500/20 shadow-[0_0_10px_rgba(14,165,233,0.1)]', + }; + + const style = variants[variant] || variants.default; + + return ( + + {children} + + ); +}; + +export default Badge; diff --git a/frontend/src/components/ui/Button.jsx b/frontend/src/components/ui/Button.jsx new file mode 100644 index 0000000..7127e35 --- /dev/null +++ b/frontend/src/components/ui/Button.jsx @@ -0,0 +1,43 @@ +/** + * Button Component + * Premium Dark Theme + */ +const Button = ({ + variant = 'primary', + size = 'md', + onClick, + disabled = false, + children, + className = '', + type = 'button', + ...props +}) => { + const baseStyles = 'font-medium rounded-lg transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-950 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center'; + + const variantStyles = { + primary: 'bg-primary-600 hover:bg-primary-500 text-white focus:ring-primary-500 shadow-[0_0_15px_rgba(79,70,229,0.3)] hover:shadow-[0_0_20px_rgba(79,70,229,0.5)] border border-primary-500', + secondary: 'bg-slate-800 hover:bg-slate-700 text-slate-200 focus:ring-slate-600 border border-slate-700', + danger: 'bg-rose-600 hover:bg-rose-500 text-white focus:ring-rose-500 shadow-[0_0_15px_rgba(225,29,72,0.3)] border border-rose-500', + }; + + const sizeStyles = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-6 py-3 text-base', + }; + + const combinedClassName = `${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`; + + return ( + + ); +}; +export default Button; diff --git a/frontend/src/components/ui/Card.jsx b/frontend/src/components/ui/Card.jsx new file mode 100644 index 0000000..89913b9 --- /dev/null +++ b/frontend/src/components/ui/Card.jsx @@ -0,0 +1,27 @@ +/** + * Card Component + * Premium Dark Theme Card + */ +const Card = ({ + title, + children, + className = '', + headerClassName = '', + bodyClassName = '', + ...props +}) => { + return ( +
+ {title && ( +
+

{title}

+
+ )} +
{children}
+
+ ); +}; +export default Card; diff --git a/frontend/src/components/ui/Input.jsx b/frontend/src/components/ui/Input.jsx new file mode 100644 index 0000000..0088316 --- /dev/null +++ b/frontend/src/components/ui/Input.jsx @@ -0,0 +1,20 @@ +import { forwardRef } from 'react'; + +const Input = forwardRef(({ label, error, className = '', ...props }, ref) => { + return ( +
+ {label && } + + {error && {error}} +
+ ); +}); + +Input.displayName = 'Input'; +export default Input; diff --git a/frontend/src/components/ui/Modal.jsx b/frontend/src/components/ui/Modal.jsx new file mode 100644 index 0000000..554c316 --- /dev/null +++ b/frontend/src/components/ui/Modal.jsx @@ -0,0 +1,40 @@ +/** + * Modal Component + * Dark Theme with glassmorphism + */ +import { X } from 'lucide-react'; + +const Modal = ({ isOpen, onClose, title, children, maxWidth = 'max-w-lg' }) => { + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+ {/* Header */} +
+

{title}

+ +
+ + {/* Body */} +
+ {children} +
+
+
+ ); +}; + +export default Modal; diff --git a/frontend/src/components/ui/Select.jsx b/frontend/src/components/ui/Select.jsx new file mode 100644 index 0000000..514586e --- /dev/null +++ b/frontend/src/components/ui/Select.jsx @@ -0,0 +1,34 @@ +import { forwardRef } from 'react'; +import { ChevronDown } from 'lucide-react'; + +const Select = forwardRef(({ label, options = [], error, className = '', ...props }, ref) => { + return ( +
+ {label && } +
+ + +
+ {error && {error}} +
+ ); +}); + +Select.displayName = 'Select'; +export default Select; diff --git a/frontend/src/components/ui/Table.jsx b/frontend/src/components/ui/Table.jsx new file mode 100644 index 0000000..cfefd2a --- /dev/null +++ b/frontend/src/components/ui/Table.jsx @@ -0,0 +1,68 @@ +/** + * Table Component + * Professional Dark Theme + */ + +const Table = ({ + columns = [], + data = [], + onRowClick, + className = '', + ...props +}) => { + return ( +
+
+ + + + {columns.map((column) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, rowIndex) => ( + onRowClick && onRowClick(row)} + className="bg-transparent hover:bg-slate-800/50 transition-colors cursor-pointer" + > + {columns.map((column) => ( + + ))} + + )) + )} + +
+ {column.label} +
+
+

No data available

+
+
+ {column.render + ? column.render(row[column.key], row) + : row[column.key]} +
+
+
+ ); +}; + +export default Table; diff --git a/construction-site-management/src/components/ui/index.js b/frontend/src/components/ui/index.js similarity index 100% rename from construction-site-management/src/components/ui/index.js rename to frontend/src/components/ui/index.js diff --git a/construction-site-management/src/context/.gitkeep b/frontend/src/context/.gitkeep similarity index 100% rename from construction-site-management/src/context/.gitkeep rename to frontend/src/context/.gitkeep diff --git a/frontend/src/context/AppContext.jsx b/frontend/src/context/AppContext.jsx new file mode 100644 index 0000000..eaf81be --- /dev/null +++ b/frontend/src/context/AppContext.jsx @@ -0,0 +1,1272 @@ +/** + * AppContext + * Global state for SiteOS Enterprise with backend API integration + * ALL data comes from PostgreSQL via API — no mock data + */ + +import { createContext, useState, useCallback, useEffect, useMemo } from 'react'; + +export const AppContext = createContext(); + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; + +const makeId = (prefix = 'id') => `${prefix}-${Math.random().toString(36).slice(2, 10)}-${Date.now().toString(36)}`; + +// ── helpers to normalize DB rows → frontend shape ── +// IDs are always stored as numbers; URL params come in as strings. +// We stringify all IDs to make comparisons safe everywhere. +const sid = (v) => (v == null ? null : String(v)); + +const mapProject = (p) => ({ + id: sid(p.project_id ?? p.id), + project_name: p.project_name, + site_location: p.site_location, + project_type: p.project_type, + start_date: p.start_date, + end_date: p.end_date, + budget: Number(p.budget || 0), + status: p.status || 'Active', + created_by: sid(p.created_by), +}); + +const mapTask = (t) => ({ + id: sid(t.task_id ?? t.id), + projectId: sid(t.project_id), + task_name: t.task_name, + assigned_to: sid(t.assigned_to), + start_date: t.start_date, + end_date: t.end_date, + status: t.status || 'Open', + priority: t.priority || 'Medium', + due_date: t.due_date, + deadline: t.deadline, + progress: t.progress || 0, + workers_assigned: t.workers_assigned || [], + materials_used: t.materials_used || [], + dependencies: t.dependencies || [], +}); + +const mapWorker = (w) => ({ + id: sid(w.worker_id ?? w.id), + user_id: sid(w.user_id), + project_id: sid(w.project_id), + name: w.name, + skill_type: w.skill_type, + contact: w.contact, + rate_type: w.rate_type, + base_rate: Number(w.base_rate || 0), + salary: Number(w.salary || 0), + attendance: w.attendance || [], +}); + +const mapInventory = (i) => ({ + id: sid(i.item_id ?? i.id), + item_name: i.item_name, + category: i.category, + uom: i.uom, + unit_cost: Number(i.unit_cost || 0), + min_stock_qty: Number(i.min_stock_qty || 0), + current_stock: Number(i.current_stock || 0), + supplier: i.supplier, +}); + +const mapVendor = (v) => ({ + id: sid(v.vendor_id ?? v.id), + vendor_name: v.vendor_name, + contact: v.contact, + email: v.email, + address: v.address, + rating: Number(v.rating || 0), +}); + +const mapProcurement = (po) => ({ + id: sid(po.id), + procurement_id: po.procurement_id, + projectId: sid(po.project_id), + vendorId: sid(po.vendor_id), + itemId: sid(po.item_id), + quantity: Number(po.quantity || 0), + unit_price: Number(po.unit_price || 0), + delivery_status: po.delivery_status, + expected_delivery: po.expected_delivery, + deliveredAt: po.delivered_at, + created_by: sid(po.created_by), +}); + +const mapWorkerAssignment = (wa) => ({ + id: sid(wa.assignment_id ?? wa.id), + workerId: sid(wa.worker_id), + taskId: sid(wa.task_id), + from_date: wa.from_date ? String(wa.from_date).slice(0, 10) : null, + to_date: wa.to_date ? String(wa.to_date).slice(0, 10) : null, +}); + +const mapAttendance = (a) => ({ + id: sid(a.attendance_id ?? a.id), + workerId: sid(a.worker_id), + projectId: sid(a.project_id), + date: a.date ? new Date(new Date(a.date).getTime() - new Date(a.date).getTimezoneOffset() * 60000).toISOString().split('T')[0] : null, + status: a.status, + hours_worked: Number(a.hours_worked || 0), + labor_cost: Number(a.labor_cost || 0), + recorded_by: sid(a.recorded_by), +}); + +const mapFinance = (f) => ({ + id: sid(f.finance_id ?? f.id), + projectId: sid(f.project_id), + cost_category: f.cost_category, + amount: Number(f.amount || 0), + date: f.date, + description: f.description, + payment_status: f.payment_status, + source: f.source, +}); + +const mapMaterialIssue = (mi) => ({ + id: sid(mi.material_issue_id ?? mi.id), + projectId: sid(mi.project_id), + taskId: sid(mi.task_id), + itemId: sid(mi.item_id), + quantity: Number(mi.quantity || 0), + issued_by: sid(mi.issued_by), + issuedAt: mi.issued_at, +}); + +const mapLeave = (l) => ({ + id: sid(l.leave_id ?? l.id), + workerId: sid(l.worker_id), + start_date: l.start_date, + end_date: l.end_date, + reason: l.reason, + status: l.status || 'Pending', + applied_at: l.applied_on, + reviewed_by: sid(l.reviewed_by), + reviewed_at: l.reviewed_on, +}); + +const mapUser = (u) => ({ + id: sid(u.user_id ?? u.id), + name: u.name, + email: u.email, + role: u.role, + phone: u.phone, + is_active: u.is_active, +}); + +const mapProjectMember = (pm) => ({ + id: sid(pm.project_member_id ?? pm.id), + projectId: sid(pm.project_id), + userId: sid(pm.user_id), + project_role: pm.member_role || 'Site_Engineer', + from_date: pm.from_date, + to_date: pm.to_date, +}); + +const mapNotification = (n) => ({ + id: sid(n.id), + user_id: sid(n.user_id), + title: n.title || n.message, + message: n.message, + type: n.type || 'general', + severity: n.severity || 'medium', + read: n.is_read || false, + createdAt: n.created_at, +}); + +export const AppProvider = ({ children }) => { + const [currentUser, setCurrentUser] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + // State for all data - initialized as empty arrays + const [users, setUsers] = useState([]); + const [projects, setProjects] = useState([]); + const [tasks, setTasks] = useState([]); + const [workers, setWorkers] = useState([]); + const [inventory, setInventory] = useState([]); + const [financeRecords, setFinanceRecords] = useState([]); + const [vendors, setVendors] = useState([]); + const [purchaseOrders, setPurchaseOrders] = useState([]); + const [materialIssues, setMaterialIssues] = useState([]); + const [workerAssignments, setWorkerAssignments] = useState([]); + const [attendanceRecords, setAttendanceRecords] = useState([]); + const [projectMembers, setProjectMembers] = useState([]); + const [dbNotifications, setDbNotifications] = useState([]); + const [localNotifications, setLocalNotifications] = useState([]); + const [leaveApplications, setLeaveApplications] = useState([]); + + // Loading states + const [loading, setLoading] = useState({ + users: false, projects: false, tasks: false, workers: false, + inventory: false, finance: false, vendors: false, procurement: false, + materialIssues: false, attendance: false, + }); + const [errors, setErrors] = useState({}); + + // ── API FETCH FUNCTIONS ── + + const fetchUsers = useCallback(async () => { + setLoading(prev => ({ ...prev, users: true })); + try { + const res = await fetch(`${API_BASE_URL}/users`); + if (!res.ok) throw new Error('Failed to fetch users'); + const data = await res.json(); + setUsers(data.map(mapUser)); + setErrors(prev => ({ ...prev, users: null })); + } catch (error) { + setErrors(prev => ({ ...prev, users: error.message })); + } finally { + setLoading(prev => ({ ...prev, users: false })); + } + }, []); + + const fetchProjects = useCallback(async () => { + setLoading(prev => ({ ...prev, projects: true })); + try { + const res = await fetch(`${API_BASE_URL}/projects`); + if (!res.ok) throw new Error('Failed to fetch projects'); + const data = await res.json(); + setProjects(data.map(mapProject)); + setErrors(prev => ({ ...prev, projects: null })); + } catch (error) { + setErrors(prev => ({ ...prev, projects: error.message })); + } finally { + setLoading(prev => ({ ...prev, projects: false })); + } + }, []); + + const fetchTasks = useCallback(async () => { + setLoading(prev => ({ ...prev, tasks: true })); + try { + const res = await fetch(`${API_BASE_URL}/tasks`); + if (!res.ok) throw new Error('Failed to fetch tasks'); + const data = await res.json(); + setTasks(data.map(mapTask)); + setErrors(prev => ({ ...prev, tasks: null })); + } catch (error) { + setErrors(prev => ({ ...prev, tasks: error.message })); + } finally { + setLoading(prev => ({ ...prev, tasks: false })); + } + }, []); + + const fetchWorkers = useCallback(async () => { + setLoading(prev => ({ ...prev, workers: true })); + try { + const res = await fetch(`${API_BASE_URL}/workers`); + if (!res.ok) throw new Error('Failed to fetch workers'); + const data = await res.json(); + setWorkers(data.map(mapWorker)); + setErrors(prev => ({ ...prev, workers: null })); + } catch (error) { + setErrors(prev => ({ ...prev, workers: error.message })); + } finally { + setLoading(prev => ({ ...prev, workers: false })); + } + }, []); + + const fetchInventory = useCallback(async () => { + setLoading(prev => ({ ...prev, inventory: true })); + try { + const res = await fetch(`${API_BASE_URL}/inventory`); + if (!res.ok) throw new Error('Failed to fetch inventory'); + const data = await res.json(); + setInventory(data.map(mapInventory)); + setErrors(prev => ({ ...prev, inventory: null })); + } catch (error) { + setErrors(prev => ({ ...prev, inventory: error.message })); + } finally { + setLoading(prev => ({ ...prev, inventory: false })); + } + }, []); + + const fetchFinance = useCallback(async () => { + setLoading(prev => ({ ...prev, finance: true })); + try { + const res = await fetch(`${API_BASE_URL}/finance`); + if (!res.ok) throw new Error('Failed to fetch finance'); + const data = await res.json(); + setFinanceRecords(data.map(mapFinance)); + setErrors(prev => ({ ...prev, finance: null })); + } catch (error) { + setErrors(prev => ({ ...prev, finance: error.message })); + } finally { + setLoading(prev => ({ ...prev, finance: false })); + } + }, []); + + const fetchVendors = useCallback(async () => { + setLoading(prev => ({ ...prev, vendors: true })); + try { + const res = await fetch(`${API_BASE_URL}/vendors`); + if (!res.ok) throw new Error('Failed to fetch vendors'); + const data = await res.json(); + setVendors(data.map(mapVendor)); + setErrors(prev => ({ ...prev, vendors: null })); + } catch (error) { + setErrors(prev => ({ ...prev, vendors: error.message })); + } finally { + setLoading(prev => ({ ...prev, vendors: false })); + } + }, []); + + const fetchProcurement = useCallback(async () => { + setLoading(prev => ({ ...prev, procurement: true })); + try { + const res = await fetch(`${API_BASE_URL}/procurement`); + if (!res.ok) throw new Error('Failed to fetch procurement'); + const data = await res.json(); + setPurchaseOrders(data.map(mapProcurement)); + setErrors(prev => ({ ...prev, procurement: null })); + } catch (error) { + setErrors(prev => ({ ...prev, procurement: error.message })); + } finally { + setLoading(prev => ({ ...prev, procurement: false })); + } + }, []); + + const fetchMaterialIssues = useCallback(async () => { + setLoading(prev => ({ ...prev, materialIssues: true })); + try { + const res = await fetch(`${API_BASE_URL}/material-issue`); + if (!res.ok) throw new Error('Failed to fetch material issues'); + const data = await res.json(); + setMaterialIssues(data.map(mapMaterialIssue)); + setErrors(prev => ({ ...prev, materialIssues: null })); + } catch (error) { + setErrors(prev => ({ ...prev, materialIssues: error.message })); + } finally { + setLoading(prev => ({ ...prev, materialIssues: false })); + } + }, []); + + const fetchAttendance = useCallback(async () => { + setLoading(prev => ({ ...prev, attendance: true })); + try { + const res = await fetch(`${API_BASE_URL}/attendance`); + if (!res.ok) throw new Error('Failed to fetch attendance'); + const data = await res.json(); + setAttendanceRecords(data.map(mapAttendance)); + setErrors(prev => ({ ...prev, attendance: null })); + } catch (error) { + setErrors(prev => ({ ...prev, attendance: error.message })); + } finally { + setLoading(prev => ({ ...prev, attendance: false })); + } + }, []); + + // ── FETCH PROJECT MEMBERS (DB-backed) ── + const fetchProjectMembers = useCallback(async () => { + try { + const res = await fetch(`${API_BASE_URL}/project-members`); + if (!res.ok) throw new Error('Failed to fetch project members'); + const data = await res.json(); + setProjectMembers(data.map(mapProjectMember)); + } catch (error) { + console.error('Error fetching project members:', error); + } + }, []); + + // ── FETCH NOTIFICATIONS (DB-backed) ── + const fetchNotifications = useCallback(async (userId) => { + try { + const url = userId + ? `${API_BASE_URL}/notifications/user/${userId}` + : `${API_BASE_URL}/notifications`; + const res = await fetch(url); + if (!res.ok) throw new Error('Failed to fetch notifications'); + const data = await res.json(); + setDbNotifications(data.map(mapNotification)); + } catch (error) { + console.error('Error fetching notifications:', error); + } + }, []); + + // ── FETCH LEAVE APPLICATIONS (DB-backed) ── + const fetchLeaveApplications = useCallback(async () => { + try { + const res = await fetch(`${API_BASE_URL}/leave`); + if (!res.ok) throw new Error('Failed to fetch leave applications'); + const data = await res.json(); + setLeaveApplications(data.map(mapLeave)); + } catch (error) { + console.error('Error fetching leave applications:', error); + } + }, []); + + const fetchWorkerAssignments = useCallback(async () => { + try { + const res = await fetch(`${API_BASE_URL}/worker-assignments`); + if (!res.ok) throw new Error('Failed to fetch worker assignments'); + const data = await res.json(); + setWorkerAssignments(data.map(mapWorkerAssignment)); + } catch (error) { + console.error('Error fetching worker assignments:', error); + } + }, []); + + // Load all data on mount + useEffect(() => { + fetchUsers(); + fetchProjects(); + fetchTasks(); + fetchWorkers(); + fetchInventory(); + fetchFinance(); + fetchVendors(); + fetchProcurement(); + fetchMaterialIssues(); + fetchAttendance(); + fetchProjectMembers(); + fetchNotifications(); + fetchLeaveApplications(); + fetchWorkerAssignments(); + }, [fetchUsers, fetchProjects, fetchTasks, fetchWorkers, fetchInventory, + fetchFinance, fetchVendors, fetchProcurement, fetchMaterialIssues, + fetchAttendance, fetchProjectMembers, fetchNotifications, fetchLeaveApplications, + fetchWorkerAssignments]); + + // ── NOTIFICATION SYSTEM ── + const pushNotification = useCallback(async (notification) => { + // Push to DB if we have a user connected, otherwise just local + try { + const res = await fetch(`${API_BASE_URL}/notifications`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + user_id: notification.user_id || null, + title: notification.title || notification.message, + message: notification.message, + type: notification.type || 'general', + severity: notification.severity || 'medium', + }), + }); + if (res.ok) { + const data = await res.json(); + setDbNotifications(prev => [mapNotification(data), ...prev]); + return; + } + } catch (e) { + // Fallback to local + } + // Local fallback + setLocalNotifications(prev => [{ + id: makeId('note'), + read: false, + createdAt: new Date().toISOString(), + severity: 'medium', + ...notification, + }, ...prev]); + }, []); + + // ── AUTH ── + const login = useCallback((user) => { + setCurrentUser(user); + setIsAuthenticated(true); + // Reload notifications for this user + if (user?.id) { + fetchNotifications(user.id); + } + }, [fetchNotifications]); + + const logout = useCallback(() => { + setCurrentUser(null); + setIsAuthenticated(false); + setDbNotifications([]); + setLocalNotifications([]); + }, []); + + // ── PROJECT ACTIONS ── + const addProject = useCallback(async (project) => { + try { + const res = await fetch(`${API_BASE_URL}/projects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(project), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to create project'); + const mapped = mapProject(data); + setProjects(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error creating project:', error); + throw error; + } + }, []); + + const updateProject = useCallback(async (id, updates) => { + try { + const res = await fetch(`${API_BASE_URL}/projects/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update project'); + const mapped = mapProject(data); + setProjects(prev => prev.map(p => p.id === sid(id) ? mapped : p)); + return mapped; + } catch (error) { + console.error('Error updating project:', error); + throw error; + } + }, []); + + const deleteProject = useCallback(async (id) => { + try { + const res = await fetch(`${API_BASE_URL}/projects/${id}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('Failed to delete project'); + setProjects(prev => prev.filter(p => p.id !== sid(id))); + return true; + } catch (error) { + console.error('Error deleting project:', error); + throw error; + } + }, []); + + // ── TASK ACTIONS ── + const addTask = useCallback(async (task) => { + try { + const apiData = { + project_id: task.projectId || task.project_id, + task_name: task.task_name, + assigned_to: task.assigned_to, + start_date: task.start_date, + end_date: task.end_date, + status: task.status || 'Open', + priority: task.priority || 'Medium', + deadline: task.deadline || task.due_date, + workers_assigned: task.workers_assigned || [], + materials_used: task.materials_used || [], + }; + const res = await fetch(`${API_BASE_URL}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(apiData), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to create task'); + const mapped = mapTask(data); + setTasks(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error creating task:', error); + throw error; + } + }, []); + + const checkDependencies = useCallback((taskId, tasksList = null) => { + const currentTasks = tasksList || tasks; + const task = currentTasks.find((t) => t.id === sid(taskId)); + if (!task || !task.dependencies || task.dependencies.length === 0) return true; + return task.dependencies.every((depId) => { + const depTask = currentTasks.find((t) => t.id === sid(depId)); + return depTask && depTask.status === 'Completed'; + }); + }, [tasks]); + + const updateTaskStatus = useCallback(async (id, status, skipValidation = false) => { + if (status === 'In Progress' && !skipValidation) { + if (!checkDependencies(id)) { + pushNotification({ type: 'error', title: 'Blocked Task', message: 'Cannot start: incomplete dependencies' }); + return false; + } + } + try { + const res = await fetch(`${API_BASE_URL}/tasks/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update task'); + setTasks(prev => prev.map(t => t.id === sid(id) ? mapTask(data) : t)); + return true; + } catch (error) { + console.error('Error updating task status:', error); + return false; + } + }, [checkDependencies, pushNotification]); + + const updateTask = useCallback(async (taskId, updates) => { + try { + const res = await fetch(`${API_BASE_URL}/tasks/${taskId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update task'); + const mapped = mapTask(data); + setTasks(prev => prev.map(t => t.id === sid(taskId) ? mapped : t)); + return mapped; + } catch (error) { + console.error('Error updating task:', error); + throw error; + } + }, []); + + const addTaskDependency = useCallback((taskId, depId) => { + setTasks(prev => prev.map(t => { + if (t.id === sid(taskId) && !t.dependencies?.includes(sid(depId))) { + return { ...t, dependencies: [...(t.dependencies || []), sid(depId)] }; + } + return t; + })); + }, []); + + const removeTaskDependency = useCallback((taskId, depId) => { + setTasks(prev => prev.map(t => + t.id === sid(taskId) ? { ...t, dependencies: (t.dependencies || []).filter(d => d !== sid(depId)) } : t + )); + }, []); + + const updateTaskProgress = useCallback((taskId, progress) => { + const p = Math.max(0, Math.min(100, Math.round(progress))); + setTasks(prev => prev.map(t => t.id === sid(taskId) ? { ...t, progress: p } : t)); + }, []); + + // ── VENDOR ACTIONS ── + const addVendor = useCallback(async (vendor) => { + try { + const res = await fetch(`${API_BASE_URL}/vendors`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(vendor), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to create vendor'); + const mapped = mapVendor(data); + setVendors(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error creating vendor:', error); + throw error; + } + }, []); + + const updateVendor = useCallback(async (vendorId, updates) => { + try { + const res = await fetch(`${API_BASE_URL}/vendors/${vendorId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update vendor'); + const mapped = mapVendor(data); + setVendors(prev => prev.map(v => v.id === sid(vendorId) ? mapped : v)); + return mapped; + } catch (error) { + console.error('Error updating vendor:', error); + throw error; + } + }, []); + + const deleteVendor = useCallback(async (vendorId) => { + try { + const res = await fetch(`${API_BASE_URL}/vendors/${vendorId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('Failed to delete vendor'); + setVendors(prev => prev.filter(v => v.id !== sid(vendorId))); + return true; + } catch (error) { + console.error('Error deleting vendor:', error); + throw error; + } + }, []); + + // ── FINANCE (DB-driven) ── + const addFinanceRecord = useCallback(async (record) => { + try { + const res = await fetch(`${API_BASE_URL}/finance`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project_id: record.projectId || record.project_id, + cost_category: record.cost_category, + amount: record.amount, + description: record.description, + payment_status: record.payment_status || 'Pending', + date: record.date || new Date().toISOString().slice(0, 10), + source: record.source || 'automation', + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to create finance record'); + const mapped = mapFinance(data); + setFinanceRecords(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error adding finance record:', error); + return null; + } + }, []); + + // ── PROCUREMENT (DB-driven) ── + const createPurchaseOrder = useCallback(async (payload, actorId) => { + try { + const res = await fetch(`${API_BASE_URL}/procurement`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project_id: payload.projectId || payload.project_id, + vendor_id: payload.vendorId || payload.vendor_id, + item_id: payload.itemId || payload.item_id, + quantity: Number(payload.quantity), + unit_price: Number(payload.unit_price), + delivery_status: payload.delivery_status || 'ordered', + expected_delivery: payload.expectedDelivery || payload.expected_delivery, + created_by: actorId && !isNaN(Number(actorId)) ? Number(actorId) : null, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to create purchase order'); + const mapped = mapProcurement(data); + setPurchaseOrders(prev => [...prev, mapped]); + + pushNotification({ + type: 'procurement_delivery', + severity: 'medium', + title: `PO created: ${mapped.procurement_id || mapped.id}`, + message: `Purchase order created for project.`, + }); + + return mapped; + } catch (error) { + console.error('Error creating purchase order:', error); + throw error; + } + }, [pushNotification]); + + const updatePurchaseDeliveryStatus = useCallback(async (poId, status) => { + try { + const target = purchaseOrders.find(po => po.id === sid(poId) || po.procurement_id === String(poId)); + const body = { delivery_status: status }; + if (status === 'delivered') { + body.delivered_at = new Date().toISOString().slice(0, 10); + } + + const res = await fetch(`${API_BASE_URL}/procurement/${poId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update procurement status'); + + const mapped = mapProcurement(data); + setPurchaseOrders(prev => prev.map(po => po.id === sid(poId) ? mapped : po)); + + // If marking as delivered — update inventory stock in DB + if (status === 'delivered' && target && target.delivery_status !== 'delivered') { + const invItem = inventory.find(i => i.id === target.itemId); + if (invItem) { + const newStock = Number(invItem.current_stock) + Number(target.quantity); + try { + const invRes = await fetch(`${API_BASE_URL}/inventory/${target.itemId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ current_stock: newStock }), + }); + if (invRes.ok) { + const invData = await invRes.json(); + setInventory(prev => prev.map(i => + i.id === target.itemId ? { ...i, current_stock: Number(invData.current_stock) } : i + )); + } + } catch (e) { + console.error('Error updating inventory after delivery:', e); + } + } + pushNotification({ + type: 'procurement_delivery', + severity: 'low', + title: `PO delivered: ${target.procurement_id || target.id}`, + message: `Delivery received. Inventory updated.`, + }); + } + + return mapped; + } catch (error) { + console.error('Error updating procurement status:', error); + throw error; + } + }, [purchaseOrders, inventory, pushNotification]); + + // ── MATERIAL ISSUE ── + const issueMaterial = useCallback(async (payload, actorId) => { + try { + const res = await fetch(`${API_BASE_URL}/material-issue`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project_id: payload.projectId || payload.project_id, + task_id: payload.taskId || payload.task_id, + item_id: payload.itemId || payload.item_id, + quantity: Number(payload.quantity), + issued_by: actorId && !isNaN(Number(actorId)) ? Number(actorId) : null, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to issue material'); + const mapped = mapMaterialIssue(data); + setMaterialIssues(prev => [...prev, mapped]); + + // update inventory stock locally + setInventory(prev => prev.map(item => + item.id === sid(payload.itemId || payload.item_id) + ? { ...item, current_stock: Math.max(0, item.current_stock - Number(payload.quantity)) } + : item + )); + + return mapped; + } catch (error) { + console.error('Error issuing material:', error); + throw error; + } + }, []); + + const addProcurement = useCallback((itemId, quantity, cost) => { + const item = inventory.find(inv => inv.id === sid(itemId)); + if (!item) return; + createPurchaseOrder({ + projectId: projects[0]?.id, + vendorId: vendors[0]?.id, + itemId, + quantity, + unit_price: cost || item.unit_cost, + delivery_status: 'delivered', + }, currentUser?.id || null); + }, [createPurchaseOrder, currentUser?.id, inventory, projects, vendors]); + + // ── WORKER ACTIONS (DB-driven) ── + const addWorker = useCallback(async (worker) => { + try { + const res = await fetch(`${API_BASE_URL}/workers`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(worker), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to add worker'); + const mapped = mapWorker(data); + setWorkers(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error adding worker:', error); + throw error; + } + }, []); + + const updateWorker = useCallback(async (workerId, updates) => { + try { + const res = await fetch(`${API_BASE_URL}/workers/${workerId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update worker'); + const mapped = mapWorker(data); + setWorkers(prev => prev.map(w => w.id === sid(workerId) ? mapped : w)); + return mapped; + } catch (error) { + console.error('Error updating worker:', error); + throw error; + } + }, []); + + const deleteWorker = useCallback(async (workerId) => { + try { + const res = await fetch(`${API_BASE_URL}/workers/${workerId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('Failed to delete worker'); + setWorkers(prev => prev.filter(w => w.id !== sid(workerId))); + return true; + } catch (error) { + console.error('Error deleting worker:', error); + throw error; + } + }, []); + + // ── WORKER ASSIGNMENT (DB-backed) ── + const assignWorkerToTask = useCallback(async (payload) => { + try { + const res = await fetch(`${API_BASE_URL}/worker-assignments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + worker_id: payload.workerId, + task_id: payload.taskId, + from_date: payload.from_date, + to_date: payload.to_date, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to assign worker'); + const mapped = mapWorkerAssignment(data); + setWorkerAssignments(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error assigning worker to task:', error); + throw error; + } + }, []); + + // ── ATTENDANCE (DB-driven per worker) ── + const calculateLaborCost = useCallback((worker, status, hoursWorked) => { + if (!worker || status === 'Absent') return 0; + if (worker.rate_type?.toLowerCase() === 'hourly') return worker.base_rate * Number(hoursWorked || 0); + if (status === 'Half Day') return worker.base_rate * 0.5; + return worker.base_rate; + }, []); + + const recordAttendance = useCallback(async (payload, actorId) => { + const worker = workers.find(w => w.id === sid(payload.workerId)); + const laborCost = calculateLaborCost(worker, payload.status, payload.hours_worked); + // Only pass recorded_by if it's a valid numeric user id + const safeActorId = actorId && !isNaN(Number(actorId)) ? Number(actorId) : null; + + try { + const res = await fetch(`${API_BASE_URL}/attendance`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + worker_id: payload.workerId, + project_id: payload.projectId, + date: payload.date, + status: payload.status, + hours_worked: Number(payload.hours_worked || 0), + labor_cost: laborCost, + recorded_by: safeActorId, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to record attendance'); + const mapped = mapAttendance(data); + + // Update attendance records safely + setAttendanceRecords(prev => { + const existIdx = prev.findIndex(e => e.workerId === mapped.workerId && e.date === mapped.date); + if (existIdx >= 0) { + const copy = [...prev]; + copy[existIdx] = mapped; + return copy; + } + return [...prev, mapped]; + }); + + // Update workers state safely directly calculating from current + new mapped + setWorkers(workersPrev => { + return workersPrev.map(w => { + if (w.id === mapped.workerId) { + // Find current records for this worker, removing old version of this date to add new one + const otherRecords = attendanceRecords.filter(a => a.workerId === mapped.workerId && a.date !== mapped.date); + const sumLaborCost = otherRecords.reduce((sum, a) => sum + Number(a.labor_cost || 0), 0) + Number(mapped.labor_cost || 0); + return { ...w, salary: sumLaborCost }; + } + return w; + }); + }); + + return mapped; + } catch (error) { + console.error('Error recording attendance:', error); + throw error; + } + }, [calculateLaborCost, workers]); + + const updateWorkerAttendance = useCallback((workerId, status, date) => { + const worker = workers.find(w => w.id === sid(workerId)); + const defaultHours = status === 'Present' ? 8 : status === 'Half Day' ? 4 : 0; + const projectId = worker?.project_id || projects[0]?.id; + + recordAttendance({ + workerId: sid(workerId), + status, + date, + hours_worked: defaultHours, + projectId: projectId || null, + }, currentUser?.id || null); + + return worker; + }, [currentUser?.id, projects, recordAttendance, workers]); + + // ── PROJECT TEAM — DB-backed ── + const assignProjectMember = useCallback(async (payload) => { + try { + const res = await fetch(`${API_BASE_URL}/project-members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + project_id: payload.projectId || payload.project_id, + user_id: payload.userId || payload.user_id, + member_role: payload.project_role || payload.member_role || 'Site_Engineer', + }), + }); + const data = await res.json(); + if (!res.ok) { + if (res.status === 409) return null; // already assigned + throw new Error(data.error || 'Failed to assign project member'); + } + const mapped = mapProjectMember(data); + setProjectMembers(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error assigning project member:', error); + throw error; + } + }, []); + + const removeProjectMember = useCallback(async (memberId) => { + try { + const res = await fetch(`${API_BASE_URL}/project-members/${memberId}`, { method: 'DELETE' }); + if (!res.ok) throw new Error('Failed to remove project member'); + setProjectMembers(prev => prev.filter(m => m.id !== sid(memberId))); + return true; + } catch (error) { + console.error('Error removing project member:', error); + throw error; + } + }, []); + + // ── INVENTORY ── + const addInventoryItem = useCallback(async (item) => { + try { + const res = await fetch(`${API_BASE_URL}/inventory`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to add inventory item'); + const mapped = mapInventory(data); + setInventory(prev => [...prev, mapped]); + return mapped; + } catch (error) { + console.error('Error adding inventory item:', error); + throw error; + } + }, []); + + const addInventoryStock = useCallback(async (itemId, quantity) => { + try { + const item = inventory.find(inv => inv.id === sid(itemId)); + if (!item) throw new Error('Inventory item not found'); + const newStock = Number(item.current_stock) + Number(quantity); + const res = await fetch(`${API_BASE_URL}/inventory/${itemId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ current_stock: newStock }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to update inventory stock'); + setInventory(prev => prev.map(inv => + inv.id === sid(itemId) ? { ...inv, current_stock: Number(data.current_stock) } : inv + )); + return data; + } catch (error) { + console.error('Error updating inventory stock:', error); + throw error; + } + }, [inventory]); + + // ── LEAVE APPLICATIONS (DB-driven) ── + const applyLeave = useCallback(async (payload) => { + try { + const res = await fetch(`${API_BASE_URL}/leave`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + worker_id: payload.workerId, + start_date: payload.start_date, + end_date: payload.end_date, + reason: payload.reason, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to apply leave'); + const mapped = mapLeave(data); + setLeaveApplications(prev => [mapped, ...prev]); + return mapped; + } catch (error) { + console.error('Error applying leave:', error); + throw error; + } + }, []); + + const approveLeave = useCallback(async (leaveId, reviewerId) => { + try { + const res = await fetch(`${API_BASE_URL}/leave/${leaveId}/approve`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ reviewerId }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to approve leave'); + const mapped = mapLeave(data); + setLeaveApplications(prev => prev.map(l => l.id === sid(leaveId) ? mapped : l)); + return mapped; + } catch (error) { + console.error('Error approving leave:', error); + throw error; + } + }, []); + + const rejectLeave = useCallback(async (leaveId, reviewerId, rejection_reason = '') => { + try { + const res = await fetch(`${API_BASE_URL}/leave/${leaveId}/reject`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ reviewerId, reason: rejection_reason }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Failed to reject leave'); + const mapped = mapLeave(data); + setLeaveApplications(prev => prev.map(l => l.id === sid(leaveId) ? mapped : l)); + return mapped; + } catch (error) { + console.error('Error rejecting leave:', error); + throw error; + } + }, []); + + // ── SALARY CALCULATOR ── + const calculateSalary = useCallback((workerId, fromDate, toDate) => { + const records = attendanceRecords.filter(entry => { + if (entry.workerId !== sid(workerId)) return false; + if (fromDate && entry.date < fromDate) return false; + if (toDate && entry.date > toDate) return false; + return true; + }); + const totalDaysWorked = records.filter(r => r.status === 'Present').length; + const halfDays = records.filter(r => r.status === 'Half Day' || r.status === 'Half_Day').length; + const totalHours = records.reduce((sum, r) => sum + Number(r.hours_worked || 0), 0); + const totalSalary = records.reduce((sum, r) => sum + Number(r.labor_cost || 0), 0); + const absentDays = records.filter(r => r.status === 'Absent').length; + const worker = workers.find(w => w.id === sid(workerId)); + const absenceDeduction = worker + ? absentDays * (worker.rate_type?.toLowerCase() === 'daily' ? worker.base_rate : worker.base_rate * 8) + : 0; + return { totalDaysWorked, halfDays, totalHours, totalSalary, absentDays, absenceDeduction, netSalary: totalSalary - absenceDeduction }; + }, [attendanceRecords, workers]); + + // ── NOTIFICATION MANAGEMENT ── + const [readSystemNotes, setReadSystemNotes] = useState(new Set()); + + // ── SYSTEM NOTIFICATIONS (computed from live data) ── + const systemNotifications = useMemo(() => { + const lowStock = inventory + .filter(item => item.current_stock < item.min_stock_qty) + .map(item => ({ + id: `sys-low-stock-${item.id}`, + type: 'low_stock', + severity: 'high', + title: `Low stock: ${item.item_name}`, + message: `${item.current_stock} ${item.uom} left (min ${item.min_stock_qty}).`, + createdAt: new Date().toISOString(), + read: readSystemNotes.has(`sys-low-stock-${item.id}`), + })); + + const overdue = tasks + .filter(task => task.status !== 'Completed' && new Date(task.deadline || task.due_date) < new Date()) + .map(task => ({ + id: `sys-overdue-${task.id}`, + type: 'overdue_tasks', + severity: 'high', + title: `Overdue task: ${task.task_name}`, + message: `Task deadline ${task.deadline || task.due_date} has passed.`, + createdAt: new Date().toISOString(), + read: readSystemNotes.has(`sys-overdue-${task.id}`), + })); + + const budgetExceed = projects + .filter(project => { + const spent = financeRecords + .filter(r => r.projectId === project.id) + .reduce((sum, r) => sum + r.amount, 0); + return spent > project.budget; + }) + .map(project => ({ + id: `sys-budget-${project.id}`, + type: 'budget_exceed', + severity: 'high', + title: `Budget exceeded: ${project.project_name}`, + message: `Project spending has exceeded planned budget.`, + createdAt: new Date().toISOString(), + read: readSystemNotes.has(`sys-budget-${project.id}`), + })); + + return [...lowStock, ...overdue, ...budgetExceed]; + }, [financeRecords, inventory, projects, tasks, readSystemNotes]); + + const allNotifications = useMemo(() => { + const map = new Map(); + [...dbNotifications, ...localNotifications, ...systemNotifications].forEach(note => { + if (!map.has(note.id)) map.set(note.id, note); + }); + return Array.from(map.values()).sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + }, [dbNotifications, localNotifications, systemNotifications]); + + const unreadNotificationCount = useMemo(() => { + return allNotifications.filter(note => !note.read).length; + }, [allNotifications]); + + const markNotificationRead = useCallback(async (id) => { + if (String(id).startsWith('sys-')) { + setReadSystemNotes(prev => new Set([...prev, id])); + return; + } + try { + // Try DB mark-read first + const res = await fetch(`${API_BASE_URL}/notifications/${id}/read`, { method: 'PUT' }); + if (res.ok) { + setDbNotifications(prev => prev.map(note => note.id === sid(id) ? { ...note, read: true } : note)); + return; + } + } catch (e) { /* ignore */ } + // Fallback: local + setLocalNotifications(prev => prev.map(note => note.id === sid(id) ? { ...note, read: true } : note)); + }, []); + + const markAllNotificationsRead = useCallback(async (userId) => { + try { + const uid = userId || currentUser?.id; + if (uid) { + await fetch(`${API_BASE_URL}/notifications/read-all/${uid}`, { method: 'PUT' }); + } + } catch (e) { /* ignore */ } + setDbNotifications(prev => prev.map(note => ({ ...note, read: true }))); + setLocalNotifications(prev => prev.map(note => ({ ...note, read: true }))); + // Mark system ones read too + setReadSystemNotes(new Set(systemNotifications.map(n => n.id))); + }, [currentUser?.id, systemNotifications]); + + + const value = { + currentUser, isAuthenticated, login, logout, + users, projects, tasks, workers, inventory, financeRecords, vendors, + purchaseOrders, materialIssues, workerAssignments, attendanceRecords, + projectMembers, leaveApplications, + notifications: allNotifications, unreadNotificationCount, + + addProject, updateProject, deleteProject, + addTask, updateTask, updateTaskStatus, checkDependencies, + addTaskDependency, removeTaskDependency, updateTaskProgress, + addVendor, updateVendor, deleteVendor, + createPurchaseOrder, updatePurchaseDeliveryStatus, + issueMaterial, addProcurement, + addWorker, updateWorker, deleteWorker, + assignWorkerToTask, recordAttendance, updateWorkerAttendance, + addInventoryItem, addInventoryStock, + assignProjectMember, removeProjectMember, + applyLeave, approveLeave, rejectLeave, calculateSalary, + addFinanceRecord, + markNotificationRead, markAllNotificationsRead, pushNotification, + fetchNotifications, + }; + + return {children}; +}; diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx new file mode 100644 index 0000000..fb7222f --- /dev/null +++ b/frontend/src/context/AuthContext.jsx @@ -0,0 +1,125 @@ +/** + * Authentication Context + * Manages authentication state and actions + */ + +import { createContext, useState, useEffect, useCallback } from 'react'; +import authService from '../services/authService'; + +export const AuthContext = createContext(); + +const normalizeRole = (role) => { + if (!role || typeof role !== 'string') return 'Site_Engineer'; + return role.trim().replace(/\s+/g, '_'); +}; + +// Normalize DB user shape: DB returns user_id, but the app reads user.id everywhere +const normalizeUser = (raw) => { + if (!raw) return null; + return { + ...raw, + id: String(raw.id ?? raw.user_id), // always expose .id + user_id: raw.user_id ?? raw.id, // keep original for backwards compat + role: normalizeRole(raw.role), + }; +}; + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + + useEffect(() => { + const currentUser = authService.getCurrentUser(); + if (currentUser) { + setUser(normalizeUser(currentUser)); + setIsAuthenticated(true); + } + setLoading(false); + }, []); + + const signup = useCallback(async (name, email, password, role) => { + setLoading(true); + setError(null); + try { + const result = await authService.signup(name, email, password, role); + if (!result.success) { + setError(result.message); + } + return result; + } catch (err) { + const message = 'An error occurred during signup'; + setError(message); + return { success: false, message }; + } finally { + setLoading(false); + } + }, []); + + const login = useCallback(async (email, password, rememberMe = false, selectedRole = null) => { + setLoading(true); + setError(null); + try { + const result = await authService.login(email, password, selectedRole); + if (result.success) { + const normalizedUser = normalizeUser(result.user); + // Also update session storage with normalized user (includes .id) + sessionStorage.setItem('siteos_user', JSON.stringify(normalizedUser)); + setUser(normalizedUser); + setIsAuthenticated(true); + return result; + } + setError(result.message); + return result; + } catch (err) { + const message = 'An error occurred during login'; + setError(message); + return { success: false, message }; + } finally { + setLoading(false); + } + }, []); + + const logout = useCallback(() => { + authService.logout(); + setUser(null); + setIsAuthenticated(false); + setError(null); + }, []); + + const resetPassword = useCallback(async (email, newPassword) => { + setLoading(true); + setError(null); + try { + const result = await authService.resetPassword(email, newPassword); + if (!result.success) { + setError(result.message); + } + return result; + } catch (err) { + const message = 'An error occurred during password reset'; + setError(message); + return { success: false, message }; + } finally { + setLoading(false); + } + }, []); + + const value = { + user, + loading, + error, + isAuthenticated, + signup, + login, + logout, + resetPassword, + }; + + return ( + + {children} + + ); +} diff --git a/construction-site-management/src/data/.gitkeep b/frontend/src/data/.gitkeep similarity index 100% rename from construction-site-management/src/data/.gitkeep rename to frontend/src/data/.gitkeep diff --git a/construction-site-management/src/hooks/useAuth.js b/frontend/src/hooks/useAuth.js similarity index 100% rename from construction-site-management/src/hooks/useAuth.js rename to frontend/src/hooks/useAuth.js diff --git a/construction-site-management/src/index.css b/frontend/src/index.css similarity index 53% rename from construction-site-management/src/index.css rename to frontend/src/index.css index 6308e05..61530a1 100644 --- a/construction-site-management/src/index.css +++ b/frontend/src/index.css @@ -2,29 +2,29 @@ @tailwind components; @tailwind utilities; -/* Base styles */ +/* Base styles for Professional Dark Theme */ @layer base { body { - @apply bg-slate-950 text-slate-50 antialiased; + @apply bg-slate-950 text-slate-50 antialiased overflow-x-hidden; } } -/* Custom scrollbar */ +/* Custom scrollbar - Dark Theme */ @layer utilities { .scrollbar-thin::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 6px; + height: 6px; } .scrollbar-thin::-webkit-scrollbar-track { - @apply bg-slate-900; + @apply bg-transparent; } .scrollbar-thin::-webkit-scrollbar-thumb { - @apply bg-slate-700 rounded; + @apply bg-slate-800 rounded-full; } .scrollbar-thin::-webkit-scrollbar-thumb:hover { - @apply bg-slate-600; + @apply bg-slate-700; } } diff --git a/construction-site-management/src/main.jsx b/frontend/src/main.jsx similarity index 100% rename from construction-site-management/src/main.jsx rename to frontend/src/main.jsx diff --git a/construction-site-management/src/pages/Assignments.jsx b/frontend/src/pages/Assignments.jsx similarity index 86% rename from construction-site-management/src/pages/Assignments.jsx rename to frontend/src/pages/Assignments.jsx index f669075..418adb7 100644 --- a/construction-site-management/src/pages/Assignments.jsx +++ b/frontend/src/pages/Assignments.jsx @@ -13,12 +13,13 @@ export default function Assignments() { const { workers, tasks, workerAssignments, assignWorkerToTask } = useContext(AppContext); const [form, setForm] = useState(defaultForm); const [taskFilter, setTaskFilter] = useState(''); + const [loading, setLoading] = useState(false); const filteredAssignments = useMemo(() => { return workerAssignments.filter((assignment) => !taskFilter || assignment.taskId === taskFilter); }, [taskFilter, workerAssignments]); - const handleAssign = (e) => { + const handleAssign = async (e) => { e.preventDefault(); if (!form.workerId || !form.taskId || !form.from_date || !form.to_date) { @@ -26,8 +27,19 @@ export default function Assignments() { return; } - assignWorkerToTask(form); - setForm(defaultForm); + setLoading(true); + try { + await assignWorkerToTask({ + ...form, + workerId: String(form.workerId), + taskId: String(form.taskId), + }); + setForm(defaultForm); + } catch (error) { + window.alert('Failed to assign worker: ' + error.message); + } finally { + setLoading(false); + } }; const getWorkerName = (id) => workers.find((worker) => worker.id === id)?.name || id; @@ -36,7 +48,7 @@ export default function Assignments() { return (
-

Worker Assignment

+

Worker Assignment

Assign workers to tasks with date ranges

@@ -89,7 +101,7 @@ export default function Assignments() { {filteredAssignments.map((assignment) => ( - {getWorkerName(assignment.workerId)} + {getWorkerName(assignment.workerId)} {getTaskName(assignment.taskId)} {assignment.from_date} {assignment.to_date} diff --git a/construction-site-management/src/pages/Attendance.jsx b/frontend/src/pages/Attendance.jsx similarity index 73% rename from construction-site-management/src/pages/Attendance.jsx rename to frontend/src/pages/Attendance.jsx index ebf6e17..5b7306e 100644 --- a/construction-site-management/src/pages/Attendance.jsx +++ b/frontend/src/pages/Attendance.jsx @@ -2,11 +2,14 @@ import { useContext, useMemo, useState } from 'react'; import { AppContext } from '../context/AppContext'; import { useAuth } from '../hooks/useAuth'; import { Card, Button, Select, Input, Badge } from '../components/ui'; +import { formatCurrency } from '../utils/currency'; + +const localToday = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; const defaultForm = { workerId: '', projectId: '', - date: new Date().toISOString().slice(0, 10), + date: localToday, status: 'Present', hours_worked: 8, }; @@ -33,23 +36,35 @@ export default function Attendance() { return; } + // workerId and projectId are already normalized strings from AppContext recordAttendance( { ...form, hours_worked: Number(form.hours_worked), }, - user?.id || 'system' + user?.id || null ); }; - const getWorkerName = (id) => workers.find((worker) => worker.id === id)?.name || id; - const getProjectName = (id) => projects.find((project) => project.id === id)?.project_name || id; + const getWorkerName = (id) => workers.find((worker) => String(worker.id) === String(id))?.name || String(id); + const getProjectName = (id) => projects.find((project) => String(project.id) === String(id))?.project_name || String(id); return (
-
-

Attendance Management

-

Track attendance, hours worked and automated labor cost

+ {/* TOP SECTION */} +
+
+
+ Home + / + Attendance +
+

Attendance Management

+

Track attendance, hours worked and automated labor cost

+
+
+ +
@@ -102,11 +117,11 @@ export default function Attendance() { setSelectedDate(e.target.value)} />

Workers Marked

-

{filtered.length}

+

{filtered.length}

Labor Cost (Auto)

-

₹{dailyLaborCost.toLocaleString()}

+

{formatCurrency(dailyLaborCost)}

@@ -123,16 +138,16 @@ export default function Attendance() { {filtered.map((entry) => ( - - {getWorkerName(entry.workerId)} - {getProjectName(entry.projectId)} + + {getWorkerName(entry.workerId)} + {getProjectName(entry.projectId)} {entry.status} - {entry.hours_worked} - ₹{Number(entry.labor_cost || 0).toLocaleString()} + {entry.hours_worked} + {formatCurrency(entry.labor_cost || 0)} ))} diff --git a/frontend/src/pages/AuthLogin.jsx b/frontend/src/pages/AuthLogin.jsx new file mode 100644 index 0000000..fdd666a --- /dev/null +++ b/frontend/src/pages/AuthLogin.jsx @@ -0,0 +1,294 @@ +/** + * Login Page + * User authentication with email, password, and role selection + */ + +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; +import FormInput from '../components/auth/FormInput'; +import Toast from '../components/auth/Toast'; +import { validateFormField } from '../utils/validation'; +import { Loader, Shield, Users, Hammer, HardHat } from 'lucide-react'; + +const ROLES = [ + { + id: 'Admin', + label: 'Admin', + description: 'Full system access', + icon: Shield, + color: 'bg-rose-500/10 border-rose-500/50', + }, + { + id: 'Project_Manager', + label: 'Project Manager', + description: 'Manage projects & finance', + icon: Users, + color: 'bg-sky-500/10 border-sky-500/50', + }, + { + id: 'Site_Engineer', + label: 'Site Engineer', + description: 'Manage workers, tasks & inventory', + icon: Hammer, + color: 'bg-primary-500/10 border-primary-500/50', + }, + { + id: 'Worker', + label: 'Worker', + description: 'View attendance & salary', + icon: HardHat, + color: 'bg-emerald-500/10 border-emerald-500/50', + }, +]; + +export default function AuthLogin() { + const navigate = useNavigate(); + const { login, loading } = useAuth(); + + const [formData, setFormData] = useState({ + email: '', + password: '', + role: 'Site_Engineer', + rememberMe: false, + }); + + const [errors, setErrors] = useState({}); + const [toast, setToast] = useState(null); + + const handleInputChange = (field, value) => { + setFormData((prev) => ({ ...prev, [field]: value })); + + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: '' })); + } + }; + + const handleBlur = (field) => { + const error = validateFormField(field, formData[field]); + if (error) { + setErrors((prev) => ({ ...prev, [field]: error })); + } + }; + + const validateForm = () => { + const newErrors = {}; + + const emailError = validateFormField('email', formData.email); + if (emailError) newErrors.email = emailError; + + const passwordError = validateFormField('password', formData.password); + if (passwordError) newErrors.password = passwordError; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) { + setToast({ + type: 'error', + message: 'Please fix the errors above', + }); + return; + } + + const result = await login( + formData.email, + formData.password, + formData.rememberMe, + formData.role + ); + + if (result.success) { + setToast({ + type: 'success', + message: `Login successful as ${formData.role.replace('_', ' ')}!`, + }); + + setTimeout(() => { + navigate(formData.role === 'Worker' ? '/worker' : '/'); + }, 1000); + } else { + setToast({ + type: 'error', + message: result.message, + }); + } + }; + + return ( +
+ {/* Background glow effects matching Dashboard's premium feel */} +
+
+ +
+ {/* Header */} +
+

+ SiteOS +

+

Construction Site Management

+
+ + {/* Toast */} + {toast && ( +
+ setToast(null)} + /> +
+ )} + + {/* Form Container with Glassmorphism matching the Dashboard Card */} +
+ {/* Credentials Section */} + +
+

+ + Login Credentials +

+ +
+ handleInputChange('email', value)} + onBlur={() => handleBlur('email')} + error={errors.email} + required + placeholder="developer@siteos.in" + /> + + handleInputChange('password', value)} + onBlur={() => handleBlur('password')} + error={errors.password} + required + placeholder="••••••••" + showToggle + /> + + {/* Remember Me */} +
+ + handleInputChange('rememberMe', e.target.checked) + } + className="w-4 h-4 rounded border-slate-700 bg-slate-900 text-primary-500 focus:ring-primary-500 focus:ring-offset-slate-950 cursor-pointer" + /> + +
+
+
+ +
+ + {/* Role Selection Section */} +
+

+ + Select Identity +

+
+ {ROLES.map((roleOption) => { + const Icon = roleOption.icon; + const isSelected = formData.role === roleOption.id; + + return ( + + ); + })} +
+
+ + {/* Submit Button */} + +
+ + {/* Links */} +
+ + Recover Password + +

+ Don't have an account?{' '} + + Sign Up + +

+
+
+
+ ); +} diff --git a/construction-site-management/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx similarity index 76% rename from construction-site-management/src/pages/Dashboard.jsx rename to frontend/src/pages/Dashboard.jsx index 69a44d0..3472c7a 100644 --- a/construction-site-management/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -10,7 +10,8 @@ import { AppContext } from '../context/AppContext'; import { useAuth } from '../hooks/useAuth'; import { Card, Badge } from '../components/ui'; import { BudgetChart, CostDistributionChart } from '../components/charts'; -import { TrendingUp, Users, AlertCircle, DollarSign, Clock, Truck, BellRing, Search } from 'lucide-react'; +import { TrendingUp, Users, AlertCircle, IndianRupee, Clock, Truck, BellRing, Search } from 'lucide-react'; +import { formatCurrency } from '../utils/currency'; const Dashboard = () => { const { @@ -38,12 +39,12 @@ const Dashboard = () => { if (user?.role === 'Project_Manager') { // Project Manager sees all projects but limited tasks filteredTasks = tasks.filter(t => { - const project = projects.find(p => p.id === t.projectId); + const project = projects.find(p => String(p.id) === String(t.projectId)); return project !== undefined; }); } else if (user?.role === 'Site_Engineer') { - // Site Engineer sees assigned tasks only - filteredTasks = tasks.filter(t => t.assigned_to === user?.id); + // Site Engineer sees assigned tasks only — compare as strings + filteredTasks = tasks.filter(t => String(t.assigned_to) === String(user?.id)); } const totalProjects = filteredProjects.length; @@ -52,7 +53,7 @@ const Dashboard = () => { (item) => item.current_stock < item.min_stock_qty ).length; const totalBudget = filteredProjects.reduce((sum, p) => sum + p.budget, 0); - const activeTasks = filteredTasks.filter(t => t.status === 'In Progress').length; + const activeTasks = filteredTasks.filter(t => t.status === 'In Progress' || t.status === 'In_Progress').length; const openProcurement = purchaseOrders.filter((po) => po.delivery_status === 'ordered').length; const unreadNotifications = notifications.filter((note) => !note.read).length; @@ -90,7 +91,7 @@ const Dashboard = () => { const budgetChartData = useMemo(() => { return projects.map((project) => { const projectFinance = financeRecords.filter( - (f) => f.projectId === project.id + (f) => String(f.projectId) === String(project.id) ); const actualExpenses = projectFinance.reduce((sum, f) => sum + f.amount, 0); @@ -141,21 +142,41 @@ const Dashboard = () => { return (
-
-

Dashboard

-

- Welcome back, {user?.name}! Here's your {user?.role?.replace('_', ' ')} overview. -

+ {/* TOP SECTION */} +
+
+
+ Home + / + Dashboard +
+

Dashboard Overview

+

+ Welcome back, {user?.name}! Here's your {user?.role?.replace('_', ' ')} summary. +

+
+ + {/* Action Buttons Aligned Right */} +
+ + {user?.role === 'Admin' && ( + + )} +
- +
- + setGlobalSearch(e.target.value)} @@ -163,11 +184,11 @@ const Dashboard = () => {
- + setFilterCategory(e.target.value)} - className="w-full px-4 py-2 bg-slate-900 border border-slate-800 rounded-lg text-slate-50 focus:border-amber-500 focus:outline-none" + className="w-full px-4 py-2 bg-slate-900 border border-slate-800 rounded-lg text-slate-100 focus:border-amber-500 focus:outline-none" > {categories.map(cat => ( @@ -92,15 +93,15 @@ export default function Inventory() { filteredInventory.map(item => ( - {item.item_name} + {item.item_name} {item.category} - ${item.unit_cost.toFixed(2)} - {item.current_stock} {item.uom} - {item.min_stock_qty} {item.uom} + {formatCurrency(item.unit_cost)} + {item.current_stock} {item.uom} + {item.min_stock_qty} {item.uom} {item.supplier || '—'} {isLowStock(item) ? ( @@ -140,14 +141,14 @@ export default function Inventory() {
{inventory.filter(isLowStock).length > 0 ? (
-

+

{inventory.filter(isLowStock).length} item(s) below minimum stock level

{inventory.filter(isLowStock).map(item => (
-

{item.item_name}

+

{item.item_name}

Current: {item.current_stock} {item.uom} | Min: {item.min_stock_qty} {item.uom}

@@ -174,7 +175,7 @@ export default function Inventory() {

Materials Issued (30 days)

-

+

{ materialIssues.filter((issue) => { const age = Date.now() - new Date(issue.issuedAt).getTime(); @@ -193,8 +194,8 @@ export default function Inventory() { setAddStockModal(null)} title="Add Stock">

- Adding stock for: - {inventory.find((i) => i.id === addStockModal)?.item_name} + Adding stock for: + {inventory.find((i) => String(i.id) === String(addStockModal))?.item_name}

-

Material Issue Module

+

Material Issue Module

Issue inventory to projects or tasks and track issued-by user

@@ -111,7 +111,7 @@ export default function MaterialIssue() { {materialIssues.map((entry) => ( {entry.issuedAt} - {getProjectName(entry.projectId)} + {getProjectName(entry.projectId)} {getTaskName(entry.taskId)} {getItemName(entry.itemId)} {entry.quantity} diff --git a/construction-site-management/src/pages/Notifications.jsx b/frontend/src/pages/Notifications.jsx similarity index 69% rename from construction-site-management/src/pages/Notifications.jsx rename to frontend/src/pages/Notifications.jsx index 5ab35ac..50c9af9 100644 --- a/construction-site-management/src/pages/Notifications.jsx +++ b/frontend/src/pages/Notifications.jsx @@ -18,12 +18,20 @@ export default function Notifications() { return (
-
+ {/* TOP SECTION */} +
-

Notification Center

-

Low stock, overdue tasks, deliveries, absences and budget alerts

+
+ Home + / + Notifications +
+

Notification Center

+

Alerts for low stock, overdue tasks, deliveries, absences and budget

+
+
+
-
@@ -47,19 +55,19 @@ export default function Notifications() { {filtered.map((note) => (
-

{note.title}

+

{note.title}

{note.severity} {!note.read && new}
-

{note.message}

-

{new Date(note.createdAt).toLocaleString()}

+

{note.message}

+

{new Date(note.createdAt).toLocaleString()}

{!note.read && ( )} diff --git a/construction-site-management/src/pages/ProjectTeam.jsx b/frontend/src/pages/ProjectTeam.jsx similarity index 94% rename from construction-site-management/src/pages/ProjectTeam.jsx rename to frontend/src/pages/ProjectTeam.jsx index 99a038d..e7117d2 100644 --- a/construction-site-management/src/pages/ProjectTeam.jsx +++ b/frontend/src/pages/ProjectTeam.jsx @@ -43,7 +43,7 @@ export default function ProjectTeam() { return (
-

Project Team Management

+

Project Team Management

Assign site engineers to projects

@@ -106,11 +106,11 @@ export default function ProjectTeam() { {filteredMembers.map((member) => ( - {getProjectName(member.projectId)} + {getProjectName(member.projectId)} {getUserName(member.userId)} {member.project_role} - diff --git a/construction-site-management/src/pages/Projects.jsx b/frontend/src/pages/Projects.jsx similarity index 96% rename from construction-site-management/src/pages/Projects.jsx rename to frontend/src/pages/Projects.jsx index 011c780..3d5cb0d 100644 --- a/construction-site-management/src/pages/Projects.jsx +++ b/frontend/src/pages/Projects.jsx @@ -11,6 +11,7 @@ import { AppContext } from '../context/AppContext'; import { useAuth } from '../hooks/useAuth'; import { Card, Button, Input, Select, Modal, Table, Badge } from '../components/ui'; import { Plus, Trash2, Edit2, Lock, ExternalLink } from 'lucide-react'; +import { formatCurrency } from '../utils/currency'; const Projects = () => { const { projects, addProject, updateProject, deleteProject, projectMembers } = @@ -154,7 +155,7 @@ const Projects = () => { { key: 'budget', label: 'Budget', - render: (value) => `$${value.toLocaleString()}`, + render: (value) => formatCurrency(value), }, { key: 'status', @@ -180,14 +181,14 @@ const Projects = () => {
-
{JSON.stringify(report.rows.slice(0, 5), null, 2)}
+
{JSON.stringify(report.rows.slice(0, 5), null, 2)}
diff --git a/frontend/src/pages/ResetPassword.jsx b/frontend/src/pages/ResetPassword.jsx new file mode 100644 index 0000000..df7f635 --- /dev/null +++ b/frontend/src/pages/ResetPassword.jsx @@ -0,0 +1,18 @@ +/** + * Reset Password Page + * Redirects to the unified /forgot-password page which now handles the full flow + */ + +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export default function ResetPassword() { + const navigate = useNavigate(); + + useEffect(() => { + // Redirect to the new unified forgot-password page + navigate('/forgot-password', { replace: true }); + }, [navigate]); + + return null; +} diff --git a/construction-site-management/src/pages/SignUp.jsx b/frontend/src/pages/SignUp.jsx similarity index 59% rename from construction-site-management/src/pages/SignUp.jsx rename to frontend/src/pages/SignUp.jsx index 0443fcd..bc55354 100644 --- a/construction-site-management/src/pages/SignUp.jsx +++ b/frontend/src/pages/SignUp.jsx @@ -21,6 +21,7 @@ export default function SignUp() { email: '', password: '', confirmPassword: '', + role: 'Site_Engineer', termsAccepted: false, }); @@ -28,18 +29,18 @@ export default function SignUp() { const [toast, setToast] = useState(null); const handleInputChange = (field, value) => { - setFormData(prev => ({ ...prev, [field]: value })); - + setFormData((prev) => ({ ...prev, [field]: value })); + // Clear error when user starts typing if (errors[field]) { - setErrors(prev => ({ ...prev, [field]: '' })); + setErrors((prev) => ({ ...prev, [field]: '' })); } }; const handleBlur = (field) => { const error = validateFormField(field, formData[field]); if (error) { - setErrors(prev => ({ ...prev, [field]: error })); + setErrors((prev) => ({ ...prev, [field]: error })); } }; @@ -58,8 +59,15 @@ export default function SignUp() { const passwordError = validateFormField('password', formData.password); if (passwordError) newErrors.password = passwordError; + if (!formData.role) { + newErrors.role = 'Please select a role'; + } + // Validate confirm password - const matchError = validatePasswordsMatch(formData.password, formData.confirmPassword); + const matchError = validatePasswordsMatch( + formData.password, + formData.confirmPassword + ); if (matchError) newErrors.confirmPassword = matchError; // Validate terms @@ -82,19 +90,21 @@ export default function SignUp() { return; } - const result = await signup(formData.name, formData.email, formData.password); + const result = await signup( + formData.name, + formData.email, + formData.password, + formData.role + ); if (result.success) { setToast({ type: 'success', - message: 'Account created! Please verify your email.', + message: 'Account created successfully!', }); - - // Store email for verification page - sessionStorage.setItem('verificationEmail', formData.email); - + setTimeout(() => { - navigate('/verify-email'); + navigate('/login'); }, 1500); } else { setToast({ @@ -106,16 +116,22 @@ export default function SignUp() { return (
-
+ {/* Background glow effects matching Dashboard's premium feel */} +
+
+ +
{/* Header */} -
-

SiteOS

+
+

+ SiteOS +

Create your account

{/* Toast */} {toast && ( -
+
)} - {/* Form */} -
+ {/* Form Container with Glassmorphism matching the Dashboard Card */} + handleBlur('email')} error={errors.email} required - placeholder="you@example.com" + placeholder="developer@siteos.in" /> +
+ + + {errors.role && ( +

{errors.role}

+ )} +
+
{ handleInputChange('termsAccepted', e.target.checked); if (e.target.checked && errors.terms) { - setErrors(prev => ({ ...prev, terms: '' })); + setErrors((prev) => ({ ...prev, terms: '' })); } }} - className="mt-1 w-4 h-4 rounded border-slate-800 bg-slate-900 text-amber-500 focus:ring-amber-500 cursor-pointer" + className="mt-1 w-4 h-4 rounded border-slate-700 bg-slate-900 text-primary-500 focus:ring-primary-500 focus:ring-offset-slate-950 cursor-pointer" /> @@ -212,7 +254,7 @@ export default function SignUp() { @@ -211,7 +212,7 @@ const Tasks = () => {
Progress - {progress}% + {progress}%
{ })} {taskList.length === 0 && ( -
+

No tasks

)} @@ -246,7 +247,7 @@ const Tasks = () => {
-

Tasks

+

Tasks

{user?.role === 'Site_Engineer' ? 'Your assigned tasks' @@ -357,7 +358,7 @@ const Tasks = () => { />

-
+ ); +} diff --git a/frontend/src/pages/Workforce.jsx b/frontend/src/pages/Workforce.jsx new file mode 100644 index 0000000..e27bf5a --- /dev/null +++ b/frontend/src/pages/Workforce.jsx @@ -0,0 +1,294 @@ +/** + * Workforce Page + * Worker management with per-worker attendance tracking + * Features: worker table, per-worker attendance buttons, add worker modal + * Role-based visibility - Admin, Project Manager and Site Engineer + */ + +import { useContext, useState, useMemo } from 'react'; +import { AppContext } from '../context/AppContext'; +import { useAuth } from '../hooks/useAuth'; +import { Card, Button, Input, Select, Modal, Badge } from '../components/ui'; +import { Plus, Lock } from 'lucide-react'; +import { formatCurrency } from '../utils/currency'; + +const defaultWorkerForm = { + name: '', + skill_type: 'Mason', + contact: '', + rate_type: 'Daily', + base_rate: '', +}; + +const Workforce = () => { + const { workers, attendanceRecords, addWorker, updateWorkerAttendance, projects } = useContext(AppContext); + const { user } = useAuth(); + + const [isModalOpen, setIsModalOpen] = useState(false); + const localToday = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000).toISOString().split('T')[0]; + const [selectedDate, setSelectedDate] = useState(localToday); + const [formData, setFormData] = useState(defaultWorkerForm); + const [submitting, setSubmitting] = useState(false); + + const canManageWorkforce = ['Admin', 'Project_Manager', 'Site_Engineer'].includes(user?.role); + + if (!canManageWorkforce) { + return ( +
+
+

Workforce

+

Manage workers and attendance

+
+ +
+ +

+ You don't have access to workforce management. Only Admin, Project Managers and Site Engineers can view this section. +

+
+
+
+ ); + } + + // Get attendance status from DB records for a specific worker on selectedDate + const getAttendanceStatus = (workerId) => { + const record = attendanceRecords.find( + a => String(a.workerId) === String(workerId) && a.date === selectedDate + ); + return record?.status || null; + }; + + // Per-worker attendance — each click updates ONLY that specific worker + const handleAttendance = (workerId, status) => { + updateWorkerAttendance(workerId, status, selectedDate); + }; + + // Attendance counts for selected date + const attendanceSummary = useMemo(() => { + const dayRecords = attendanceRecords.filter(a => a.date === selectedDate); + return { + present: dayRecords.filter(a => a.status === 'Present').length, + halfDay: dayRecords.filter(a => a.status === 'Half Day' || a.status === 'Half_Day').length, + absent: dayRecords.filter(a => a.status === 'Absent').length, + }; + }, [attendanceRecords, selectedDate]); + + // Handle Add Worker form submission + const handleAddWorker = async (e) => { + e.preventDefault(); + if (!formData.name || !formData.base_rate) { + window.alert('Name and base rate are required'); + return; + } + setSubmitting(true); + try { + await addWorker({ + name: formData.name, + skill_type: formData.skill_type, + contact: formData.contact, + rate_type: formData.rate_type, + base_rate: Number(formData.base_rate), + }); + setFormData(defaultWorkerForm); + setIsModalOpen(false); + } catch (err) { + window.alert('Failed to add worker: ' + err.message); + } finally { + setSubmitting(false); + } + }; + + const AttendanceButtons = ({ workerId }) => { + const currentStatus = getAttendanceStatus(workerId); + + return ( +
+ + + +
+ ); + }; + + return ( +
+
+
+

Workforce

+

Manage workers and attendance

+
+ +
+ + {/* Date Selector */} + +
+ + setSelectedDate(e.target.value)} + className="px-4 py-2 bg-slate-900 border border-slate-800 rounded-lg text-slate-100 focus:outline-none focus:border-amber-500" + /> +
+
+ + {/* Workers Table */} + +
+ + + + + + + + + + + + + {workers.length === 0 ? ( + + + + ) : ( + workers.map((worker) => ( + + + + + + + + + )) + )} + +
NameSkillContactRate TypeBase RateAttendance
+ No workers available +
{worker.name}{worker.skill_type}{worker.contact}{worker.rate_type} + {formatCurrency(worker.base_rate)}/{worker.rate_type?.toLowerCase() === 'daily' ? 'day' : 'hr'} + + +
+
+
+ + {/* Attendance Summary */} + +
+
+

Present

+

{attendanceSummary.present}

+
+
+

Half Day

+

{attendanceSummary.halfDay}

+
+
+

Absent

+

{attendanceSummary.absent}

+
+
+
+ + {/* Add Worker Modal */} + setIsModalOpen(false)} title="Add New Worker"> +
+ setFormData({ ...formData, name: e.target.value })} + placeholder="Enter worker name" + /> +
+ setFormData({ ...formData, rate_type: e.target.value })} + /> +
+
+ setFormData({ ...formData, contact: e.target.value })} + placeholder="Phone number" + /> + setFormData({ ...formData, base_rate: e.target.value })} + placeholder="e.g. 900" + /> +
+
+ + +
+
+
+
+ ); +}; + +export default Workforce; diff --git a/construction-site-management/src/pages/projects/ProjectDetails.jsx b/frontend/src/pages/projects/ProjectDetails.jsx similarity index 82% rename from construction-site-management/src/pages/projects/ProjectDetails.jsx rename to frontend/src/pages/projects/ProjectDetails.jsx index 4308fbf..f234343 100644 --- a/construction-site-management/src/pages/projects/ProjectDetails.jsx +++ b/frontend/src/pages/projects/ProjectDetails.jsx @@ -15,7 +15,7 @@ import { Trash2, UserPlus, UserMinus, - DollarSign, + IndianRupee, Users, Package, ClipboardList, @@ -23,6 +23,7 @@ import { LayoutDashboard, AlertTriangle, } from 'lucide-react'; +import { formatCurrency } from '../../utils/currency'; const TABS = [ { id: 'overview', label: 'Overview', icon: LayoutDashboard }, @@ -75,45 +76,48 @@ export default function ProjectDetails() { assigned_to: '', }); - const project = useMemo(() => projects.find((p) => p.id === projectId), [projects, projectId]); + // NOTE: projectId from useParams is a string; AppContext mappers normalize all IDs to strings. + const pid = String(projectId); - const projectTasks = useMemo(() => tasks.filter((t) => t.projectId === projectId), [tasks, projectId]); + const project = useMemo(() => projects.find((p) => String(p.id) === pid), [projects, pid]); + + const projectTasks = useMemo(() => tasks.filter((t) => String(t.projectId) === pid), [tasks, pid]); const projectMemb = useMemo( - () => projectMembers.filter((pm) => pm.projectId === projectId), - [projectMembers, projectId] + () => projectMembers.filter((pm) => String(pm.projectId) === pid), + [projectMembers, pid] ); const assignedSiteEngineers = useMemo(() => { return projectMemb .filter((pm) => pm.project_role === 'Site_Engineer') - .map((pm) => users.find((u) => u.id === pm.userId)) + .map((pm) => users.find((u) => String(u.id) === String(pm.userId))) .filter(Boolean); }, [projectMemb, users]); const projectWorkers = useMemo(() => { const workerIds = new Set( - projectTasks.flatMap((t) => t.workers_assigned || []) + projectTasks.flatMap((t) => t.workers_assigned || []).map(String) ); - return workers.filter((w) => workerIds.has(w.id) || w.projectId === projectId); - }, [projectTasks, workers, projectId]); + return workers.filter((w) => workerIds.has(String(w.id)) || String(w.project_id) === pid); + }, [projectTasks, workers, pid]); const projectFinance = useMemo( - () => financeRecords.filter((f) => f.projectId === projectId), - [financeRecords, projectId] + () => financeRecords.filter((f) => String(f.projectId) === pid), + [financeRecords, pid] ); const projectPOs = useMemo( - () => purchaseOrders.filter((po) => po.projectId === projectId), - [purchaseOrders, projectId] + () => purchaseOrders.filter((po) => String(po.projectId) === pid), + [purchaseOrders, pid] ); const projectInventory = useMemo(() => { const usedItemIds = new Set( - materialIssues.filter((mi) => mi.projectId === projectId).map((mi) => mi.itemId) + materialIssues.filter((mi) => String(mi.projectId) === pid).map((mi) => String(mi.itemId)) ); - return inventory.filter((item) => usedItemIds.has(item.id)); - }, [inventory, materialIssues, projectId]); + return inventory.filter((item) => usedItemIds.has(String(item.id))); + }, [inventory, materialIssues, pid]); const totalSpent = useMemo( () => projectFinance.reduce((sum, f) => sum + f.amount, 0), @@ -123,8 +127,8 @@ export default function ProjectDetails() { const budgetUsedPct = project ? Math.min(100, Math.round((totalSpent / project.budget) * 100)) : 0; const availableSiteEngineers = useMemo(() => { - const already = new Set(projectMemb.map((pm) => pm.userId)); - return users.filter((u) => u.role === 'Site_Engineer' && !already.has(u.id)); + const already = new Set(projectMemb.map((pm) => String(pm.userId))); + return users.filter((u) => u.role === 'Site_Engineer' && !already.has(String(u.id))); }, [users, projectMemb]); const canManage = ['Admin', 'Project_Manager'].includes(user?.role); @@ -143,31 +147,43 @@ export default function ProjectDetails() { } const handleDeleteProject = () => { - deleteProject(projectId); + deleteProject(pid); navigate('/projects'); }; - const handleAssignEngineer = (e) => { + const handleAssignEngineer = async (e) => { e.preventDefault(); if (!assignUserId) return; - assignProjectMember({ projectId, userId: assignUserId, project_role: 'Site_Engineer' }); - setAssignUserId(''); - setShowAssignModal(false); + try { + const result = await assignProjectMember({ projectId: pid, userId: assignUserId, project_role: 'Site_Engineer' }); + if (!result) { + window.alert('This engineer is already assigned to this project'); + return; + } + setAssignUserId(''); + setShowAssignModal(false); + } catch (err) { + window.alert('Failed to assign site engineer: ' + err.message); + } }; - const handleAddTask = (e) => { + const handleAddTask = async (e) => { e.preventDefault(); if (!taskForm.task_name || !taskForm.due_date) return; - addTask({ - ...taskForm, - projectId, - workers_assigned: [], - materials_used: [], - progress: 0, - dependencies: [], - }); - setTaskForm({ task_name: '', priority: 'Medium', due_date: '', assigned_to: '' }); - setShowAddTask(false); + try { + await addTask({ + ...taskForm, + projectId: pid, + workers_assigned: [], + materials_used: [], + progress: 0, + dependencies: [], + }); + setTaskForm({ task_name: '', priority: 'Medium', due_date: '', assigned_to: '' }); + setShowAddTask(false); + } catch (err) { + window.alert('Failed to add task: ' + err.message); + } }; const getVendorName = (id) => vendors.find((v) => v.id === id)?.vendor_name || id; @@ -180,12 +196,12 @@ export default function ProjectDetails() {
-

{project.project_name}

+

{project.project_name}

{project.site_location} · {project.project_type}

@@ -226,8 +242,8 @@ export default function ProjectDetails() {
- - + +
@@ -236,10 +252,10 @@ export default function ProjectDetails() {
- Spent: ${totalSpent.toLocaleString()} - Budget: ${Number(project.budget).toLocaleString()} + Spent: {formatCurrency(totalSpent)} + Budget: {formatCurrency(project.budget)}
-
+
= 90 ? 'bg-rose-500' : budgetUsedPct >= 70 ? 'bg-amber-500' : 'bg-emerald-500' @@ -257,7 +273,7 @@ export default function ProjectDetails() { headerClassName="flex items-center justify-between" >
-

Site Engineers Assigned

+

Site Engineers Assigned

{canManage && ( + ), + }, + { key: 'site_location', label: 'Location' }, + { key: 'project_type', label: 'Type' }, + { + key: 'start_date', + label: 'Start Date', + render: (value) => new Date(value).toLocaleDateString(), + }, + { + key: 'budget', + label: 'Budget', + render: (value) => formatCurrencyINR(value), + }, + { + key: 'status', + label: 'Status', + render: (value) => { + const statusColor = { + Planning: 'status', + Active: 'success', + Completed: 'warning', + 'On Hold': 'danger', + }; + return {value}; + }, + }, + ]; + + // Add actions column only if user can manage + if (canManageProjects) { + columns.push({ + key: 'id', + label: 'Actions', + render: (value, row) => ( +
+ + +
+ ), + }); + } + + return ( +
+
+
+

Projects

+

+ {canManageProjects ? 'Manage construction projects' : 'View construction projects'} +

+
+ {canManageProjects && ( + + )} +
+ + {/* Search and Filter */} + +
+ setSearchTerm(e.target.value)} + /> + + setFormData({ ...formData, project_name: e.target.value }) + } + placeholder="e.g., Downtown Office Complex" + /> + + + setFormData({ ...formData, site_location: e.target.value }) + } + placeholder="e.g., New York, NY" + /> + + + setFormData({ ...formData, start_date: e.target.value }) + } + /> + + + setFormData({ ...formData, end_date: e.target.value }) + } + /> +
+ + + setFormData({ ...formData, budget: e.target.value }) + } + placeholder="e.g., 5000000" + /> + +
+ + +
+ + + )} +
+ ); +}; + +export default Projects; diff --git a/construction-site-management/src/pages/worker/LeaveApplication.jsx b/frontend/src/pages/worker/LeaveApplication.jsx similarity index 91% rename from construction-site-management/src/pages/worker/LeaveApplication.jsx rename to frontend/src/pages/worker/LeaveApplication.jsx index 6325f09..61618a2 100644 --- a/construction-site-management/src/pages/worker/LeaveApplication.jsx +++ b/frontend/src/pages/worker/LeaveApplication.jsx @@ -35,7 +35,7 @@ export default function LeaveApplication() { // Find this user's worker record const myWorker = useMemo( - () => workers.find((w) => w.userId === user?.id), + () => workers.find((w) => String(w.user_id) === String(user?.id)), [workers, user] ); @@ -116,7 +116,7 @@ export default function LeaveApplication() {
-

Leave Applications

+

Leave Applications

{isWorker ? 'Submit and track your leave requests' : 'Review and manage worker leave requests'}

@@ -137,8 +137,8 @@ export default function LeaveApplication() { onClick={() => setFilterStatus(status)} className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ filterStatus === status - ? 'bg-amber-500 text-slate-900' - : 'bg-slate-800 text-slate-400 hover:text-slate-50' + ? 'bg-amber-500 text-slate-100' + : 'bg-slate-800/40 text-slate-400 hover:text-slate-50' }`} > {status === 'all' ? 'All' : status} @@ -156,7 +156,7 @@ export default function LeaveApplication() { {visibleApplications.length === 0 ? (
- +

No leave applications found.

) : ( @@ -176,16 +176,16 @@ export default function LeaveApplication() { {visibleApplications.map((leave) => ( - + {!isWorker && ( - {getWorkerName(leave.workerId)} + {getWorkerName(leave.workerId)} )} {leave.leave_type} {leave.start_date} {leave.end_date} - {leave.reason} + {leave.reason} {leave.status} @@ -210,7 +210,7 @@ export default function LeaveApplication() {
) : ( - + )} )} @@ -253,7 +253,7 @@ export default function LeaveApplication() { value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="Describe your reason for leave..." - className="w-full px-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-50 placeholder-slate-500 focus:border-amber-500 focus:outline-none resize-none" + className="w-full px-4 py-2 bg-slate-800/40 border border-slate-800 rounded-lg text-slate-100 placeholder-slate-500 focus:border-amber-500 focus:outline-none resize-none" />
@@ -272,7 +272,7 @@ export default function LeaveApplication() { value={rejectReason} onChange={(e) => setRejectReason(e.target.value)} placeholder="Rejection reason..." - className="w-full px-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-50 placeholder-slate-500 focus:border-amber-500 focus:outline-none resize-none" + className="w-full px-4 py-2 bg-slate-800/40 border border-slate-800 rounded-lg text-slate-100 placeholder-slate-500 focus:border-amber-500 focus:outline-none resize-none" />
diff --git a/construction-site-management/src/pages/worker/WorkerAttendance.jsx b/frontend/src/pages/worker/WorkerAttendance.jsx similarity index 92% rename from construction-site-management/src/pages/worker/WorkerAttendance.jsx rename to frontend/src/pages/worker/WorkerAttendance.jsx index 790fe13..483813f 100644 --- a/construction-site-management/src/pages/worker/WorkerAttendance.jsx +++ b/frontend/src/pages/worker/WorkerAttendance.jsx @@ -93,7 +93,7 @@ export default function WorkerAttendance() { if (!worker) { return (
-

My Attendance

+

My Attendance

Worker profile not found.

); @@ -102,7 +102,7 @@ export default function WorkerAttendance() { return (
-

My Attendance

+

My Attendance

View your attendance history including lunar holidays

@@ -114,7 +114,7 @@ export default function WorkerAttendance() { setSelectedYear(Number(e.target.value))} - className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-50 focus:border-amber-500 focus:outline-none" + className="px-3 py-2 bg-slate-800/40 border border-slate-800 rounded-lg text-slate-100 focus:border-amber-500 focus:outline-none" > {years.map((y) => ( @@ -190,7 +190,7 @@ export default function WorkerAttendance() { isHolidayDate ? 'bg-amber-500/5' : isSunday ? 'bg-slate-800/20' : '' }`} > - {entry.date} + {entry.date} {dayName} {entry.lunarType ? ( @@ -201,16 +201,16 @@ export default function WorkerAttendance() { ) : entry.record ? ( getStatusBadge(entry) ) : ( - + )} {entry.record && !entry.lunarType ? entry.record.hours_worked : '—'} - + {entry.record && !entry.lunarType ? `$${Number(entry.record.labor_cost || 0).toLocaleString()}` : '—'} - + {entry.lunarType ? `Lunar holiday: ${entry.lunarType}` : isSunday ? 'Sunday' : ''} diff --git a/construction-site-management/src/pages/worker/WorkerDashboard.jsx b/frontend/src/pages/worker/WorkerDashboard.jsx similarity index 71% rename from construction-site-management/src/pages/worker/WorkerDashboard.jsx rename to frontend/src/pages/worker/WorkerDashboard.jsx index 0b19b53..5c52c9f 100644 --- a/construction-site-management/src/pages/worker/WorkerDashboard.jsx +++ b/frontend/src/pages/worker/WorkerDashboard.jsx @@ -7,7 +7,8 @@ import { useContext, useMemo } from 'react'; import { AppContext } from '../../context/AppContext'; import { useAuth } from '../../hooks/useAuth'; import { Card, Badge } from '../../components/ui'; -import { CalendarClock, DollarSign, CheckSquare, Clock, FileText } from 'lucide-react'; +import { CalendarClock, IndianRupee, CheckSquare, Clock, FileText } from 'lucide-react'; +import { formatCurrency } from '../../utils/currency'; export default function WorkerDashboard() { const { workers, attendanceRecords, workerAssignments, tasks, leaveApplications, projects } = @@ -16,7 +17,7 @@ export default function WorkerDashboard() { // Find this worker's record linked to the logged-in user const worker = useMemo( - () => workers.find((w) => w.userId === user?.id) || workers[0], + () => workers.find((w) => String(w.user_id) === String(user?.id)) || workers[0], [workers, user] ); @@ -58,7 +59,7 @@ export default function WorkerDashboard() { if (!worker) { return (
-

My Dashboard

+

My Dashboard

Worker profile not found.

); @@ -66,15 +67,33 @@ export default function WorkerDashboard() { return (
+ {/* TOP SECTION */} +
+
+
+ Worker + / + Dashboard +
+

Worker Dashboard

+

Overview of your shifts, earnings, and assignments.

+
+
+ +
+
+ {/* Profile Header */} -
-
- {worker.name[0]} +
+
+ {worker.name[0]}
-

{worker.name}

-

{worker.skill_type} · {worker.rate_type} Rate · ₹{worker.base_rate}/{worker.rate_type === 'Hourly' ? 'hr' : 'day'}

- {project &&

Assigned: {project.project_name}

} +

{worker.name}

+

{worker.skill_type} · {worker.rate_type} Rate · {formatCurrency(worker.base_rate)}/{worker.rate_type?.toLowerCase() === 'daily' ? 'day' : 'hr'}

+ {project &&

Assigned: {project.project_name}

}
@@ -82,7 +101,7 @@ export default function WorkerDashboard() {
- +
@@ -93,9 +112,9 @@ export default function WorkerDashboard() { ) : (
{myAssignments.slice(0, 5).map((task) => ( -
+
-

{task.task_name}

+

{task.task_name}

Due: {task.due_date || task.deadline}

@@ -125,7 +144,7 @@ export default function WorkerDashboard() { {recentAttendance.map((entry) => ( - {entry.date} + {entry.date} {entry.hours_worked} - ${Number(entry.labor_cost || 0).toLocaleString()} + {formatCurrency(entry.labor_cost)} ))} diff --git a/construction-site-management/src/pages/WorkerPortal.jsx b/frontend/src/pages/worker/WorkerPortal.jsx similarity index 84% rename from construction-site-management/src/pages/WorkerPortal.jsx rename to frontend/src/pages/worker/WorkerPortal.jsx index fabbf62..d633e00 100644 --- a/construction-site-management/src/pages/WorkerPortal.jsx +++ b/frontend/src/pages/worker/WorkerPortal.jsx @@ -1,6 +1,7 @@ import { useContext, useMemo, useState } from 'react'; -import { AppContext } from '../context/AppContext'; -import { Card, Select, Badge } from '../components/ui'; +import { AppContext } from '../../context/AppContext'; +import { Card, Select, Badge } from '../../components/ui'; +import { formatCurrency } from '../../utils/currency'; export default function WorkerPortal() { const { workers, attendanceRecords, workerAssignments, tasks } = useContext(AppContext); @@ -29,7 +30,7 @@ export default function WorkerPortal() { return (
-

Worker Portal

+

Worker Portal

View attendance history, salary and assigned tasks

@@ -45,15 +46,15 @@ export default function WorkerPortal() {

Current Salary (Profile)

-

${Number(worker?.salary || 0).toLocaleString()}

+

{formatCurrency(worker?.salary)}

Salary from Attendance

-

${salaryFromAttendance.toLocaleString()}

+

{formatCurrency(salaryFromAttendance)}

Assigned Tasks

-

{assignedTasks.length}

+

{assignedTasks.length}

@@ -71,14 +72,14 @@ export default function WorkerPortal() { {attendanceHistory.map((entry) => ( - {entry.date} + {entry.date} {entry.status} {entry.hours_worked} - ${Number(entry.labor_cost || 0).toLocaleString()} + {formatCurrency(entry.labor_cost)} ))} @@ -91,7 +92,7 @@ export default function WorkerPortal() { {assignedTasks.map((task) => (
-

{task.task_name}

+

{task.task_name}

Deadline: {task.deadline || task.due_date}

diff --git a/construction-site-management/src/pages/worker/WorkerSalary.jsx b/frontend/src/pages/worker/WorkerSalary.jsx similarity index 91% rename from construction-site-management/src/pages/worker/WorkerSalary.jsx rename to frontend/src/pages/worker/WorkerSalary.jsx index 7ba42de..5063c0f 100644 --- a/construction-site-management/src/pages/worker/WorkerSalary.jsx +++ b/frontend/src/pages/worker/WorkerSalary.jsx @@ -8,7 +8,7 @@ import { useContext, useMemo, useState } from 'react'; import { AppContext } from '../../context/AppContext'; import { useAuth } from '../../hooks/useAuth'; import { Card, Badge } from '../../components/ui'; -import { DollarSign, TrendingDown, Calendar, Clock } from 'lucide-react'; +import { IndianRupee, TrendingDown, Calendar, Clock } from 'lucide-react'; const today = new Date(); @@ -55,7 +55,7 @@ export default function WorkerSalary() { if (!worker || !salary) { return (
-

My Salary

+

My Salary

Worker profile not found.

); @@ -64,7 +64,7 @@ export default function WorkerSalary() { return (
-

My Salary

+

My Salary

Salary breakdown based on attendance records

@@ -76,7 +76,7 @@ export default function WorkerSalary() { setSelectedYear(Number(e.target.value))} - className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-slate-50 focus:border-amber-500 focus:outline-none" + className="px-3 py-2 bg-slate-800/40 border border-slate-800 rounded-lg text-slate-100 focus:border-amber-500 focus:outline-none" > {[2024, 2025, 2026, 2027].map((y) => ( @@ -118,7 +118,7 @@ export default function WorkerSalary() { color="text-blue-400" />
- +
@@ -176,7 +176,7 @@ export default function WorkerSalary() { {monthRecords.map((entry) => ( - {entry.date} + {entry.date} {entry.hours_worked} - + ₹{Number(entry.labor_cost || 0).toLocaleString()} diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..bce92ce --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,112 @@ +/** + * Authentication Service + * Handles user login and session management with backend API + */ + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'; + +class AuthService { + async login(email, password, role) { + try { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password, role }), + }); + + const data = await response.json(); + if (!response.ok) { + return { success: false, message: data.error || 'Login failed' }; + } + + if (data.user) { + sessionStorage.setItem('siteos_user', JSON.stringify(data.user)); + } + + return { success: true, user: data.user, message: data.message }; + } catch (error) { + console.error('Login error:', error); + return { success: false, message: 'Network error. Please try again.' }; + } + } + + async signup(name, email, password, role, phone = '') { + try { + const response = await fetch(`${API_BASE_URL}/auth/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, email, password, role, phone }), + }); + + const data = await response.json(); + if (!response.ok) { + return { success: false, message: data.error || 'Signup failed' }; + } + + return { success: true, user: data.user, message: data.message }; + } catch (error) { + console.error('Signup error:', error); + return { success: false, message: 'Network error. Please try again.' }; + } + } + + async resetPassword(email, newPassword) { + try { + const response = await fetch(`${API_BASE_URL}/auth/reset-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, newPassword }), + }); + + const data = await response.json(); + if (!response.ok) { + return { + success: false, + message: data.error || 'Password reset failed', + limitReached: data.limitReached || false, + }; + } + + return { + success: true, + message: data.message, + resetsRemaining: data.resetsRemaining, + }; + } catch (error) { + console.error('Password reset error:', error); + return { success: false, message: 'Network error. Please try again.' }; + } + } + + logout() { + sessionStorage.removeItem('siteos_user'); + localStorage.removeItem('siteos_user'); // Clear legacy localstorage + } + + getCurrentUser() { + try { + const user = sessionStorage.getItem('siteos_user'); + return user ? JSON.parse(user) : null; + } catch (error) { + console.error('Error getting current user:', error); + return null; + } + } + + isLoggedIn() { + return this.getCurrentUser() !== null; + } + + getUserRole() { + const user = this.getCurrentUser(); + return user ? user.role : null; + } +} + +export default new AuthService(); diff --git a/construction-site-management/src/test/setup.js b/frontend/src/test/setup.js similarity index 100% rename from construction-site-management/src/test/setup.js rename to frontend/src/test/setup.js diff --git a/construction-site-management/src/pages/.gitkeep b/frontend/src/utils/.gitkeep similarity index 100% rename from construction-site-management/src/pages/.gitkeep rename to frontend/src/utils/.gitkeep diff --git a/construction-site-management/src/utils/crypto.js b/frontend/src/utils/crypto.js similarity index 100% rename from construction-site-management/src/utils/crypto.js rename to frontend/src/utils/crypto.js diff --git a/frontend/src/utils/currency.js b/frontend/src/utils/currency.js new file mode 100644 index 0000000..3f40386 --- /dev/null +++ b/frontend/src/utils/currency.js @@ -0,0 +1,10 @@ +export function formatCurrency(amount) { + if (amount === null || amount === undefined || isNaN(amount)) { + amount = 0; + } + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + maximumFractionDigits: 0 + }).format(amount); +} diff --git a/frontend/src/utils/formatCurrency.js b/frontend/src/utils/formatCurrency.js new file mode 100644 index 0000000..e9d1cf4 --- /dev/null +++ b/frontend/src/utils/formatCurrency.js @@ -0,0 +1,52 @@ +/** + * Currency Formatting Utility for Indian Rupees + * Formats numbers to Indian currency format with proper localization + */ + +export function formatCurrencyINR(amount) { + if (amount === null || amount === undefined) { + return '₹0'; + } + + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + maximumFractionDigits: 0 + }).format(amount); +} + +/** + * Format currency for table display with rupee symbol + * @param {number} amount - The amount to format + * @returns {string} Formatted string with rupee symbol + */ +export function formatCurrencyDisplay(amount) { + if (amount === null || amount === undefined) { + return '₹0'; + } + + const formatted = new Intl.NumberFormat('en-IN', { + maximumFractionDigits: 0 + }).format(Math.abs(amount)); + + return amount < 0 ? `-₹${formatted}` : `₹${formatted}`; +} + +/** + * Format currency with custom decimals + * @param {number} amount - The amount to format + * @param {number} decimals - Number of decimal places + * @returns {string} Formatted string + */ +export function formatCurrencyWithDecimals(amount, decimals = 2) { + if (amount === null || amount === undefined) { + return '₹0'; + } + + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + maximumFractionDigits: decimals, + minimumFractionDigits: decimals + }).format(amount); +} diff --git a/construction-site-management/src/utils/lunarHolidays.js b/frontend/src/utils/lunarHolidays.js similarity index 100% rename from construction-site-management/src/utils/lunarHolidays.js rename to frontend/src/utils/lunarHolidays.js diff --git a/construction-site-management/src/utils/validation.js b/frontend/src/utils/validation.js similarity index 100% rename from construction-site-management/src/utils/validation.js rename to frontend/src/utils/validation.js diff --git a/construction-site-management/tailwind.config.js b/frontend/tailwind.config.js similarity index 64% rename from construction-site-management/tailwind.config.js rename to frontend/tailwind.config.js index 7689b99..0db2879 100644 --- a/construction-site-management/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -6,7 +6,15 @@ export default { ], theme: { extend: { + fontFamily: { + sans: ['Inter', 'Segoe UI', 'system-ui', 'sans-serif'], + }, colors: { + primary: { + 500: '#6366f1', + 600: '#4f46e5', + 700: '#4338ca', + }, slate: { 950: '#020617', 900: '#0f172a', @@ -25,16 +33,17 @@ export default { 600: '#d97706', }, emerald: { - 500: '#10b981', - }, - yellow: { - 500: '#eab308', + 500: '#22c55e', + 600: '#16a34a', }, rose: { - 500: '#f43f5e', - 600: '#e11d48', + 500: '#ef4444', + 600: '#dc2626', }, }, + boxShadow: { + 'soft': '0 4px 12px rgba(0, 0, 0, 0.05)', + } }, }, plugins: [], diff --git a/construction-site-management/vite.config.js b/frontend/vite.config.js similarity index 72% rename from construction-site-management/vite.config.js rename to frontend/vite.config.js index 1c8e2f5..fedac13 100644 --- a/construction-site-management/vite.config.js +++ b/frontend/vite.config.js @@ -3,6 +3,15 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:5000', + changeOrigin: true + } + } + }, test: { globals: true, environment: 'jsdom', diff --git a/git b/git deleted file mode 100644 index e69de29..0000000