first commit

This commit is contained in:
Fraggle 2025-12-07 12:14:33 -04:00
commit 5958758b3f
No known key found for this signature in database
57 changed files with 13876 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# 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?
*.db

151
README.md Normal file
View File

@ -0,0 +1,151 @@
# Axion - Unified HR/Payroll/Finance/Employee System
A comprehensive dashboard UI wireframe for a unified HR/Payroll/Finance/Employee system with scheduling and timecards. Built with React, TypeScript, and Tailwind CSS.
## Features
### Authentication & Security
- **Login System**: Secure authentication with role-based access
- **Protected Routes**: All pages require authentication
- **Session Management**: Persistent login sessions
- **Demo Accounts**: Pre-configured accounts for all roles
### Role-Based Access
- **Employee**: Clock in/out, view timecards, schedule, documents, submit receipts
- **Manager**: All employee features plus team management, approvals, incident reports
- **HR**: Employee management, disciplinary actions, performance reviews, documents
- **Payroll**: Payroll runs, expenditures, invoices, OCR review, financial reports
- **Admin**: System settings, user management, automation workflows, audit logs
### User Management
- **Create Users**: Add new employees/users with full details
- **Edit Users**: Update user information and roles
- **Delete Users**: Remove users from the system
- **Role Assignment**: Assign appropriate roles (Employee, Manager, HR, Payroll, Admin)
- **Search & Filter**: Quickly find users in the system
### Database
- **Mock Database**: LocalStorage-based data persistence
- **CRUD Operations**: Full create, read, update, delete functionality
- **Data Persistence**: Data persists across browser sessions
- **Easy Migration**: Structure ready for backend API integration
### Key Screens
1. **Dashboard** - Role-specific home screen with relevant metrics and quick actions
2. **Clock In/Out** - Large, intuitive time tracking interface
3. **Timecards** - Weekly/monthly views with approval status
4. **Scheduling** - Visual calendar for shifts with drag-and-drop support
5. **Team Management** - Manager views for team timecards and schedules
6. **Receipts/Invoices** - Upload with OCR processing and review queue
7. **Disciplinary Actions** - Incident reporting and HR review workflow
8. **Employees** - Comprehensive employee management and details
9. **Payroll Runs** - Payroll processing with preview and approval
10. **OCR Review** - Queue for reviewing OCR-extracted receipt data
11. **System Settings** - Configuration for users, API keys, automation, branding
12. **Audit Logs** - Complete audit trail with filtering and detail views
## Getting Started
### Prerequisites
- Node.js 18+ and npm
### Installation
1. Install dependencies:
```bash
npm install
```
2. Start the development server:
```bash
npm run dev
```
3. Open your browser to `http://localhost:5173`
### Building for Production
```bash
npm run build
```
The built files will be in the `dist` directory.
## Project Structure
```
src/
├── components/
│ └── Layout/
│ ├── TopBar.tsx # Top navigation bar
│ ├── Sidebar.tsx # Role-filtered sidebar navigation
│ └── Layout.tsx # Main layout wrapper
├── context/
│ └── UserContext.tsx # User state and role management
├── pages/
│ ├── Dashboard.tsx # Role-specific dashboard
│ ├── ClockInOut.tsx # Time tracking interface
│ ├── Timecards.tsx # Employee timecard view
│ ├── Schedule.tsx # Employee schedule view
│ ├── TeamTimecards.tsx # Manager team timecards
│ ├── TeamSchedules.tsx # Manager team scheduling
│ ├── Receipts.tsx # Receipt/invoice upload
│ ├── DisciplinaryActions.tsx # Incident reports & HR review
│ ├── Employees.tsx # Employee management
│ ├── PayrollRuns.tsx # Payroll processing
│ ├── OCRReview.tsx # OCR review queue
│ ├── SystemSettings.tsx # Admin system settings
│ ├── AuditLogs.tsx # Audit log viewer
│ └── Placeholder.tsx # Placeholder for future pages
├── types.ts # TypeScript type definitions
├── App.tsx # Main app with routing
└── main.tsx # Application entry point
```
## Login & Demo Accounts
The application includes a login screen with pre-configured demo accounts:
| Role | Email | Password |
|------|-------|----------|
| Admin | admin@company.com | admin123 |
| HR | hr@company.com | hr123 |
| Payroll | payroll@company.com | payroll123 |
| Manager | manager@company.com | manager123 |
| Employee | employee@company.com | employee123 |
To test different roles, simply log out and log in with a different account.
## User Management
Admin users can access the "User & Role Management" page from the sidebar to:
- Create new users
- Edit existing users
- Delete users
- Assign roles
- Search and filter users
All changes are persisted in localStorage and will remain after page refresh.
## Technologies Used
- **React 18** - UI library
- **TypeScript** - Type safety
- **React Router** - Navigation
- **Tailwind CSS** - Styling
- **Vite** - Build tool
- **Lucide React** - Icons
- **date-fns** - Date formatting
## Notes
- This is a UI wireframe/prototype. Backend integration would be needed for full functionality.
- All data is currently mock/placeholder data.
- The OCR processing, n8n workflows, and other integrations are represented in the UI but not implemented.
- Role-based navigation automatically filters menu items based on user role.
## License
MIT

127
TIMECLOCK.md Normal file
View File

@ -0,0 +1,127 @@
# Time Clock Page
A public-facing time clock interface for employees to sign in and out of their shifts.
## Features
- **Public Access**: No login required to view the page
- **Password-Only Authentication**: Employees only need their password to clock in/out
- **Visual Time Bars**: Each employee has a time bar showing 8 AM - 6 PM
- **Real-Time Status**:
- Red bar = Clocked out
- Green bar = Clocked in
- Yellow bar = On break
- **Schedule Integration**: Shows scheduled shift times and detects late arrivals/early departures
- **Auto-Refresh**: Status updates every 30 seconds
- **Timecard Integration**: Automatically creates/updates timecard entries
## Access
Navigate to: `http://localhost:5173/timeclock`
## Usage
1. **View Status**: All employees are displayed in a grid with their current status
2. **Clock In/Out**: Click on an employee's name
3. **Enter Password**: Enter the employee's password (not email)
4. **Confirm**: Click "Clock In" or "Clock Out" button
## Time Bar Visualization
- **Background**: Gray grid showing hourly markers (8:00 AM - 6:00 PM)
- **Scheduled Time**: Light blue overlay showing scheduled shift times
- **Current Status**: Colored bar (red/green/yellow) showing clock status
- **Current Time Indicator**: Black vertical line showing current time
- **Late Indicator**: Red marker if employee arrived late
## API Endpoints
### GET `/api/timeclock/status`
Returns all employees with their current clock status.
**No authentication required**
Response:
```json
[
{
"id": "1",
"name": "John Doe",
"email": "employee@company.com",
"currentStatus": "clocked_in",
"clockInTime": "08:15",
"scheduledStart": "08:00",
"scheduledEnd": "17:00",
"isLate": true,
"leftEarly": false
}
]
```
### POST `/api/timeclock/action`
Clock in/out action with password authentication.
**No authentication required**
Request:
```json
{
"employeeId": "1",
"password": "employee123",
"action": "clock_in"
}
```
Actions:
- `clock_in` - Clock in for the day
- `clock_out` - Clock out for the day
- `break_start` - Start break
- `break_end` - End break
## Integration
### With Schedule
- Automatically checks if employee has a scheduled shift for today
- Compares clock in time with scheduled start time
- Marks timecard as "Late arrival" if clocked in after scheduled time
- Marks timecard as "Left early" if clocked out before scheduled end time
### With Timecards
- Automatically creates a timecard entry when employee clocks in
- Updates timecard with clock out time
- Calculates total hours and overtime
- Deducts break time from total hours
## Security
- Password verification using bcrypt
- No JWT tokens required for viewing
- Only password needed for clock actions
- All actions are logged in timecards table
## Customization
### Change Time Range
Edit the time bar range in `TimeClock.tsx`:
```typescript
const startMinutes = 8 * 60; // 8:00 AM
const endMinutes = 18 * 60; // 6:00 PM
```
### Change Refresh Interval
Modify the interval in `TimeClock.tsx`:
```typescript
const interval = setInterval(fetchEmployees, 30000); // 30 seconds
```
### Change Colors
Modify the status colors:
```typescript
const getStatusColor = (employee: EmployeeStatus): string => {
if (employee.currentStatus === 'clocked_in') return 'bg-green-500';
if (employee.currentStatus === 'on_break') return 'bg-yellow-500';
return 'bg-red-500';
};
```

57
TROUBLESHOOTING.md Normal file
View File

@ -0,0 +1,57 @@
# Troubleshooting Blank Page Issue
If you're seeing a blank white page, try these steps:
## 1. Check the Correct Port
The dev server might be running on a different port. Check the terminal output - it should show:
```
➜ Local: http://localhost:5174/
```
Make sure you're accessing the correct port (might be 5173, 5174, or another port).
## 2. Check Browser Console
Open your browser's developer console (F12 or Right-click → Inspect → Console tab) and look for any red error messages. Common issues:
- Module not found errors
- Import errors
- TypeScript errors
## 3. Clear Browser Cache
Try a hard refresh:
- **Chrome/Edge**: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
- **Firefox**: Ctrl+F5 (Windows) or Cmd+Shift+R (Mac)
## 4. Restart Dev Server
Stop the dev server (Ctrl+C) and restart it:
```bash
npm run dev
```
## 5. Reinstall Dependencies
If the above doesn't work:
```bash
rm -rf node_modules package-lock.json
npm install
npm run dev
```
## 6. Check for Build Errors
Run the build command to see if there are any TypeScript or build errors:
```bash
npm run build
```
## Common Issues
### "Cannot find module" errors
- Make sure all dependencies are installed: `npm install`
- Check that all import paths are correct
### Tailwind CSS not working
- Make sure `postcss.config.js` and `tailwind.config.js` exist
- Check that `index.css` imports Tailwind directives
### React Router issues
- Make sure you're accessing the root path `/`
- Check that all route components are properly exported

15
index.html Normal file
View File

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Axion - HR/Payroll System</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

223
n8n-workflows/README.md Normal file
View File

@ -0,0 +1,223 @@
# n8n Workflow: Receipt OCR Analysis
This workflow processes receipt images uploaded from the Axion HR system, extracts key information using OCR, and saves it to the backend database.
## Workflow Overview
1. **Webhook Receives Receipt** - Receives POST request with receipt image (base64) and user ID
2. **Extract Data** - Extracts image and user ID from request
3. **OCR API Call** - Sends image to OCR.space API for text extraction
4. **Parse Receipt Data** - Uses regex patterns to extract:
- Amount (total)
- Date
- Vendor name
- Tax amount
- Calculates confidence score
5. **Save to Backend** - Saves extracted data to backend API
6. **Respond Success** - Returns success response with receipt ID and extracted amount
## Setup Instructions
### 1. Import the Workflow
1. Open your n8n instance
2. Click "Workflows" → "Import from File"
3. Select `receipt-ocr-workflow.json`
4. The workflow will be imported with all nodes configured
### 2. Configure Environment Variables
Set these environment variables in your n8n instance:
```bash
OCR_API_KEY=your_ocr_space_api_key
BACKEND_API_URL=https://your-backend-api.com
BACKEND_API_KEY=your_backend_api_key
```
**Getting an OCR API Key:**
- Sign up at https://ocr.space/ocrapi
- Get your free API key (25,000 requests/month free)
- Or use alternative OCR services (Google Vision, AWS Textract, etc.)
### 3. Configure Webhook URL
1. Click on the "Webhook - Receipt Upload" node
2. Note the webhook URL (e.g., `https://your-n8n.com/webhook/receipt-upload`)
3. Update your frontend to POST to this URL
### 4. Update Backend API Endpoint
1. Click on the "Save to Backend" node
2. Update the URL to match your backend API endpoint
3. Ensure your backend expects this data structure:
```json
{
"userId": "string",
"amount": "number",
"date": "string",
"vendor": "string",
"tax": "number",
"confidence": "number",
"status": "string",
"extractedText": "string"
}
```
## Frontend Integration
Update your `Receipts.tsx` component to call the n8n webhook:
```typescript
const handleFile = async (file: File) => {
setUploading(true);
// Convert file to base64
const reader = new FileReader();
reader.onloadend = async () => {
const base64Image = reader.result as string;
try {
const response = await fetch('YOUR_N8N_WEBHOOK_URL', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image: base64Image,
userId: currentUser.id,
}),
});
const result = await response.json();
if (result.success) {
// Update UI with extracted data
setFormData({
amount: result.amount,
// ... other fields
});
}
} catch (error) {
console.error('OCR processing failed:', error);
} finally {
setUploading(false);
}
};
reader.readAsDataURL(file);
};
```
## Workflow Customization
### Using Different OCR Service
Replace the "OCR API Call" node with your preferred service:
**Google Vision API:**
```javascript
// Use Google Vision API node or HTTP Request
POST https://vision.googleapis.com/v1/images:annotate
```
**AWS Textract:**
```javascript
// Use AWS Textract node
```
### Improving Amount Extraction
Modify the regex in "Parse Receipt Data" node:
```javascript
// More robust amount regex
const amountRegex = /(?:total|amount|sum|balance|due|\\$|€|£|USD|EUR)\\s*:?\\s*([\\d,]+\\.[\\d]{2})/i;
```
### Adding Category Detection
Add a Code node after parsing to detect category:
```javascript
const categoryKeywords = {
'Office Supplies': ['office', 'supplies', 'staples', 'paper'],
'Meals': ['restaurant', 'cafe', 'food', 'dining'],
'Transportation': ['uber', 'lyft', 'taxi', 'gas', 'fuel'],
};
// Detect category based on vendor name
let category = 'Other';
for (const [cat, keywords] of Object.entries(categoryKeywords)) {
if (keywords.some(kw => vendor?.toLowerCase().includes(kw))) {
category = cat;
break;
}
}
```
## Testing
### Test the Workflow
1. Use n8n's "Execute Workflow" button
2. Or send a test POST request:
```bash
curl -X POST https://your-n8n.com/webhook/receipt-upload \
-H "Content-Type: application/json" \
-d '{
"image": "base64_encoded_image_here",
"userId": "test-user-123"
}'
```
### Expected Response
```json
{
"success": true,
"receiptId": "receipt-123",
"amount": 45.99,
"confidence": 0.85,
"status": "needs_review"
}
```
## Troubleshooting
### OCR Not Extracting Amount
- Check OCR API key is valid
- Verify image quality (clear, readable text)
- Adjust regex patterns in "Parse Receipt Data" node
- Check OCR API response in node output
### Backend Save Failing
- Verify backend API URL is correct
- Check API authentication headers
- Ensure backend endpoint accepts the data structure
- Check n8n execution logs for errors
### Low Confidence Scores
- Improve image quality before upload
- Adjust regex patterns to match your receipt format
- Add more extraction patterns for different receipt types
- Consider using ML-based extraction for better accuracy
## Alternative OCR Services
If OCR.space doesn't meet your needs:
1. **Google Cloud Vision API** - High accuracy, pay-per-use
2. **AWS Textract** - Good for structured documents
3. **Azure Computer Vision** - Microsoft's OCR service
4. **Tesseract.js** - Open source, runs locally
5. **ABBYY FineReader** - Enterprise-grade OCR
Update the "OCR API Call" node accordingly.

View File

@ -0,0 +1,228 @@
{
"name": "Receipt OCR Analysis",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "receipt-upload",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-receipt-upload",
"name": "Webhook - Receipt Upload",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 300],
"webhookId": "receipt-upload-webhook"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "extract-image",
"name": "image",
"value": "={{ $json.body.image }}",
"type": "string"
},
{
"id": "extract-user-id",
"name": "userId",
"value": "={{ $json.body.userId }}",
"type": "string"
}
]
},
"options": {}
},
"id": "extract-data",
"name": "Extract Receipt Data",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [450, 300]
},
{
"parameters": {
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"httpHeaderAuth": {
"name": "Authorization",
"value": "Bearer {{ $env.OCR_API_KEY }}"
},
"requestMethod": "POST",
"url": "https://api.ocr.space/parse/image",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.OCR_API_KEY }}"
},
{
"name": "base64Image",
"value": "={{ $json.image }}"
},
{
"name": "language",
"value": "eng"
},
{
"name": "isOverlayRequired",
"value": "false"
}
]
},
"options": {}
},
"id": "ocr-api-call",
"name": "OCR API Call",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [650, 300]
},
{
"parameters": {
"jsCode": "// Extract text from OCR response\nconst ocrResponse = $input.item.json;\nconst parsedText = ocrResponse.ParsedResults?.[0]?.ParsedText || '';\n\n// Regular expressions to extract receipt data\nconst amountRegex = /(?:total|amount|sum|\\$|€|£)\\s*:?\\s*([\\d,]+\\.[\\d]{2})/i;\nconst dateRegex = /(\\d{1,2}[\\/\\-]\\d{1,2}[\\/\\-]\\d{2,4})/;\nconst vendorRegex = /^([A-Z][A-Za-z\\s&]+?)(?:\\s|$)/m;\nconst taxRegex = /(?:tax|vat|gst)\\s*:?\\s*([\\d,]+\\.[\\d]{2})/i;\n\n// Extract amount\nlet amount = null;\nconst amountMatch = parsedText.match(amountRegex);\nif (amountMatch) {\n amount = parseFloat(amountMatch[1].replace(/,/g, ''));\n}\n\n// Extract date\nlet date = null;\nconst dateMatch = parsedText.match(dateRegex);\nif (dateMatch) {\n date = dateMatch[1];\n}\n\n// Extract vendor (first line or company name)\nlet vendor = null;\nconst vendorMatch = parsedText.match(vendorRegex);\nif (vendorMatch) {\n vendor = vendorMatch[1].trim();\n}\n\n// Extract tax\nlet tax = null;\nconst taxMatch = parsedText.match(taxRegex);\nif (taxMatch) {\n tax = parseFloat(taxMatch[1].replace(/,/g, ''));\n}\n\n// Calculate confidence score based on extracted data\nlet confidence = 0;\nif (amount) confidence += 0.4;\nif (date) confidence += 0.2;\nif (vendor) confidence += 0.2;\nif (tax) confidence += 0.2;\n\nreturn {\n json: {\n userId: $input.item.json.userId,\n originalImage: $input.item.json.image,\n extractedText: parsedText,\n amount: amount,\n date: date,\n vendor: vendor,\n tax: tax || 0,\n confidence: confidence,\n status: confidence >= 0.6 ? 'needs_review' : 'pending',\n ocrRawResponse: ocrResponse\n }\n};"
},
"id": "parse-receipt-data",
"name": "Parse Receipt Data",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [850, 300]
},
{
"parameters": {
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"httpHeaderAuth": {
"name": "Authorization",
"value": "Bearer {{ $env.BACKEND_API_KEY }}"
},
"requestMethod": "POST",
"url": "={{ $env.BACKEND_API_URL }}/api/receipts",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "userId",
"value": "={{ $json.userId }}"
},
{
"name": "amount",
"value": "={{ $json.amount }}"
},
{
"name": "date",
"value": "={{ $json.date }}"
},
{
"name": "vendor",
"value": "={{ $json.vendor }}"
},
{
"name": "tax",
"value": "={{ $json.tax }}"
},
{
"name": "confidence",
"value": "={{ $json.confidence }}"
},
{
"name": "status",
"value": "={{ $json.status }}"
},
{
"name": "extractedText",
"value": "={{ $json.extractedText }}"
}
]
},
"options": {}
},
"id": "save-to-backend",
"name": "Save to Backend",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 1,
"position": [1050, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { \"success\": true, \"receiptId\": $json.id, \"amount\": $json.amount, \"confidence\": $json.confidence, \"status\": $json.status } }}"
},
"id": "respond-success",
"name": "Respond Success",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [1250, 300]
}
],
"connections": {
"Webhook - Receipt Upload": {
"main": [
[
{
"node": "Extract Receipt Data",
"type": "main",
"index": 0
}
]
]
},
"Extract Receipt Data": {
"main": [
[
{
"node": "OCR API Call",
"type": "main",
"index": 0
}
]
]
},
"OCR API Call": {
"main": [
[
{
"node": "Parse Receipt Data",
"type": "main",
"index": 0
}
]
]
},
"Parse Receipt Data": {
"main": [
[
{
"node": "Save to Backend",
"type": "main",
"index": 0
}
]
]
},
"Save to Backend": {
"main": [
[
{
"node": "Respond Success",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 1,
"updatedAt": "2024-01-01T00:00:00.000Z",
"versionId": "1"
}

4220
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "axion-hr-system",
"version": "1.0.0",
"description": "Unified HR/Payroll/Finance/Employee system with scheduling and timecards",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"lucide-react": "^0.294.0",
"date-fns": "^2.30.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}

8
postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

7
server/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules/
data/
.env
*.log
.DS_Store

193
server/README.md Normal file
View File

@ -0,0 +1,193 @@
# Axion Backend API
Backend server for the Axion HR/Payroll System with SQLite database.
## Features
- SQLite database with comprehensive schema
- RESTful API endpoints
- JWT authentication
- Role-based access control
- CORS enabled for frontend integration
## Database Schema
The database includes tables for:
- Users (authentication)
- Employees (extended employee info)
- Timecards
- Shifts (scheduling)
- Disciplinary Actions
- Receipts/Invoices
- Payroll Runs
- Payroll Line Items
- Audit Logs
- Performance Reviews
- Documents
## Setup
### 1. Install Dependencies
```bash
cd server
npm install
```
### 2. Initialize Database
```bash
npm run init-db
```
This will:
- Create the database file at `server/data/axion.db`
- Create all tables
- Insert default users (admin, hr, payroll, manager, employee)
### 3. Configure Environment
Create a `.env` file:
```bash
cp .env.example .env
```
Edit `.env` and set:
- `PORT` - Server port (default: 3001)
- `JWT_SECRET` - Secret key for JWT tokens (change in production!)
### 4. Start Server
Development mode (with auto-reload):
```bash
npm run dev
```
Production mode:
```bash
npm start
```
The server will run on `http://localhost:3001`
## API Endpoints
### Authentication
- `POST /api/auth/login` - Login with email/password
- `GET /api/auth/me` - Get current user info
### Users
- `GET /api/users` - Get all users (admin/hr only)
- `GET /api/users/:id` - Get user by ID
- `POST /api/users` - Create user (admin only)
- `PUT /api/users/:id` - Update user (admin/hr only)
- `DELETE /api/users/:id` - Delete user (admin only)
### Receipts
- `GET /api/receipts` - Get receipts (filtered by user)
- `GET /api/receipts/:id` - Get receipt by ID
- `POST /api/receipts` - Create receipt (from OCR)
- `PUT /api/receipts/:id` - Update receipt
- `DELETE /api/receipts/:id` - Delete receipt
### Timecards
- `GET /api/timecards` - Get timecards
- `POST /api/timecards` - Create timecard
- `PATCH /api/timecards/:id/status` - Update timecard status
## Default Users
After initialization, these users are available:
| Email | Password | Role |
|-------|----------|------|
| admin@company.com | admin123 | admin |
| hr@company.com | hr123 | hr |
| payroll@company.com | payroll123 | payroll |
| manager@company.com | manager123 | manager |
| employee@company.com | employee123 | employee |
## Database Location
The SQLite database file is stored at:
```
server/data/axion.db
```
This file is gitignored. To backup, copy this file.
## Development
### Database Migrations
To add new tables or modify schema:
1. Update `src/database/schema.sql`
2. Run `npm run init-db` (this will recreate the database)
For production, use proper migration tools.
### Adding New Routes
1. Create route file in `src/routes/`
2. Import and use in `src/server.js`
Example:
```javascript
import newRoutes from './routes/new.js';
app.use('/api/new', newRoutes);
```
## Production Considerations
1. **Change JWT_SECRET** - Use a strong, random secret
2. **Use PostgreSQL/MySQL** - SQLite is fine for development, but use a proper database for production
3. **Add rate limiting** - Prevent abuse
4. **Enable HTTPS** - Use reverse proxy (nginx) with SSL
5. **Database backups** - Set up regular backups
6. **Environment variables** - Never commit `.env` file
## Frontend Integration
Update your frontend to use the API:
```javascript
const API_URL = 'http://localhost:3001/api';
// Login
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const { token, user } = await response.json();
localStorage.setItem('token', token);
// Authenticated requests
const usersResponse = await fetch(`${API_URL}/users`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
```
## Troubleshooting
### Database locked error
- SQLite uses WAL mode for better concurrency
- If issues persist, check file permissions
### Port already in use
- Change PORT in `.env`
- Or kill the process using port 3001
### Module not found
- Run `npm install` again
- Check Node.js version (requires Node 18+)

2764
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
server/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "axion-backend",
"version": "1.0.0",
"description": "Backend API for Axion HR/Payroll System",
"main": "src/server.js",
"type": "module",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js",
"init-db": "node src/database/init.js",
"migrate": "node src/database/migrate.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"sqlite3": "^5.1.6",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"dotenv": "^16.3.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^20.10.5"
}
}

26
server/src/database/db.js Normal file
View File

@ -0,0 +1,26 @@
import sqlite3 from 'sqlite3';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { promisify } from 'util';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dbPath = join(__dirname, '../../data/axion.db');
// Create database connection with promise wrapper
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error opening database:', err);
} else {
// Enable foreign keys
db.run('PRAGMA foreign_keys = ON');
}
});
// Promisify methods
db.runAsync = promisify(db.run.bind(db));
db.getAsync = promisify(db.get.bind(db));
db.allAsync = promisify(db.all.bind(db));
export default db;

144
server/src/database/init.js Normal file
View File

@ -0,0 +1,144 @@
import sqlite3 from 'sqlite3';
import { readFileSync, mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import bcrypt from 'bcryptjs';
import { promisify } from 'util';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Ensure data directory exists
const dataDir = join(__dirname, '../../data');
try {
mkdirSync(dataDir, { recursive: true });
} catch (error) {
// Directory might already exist
}
const dbPath = join(dataDir, 'axion.db');
console.log('Initializing database...');
const db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error('Error creating database:', err);
process.exit(1);
}
// Enable foreign keys
db.run('PRAGMA foreign_keys = ON');
// Read and execute schema
const schemaPath = join(__dirname, 'schema.sql');
const schema = readFileSync(schemaPath, 'utf-8');
db.exec(schema, (err) => {
if (err) {
console.error('Error executing schema:', err);
process.exit(1);
}
console.log('Database schema created successfully.');
// Insert default users
insertDefaultUsers();
});
});
function insertDefaultUsers() {
const defaultUsers = [
{
id: '1',
email: 'admin@company.com',
password: 'admin123',
name: 'Admin User',
role: 'admin'
},
{
id: '2',
email: 'hr@company.com',
password: 'hr123',
name: 'HR Manager',
role: 'hr'
},
{
id: '3',
email: 'payroll@company.com',
password: 'payroll123',
name: 'Payroll Admin',
role: 'payroll'
},
{
id: '4',
email: 'manager@company.com',
password: 'manager123',
name: 'Team Manager',
role: 'manager'
},
{
id: '5',
email: 'employee@company.com',
password: 'employee123',
name: 'John Doe',
role: 'employee'
}
];
let completed = 0;
const total = defaultUsers.length;
defaultUsers.forEach((user) => {
const passwordHash = bcrypt.hashSync(user.password, 10);
db.run(
`INSERT OR IGNORE INTO users (id, email, password_hash, name, role)
VALUES (?, ?, ?, ?, ?)`,
[user.id, user.email, passwordHash, user.name, user.role],
function(err) {
if (err) {
console.error(`Error inserting user ${user.email}:`, err);
} else {
// Create employee records for non-admin users
if (user.role !== 'admin') {
const jobTitles = {
'hr': 'HR Manager',
'payroll': 'Payroll Admin',
'manager': 'Team Manager',
'employee': 'Cashier'
};
const departments = {
'hr': 'Human Resources',
'payroll': 'Finance',
'manager': 'Retail',
'employee': 'Retail'
};
db.run(
`INSERT OR IGNORE INTO employees (id, user_id, job_title, department, status)
VALUES (?, ?, ?, ?, 'Active')`,
[`emp-${user.id}`, user.id, jobTitles[user.role] || 'Employee', departments[user.role] || 'General'],
(err) => {
if (err) console.error(`Error inserting employee for ${user.email}:`, err);
}
);
}
}
completed++;
if (completed === total) {
console.log('Default users created:');
console.log(' Admin: admin@company.com / admin123');
console.log(' HR: hr@company.com / hr123');
console.log(' Payroll: payroll@company.com / payroll123');
console.log(' Manager: manager@company.com / manager123');
console.log(' Employee: employee@company.com / employee123');
console.log('Database initialization complete!');
console.log(`Database file: ${dbPath}`);
db.close();
}
}
);
});
}

View File

@ -0,0 +1,182 @@
-- Axion HR/Payroll System Database Schema
-- Users table (for authentication and user management)
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('employee', 'manager', 'hr', 'payroll', 'admin')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Employees table (extended employee information)
CREATE TABLE IF NOT EXISTS employees (
id TEXT PRIMARY KEY,
user_id TEXT UNIQUE NOT NULL,
job_title TEXT,
department TEXT,
manager_id TEXT,
phone TEXT,
address TEXT,
hire_date DATE,
status TEXT DEFAULT 'Active' CHECK(status IN ('Active', 'Inactive', 'On Leave')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (manager_id) REFERENCES users(id) ON DELETE SET NULL
);
-- Timecards table
CREATE TABLE IF NOT EXISTS timecards (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
date DATE NOT NULL,
clock_in DATETIME,
clock_out DATETIME,
break_start DATETIME,
break_end DATETIME,
total_hours REAL DEFAULT 0,
overtime_hours REAL DEFAULT 0,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'approved', 'flagged')),
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Shifts table (scheduling)
CREATE TABLE IF NOT EXISTS shifts (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
role TEXT,
location TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Disciplinary actions table
CREATE TABLE IF NOT EXISTS disciplinary_actions (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
reported_by TEXT NOT NULL,
incident_date DATE NOT NULL,
details TEXT NOT NULL,
severity TEXT NOT NULL CHECK(severity IN ('low', 'medium', 'high', 'critical')),
status TEXT DEFAULT 'draft' CHECK(status IN ('draft', 'pending_approval', 'finalized')),
document_url TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (reported_by) REFERENCES users(id) ON DELETE CASCADE
);
-- Receipts/Invoices table
CREATE TABLE IF NOT EXISTS receipts (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
vendor TEXT,
amount REAL NOT NULL,
date DATE,
tax REAL DEFAULT 0,
category TEXT,
notes TEXT,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'needs_review', 'approved', 'rejected')),
ocr_confidence REAL,
image_url TEXT,
extracted_text TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Payroll runs table
CREATE TABLE IF NOT EXISTS payroll_runs (
id TEXT PRIMARY KEY,
pay_period_start DATE NOT NULL,
pay_period_end DATE NOT NULL,
status TEXT DEFAULT 'preview' CHECK(status IN ('preview', 'pending', 'completed')),
total_hours REAL DEFAULT 0,
total_gross REAL DEFAULT 0,
total_net REAL DEFAULT 0,
errors INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Payroll line items (individual employee payroll entries)
CREATE TABLE IF NOT EXISTS payroll_line_items (
id TEXT PRIMARY KEY,
payroll_run_id TEXT NOT NULL,
employee_id TEXT NOT NULL,
hours REAL NOT NULL,
rate REAL NOT NULL,
gross_pay REAL NOT NULL,
deductions REAL DEFAULT 0,
taxes REAL DEFAULT 0,
net_pay REAL NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (payroll_run_id) REFERENCES payroll_runs(id) ON DELETE CASCADE,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Audit logs table
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
actor_id TEXT NOT NULL,
action TEXT NOT NULL,
table_name TEXT NOT NULL,
record_id TEXT,
ip_address TEXT,
severity TEXT DEFAULT 'low' CHECK(severity IN ('low', 'medium', 'high')),
before_data TEXT,
after_data TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Performance reviews table
CREATE TABLE IF NOT EXISTS performance_reviews (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
reviewer_id TEXT NOT NULL,
review_date DATE NOT NULL,
rating INTEGER CHECK(rating >= 1 AND rating <= 5),
comments TEXT,
goals TEXT,
status TEXT DEFAULT 'draft' CHECK(status IN ('draft', 'submitted', 'completed')),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (reviewer_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Documents table
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
employee_id TEXT,
name TEXT NOT NULL,
type TEXT NOT NULL,
file_url TEXT NOT NULL,
expiry_date DATE,
uploaded_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE CASCADE
);
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_timecards_employee_date ON timecards(employee_id, date);
CREATE INDEX IF NOT EXISTS idx_shifts_employee_date ON shifts(employee_id, date);
CREATE INDEX IF NOT EXISTS idx_receipts_user_status ON receipts(user_id, status);
CREATE INDEX IF NOT EXISTS idx_audit_logs_actor ON audit_logs(actor_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at);
CREATE INDEX IF NOT EXISTS idx_employees_user ON employees(user_id);
CREATE INDEX IF NOT EXISTS idx_payroll_line_items_run ON payroll_line_items(payroll_run_id);

View File

@ -0,0 +1,36 @@
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
};
export const requireRole = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};

74
server/src/routes/auth.js Normal file
View File

@ -0,0 +1,74 @@
import express from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import db from '../database/db.js';
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await db.getAsync('SELECT * FROM users WHERE email = ?', email);
if (!user) {
return res.status(401).json({ error: 'Invalid email or password' });
}
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid email or password' });
}
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get current user
router.get('/me', async (req, res) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = jwt.verify(token, JWT_SECRET);
const user = await db.getAsync('SELECT id, email, name, role FROM users WHERE id = ?', decoded.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ user });
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
});
export default router;

View File

@ -0,0 +1,167 @@
import express from 'express';
import { authenticateToken } from '../middleware/auth.js';
import db from '../database/db.js';
import { v4 as uuidv4 } from 'uuid';
const router = express.Router();
// Get all receipts (filtered by user unless admin/payroll)
router.get('/', authenticateToken, async (req, res) => {
try {
let query = 'SELECT * FROM receipts';
let params = [];
if (!['admin', 'payroll'].includes(req.user.role)) {
query += ' WHERE user_id = ?';
params.push(req.user.id);
}
query += ' ORDER BY created_at DESC';
const receipts = await db.allAsync(query, ...params);
res.json(receipts);
} catch (error) {
console.error('Error fetching receipts:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get receipt by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const receipt = await db.getAsync('SELECT * FROM receipts WHERE id = ?', id);
if (!receipt) {
return res.status(404).json({ error: 'Receipt not found' });
}
// Check permissions
if (receipt.user_id !== req.user.id && !['admin', 'payroll'].includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
res.json(receipt);
} catch (error) {
console.error('Error fetching receipt:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Create receipt (from OCR processing)
router.post('/', authenticateToken, async (req, res) => {
try {
const {
vendor,
amount,
date,
tax,
category,
notes,
status,
ocrConfidence,
imageUrl,
extractedText
} = req.body;
if (!amount) {
return res.status(400).json({ error: 'Amount is required' });
}
const id = uuidv4();
const receipt = {
id,
user_id: req.user.id,
vendor: vendor || null,
amount: parseFloat(amount),
date: date || new Date().toISOString().split('T')[0],
tax: tax ? parseFloat(tax) : 0,
category: category || null,
notes: notes || null,
status: status || 'pending',
ocr_confidence: ocrConfidence || null,
image_url: imageUrl || null,
extracted_text: extractedText || null
};
await db.runAsync(`
INSERT INTO receipts (
id, user_id, vendor, amount, date, tax, category, notes,
status, ocr_confidence, image_url, extracted_text
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
receipt.id, receipt.user_id, receipt.vendor, receipt.amount,
receipt.date, receipt.tax, receipt.category, receipt.notes,
receipt.status, receipt.ocr_confidence, receipt.image_url, receipt.extracted_text
);
res.status(201).json(receipt);
} catch (error) {
console.error('Error creating receipt:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Update receipt
router.put('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const receipt = await db.getAsync('SELECT * FROM receipts WHERE id = ?', id);
if (!receipt) {
return res.status(404).json({ error: 'Receipt not found' });
}
// Check permissions
if (receipt.user_id !== req.user.id && !['admin', 'payroll'].includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
const {
vendor,
amount,
date,
tax,
category,
notes,
status
} = req.body;
await db.runAsync(`
UPDATE receipts
SET vendor = ?, amount = ?, date = ?, tax = ?, category = ?,
notes = ?, status = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, vendor, amount, date, tax, category, notes, status, id);
res.json({ message: 'Receipt updated successfully' });
} catch (error) {
console.error('Error updating receipt:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Delete receipt
router.delete('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const receipt = await db.getAsync('SELECT * FROM receipts WHERE id = ?', id);
if (!receipt) {
return res.status(404).json({ error: 'Receipt not found' });
}
// Check permissions
if (receipt.user_id !== req.user.id && !['admin', 'payroll'].includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
await db.runAsync('DELETE FROM receipts WHERE id = ?', id);
res.json({ message: 'Receipt deleted successfully' });
} catch (error) {
console.error('Error deleting receipt:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

View File

@ -0,0 +1,116 @@
import express from 'express';
import { authenticateToken } from '../middleware/auth.js';
import db from '../database/db.js';
import { v4 as uuidv4 } from 'uuid';
const router = express.Router();
// Get timecards
router.get('/', authenticateToken, async (req, res) => {
try {
const { employeeId, startDate, endDate } = req.query;
let query = 'SELECT * FROM timecards WHERE 1=1';
let params = [];
// Filter by employee unless admin/manager/hr
if (!['admin', 'manager', 'hr'].includes(req.user.role)) {
query += ' AND employee_id = ?';
params.push(req.user.id);
} else if (employeeId) {
query += ' AND employee_id = ?';
params.push(employeeId);
}
if (startDate) {
query += ' AND date >= ?';
params.push(startDate);
}
if (endDate) {
query += ' AND date <= ?';
params.push(endDate);
}
query += ' ORDER BY date DESC';
const timecards = await db.allAsync(query, ...params);
res.json(timecards);
} catch (error) {
console.error('Error fetching timecards:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Create/Update timecard
router.post('/', authenticateToken, async (req, res) => {
try {
const {
date,
clockIn,
clockOut,
breakStart,
breakEnd,
notes
} = req.body;
const employeeId = req.body.employeeId || req.user.id;
// Check permissions
if (employeeId !== req.user.id && !['admin', 'manager', 'hr'].includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
// Calculate hours
let totalHours = 0;
if (clockIn && clockOut) {
const start = new Date(clockIn);
const end = new Date(clockOut);
const diff = (end - start) / (1000 * 60 * 60); // hours
let breakTime = 0;
if (breakStart && breakEnd) {
const breakStartTime = new Date(breakStart);
const breakEndTime = new Date(breakEnd);
breakTime = (breakEndTime - breakStartTime) / (1000 * 60 * 60);
}
totalHours = diff - breakTime;
}
const overtimeHours = totalHours > 8 ? totalHours - 8 : 0;
const id = uuidv4();
await db.runAsync(`
INSERT INTO timecards (
id, employee_id, date, clock_in, clock_out, break_start, break_end,
total_hours, overtime_hours, notes, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
`, id, employeeId, date, clockIn, clockOut, breakStart, breakEnd,
totalHours, overtimeHours, notes);
res.status(201).json({ id, message: 'Timecard created successfully' });
} catch (error) {
console.error('Error creating timecard:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Update timecard status (approve/flag)
router.patch('/:id/status', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
const { status } = req.body;
if (!['admin', 'manager', 'hr'].includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
await db.runAsync('UPDATE timecards SET status = ? WHERE id = ?', status, id);
res.json({ message: 'Timecard status updated' });
} catch (error) {
console.error('Error updating timecard:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

View File

@ -0,0 +1,310 @@
import express from 'express';
import bcrypt from 'bcryptjs';
import db from '../database/db.js';
import { v4 as uuidv4 } from 'uuid';
const router = express.Router();
// Get all employees with current status (PUBLIC - no auth required)
router.get('/status', async (req, res) => {
try {
const now = new Date();
const today = now.toISOString().split('T')[0];
const currentTime = now.toTimeString().split(' ')[0].substring(0, 5); // HH:mm format
// Get all active employees with their current status
const employees = await db.allAsync(`
SELECT
u.id,
u.name,
u.email,
e.job_title,
s.start_time as scheduled_start,
s.end_time as scheduled_end
FROM users u
LEFT JOIN employees e ON u.id = e.user_id
LEFT JOIN shifts s ON u.id = s.employee_id AND s.date = ?
WHERE e.status = 'Active' AND u.role != 'admin'
ORDER BY u.name
`, today);
// Get current timecard status for each employee
const statuses = await Promise.all(employees.map(async (emp) => {
const timecard = await db.getAsync(`
SELECT clock_in, clock_out, break_start, break_end, status
FROM timecards
WHERE employee_id = ? AND date = ?
ORDER BY created_at DESC
LIMIT 1
`, emp.id, today);
let currentStatus = 'clocked_out';
let clockInTime = null;
let clockOutTime = null;
let breakStartTime = null;
let isLate = false;
let leftEarly = false;
if (timecard) {
clockInTime = timecard.clock_in;
clockOutTime = timecard.clock_out;
// Status is only "clocked_in" or "on_break" if they clocked in but haven't clocked out yet
if (timecard.clock_in && !timecard.clock_out) {
if (timecard.break_start && !timecard.break_end) {
currentStatus = 'on_break';
breakStartTime = timecard.break_start;
} else {
currentStatus = 'clocked_in';
}
} else {
// If they've clocked out, status is clocked_out (even if they clocked in earlier)
currentStatus = 'clocked_out';
}
// Check if late (clocked in after scheduled start)
if (emp.scheduled_start && clockInTime) {
const scheduledTime = emp.scheduled_start;
const clockInTimeOnly = clockInTime.includes(' ')
? clockInTime.split(' ')[1]?.substring(0, 5)
: clockInTime.substring(0, 5);
if (clockInTimeOnly && clockInTimeOnly > scheduledTime) {
isLate = true;
}
}
// Check if left early (clocked out before scheduled end)
if (emp.scheduled_end && timecard.clock_out) {
const scheduledEndTime = emp.scheduled_end;
const clockOutTimeOnly = timecard.clock_out.includes(' ')
? timecard.clock_out.split(' ')[1]?.substring(0, 5)
: timecard.clock_out.substring(0, 5);
if (clockOutTimeOnly && clockOutTimeOnly < scheduledEndTime) {
leftEarly = true;
}
}
}
// Format times to HH:mm
let formattedClockIn = null;
if (clockInTime) {
const timePart = clockInTime.includes(' ') ? clockInTime.split(' ')[1] : clockInTime;
formattedClockIn = timePart.substring(0, 5); // HH:mm
}
let formattedClockOut = null;
if (clockOutTime) {
const timePart = clockOutTime.includes(' ') ? clockOutTime.split(' ')[1] : clockOutTime;
formattedClockOut = timePart.substring(0, 5);
}
let formattedBreakStart = null;
if (breakStartTime) {
const timePart = breakStartTime.includes(' ') ? breakStartTime.split(' ')[1] : breakStartTime;
formattedBreakStart = timePart.substring(0, 5);
}
return {
id: emp.id,
name: emp.name,
email: emp.email,
currentStatus,
clockInTime: formattedClockIn,
clockOutTime: formattedClockOut,
breakStartTime: formattedBreakStart,
scheduledStart: emp.scheduled_start,
scheduledEnd: emp.scheduled_end,
isLate,
leftEarly,
};
}));
res.json(statuses);
} catch (error) {
console.error('Error fetching employee status:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Clock in/out action (PUBLIC - password only)
router.post('/action', async (req, res) => {
try {
const { employeeId, password, action } = req.body;
if (!employeeId || !password || !action) {
return res.status(400).json({ error: 'Employee ID, password, and action are required' });
}
if (!['clock_in', 'clock_out', 'break_start', 'break_end'].includes(action)) {
return res.status(400).json({ error: 'Invalid action' });
}
// Verify password
const user = await db.getAsync('SELECT * FROM users WHERE id = ?', employeeId);
if (!user) {
return res.status(404).json({ error: 'Employee not found' });
}
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid password' });
}
const now = new Date();
const today = now.toISOString().split('T')[0];
const currentDateTime = now.toISOString();
// Get or create today's timecard
let timecard = await db.getAsync(`
SELECT * FROM timecards
WHERE employee_id = ? AND date = ?
ORDER BY created_at DESC
LIMIT 1
`, employeeId, today);
if (!timecard) {
// Create new timecard
const timecardId = uuidv4();
await db.runAsync(`
INSERT INTO timecards (id, employee_id, date, status)
VALUES (?, ?, ?, 'pending')
`, timecardId, employeeId, today);
timecard = { id: timecardId, employee_id: employeeId, date: today };
}
// Get scheduled shift for today
const shift = await db.getAsync(`
SELECT start_time, end_time FROM shifts
WHERE employee_id = ? AND date = ?
LIMIT 1
`, employeeId, today);
// Perform action
if (action === 'clock_in') {
// Allow clock in if they haven't clocked in today, OR if they've already clocked out
if (timecard.clock_in && !timecard.clock_out) {
return res.status(400).json({ error: 'Already clocked in. Please clock out first.' });
}
// If they clocked out earlier today, create a new timecard entry for a new shift
if (timecard.clock_out) {
const newTimecardId = uuidv4();
await db.runAsync(`
INSERT INTO timecards (id, employee_id, date, status)
VALUES (?, ?, ?, 'pending')
`, newTimecardId, employeeId, today);
timecard = { id: newTimecardId, employee_id: employeeId, date: today, clock_in: null, clock_out: null };
}
await db.runAsync(`
UPDATE timecards
SET clock_in = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, currentDateTime, timecard.id);
// Check if late
if (shift && shift.start_time) {
const scheduledTime = new Date(`${today}T${shift.start_time}`);
if (now > scheduledTime) {
// Mark as late in notes
await db.runAsync(`
UPDATE timecards
SET notes = 'Late arrival', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, timecard.id);
}
}
} else if (action === 'clock_out') {
if (!timecard.clock_in) {
return res.status(400).json({ error: 'Must clock in first' });
}
if (timecard.clock_out) {
return res.status(400).json({ error: 'Already clocked out' });
}
// Calculate hours
const clockIn = new Date(timecard.clock_in);
const clockOut = now;
const diffMs = clockOut - clockIn;
const diffHours = diffMs / (1000 * 60 * 60);
let breakTime = 0;
if (timecard.break_start && timecard.break_end) {
const breakStart = new Date(timecard.break_start);
const breakEnd = new Date(timecard.break_end);
breakTime = (breakEnd - breakStart) / (1000 * 60 * 60);
}
const totalHours = diffHours - breakTime;
const overtimeHours = totalHours > 8 ? totalHours - 8 : 0;
await db.runAsync(`
UPDATE timecards
SET clock_out = ?,
total_hours = ?,
overtime_hours = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, currentDateTime, totalHours, overtimeHours, timecard.id);
// Check if left early
if (shift && shift.end_time) {
const scheduledEnd = new Date(`${today}T${shift.end_time}`);
if (now < scheduledEnd) {
const currentNotes = timecard.notes || '';
await db.runAsync(`
UPDATE timecards
SET notes = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, currentNotes ? `${currentNotes}; Left early` : 'Left early', timecard.id);
}
}
} else if (action === 'break_start') {
if (!timecard.clock_in) {
return res.status(400).json({ error: 'Must clock in first' });
}
if (timecard.break_start && !timecard.break_end) {
return res.status(400).json({ error: 'Already on break' });
}
await db.runAsync(`
UPDATE timecards
SET break_start = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, currentDateTime, timecard.id);
} else if (action === 'break_end') {
if (!timecard.break_start) {
return res.status(400).json({ error: 'Not on break' });
}
if (timecard.break_end) {
return res.status(400).json({ error: 'Break already ended' });
}
await db.runAsync(`
UPDATE timecards
SET break_end = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, currentDateTime, timecard.id);
}
res.json({
success: true,
message: `Successfully ${action.replace('_', ' ')}`,
timecardId: timecard.id
});
} catch (error) {
console.error('Error processing clock action:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

143
server/src/routes/users.js Normal file
View File

@ -0,0 +1,143 @@
import express from 'express';
import bcrypt from 'bcryptjs';
import { authenticateToken, requireRole } from '../middleware/auth.js';
import db from '../database/db.js';
import { v4 as uuidv4 } from 'uuid';
const router = express.Router();
// Get all users
router.get('/', authenticateToken, requireRole('admin', 'hr'), async (req, res) => {
try {
const users = await db.allAsync(`
SELECT u.id, u.email, u.name, u.role, u.created_at,
e.job_title, e.department, e.status, e.phone
FROM users u
LEFT JOIN employees e ON u.id = e.user_id
ORDER BY u.name
`);
res.json(users);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get user by ID
router.get('/:id', authenticateToken, async (req, res) => {
try {
const { id } = req.params;
// Users can only view their own profile unless admin/hr
if (req.user.id !== id && !['admin', 'hr'].includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
const user = await db.getAsync(`
SELECT u.id, u.email, u.name, u.role, u.created_at,
e.job_title, e.department, e.manager_id, e.phone, e.address,
e.hire_date, e.status
FROM users u
LEFT JOIN employees e ON u.id = e.user_id
WHERE u.id = ?
`, 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: 'Internal server error' });
}
});
// Create user
router.post('/', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { name, email, role, jobTitle, department, phone, address } = req.body;
if (!name || !email || !role) {
return res.status(400).json({ error: 'Name, email, and role are required' });
}
const id = uuidv4();
const defaultPassword = 'password123'; // Should be changed on first login
const passwordHash = bcrypt.hashSync(defaultPassword, 10);
// Insert user
await db.runAsync(`
INSERT INTO users (id, email, password_hash, name, role)
VALUES (?, ?, ?, ?, ?)
`, id, email, passwordHash, name, role);
// Insert employee record if not admin
if (role !== 'admin') {
await db.runAsync(`
INSERT INTO employees (id, user_id, job_title, department, phone, address, status)
VALUES (?, ?, ?, ?, ?, ?, 'Active')
`, uuidv4(), id, jobTitle, department, phone, address);
}
res.status(201).json({ id, message: 'User created successfully' });
} catch (error) {
if (error.message && error.message.includes('UNIQUE constraint')) {
return res.status(409).json({ error: 'Email already exists' });
}
console.error('Error creating user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Update user
router.put('/:id', authenticateToken, requireRole('admin', 'hr'), async (req, res) => {
try {
const { id } = req.params;
const { name, email, role, jobTitle, department, phone, address, status } = req.body;
// Update user
await db.runAsync(`
UPDATE users
SET name = ?, email = ?, role = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, name, email, role, id);
// Update employee record
const employee = await db.getAsync('SELECT id FROM employees WHERE user_id = ?', id);
if (employee) {
await db.runAsync(`
UPDATE employees
SET job_title = ?, department = ?, phone = ?, address = ?,
status = ?, updated_at = CURRENT_TIMESTAMP
WHERE user_id = ?
`, jobTitle, department, phone, address, status, id);
}
res.json({ message: 'User updated successfully' });
} catch (error) {
console.error('Error updating user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Delete user
router.delete('/:id', authenticateToken, requireRole('admin'), async (req, res) => {
try {
const { id } = req.params;
if (id === req.user.id) {
return res.status(400).json({ error: 'Cannot delete your own account' });
}
await db.runAsync('DELETE FROM users WHERE id = ?', id);
res.json({ message: 'User deleted successfully' });
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;

41
server/src/server.js Normal file
View File

@ -0,0 +1,41 @@
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import authRoutes from './routes/auth.js';
import userRoutes from './routes/users.js';
import receiptRoutes from './routes/receipts.js';
import timecardRoutes from './routes/timecards.js';
import timeclockRoutes from './routes/timeclock.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json());
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/receipts', receiptRoutes);
app.use('/api/timecards', timecardRoutes);
app.use('/api/timeclock', timeclockRoutes);
// Error handling
app.use((err, req, res, next) => {
console.error('Error:', err);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Health check: http://localhost:${PORT}/health`);
});

235
src/App.tsx Normal file
View File

@ -0,0 +1,235 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Login } from './pages/Login';
import { TimeClock } from './pages/TimeClock';
import { Dashboard } from './pages/Dashboard';
import { ClockInOut } from './pages/ClockInOut';
import { Timecards } from './pages/Timecards';
import { Schedule } from './pages/Schedule';
import { TeamTimecards } from './pages/TeamTimecards';
import { TeamSchedules } from './pages/TeamSchedules';
import { Receipts } from './pages/Receipts';
import { DisciplinaryActions } from './pages/DisciplinaryActions';
import { Employees } from './pages/Employees';
import { PayrollRuns } from './pages/PayrollRuns';
import { OCRReview } from './pages/OCRReview';
import { SystemSettings } from './pages/SystemSettings';
import { AuditLogs } from './pages/AuditLogs';
import { UserManagement } from './pages/UserManagement';
import { Placeholder } from './pages/Placeholder';
import { ProtectedRoute } from './components/ProtectedRoute';
import { useAuth } from './context/AuthContext';
function LoginRoute() {
const { isAuthenticated } = useAuth();
return isAuthenticated ? <Navigate to="/" replace /> : <Login />;
}
function AppRoutes() {
return (
<Routes>
<Route path="/timeclock" element={<TimeClock />} />
<Route path="/login" element={<LoginRoute />} />
<Route
path="/"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/clock"
element={
<ProtectedRoute>
<ClockInOut />
</ProtectedRoute>
}
/>
<Route
path="/timecards"
element={
<ProtectedRoute>
<Timecards />
</ProtectedRoute>
}
/>
<Route
path="/schedule"
element={
<ProtectedRoute>
<Schedule />
</ProtectedRoute>
}
/>
<Route
path="/documents"
element={
<ProtectedRoute>
<Placeholder title="My Documents" />
</ProtectedRoute>
}
/>
<Route
path="/receipts"
element={
<ProtectedRoute>
<Receipts />
</ProtectedRoute>
}
/>
<Route
path="/help"
element={
<ProtectedRoute>
<Placeholder title="Help" />
</ProtectedRoute>
}
/>
<Route
path="/team-timecards"
element={
<ProtectedRoute>
<TeamTimecards />
</ProtectedRoute>
}
/>
<Route
path="/team-schedules"
element={
<ProtectedRoute>
<TeamSchedules />
</ProtectedRoute>
}
/>
<Route
path="/approvals"
element={
<ProtectedRoute>
<Placeholder title="Approvals" />
</ProtectedRoute>
}
/>
<Route
path="/incident-report"
element={
<ProtectedRoute>
<DisciplinaryActions />
</ProtectedRoute>
}
/>
<Route
path="/employees"
element={
<ProtectedRoute>
<Employees />
</ProtectedRoute>
}
/>
<Route
path="/disciplinary-actions"
element={
<ProtectedRoute>
<DisciplinaryActions />
</ProtectedRoute>
}
/>
<Route
path="/performance-reviews"
element={
<ProtectedRoute>
<Placeholder title="Performance Reviews" />
</ProtectedRoute>
}
/>
<Route
path="/hr-documents"
element={
<ProtectedRoute>
<Placeholder title="HR Documents" />
</ProtectedRoute>
}
/>
<Route
path="/payroll-runs"
element={
<ProtectedRoute>
<PayrollRuns />
</ProtectedRoute>
}
/>
<Route
path="/expenditures"
element={
<ProtectedRoute>
<Placeholder title="Expenditures" />
</ProtectedRoute>
}
/>
<Route
path="/invoices"
element={
<ProtectedRoute>
<Placeholder title="Invoices" />
</ProtectedRoute>
}
/>
<Route
path="/ocr-review"
element={
<ProtectedRoute>
<OCRReview />
</ProtectedRoute>
}
/>
<Route
path="/financial-reports"
element={
<ProtectedRoute>
<Placeholder title="Financial Reports" />
</ProtectedRoute>
}
/>
<Route
path="/system-settings"
element={
<ProtectedRoute>
<SystemSettings />
</ProtectedRoute>
}
/>
<Route
path="/user-management"
element={
<ProtectedRoute>
<UserManagement />
</ProtectedRoute>
}
/>
<Route
path="/automation"
element={
<ProtectedRoute>
<Placeholder title="Automation Workflows" />
</ProtectedRoute>
}
/>
<Route
path="/audit-logs"
element={
<ProtectedRoute>
<AuditLogs />
</ProtectedRoute>
}
/>
</Routes>
);
}
function App() {
return <AppRoutes />;
}
export default App;

View File

@ -0,0 +1,44 @@
import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
error: null,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
public render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>Something went wrong.</h1>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.error?.stack}
</details>
</div>
);
}
return this.props.children;
}
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import { TopBar } from './TopBar';
import { Sidebar } from './Sidebar';
export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="min-h-screen bg-gray-50">
<TopBar />
<div className="flex">
<Sidebar />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
);
};

View File

@ -0,0 +1,95 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
Home,
Clock,
FileText,
Calendar,
FolderOpen,
Receipt,
HelpCircle,
Users,
AlertTriangle,
Star,
FileCheck,
DollarSign,
TrendingUp,
FileSearch,
BarChart3,
Settings,
UserCog,
GitBranch,
History,
} from 'lucide-react';
import { useUser } from '../../context/UserContext';
import { UserRole } from '../../types';
interface NavItem {
path: string;
label: string;
icon: React.ReactNode;
roles: UserRole[];
}
const navItems: NavItem[] = [
{ path: '/', label: 'Home', icon: <Home className="w-5 h-5" />, roles: ['employee', 'manager', 'hr', 'payroll', 'admin'] },
{ path: '/clock', label: 'Clock In/Out', icon: <Clock className="w-5 h-5" />, roles: ['employee', 'manager'] },
{ path: '/timecards', label: 'My Timecards', icon: <FileText className="w-5 h-5" />, roles: ['employee'] },
{ path: '/schedule', label: 'My Schedule', icon: <Calendar className="w-5 h-5" />, roles: ['employee'] },
{ path: '/documents', label: 'My Documents', icon: <FolderOpen className="w-5 h-5" />, roles: ['employee'] },
{ path: '/receipts', label: 'Submit Receipt', icon: <Receipt className="w-5 h-5" />, roles: ['employee', 'manager'] },
{ path: '/help', label: 'Help', icon: <HelpCircle className="w-5 h-5" />, roles: ['employee', 'manager', 'hr', 'payroll', 'admin'] },
// Manager items
{ path: '/team-timecards', label: 'Team Timecards', icon: <FileText className="w-5 h-5" />, roles: ['manager'] },
{ path: '/team-schedules', label: 'Team Schedules', icon: <Calendar className="w-5 h-5" />, roles: ['manager'] },
{ path: '/approvals', label: 'Approvals', icon: <FileCheck className="w-5 h-5" />, roles: ['manager'] },
{ path: '/incident-report', label: 'Incident Report', icon: <AlertTriangle className="w-5 h-5" />, roles: ['manager'] },
// HR items
{ path: '/employees', label: 'Employees', icon: <Users className="w-5 h-5" />, roles: ['hr', 'admin'] },
{ path: '/disciplinary-actions', label: 'Disciplinary Actions', icon: <AlertTriangle className="w-5 h-5" />, roles: ['hr', 'admin'] },
{ path: '/performance-reviews', label: 'Performance Reviews', icon: <Star className="w-5 h-5" />, roles: ['hr', 'admin'] },
{ path: '/hr-documents', label: 'HR Documents', icon: <FolderOpen className="w-5 h-5" />, roles: ['hr', 'admin'] },
// Payroll items
{ path: '/payroll-runs', label: 'Payroll Runs', icon: <DollarSign className="w-5 h-5" />, roles: ['payroll', 'admin'] },
{ path: '/expenditures', label: 'Expenditures', icon: <TrendingUp className="w-5 h-5" />, roles: ['payroll', 'admin'] },
{ path: '/invoices', label: 'Invoices', icon: <FileText className="w-5 h-5" />, roles: ['payroll', 'admin'] },
{ path: '/ocr-review', label: 'OCR Review Queue', icon: <FileSearch className="w-5 h-5" />, roles: ['payroll', 'admin'] },
{ path: '/financial-reports', label: 'Financial Reports', icon: <BarChart3 className="w-5 h-5" />, roles: ['payroll', 'admin'] },
// Admin items
{ path: '/system-settings', label: 'System Settings', icon: <Settings className="w-5 h-5" />, roles: ['admin'] },
{ path: '/user-management', label: 'User & Role Management', icon: <UserCog className="w-5 h-5" />, roles: ['admin'] },
{ path: '/automation', label: 'Automation Workflows', icon: <GitBranch className="w-5 h-5" />, roles: ['admin'] },
{ path: '/audit-logs', label: 'Audit Logs', icon: <History className="w-5 h-5" />, roles: ['admin'] },
];
export const Sidebar: React.FC = () => {
const { user } = useUser();
const location = useLocation();
const filteredItems = navItems.filter(item => item.roles.includes(user.role));
return (
<div className="w-64 bg-gray-50 border-r border-gray-200 h-[calc(100vh-4rem)] overflow-y-auto">
<nav className="p-4 space-y-1">
{filteredItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
isActive
? 'bg-primary-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{item.icon}
<span className="font-medium">{item.label}</span>
</Link>
);
})}
</nav>
</div>
);
};

View File

@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Search, Bell, User, Settings, LogOut } from 'lucide-react';
import { useUser } from '../../context/UserContext';
import { useAuth } from '../../context/AuthContext';
export const TopBar: React.FC = () => {
const { user } = useUser();
const { logout } = useAuth();
const navigate = useNavigate();
const [showProfileMenu, setShowProfileMenu] = useState(false);
const handleLogout = () => {
logout();
navigate('/login');
};
return (
<div className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 sticky top-0 z-50">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">A</span>
</div>
<span className="text-xl font-bold text-gray-900">Axion</span>
</div>
<div className="hidden md:flex items-center gap-2 bg-gray-100 rounded-lg px-4 py-2 w-64">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search..."
className="bg-transparent border-none outline-none flex-1 text-sm"
/>
</div>
</div>
<div className="flex items-center gap-4">
<button className="relative p-2 hover:bg-gray-100 rounded-lg transition-colors">
<Bell className="w-5 h-5 text-gray-600" />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
<div className="relative">
<button
onClick={() => setShowProfileMenu(!showProfileMenu)}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<div className="w-8 h-8 bg-primary-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-medium">
{user.name.split(' ').map(n => n[0]).join('')}
</span>
</div>
<span className="hidden md:block text-sm font-medium text-gray-700">{user.name}</span>
<User className="w-4 h-4 text-gray-600" />
</button>
{showProfileMenu && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2">
<a
href="#"
className="flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-gray-700"
>
<User className="w-4 h-4" />
My Profile
</a>
<a
href="#"
className="flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-gray-700"
>
<Settings className="w-4 h-4" />
Settings
</a>
<hr className="my-2" />
<button
onClick={handleLogout}
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-red-600 text-left"
>
<LogOut className="w-4 h-4" />
Log Out
</button>
</div>
)}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};

109
src/context/AuthContext.tsx Normal file
View File

@ -0,0 +1,109 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { User, UserRole } from '../types';
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<boolean>;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
// Mock users database - in production, this would be in a real database
const MOCK_USERS = [
{
id: '1',
email: 'admin@company.com',
password: 'admin123',
name: 'Admin User',
role: 'admin' as UserRole,
},
{
id: '2',
email: 'hr@company.com',
password: 'hr123',
name: 'HR Manager',
role: 'hr' as UserRole,
},
{
id: '3',
email: 'payroll@company.com',
password: 'payroll123',
name: 'Payroll Admin',
role: 'payroll' as UserRole,
},
{
id: '4',
email: 'manager@company.com',
password: 'manager123',
name: 'Team Manager',
role: 'manager' as UserRole,
},
{
id: '5',
email: 'employee@company.com',
password: 'employee123',
name: 'John Doe',
role: 'employee' as UserRole,
},
];
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// Check for stored session
const storedUser = localStorage.getItem('user');
if (storedUser) {
try {
setUser(JSON.parse(storedUser));
} catch (e) {
localStorage.removeItem('user');
}
}
}, []);
const login = async (email: string, password: string): Promise<boolean> => {
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 500));
const foundUser = MOCK_USERS.find(
u => u.email === email && u.password === password
);
if (foundUser) {
const userData: User = {
id: foundUser.id,
name: foundUser.name,
email: foundUser.email,
role: foundUser.role,
};
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
return true;
}
return false;
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
};

View File

@ -0,0 +1,41 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { User, ClockStatus } from '../types';
import { useAuth } from './AuthContext';
interface UserContextType {
user: User;
clockStatus: ClockStatus;
setClockStatus: (status: ClockStatus) => void;
}
const UserContext = createContext<UserContextType | undefined>(undefined);
export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error('useUser must be used within UserProvider');
}
return context;
};
export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { user: authUser } = useAuth();
const [clockStatus, setClockStatus] = useState<ClockStatus>('clocked_out');
if (!authUser) {
return <>{children}</>;
}
const contextValue = {
user: authUser,
clockStatus,
setClockStatus,
};
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
};

109
src/data/mockDatabase.ts Normal file
View File

@ -0,0 +1,109 @@
import { User, Timecard, Shift, DisciplinaryAction, Receipt, PayrollRun } from '../types';
// Mock database - In production, this would be replaced with API calls to a real backend
class MockDatabase {
private users: User[] = [
{
id: '1',
name: 'John Doe',
email: 'john.doe@company.com',
role: 'employee',
},
{
id: '2',
name: 'Jane Smith',
email: 'jane.smith@company.com',
role: 'employee',
},
{
id: '3',
name: 'Bob Johnson',
email: 'bob.johnson@company.com',
role: 'employee',
},
{
id: '4',
name: 'Alice Brown',
email: 'alice.brown@company.com',
role: 'manager',
},
{
id: '5',
name: 'Charlie Wilson',
email: 'charlie.wilson@company.com',
role: 'employee',
},
];
private timecards: Timecard[] = [];
private shifts: Shift[] = [];
private disciplinaryActions: DisciplinaryAction[] = [];
private receipts: Receipt[] = [];
private payrollRuns: PayrollRun[] = [];
// User CRUD operations
getUsers(): User[] {
return [...this.users];
}
getUserById(id: string): User | undefined {
return this.users.find(u => u.id === id);
}
createUser(userData: Omit<User, 'id'>): User {
const newUser: User = {
...userData,
id: String(Date.now()),
};
this.users.push(newUser);
this.saveToLocalStorage();
return newUser;
}
updateUser(id: string, updates: Partial<User>): User | null {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return null;
this.users[index] = { ...this.users[index], ...updates };
this.saveToLocalStorage();
return this.users[index];
}
deleteUser(id: string): boolean {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
this.saveToLocalStorage();
return true;
}
// Local storage persistence
private saveToLocalStorage() {
try {
localStorage.setItem('mockUsers', JSON.stringify(this.users));
} catch (e) {
console.error('Failed to save to localStorage:', e);
}
}
loadFromLocalStorage() {
try {
const stored = localStorage.getItem('mockUsers');
if (stored) {
this.users = JSON.parse(stored);
}
} catch (e) {
console.error('Failed to load from localStorage:', e);
}
}
// Initialize
constructor() {
this.loadFromLocalStorage();
}
}
export const mockDatabase = new MockDatabase();

26
src/index.css Normal file
View File

@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
padding: 0;
min-height: 100vh;
background-color: #f9fafb;
}
#root {
min-height: 100vh;
}

42
src/main.tsx Normal file
View File

@ -0,0 +1,42 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
import { AuthProvider } from './context/AuthContext.tsx'
import { UserProvider } from './context/UserContext.tsx'
import { ErrorBoundary } from './components/ErrorBoundary.tsx'
import './index.css'
const rootElement = document.getElementById('root')
if (!rootElement) {
throw new Error('Root element not found. Make sure index.html has <div id="root"></div>')
}
try {
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<ErrorBoundary>
<BrowserRouter>
<AuthProvider>
<UserProvider>
<App />
</UserProvider>
</AuthProvider>
</BrowserRouter>
</ErrorBoundary>
</React.StrictMode>,
)
} catch (error) {
console.error('Failed to render app:', error)
rootElement.innerHTML = `
<div style="padding: 20px; font-family: sans-serif;">
<h1>Failed to load application</h1>
<p>Check the browser console for details.</p>
<pre style="background: #f5f5f5; padding: 10px; margin-top: 10px; overflow: auto;">
${error instanceof Error ? error.stack : String(error)}
</pre>
</div>
`
}

222
src/pages/AuditLogs.tsx Normal file
View File

@ -0,0 +1,222 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { History, Filter, Search, AlertCircle, Info, XCircle } from 'lucide-react';
import { format } from 'date-fns';
export const AuditLogs: React.FC = () => {
const [selectedLog, setSelectedLog] = useState<string | null>(null);
const [filterActor, setFilterActor] = useState<string>('all');
const [filterTable, setFilterTable] = useState<string>('all');
const [filterSeverity, setFilterSeverity] = useState<string>('all');
const logs = [
{ id: '1', timestamp: new Date(), actor: 'Admin', action: 'UPDATE', table: 'users', object: 'User #123', ip: '192.168.1.1', severity: 'low', before: { role: 'employee' }, after: { role: 'manager' } },
{ id: '2', timestamp: new Date(Date.now() - 3600000), actor: 'HR Manager', action: 'CREATE', table: 'disciplinary_actions', object: 'Action #45', ip: '192.168.1.2', severity: 'medium', before: null, after: { employeeId: '123', severity: 'high' } },
{ id: '3', timestamp: new Date(Date.now() - 7200000), actor: 'Payroll Admin', action: 'DELETE', table: 'timecards', object: 'Timecard #789', ip: '192.168.1.3', severity: 'high', before: { hours: 8 }, after: null },
];
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'high':
return <XCircle className="w-4 h-4 text-red-600" />;
case 'medium':
return <AlertCircle className="w-4 h-4 text-yellow-600" />;
case 'low':
return <Info className="w-4 h-4 text-blue-600" />;
default:
return <Info className="w-4 h-4 text-gray-600" />;
}
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high':
return 'bg-red-100 text-red-800 border-red-200';
case 'medium':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low':
return 'bg-blue-100 text-blue-800 border-blue-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Audit Logs</h1>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search logs..."
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
/>
</div>
<select
value={filterActor}
onChange={(e) => setFilterActor(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="all">All Actors</option>
<option value="Admin">Admin</option>
<option value="HR Manager">HR Manager</option>
<option value="Payroll Admin">Payroll Admin</option>
</select>
<select
value={filterTable}
onChange={(e) => setFilterTable(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="all">All Tables</option>
<option value="users">Users</option>
<option value="timecards">Timecards</option>
<option value="disciplinary_actions">Disciplinary Actions</option>
</select>
<select
value={filterSeverity}
onChange={(e) => setFilterSeverity(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="all">All Severities</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<input
type="date"
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
placeholder="Date Range"
/>
</div>
</div>
{/* Logs Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Timestamp</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actor</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Table</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Object</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP Address</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Severity</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{logs.map(log => (
<tr key={log.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{format(log.timestamp, 'MMM d, yyyy HH:mm:ss')}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{log.actor}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded text-xs font-medium ${
log.action === 'CREATE' ? 'bg-green-100 text-green-800' :
log.action === 'UPDATE' ? 'bg-blue-100 text-blue-800' :
'bg-red-100 text-red-800'
}`}>
{log.action}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{log.table}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{log.object}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">{log.ip}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded text-xs font-medium border flex items-center gap-1 w-fit ${getSeverityColor(log.severity)}`}>
{getSeverityIcon(log.severity)}
{log.severity.toUpperCase()}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => setSelectedLog(log.id)}
className="text-primary-600 hover:text-primary-900"
>
View Details
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Detail Modal */}
{selectedLog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">Audit Log Details</h2>
<button
onClick={() => setSelectedLog(null)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
{(() => {
const log = logs.find(l => l.id === selectedLog);
return log ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600 mb-1">Timestamp</p>
<p className="font-medium">{format(log.timestamp, 'MMM d, yyyy HH:mm:ss')}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Actor</p>
<p className="font-medium">{log.actor}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Action</p>
<p className="font-medium">{log.action}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">IP Address</p>
<p className="font-medium">{log.ip}</p>
</div>
</div>
{log.before && (
<div>
<p className="text-sm text-gray-600 mb-2">Before</p>
<pre className="bg-gray-50 p-3 rounded text-sm overflow-x-auto">
{JSON.stringify(log.before, null, 2)}
</pre>
</div>
)}
{log.after && (
<div>
<p className="text-sm text-gray-600 mb-2">After</p>
<pre className="bg-gray-50 p-3 rounded text-sm overflow-x-auto">
{JSON.stringify(log.after, null, 2)}
</pre>
</div>
)}
</div>
) : null;
})()}
</div>
</div>
)}
</div>
</Layout>
);
};

128
src/pages/ClockInOut.tsx Normal file
View File

@ -0,0 +1,128 @@
import React from 'react';
import { Layout } from '../components/Layout/Layout';
import { useUser } from '../context/UserContext';
import { Clock, Coffee } from 'lucide-react';
export const ClockInOut: React.FC = () => {
const { clockStatus, setClockStatus } = useUser();
const handleClockIn = () => {
setClockStatus('clocked_in');
};
const handleClockOut = () => {
setClockStatus('clocked_out');
};
const handleStartBreak = () => {
setClockStatus('on_break');
};
const handleEndBreak = () => {
setClockStatus('clocked_in');
};
return (
<Layout>
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Clock In / Clock Out</h1>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
{/* Main Action Button */}
<div className="flex flex-col items-center justify-center mb-8">
{clockStatus === 'clocked_out' && (
<button
onClick={handleClockIn}
className="w-64 h-64 bg-primary-600 hover:bg-primary-700 text-white rounded-full flex flex-col items-center justify-center transition-all transform hover:scale-105 shadow-lg"
>
<Clock className="w-16 h-16 mb-4" />
<span className="text-2xl font-bold">CLOCK IN</span>
</button>
)}
{clockStatus === 'clocked_in' && (
<button
onClick={handleClockOut}
className="w-64 h-64 bg-red-600 hover:bg-red-700 text-white rounded-full flex flex-col items-center justify-center transition-all transform hover:scale-105 shadow-lg"
>
<Clock className="w-16 h-16 mb-4" />
<span className="text-2xl font-bold">CLOCK OUT</span>
</button>
)}
{clockStatus === 'on_break' && (
<button
onClick={handleEndBreak}
className="w-64 h-64 bg-yellow-600 hover:bg-yellow-700 text-white rounded-full flex flex-col items-center justify-center transition-all transform hover:scale-105 shadow-lg"
>
<Coffee className="w-16 h-16 mb-4" />
<span className="text-2xl font-bold">END BREAK</span>
</button>
)}
</div>
{/* Secondary Actions */}
{clockStatus === 'clocked_in' && (
<div className="flex justify-center gap-4 mb-8">
<button
onClick={handleStartBreak}
className="px-8 py-4 bg-yellow-500 hover:bg-yellow-600 text-white rounded-lg font-medium transition-colors flex items-center gap-2"
>
<Coffee className="w-5 h-5" />
Start Break
</button>
</div>
)}
{/* Shift Timeline */}
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Today's Timeline</h3>
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<div className="flex-1">
<p className="font-medium">Clock In</p>
<p className="text-sm text-gray-600">8:02 AM</p>
</div>
</div>
{clockStatus === 'on_break' && (
<div className="flex items-center gap-4">
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div>
<div className="flex-1">
<p className="font-medium">Break Started</p>
<p className="text-sm text-gray-600">12:00 PM</p>
</div>
</div>
)}
</div>
</div>
{/* Rules/Notes */}
<div className="border-t border-gray-200 pt-6 mt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-2">Notes</h3>
<ul className="space-y-2 text-sm text-gray-600">
<li> Lunch break is automatically deducted after 5 hours of work</li>
<li> Please clock in/out at your scheduled location</li>
<li> Contact HR if you need to correct a timecard</li>
</ul>
</div>
{/* Audit Info */}
<div className="border-t border-gray-200 pt-6 mt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Last clock in:</span>
<span className="font-medium">8:02 AM</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Last clock out:</span>
<span className="font-medium">Yesterday at 5:14 PM</span>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
};

408
src/pages/Dashboard.tsx Normal file
View File

@ -0,0 +1,408 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Layout } from '../components/Layout/Layout';
import { useUser } from '../context/UserContext';
import { Clock, Calendar, FileText, AlertCircle, Users, CheckCircle, XCircle, Coffee, TrendingUp, DollarSign, Settings, Star } from 'lucide-react';
import { format } from 'date-fns';
export const Dashboard: React.FC = () => {
const { user, clockStatus } = useUser();
const renderEmployeeView = () => (
<div className="space-y-6">
{/* Clock Status */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Current Status</h2>
</div>
<div className="flex items-center gap-4">
<div className={`px-4 py-2 rounded-full text-sm font-medium ${
clockStatus === 'clocked_in' ? 'bg-green-100 text-green-800' :
clockStatus === 'on_break' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{clockStatus === 'clocked_in' ? 'CLOCKED IN' :
clockStatus === 'on_break' ? 'ON BREAK' :
'CLOCKED OUT'}
</div>
<button className="px-6 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 transition-colors">
{clockStatus === 'clocked_out' ? 'Clock In' :
clockStatus === 'clocked_in' ? 'Clock Out' :
'End Break'}
</button>
{clockStatus === 'clocked_in' && (
<button className="px-6 py-3 bg-yellow-500 text-white rounded-lg font-medium hover:bg-yellow-600 transition-colors">
Start Break
</button>
)}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Today's Schedule */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calendar className="w-5 h-5 text-primary-600" />
Today's Schedule
</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">Start Time</span>
<span className="font-medium">8:00 AM</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">End Time</span>
<span className="font-medium">5:00 PM</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Break</span>
<span className="font-medium">12:00 PM - 1:00 PM</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Location</span>
<span className="font-medium">Main Office</span>
</div>
</div>
</div>
{/* Recent Timecard Summary */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<FileText className="w-5 h-5 text-primary-600" />
Timecard Summary
</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-gray-600">Hours Today</span>
<span className="text-2xl font-bold text-gray-900">4.5</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-600">Hours This Week</span>
<span className="text-2xl font-bold text-gray-900">32.0</span>
</div>
</div>
</div>
</div>
{/* Upcoming Shifts */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Upcoming Shifts</h3>
<div className="space-y-2">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<span className="font-medium">{format(new Date(Date.now() + i * 24 * 60 * 60 * 1000), 'EEEE, MMM d')}</span>
<span className="text-gray-600 ml-2">8:00 AM - 5:00 PM</span>
</div>
<span className="text-sm text-gray-500">Main Office</span>
</div>
))}
</div>
</div>
{/* Notices */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-yellow-600" />
Notices
</h3>
<div className="space-y-2">
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">Schedule change: Your shift on Friday has been moved to 9:00 AM - 6:00 PM</p>
</div>
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800">HR Notice: Performance review scheduled for next week</p>
</div>
</div>
</div>
</div>
);
const renderManagerView = () => (
<div className="space-y-6">
{renderEmployeeView()}
{/* Team Overview */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-sm font-medium text-gray-600 mb-2">Currently Clocked In</h3>
<div className="text-3xl font-bold text-gray-900 mb-2">8</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>John Doe</span>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Jane Smith</span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-sm font-medium text-gray-600 mb-2">Late Today</h3>
<div className="text-3xl font-bold text-red-600 mb-2">2</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-red-600">
<XCircle className="w-4 h-4" />
<span>Bob Johnson</span>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-sm font-medium text-gray-600 mb-2">On Break</h3>
<div className="text-3xl font-bold text-yellow-600 mb-2">3</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm">
<Coffee className="w-4 h-4 text-yellow-600" />
<span>Alice Brown</span>
</div>
</div>
</div>
</div>
{/* Approval Notifications */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Approval Notifications</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div>
<p className="font-medium">5 timecards awaiting approval</p>
<p className="text-sm text-gray-600">From last week</p>
</div>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm hover:bg-primary-700">
Review
</button>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div>
<p className="font-medium">3 receipts requiring review</p>
<p className="text-sm text-gray-600">Submitted yesterday</p>
</div>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm hover:bg-primary-700">
Review
</button>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<button className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left">
<h3 className="font-semibold text-gray-900 mb-2">Create Incident Report</h3>
<p className="text-sm text-gray-600">Report a workplace incident</p>
</button>
<button className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left">
<h3 className="font-semibold text-gray-900 mb-2">Edit Team Schedule</h3>
<p className="text-sm text-gray-600">Manage upcoming shifts</p>
</button>
</div>
</div>
);
const renderHRView = () => (
<div className="space-y-6">
{/* Global Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-3">
<Users className="w-8 h-8 text-primary-600" />
<div>
<p className="text-sm text-gray-600">Total Employees</p>
<p className="text-2xl font-bold text-gray-900">247</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-3">
<AlertCircle className="w-8 h-8 text-red-600" />
<div>
<p className="text-sm text-gray-600">Active Disciplinary Cases</p>
<p className="text-2xl font-bold text-gray-900">3</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-3">
<Star className="w-8 h-8 text-yellow-600" />
<div>
<p className="text-sm text-gray-600">Pending Reviews</p>
<p className="text-2xl font-bold text-gray-900">12</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-3">
<FileText className="w-8 h-8 text-blue-600" />
<div>
<p className="text-sm text-gray-600">Expiring Documents</p>
<p className="text-2xl font-bold text-gray-900">8</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<button className="p-6 bg-white rounded-lg shadow-sm border-2 border-primary-500 hover:bg-primary-50 transition-colors">
<h3 className="font-semibold text-gray-900 mb-2">New Employee</h3>
<p className="text-sm text-gray-600">Add a new employee to the system</p>
</button>
<button className="p-6 bg-white rounded-lg shadow-sm border-2 border-red-500 hover:bg-red-50 transition-colors">
<h3 className="font-semibold text-gray-900 mb-2">Start Disciplinary Action</h3>
<p className="text-sm text-gray-600">Initiate a disciplinary process</p>
</button>
<button className="p-6 bg-white rounded-lg shadow-sm border-2 border-yellow-500 hover:bg-yellow-50 transition-colors">
<h3 className="font-semibold text-gray-900 mb-2">Schedule Review</h3>
<p className="text-sm text-gray-600">Plan performance reviews</p>
</button>
</div>
{/* Alerts */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alerts</h3>
<div className="space-y-2">
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-800">3 performance reviews overdue</p>
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">8 documents expiring in the next 30 days</p>
</div>
</div>
</div>
</div>
);
const renderPayrollView = () => (
<div className="space-y-6">
{/* Payroll Countdown */}
<div className="bg-gradient-to-r from-primary-600 to-primary-700 rounded-lg shadow-sm p-6 text-white">
<h2 className="text-2xl font-bold mb-2">Next Payroll Run</h2>
<p className="text-3xl font-bold mb-4">3 days</p>
<p className="text-primary-100">Scheduled for Friday, December 15, 2023</p>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<button className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left">
<DollarSign className="w-8 h-8 text-primary-600 mb-3" />
<h3 className="font-semibold text-gray-900 mb-2">Start Payroll Preview</h3>
<p className="text-sm text-gray-600">Generate preview for next payroll</p>
</button>
<button className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left">
<FileText className="w-8 h-8 text-primary-600 mb-3" />
<h3 className="font-semibold text-gray-900 mb-2">Review Receipts/Invoices</h3>
<p className="text-sm text-gray-600">Process expense submissions</p>
</button>
<button className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left">
<TrendingUp className="w-8 h-8 text-primary-600 mb-3" />
<h3 className="font-semibold text-gray-900 mb-2">Expense Summaries</h3>
<p className="text-sm text-gray-600">View expense reports</p>
</button>
</div>
{/* Alerts */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alerts</h3>
<div className="space-y-2">
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-800">5 OCR errors require attention</p>
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">12 unapproved timecards</p>
</div>
</div>
</div>
</div>
);
const renderAdminView = () => (
<div className="space-y-6">
{/* System Health */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">API Status</span>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
<p className="text-2xl font-bold text-gray-900">99.9%</p>
<p className="text-xs text-gray-500 mt-1">Uptime</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">n8n Status</span>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
<p className="text-2xl font-bold text-gray-900">Online</p>
<p className="text-xs text-gray-500 mt-1">All workflows active</p>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">Backup Status</span>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
</div>
<p className="text-2xl font-bold text-gray-900">Current</p>
<p className="text-xs text-gray-500 mt-1">Last: 2 hours ago</p>
</div>
</div>
{/* Recent Audit Logs */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Audit Logs</h3>
<div className="space-y-2">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p className="text-sm font-medium">User role changed</p>
<p className="text-xs text-gray-500">Admin 2 minutes ago</p>
</div>
<span className="text-xs text-gray-500">192.168.1.1</span>
</div>
))}
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Link to="/system-settings" className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left block">
<Settings className="w-8 h-8 text-primary-600 mb-3" />
<h3 className="font-semibold text-gray-900 mb-2">System Settings</h3>
<p className="text-sm text-gray-600">Configure system parameters</p>
</Link>
<Link to="/user-management" className="p-6 bg-white rounded-lg shadow-sm border border-gray-200 hover:border-primary-500 transition-colors text-left block">
<Users className="w-8 h-8 text-primary-600 mb-3" />
<h3 className="font-semibold text-gray-900 mb-2">User Provisioning</h3>
<p className="text-sm text-gray-600">Manage user accounts</p>
</Link>
</div>
</div>
);
const renderContent = () => {
switch (user.role) {
case 'employee':
return renderEmployeeView();
case 'manager':
return renderManagerView();
case 'hr':
return renderHRView();
case 'payroll':
return renderPayrollView();
case 'admin':
return renderAdminView();
default:
return renderEmployeeView();
}
};
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Dashboard</h1>
{renderContent()}
</div>
</Layout>
);
};

View File

@ -0,0 +1,211 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { useUser } from '../context/UserContext';
import { AlertTriangle, FileText, Send, CheckCircle, XCircle, Clock } from 'lucide-react';
import { format } from 'date-fns';
export const DisciplinaryActions: React.FC = () => {
const { user } = useUser();
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({
employeeId: '',
date: '',
details: '',
severity: 'low' as 'low' | 'medium' | 'high' | 'critical',
});
const actions = [
{ id: '1', employeeName: 'John Doe', date: '2023-12-10', severity: 'medium', status: 'pending_approval', details: 'Late arrival on multiple occasions' },
{ id: '2', employeeName: 'Jane Smith', date: '2023-12-08', severity: 'high', status: 'finalized', details: 'Policy violation' },
{ id: '3', employeeName: 'Bob Johnson', date: '2023-12-05', severity: 'low', status: 'draft', details: 'Performance issue' },
];
const employees = [
{ id: '1', name: 'John Doe' },
{ id: '2', name: 'Jane Smith' },
{ id: '3', name: 'Bob Johnson' },
];
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical':
return 'bg-red-100 text-red-800 border-red-200';
case 'high':
return 'bg-orange-100 text-orange-800 border-orange-200';
case 'medium':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low':
return 'bg-blue-100 text-blue-800 border-blue-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'finalized':
return <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-medium flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Finalized</span>;
case 'pending_approval':
return <span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs font-medium flex items-center gap-1"><Clock className="w-3 h-3" /> Pending Approval</span>;
case 'draft':
return <span className="px-2 py-1 bg-gray-100 text-gray-800 rounded text-xs font-medium">Draft</span>;
default:
return null;
}
};
if (user.role === 'manager') {
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">Incident Report</h1>
<button
onClick={() => setShowForm(!showForm)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700"
>
{showForm ? 'Cancel' : 'New Incident Report'}
</button>
</div>
{showForm && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Report Incident</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Employee</label>
<select
value={formData.employeeId}
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
>
<option value="">Select employee</option>
{employees.map(emp => (
<option key={emp.id} value={emp.id}>{emp.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Date of Incident</label>
<input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Severity</label>
<select
value={formData.severity}
onChange={(e) => setFormData({ ...formData, severity: e.target.value as any })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Details</label>
<textarea
value={formData.details}
onChange={(e) => setFormData({ ...formData, details: e.target.value })}
rows={6}
className="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="Describe the incident in detail..."
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Attachments (Optional)</label>
<input type="file" className="w-full px-3 py-2 border border-gray-300 rounded-lg" multiple />
</div>
<button className="px-6 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700">
Submit Report
</button>
</div>
</div>
)}
</div>
</Layout>
);
}
// HR View
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Disciplinary Actions</h1>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Employee</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Incident Date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Severity</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{actions.map(action => (
<tr key={action.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{action.employeeName}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{format(new Date(action.date), 'MMM d, yyyy')}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded text-xs font-medium border ${getSeverityColor(action.severity)}`}>
{action.severity.toUpperCase()}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(action.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-primary-600 hover:text-primary-900 mr-4">Review</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Detail View Modal would go here */}
<div className="mt-6 bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Incident Details</h2>
<div className="space-y-4">
<div>
<p className="text-sm text-gray-600 mb-1">Employee</p>
<p className="font-medium">John Doe</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Details</p>
<p className="text-gray-900">Late arrival on multiple occasions without proper notification...</p>
</div>
<div className="flex gap-2">
<button className="px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 flex items-center gap-2">
<CheckCircle className="w-4 h-4" />
Approve
</button>
<button className="px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 flex items-center gap-2">
<XCircle className="w-4 h-4" />
Reject
</button>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center gap-2">
<Send className="w-4 h-4" />
Send to Employee
</button>
</div>
</div>
</div>
</div>
</Layout>
);
};

425
src/pages/Employees.tsx Normal file
View File

@ -0,0 +1,425 @@
import React, { useState, useEffect, useRef } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Users, Edit, Eye, MoreVertical, Search, Filter, Trash2, Save, X } from 'lucide-react';
interface Employee {
id: string;
name: string;
jobTitle: string;
department: string;
status: string;
manager: string;
email?: string;
phone?: string;
address?: string;
hireDate?: string;
}
export const Employees: React.FC = () => {
const [selectedEmployee, setSelectedEmployee] = useState<string | null>(null);
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
const [showEditForm, setShowEditForm] = useState(false);
const [showMenu, setShowMenu] = useState<string | null>(null);
const menuRef = useRef<HTMLDivElement>(null);
const [employees, setEmployees] = useState<Employee[]>([
{ id: '1', name: 'John Doe', jobTitle: 'Cashier', department: 'Retail', status: 'Active', manager: 'Jane Manager', email: 'john.doe@company.com', phone: '(555) 123-4567', address: '123 Main St, City, ST 12345', hireDate: 'January 15, 2022' },
{ id: '2', name: 'Jane Smith', jobTitle: 'Stock Associate', department: 'Warehouse', status: 'Active', manager: 'Bob Manager', email: 'jane.smith@company.com', phone: '(555) 234-5678', address: '456 Oak Ave, City, ST 12345', hireDate: 'March 20, 2021' },
{ id: '3', name: 'Bob Johnson', jobTitle: 'Manager', department: 'Retail', status: 'Active', manager: 'Alice Director', email: 'bob.johnson@company.com', phone: '(555) 345-6789', address: '789 Pine Rd, City, ST 12345', hireDate: 'June 10, 2020' },
]);
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setShowMenu(null);
}
};
if (showMenu) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showMenu]);
const handleEdit = (emp: Employee) => {
setEditingEmployee({ ...emp });
setShowEditForm(true);
setShowMenu(null);
};
const handleSave = () => {
if (editingEmployee) {
setEmployees(employees.map(emp =>
emp.id === editingEmployee.id ? editingEmployee : emp
));
setShowEditForm(false);
setEditingEmployee(null);
}
};
const handleDelete = (id: string) => {
if (window.confirm('Are you sure you want to delete this employee?')) {
setEmployees(employees.filter(emp => emp.id !== id));
setShowMenu(null);
}
};
if (selectedEmployee) {
const emp = employees.find(e => e.id === selectedEmployee);
return (
<Layout>
<div>
<button
onClick={() => setSelectedEmployee(null)}
className="mb-4 text-primary-600 hover:text-primary-700 font-medium"
>
Back to List
</button>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Employee Details</h1>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-6">
{/* Personal Info */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Personal Information</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600 mb-1">Full Name</p>
<p className="font-medium">{emp?.name}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Email</p>
<p className="font-medium">{emp?.email || 'N/A'}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Phone</p>
<p className="font-medium">{emp?.phone || 'N/A'}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Address</p>
<p className="font-medium">{emp?.address || 'N/A'}</p>
</div>
</div>
</div>
{/* Employment Data */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Employment Information</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600 mb-1">Job Title</p>
<p className="font-medium">{emp?.jobTitle}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Department</p>
<p className="font-medium">{emp?.department}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Manager</p>
<p className="font-medium">{emp?.manager}</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Hire Date</p>
<p className="font-medium">{emp?.hireDate || 'N/A'}</p>
</div>
</div>
</div>
{/* Documents */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Documents</h2>
<div className="space-y-2">
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<span className="text-sm font-medium">Employment Contract</span>
<button className="text-primary-600 hover:text-primary-700 text-sm">View</button>
</div>
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<span className="text-sm font-medium">ID Verification</span>
<button className="text-primary-600 hover:text-primary-700 text-sm">View</button>
</div>
</div>
</div>
{/* Disciplinary History */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Disciplinary History</h2>
<p className="text-sm text-gray-600">No disciplinary actions on record</p>
</div>
</div>
<div className="space-y-6">
{/* Quick Stats */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Summary</h2>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">Hours This Week</p>
<p className="text-2xl font-bold">32.0</p>
</div>
<div>
<p className="text-sm text-gray-600">Timecards Pending</p>
<p className="text-2xl font-bold">2</p>
</div>
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">Employees</h1>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700">
New Employee
</button>
</div>
{/* Search and Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex items-center gap-4">
<div className="flex-1 flex items-center gap-2 bg-gray-100 rounded-lg px-4 py-2">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search employees..."
className="bg-transparent border-none outline-none flex-1"
/>
</div>
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<Filter className="w-4 h-4" />
Filter
</button>
</div>
</div>
{/* Employees Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job Title</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Department</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Manager</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{employees.map(emp => (
<tr key={emp.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{emp.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{emp.jobTitle}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{emp.department}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-medium">
{emp.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{emp.manager}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-2 relative">
<button
onClick={() => setSelectedEmployee(emp.id)}
className="text-primary-600 hover:text-primary-900"
title="View"
>
<Eye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(emp)}
className="text-gray-600 hover:text-gray-900"
title="Edit"
>
<Edit className="w-4 h-4" />
</button>
<div className="relative" ref={menuRef}>
<button
onClick={() => setShowMenu(showMenu === emp.id ? null : emp.id)}
className="text-gray-600 hover:text-gray-900"
title="More options"
>
<MoreVertical className="w-4 h-4" />
</button>
{showMenu === emp.id && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-10">
<button
onClick={() => {
handleEdit(emp);
setShowMenu(null);
}}
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-gray-700 text-left"
>
<Edit className="w-4 h-4" />
Edit Employee
</button>
<button
onClick={() => handleDelete(emp.id)}
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-red-600 text-left"
>
<Trash2 className="w-4 h-4" />
Delete Employee
</button>
</div>
)}
</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Edit Form Modal */}
{showEditForm && editingEmployee && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">Edit Employee</h2>
<button
onClick={() => {
setShowEditForm(false);
setEditingEmployee(null);
}}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Full Name</label>
<input
type="text"
value={editingEmployee.name}
onChange={(e) => setEditingEmployee({ ...editingEmployee, name: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input
type="email"
value={editingEmployee.email || ''}
onChange={(e) => setEditingEmployee({ ...editingEmployee, email: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Job Title</label>
<input
type="text"
value={editingEmployee.jobTitle}
onChange={(e) => setEditingEmployee({ ...editingEmployee, jobTitle: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Department</label>
<input
type="text"
value={editingEmployee.department}
onChange={(e) => setEditingEmployee({ ...editingEmployee, department: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Phone</label>
<input
type="tel"
value={editingEmployee.phone || ''}
onChange={(e) => setEditingEmployee({ ...editingEmployee, phone: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Manager</label>
<input
type="text"
value={editingEmployee.manager}
onChange={(e) => setEditingEmployee({ ...editingEmployee, manager: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
value={editingEmployee.status}
onChange={(e) => setEditingEmployee({ ...editingEmployee, status: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
<option value="On Leave">On Leave</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Hire Date</label>
<input
type="text"
value={editingEmployee.hireDate || ''}
onChange={(e) => setEditingEmployee({ ...editingEmployee, hireDate: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">Address</label>
<input
type="text"
value={editingEmployee.address || ''}
onChange={(e) => setEditingEmployee({ ...editingEmployee, address: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
</div>
<div className="flex gap-2 pt-4">
<button
type="button"
onClick={() => {
setShowEditForm(false);
setEditingEmployee(null);
}}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
>
Cancel
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center justify-center gap-2"
>
<Save className="w-4 h-4" />
Save Changes
</button>
</div>
</form>
</div>
</div>
)}
</div>
</Layout>
);
};

130
src/pages/Login.tsx Normal file
View File

@ -0,0 +1,130 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { LogIn, AlertCircle } from 'lucide-react';
export const Login: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
const success = await login(email, password);
if (success) {
navigate('/');
} else {
setError('Invalid email or password');
}
setLoading(false);
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-primary-100 flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="bg-white rounded-2xl shadow-xl p-8">
<div className="text-center mb-8">
<div className="w-16 h-16 bg-primary-600 rounded-xl flex items-center justify-center mx-auto mb-4">
<span className="text-white font-bold text-2xl">A</span>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Axion</h1>
<p className="text-gray-600">HR/Payroll System</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-red-600" />
<span className="text-sm text-red-800">{error}</span>
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition"
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition"
placeholder="Enter your password"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full px-4 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
Signing in...
</>
) : (
<>
<LogIn className="w-5 h-5" />
Sign In
</>
)}
</button>
</form>
<div className="mt-8 pt-6 border-t border-gray-200">
<p className="text-sm text-gray-600 text-center mb-4">Demo Accounts:</p>
<div className="space-y-2 text-xs">
<div className="flex justify-between p-2 bg-gray-50 rounded">
<span className="font-medium">Admin:</span>
<span className="text-gray-600">admin@company.com / admin123</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded">
<span className="font-medium">HR:</span>
<span className="text-gray-600">hr@company.com / hr123</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded">
<span className="font-medium">Payroll:</span>
<span className="text-gray-600">payroll@company.com / payroll123</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded">
<span className="font-medium">Manager:</span>
<span className="text-gray-600">manager@company.com / manager123</span>
</div>
<div className="flex justify-between p-2 bg-gray-50 rounded">
<span className="font-medium">Employee:</span>
<span className="text-gray-600">employee@company.com / employee123</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

157
src/pages/OCRReview.tsx Normal file
View File

@ -0,0 +1,157 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { CheckCircle, XCircle, Edit, Tag, DollarSign, Calendar, Building2 } from 'lucide-react';
import { format } from 'date-fns';
export const OCRReview: React.FC = () => {
const [selectedReceipt, setSelectedReceipt] = useState<string | null>(null);
const receipts = [
{ id: '1', imageUrl: '', vendor: 'Office Depot', amount: 45.99, date: '2023-12-10', tax: 3.68, category: 'Office Supplies', confidence: 0.92 },
{ id: '2', imageUrl: '', vendor: 'Starbucks', amount: 12.50, date: '2023-12-11', tax: 1.00, category: 'Meals', confidence: 0.88 },
{ id: '3', imageUrl: '', vendor: 'Uber', amount: 28.75, date: '2023-12-11', tax: 0, category: 'Transportation', confidence: 0.65 },
];
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return 'text-green-600';
if (confidence >= 0.6) return 'text-yellow-600';
return 'text-red-600';
};
if (selectedReceipt) {
const receipt = receipts.find(r => r.id === selectedReceipt);
return (
<Layout>
<div>
<button
onClick={() => setSelectedReceipt(null)}
className="mb-4 text-primary-600 hover:text-primary-700 font-medium"
>
Back to Queue
</button>
<h1 className="text-3xl font-bold text-gray-900 mb-6">OCR Review</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Receipt Image</h2>
<div className="bg-gray-100 rounded-lg p-8 text-center">
<p className="text-gray-500">Receipt thumbnail would appear here</p>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Parsed Fields</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-2">
<Building2 className="w-4 h-4" />
Vendor
</label>
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue={receipt?.vendor} />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-2">
<DollarSign className="w-4 h-4" />
Amount
</label>
<input type="number" step="0.01" className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue={receipt?.amount} />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Date
</label>
<input type="date" className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue={receipt?.date} />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 flex items-center gap-2">
<Tag className="w-4 h-4" />
Category
</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue={receipt?.category}>
<option>Office Supplies</option>
<option>Meals</option>
<option>Transportation</option>
<option>Equipment</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">OCR Confidence</label>
<div className="flex items-center gap-2">
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getConfidenceColor(receipt?.confidence || 0)}`}
style={{ width: `${(receipt?.confidence || 0) * 100}%`, backgroundColor: receipt && receipt.confidence >= 0.8 ? '#10b981' : receipt && receipt.confidence >= 0.6 ? '#f59e0b' : '#ef4444' }}
></div>
</div>
<span className={`text-sm font-medium ${getConfidenceColor(receipt?.confidence || 0)}`}>
{(receipt?.confidence || 0) * 100}%
</span>
</div>
</div>
</div>
<div className="flex gap-2 mt-6">
<button className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 flex items-center justify-center gap-2">
<CheckCircle className="w-4 h-4" />
Approve
</button>
<button className="flex-1 px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 flex items-center justify-center gap-2">
<XCircle className="w-4 h-4" />
Reject
</button>
</div>
</div>
</div>
</div>
</Layout>
);
}
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">OCR Review Queue</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{receipts.map(receipt => (
<div
key={receipt.id}
className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:border-primary-500 transition-colors cursor-pointer"
onClick={() => setSelectedReceipt(receipt.id)}
>
<div className="mb-4">
<div className="bg-gray-100 rounded-lg h-32 flex items-center justify-center mb-4">
<p className="text-gray-500 text-sm">Receipt Image</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">{receipt.vendor}</span>
<span className={`text-xs font-medium ${getConfidenceColor(receipt.confidence)}`}>
{(receipt.confidence * 100).toFixed(0)}%
</span>
</div>
<div className="text-sm text-gray-600">
<p>Amount: ${receipt.amount.toFixed(2)}</p>
<p>Date: {format(new Date(receipt.date), 'MMM d, yyyy')}</p>
<p>Category: {receipt.category}</p>
</div>
<div className="flex gap-2 mt-4">
<button className="flex-1 px-3 py-2 bg-green-600 text-white rounded text-sm font-medium hover:bg-green-700">
Approve
</button>
<button className="flex-1 px-3 py-2 bg-gray-200 text-gray-700 rounded text-sm font-medium hover:bg-gray-300 flex items-center justify-center gap-1">
<Edit className="w-3 h-3" />
Fix
</button>
</div>
</div>
</div>
))}
</div>
</div>
</Layout>
);
};

190
src/pages/PayrollRuns.tsx Normal file
View File

@ -0,0 +1,190 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { DollarSign, FileText, Download, CheckCircle, Clock, AlertCircle } from 'lucide-react';
import { format } from 'date-fns';
export const PayrollRuns: React.FC = () => {
const [selectedRun, setSelectedRun] = useState<string | null>(null);
const payrollRuns = [
{ id: '1', payPeriod: '2023-12-01 to 2023-12-15', status: 'completed', totalHours: 1240, totalGross: 24800, totalNet: 19840, errors: 0 },
{ id: '2', payPeriod: '2023-11-16 to 2023-11-30', status: 'pending', totalHours: 1200, totalGross: 24000, totalNet: 19200, errors: 2 },
{ id: '3', payPeriod: '2023-11-01 to 2023-11-15', status: 'preview', totalHours: 1180, totalGross: 23600, totalNet: 18880, errors: 0 },
];
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-medium flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Completed</span>;
case 'pending':
return <span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs font-medium flex items-center gap-1"><Clock className="w-3 h-3" /> Pending</span>;
case 'preview':
return <span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs font-medium flex items-center gap-1"><FileText className="w-3 h-3" /> Preview</span>;
default:
return null;
}
};
if (selectedRun) {
const run = payrollRuns.find(r => r.id === selectedRun);
return (
<Layout>
<div>
<button
onClick={() => setSelectedRun(null)}
className="mb-4 text-primary-600 hover:text-primary-700 font-medium"
>
Back to List
</button>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Payroll Run Details</h1>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold text-gray-900">Pay Period: {run?.payPeriod}</h2>
{getStatusBadge(run?.status || '')}
</div>
<div className="flex gap-2">
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<Download className="w-4 h-4" />
Export CSV
</button>
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<FileText className="w-4 h-4" />
Export PDF
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 mb-1">Total Hours</p>
<p className="text-2xl font-bold text-gray-900">{run?.totalHours.toLocaleString()}</p>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 mb-1">Total Gross</p>
<p className="text-2xl font-bold text-gray-900">${run?.totalGross.toLocaleString()}</p>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 mb-1">Total Net</p>
<p className="text-2xl font-bold text-gray-900">${run?.totalNet.toLocaleString()}</p>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-600 mb-1">Errors</p>
<p className={`text-2xl font-bold ${run && run.errors > 0 ? 'text-red-600' : 'text-green-600'}`}>
{run?.errors}
</p>
</div>
</div>
{/* Employee Line Items */}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Employee</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Hours</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Rate</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Gross</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Deductions</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Taxes</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Net</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{[1, 2, 3, 4, 5].map((i) => (
<tr key={i}>
<td className="px-4 py-3 text-sm font-medium text-gray-900">Employee {i}</td>
<td className="px-4 py-3 text-sm text-gray-900">40</td>
<td className="px-4 py-3 text-sm text-gray-900">$20.00</td>
<td className="px-4 py-3 text-sm text-gray-900">$800.00</td>
<td className="px-4 py-3 text-sm text-gray-900">$50.00</td>
<td className="px-4 py-3 text-sm text-gray-900">$200.00</td>
<td className="px-4 py-3 text-sm font-medium text-gray-900">$550.00</td>
</tr>
))}
</tbody>
</table>
</div>
{run?.status === 'preview' && (
<div className="mt-6 flex gap-2">
<button className="px-6 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700">
Approve & Finalize
</button>
<button className="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300">
Make Changes
</button>
</div>
)}
</div>
</div>
</Layout>
);
}
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">Payroll Runs</h1>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700">
Start Payroll Preview
</button>
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pay Period</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Hours</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Gross</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total Net</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Errors</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{payrollRuns.map(run => (
<tr key={run.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{run.payPeriod}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(run.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{run.totalHours.toLocaleString()}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">${run.totalGross.toLocaleString()}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">${run.totalNet.toLocaleString()}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className={`text-sm font-medium ${run.errors > 0 ? 'text-red-600' : 'text-green-600'}`}>
{run.errors}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button
onClick={() => setSelectedRun(run.id)}
className="text-primary-600 hover:text-primary-900"
>
View Details
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</Layout>
);
};

18
src/pages/Placeholder.tsx Normal file
View File

@ -0,0 +1,18 @@
import React from 'react';
import { Layout } from '../components/Layout/Layout';
export const Placeholder: React.FC<{ title: string }> = ({ title }) => {
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">{title}</h1>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
<p className="text-gray-600">This page is under construction.</p>
<p className="text-sm text-gray-500 mt-2">Content will be implemented here.</p>
</div>
</div>
</Layout>
);
};

184
src/pages/Receipts.tsx Normal file
View File

@ -0,0 +1,184 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Upload, FileText, CheckCircle, AlertCircle, Clock, DollarSign, Calendar, Tag } from 'lucide-react';
import { format } from 'date-fns';
export const Receipts: React.FC = () => {
const [dragActive, setDragActive] = useState(false);
const [uploading, setUploading] = useState(false);
const receipts = [
{ id: '1', vendor: 'Office Depot', amount: 45.99, date: '2023-12-10', tax: 3.68, category: 'Office Supplies', status: 'approved', imageUrl: '' },
{ id: '2', vendor: 'Starbucks', amount: 12.50, date: '2023-12-11', tax: 1.00, category: 'Meals', status: 'pending', imageUrl: '' },
{ id: '3', vendor: 'Uber', amount: 28.75, date: '2023-12-11', tax: 0, category: 'Transportation', status: 'needs_review', ocrConfidence: 0.65, imageUrl: '' },
];
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true);
} else if (e.type === 'dragleave') {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFile(e.dataTransfer.files[0]);
}
};
const handleFile = (file: File) => {
setUploading(true);
// Simulate OCR processing
setTimeout(() => {
setUploading(false);
}, 2000);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'approved':
return <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-medium flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Approved</span>;
case 'pending':
return <span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs font-medium flex items-center gap-1"><Clock className="w-3 h-3" /> Pending</span>;
case 'needs_review':
return <span className="px-2 py-1 bg-red-100 text-red-800 rounded text-xs font-medium flex items-center gap-1"><AlertCircle className="w-3 h-3" /> Needs Review</span>;
default:
return null;
}
};
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Submit Receipt / Invoice</h1>
{/* Upload Area */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 mb-6">
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`border-2 border-dashed rounded-lg p-12 text-center transition-colors ${
dragActive
? 'border-primary-500 bg-primary-50'
: 'border-gray-300 hover:border-primary-400'
}`}
>
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Drag and drop your receipt or invoice
</h3>
<p className="text-gray-600 mb-4">or</p>
<label className="inline-block px-6 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 cursor-pointer">
Browse Files
<input type="file" className="hidden" accept="image/*,.pdf" onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])} />
</label>
<p className="text-sm text-gray-500 mt-4">Supports JPG, PNG, PDF up to 10MB</p>
</div>
{uploading && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"></div>
<span className="text-sm text-blue-800">Processing via OCR...</span>
</div>
</div>
)}
</div>
{/* Field Preview (shown after upload) */}
{uploading && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Extracted Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Vendor</label>
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="Auto-filled from OCR" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Amount</label>
<input type="number" step="0.01" className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="0.00" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Date</label>
<input type="date" className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tax</label>
<input type="number" step="0.01" className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="0.00" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-lg">
<option>Office Supplies</option>
<option>Meals</option>
<option>Transportation</option>
<option>Equipment</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notes</label>
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="Optional" />
</div>
</div>
<button className="mt-4 px-6 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700">
Submit Receipt
</button>
</div>
)}
{/* Receipts List */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Recent Submissions</h2>
<div className="space-y-4">
{receipts.map(receipt => (
<div key={receipt.id} className="p-4 border border-gray-200 rounded-lg hover:border-primary-300 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-5 h-5 text-primary-600" />
<span className="font-semibold text-gray-900">{receipt.vendor}</span>
{getStatusBadge(receipt.status)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div className="flex items-center gap-2">
<DollarSign className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">Amount:</span>
<span className="font-medium">${receipt.amount.toFixed(2)}</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{format(new Date(receipt.date), 'MMM d, yyyy')}</span>
</div>
<div className="flex items-center gap-2">
<Tag className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">{receipt.category}</span>
</div>
{receipt.ocrConfidence && (
<div className="flex items-center gap-2">
<span className="text-gray-600">OCR Confidence:</span>
<span className={`font-medium ${receipt.ocrConfidence < 0.7 ? 'text-red-600' : 'text-green-600'}`}>
{(receipt.ocrConfidence * 100).toFixed(0)}%
</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</Layout>
);
};

100
src/pages/Schedule.tsx Normal file
View File

@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Calendar, Clock, MapPin, FileText } from 'lucide-react';
import { format, addDays, startOfWeek } from 'date-fns';
export const Schedule: React.FC = () => {
const [view, setView] = useState<'weekly' | 'biweekly'>('weekly');
const [currentWeek] = useState(startOfWeek(new Date()));
const shifts = [
{ date: addDays(currentWeek, 0), startTime: '8:00 AM', endTime: '5:00 PM', role: 'Cashier', location: 'Main Store', notes: 'Opening shift' },
{ date: addDays(currentWeek, 1), startTime: '8:00 AM', endTime: '5:00 PM', role: 'Cashier', location: 'Main Store' },
{ date: addDays(currentWeek, 2), startTime: '9:00 AM', endTime: '6:00 PM', role: 'Stock Associate', location: 'Warehouse' },
{ date: addDays(currentWeek, 3), startTime: '8:00 AM', endTime: '5:00 PM', role: 'Cashier', location: 'Main Store' },
{ date: addDays(currentWeek, 4), startTime: '8:00 AM', endTime: '5:00 PM', role: 'Cashier', location: 'Main Store' },
];
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">My Schedule</h1>
<div className="flex gap-2">
<button
onClick={() => setView('weekly')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
view === 'weekly' ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Weekly
</button>
<button
onClick={() => setView('biweekly')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
view === 'biweekly' ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Biweekly
</button>
</div>
</div>
{/* Next Shift Countdown */}
<div className="bg-gradient-to-r from-primary-600 to-primary-700 rounded-lg shadow-sm p-6 text-white mb-6">
<h2 className="text-lg font-semibold mb-2">Next Shift</h2>
<p className="text-2xl font-bold mb-1">Tomorrow at 8:00 AM</p>
<p className="text-primary-100">Main Store - Cashier</p>
</div>
{/* Calendar View */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Schedule Calendar</h2>
<div className="space-y-4">
{shifts.map((shift, idx) => (
<div
key={idx}
className="p-4 border border-gray-200 rounded-lg hover:border-primary-500 transition-colors"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Calendar className="w-4 h-4 text-primary-600" />
<span className="font-semibold text-gray-900">
{format(shift.date, 'EEEE, MMM d, yyyy')}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">
{shift.startTime} - {shift.endTime}
</span>
</div>
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">{shift.role}</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600">{shift.location}</span>
</div>
</div>
{shift.notes && (
<p className="text-sm text-gray-600 mt-2 italic">{shift.notes}</p>
)}
</div>
<button className="px-4 py-2 text-sm text-primary-600 hover:text-primary-700 font-medium">
Request Change
</button>
</div>
</div>
))}
</div>
</div>
</div>
</Layout>
);
};

View File

@ -0,0 +1,192 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Settings, Key, Database, Palette, Save } from 'lucide-react';
export const SystemSettings: React.FC = () => {
const [activeTab, setActiveTab] = useState<'users' | 'api' | 'automation' | 'backup' | 'branding'>('users');
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">System Settings</h1>
<div className="flex gap-6">
{/* Sidebar */}
<div className="w-64 bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<nav className="space-y-1">
{[
{ id: 'users', label: 'User & Roles', icon: <Settings className="w-4 h-4" /> },
{ id: 'api', label: 'API Keys', icon: <Key className="w-4 h-4" /> },
{ id: 'automation', label: 'n8n Connections', icon: <Settings className="w-4 h-4" /> },
{ id: 'backup', label: 'Backup/Restore', icon: <Database className="w-4 h-4" /> },
{ id: 'branding', label: 'Branding', icon: <Palette className="w-4 h-4" /> },
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`w-full flex items-center gap-2 px-4 py-3 rounded-lg transition-colors ${
activeTab === tab.id
? 'bg-primary-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{tab.icon}
<span>{tab.label}</span>
</button>
))}
</nav>
</div>
{/* Content */}
<div className="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 p-6">
{activeTab === 'users' && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">User & Role Management</h2>
<div className="space-y-4">
<div>
<h3 className="font-medium text-gray-900 mb-2">Permissions Matrix</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left">Role</th>
<th className="px-4 py-2 text-center">View</th>
<th className="px-4 py-2 text-center">Edit</th>
<th className="px-4 py-2 text-center">Delete</th>
<th className="px-4 py-2 text-center">Admin</th>
</tr>
</thead>
<tbody>
{['Employee', 'Manager', 'HR', 'Payroll', 'Admin'].map(role => (
<tr key={role} className="border-t">
<td className="px-4 py-2 font-medium">{role}</td>
<td className="px-4 py-2 text-center">
<input type="checkbox" defaultChecked className="rounded" />
</td>
<td className="px-4 py-2 text-center">
<input type="checkbox" defaultChecked={['Manager', 'HR', 'Admin'].includes(role)} className="rounded" />
</td>
<td className="px-4 py-2 text-center">
<input type="checkbox" defaultChecked={role === 'Admin'} className="rounded" />
</td>
<td className="px-4 py-2 text-center">
<input type="checkbox" defaultChecked={role === 'Admin'} className="rounded" />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
)}
{activeTab === 'api' && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">API Keys</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">OCR API Key</label>
<div className="flex gap-2">
<input type="password" className="flex-1 px-3 py-2 border border-gray-300 rounded-lg" defaultValue="••••••••••••" />
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300">Show</button>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">Regenerate</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">LLM API Key</label>
<div className="flex gap-2">
<input type="password" className="flex-1 px-3 py-2 border border-gray-300 rounded-lg" defaultValue="••••••••••••" />
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300">Show</button>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">Regenerate</button>
</div>
</div>
</div>
</div>
)}
{activeTab === 'automation' && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">n8n Connections</h2>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-medium">Email Workflow</span>
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">Active</span>
</div>
<p className="text-sm text-gray-600">n8n workflow URL: https://n8n.company.com/webhook/email</p>
</div>
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-medium">SMS Workflow</span>
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">Active</span>
</div>
<p className="text-sm text-gray-600">n8n workflow URL: https://n8n.company.com/webhook/sms</p>
</div>
</div>
</div>
)}
{activeTab === 'backup' && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">Backup & Restore</h2>
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="font-medium">Last Backup</span>
<span className="text-sm text-gray-600">2 hours ago</span>
</div>
<button className="mt-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700">
Create Backup Now
</button>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Restore from Backup</label>
<input type="file" className="w-full px-3 py-2 border border-gray-300 rounded-lg" />
<button className="mt-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700">
Restore
</button>
</div>
</div>
</div>
)}
{activeTab === 'branding' && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">Branding</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Company Logo</label>
<div className="flex items-center gap-4">
<div className="w-32 h-32 bg-gray-100 rounded-lg flex items-center justify-center">
<span className="text-gray-400">Logo</span>
</div>
<div>
<input type="file" className="mb-2" />
<p className="text-xs text-gray-500">Recommended: 200x200px PNG</p>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Primary Color</label>
<input type="color" className="w-32 h-10 rounded" defaultValue="#0ea5e9" />
</div>
</div>
</div>
)}
<div className="mt-6 flex justify-end">
<button className="px-6 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center gap-2">
<Save className="w-4 h-4" />
Save Changes
</button>
</div>
</div>
</div>
</div>
</Layout>
);
};

194
src/pages/TeamSchedules.tsx Normal file
View File

@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Calendar, Plus, Copy, Zap, Send, Edit, Trash2 } from 'lucide-react';
import { format, startOfWeek, addDays } from 'date-fns';
export const TeamSchedules: React.FC = () => {
const [currentWeek] = useState(startOfWeek(new Date()));
const [selectedShift, setSelectedShift] = useState<string | null>(null);
const employees = ['John Doe', 'Jane Smith', 'Bob Johnson', 'Alice Brown', 'Charlie Wilson'];
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(currentWeek, i));
const shifts: Record<string, Record<string, { start: string; end: string; role: string }>> = {
'John Doe': {
'0': { start: '8:00 AM', end: '5:00 PM', role: 'Cashier' },
'1': { start: '8:00 AM', end: '5:00 PM', role: 'Cashier' },
'3': { start: '9:00 AM', end: '6:00 PM', role: 'Manager' },
},
'Jane Smith': {
'1': { start: '9:00 AM', end: '6:00 PM', role: 'Stock' },
'2': { start: '8:00 AM', end: '5:00 PM', role: 'Cashier' },
'4': { start: '8:00 AM', end: '5:00 PM', role: 'Cashier' },
},
};
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">Team Schedules</h1>
<div className="flex gap-2">
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<Calendar className="w-4 h-4" />
Week Selector
</button>
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center gap-2">
<Send className="w-4 h-4" />
Publish Week
</button>
</div>
</div>
{/* Top Controls */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex items-center gap-2 flex-wrap">
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center gap-2">
<Plus className="w-4 h-4" />
Add Shift
</button>
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<Copy className="w-4 h-4" />
Copy Last Week
</button>
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<Zap className="w-4 h-4" />
Auto-Generate
</button>
</div>
</div>
<div className="flex gap-6">
{/* Main Grid */}
<div className="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 z-10 border-r border-gray-200">
Employee
</th>
{weekDays.map((day, idx) => (
<th key={idx} className="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider min-w-[120px]">
<div>{format(day, 'EEE')}</div>
<div className="text-xs text-gray-400">{format(day, 'MMM d')}</div>
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{employees.map((emp, empIdx) => (
<tr key={empIdx}>
<td className="px-4 py-3 font-medium text-gray-900 sticky left-0 bg-white z-10 border-r border-gray-200">
{emp}
</td>
{weekDays.map((day, dayIdx) => {
const shift = shifts[emp]?.[dayIdx.toString()];
return (
<td key={dayIdx} className="px-2 py-2">
{shift ? (
<div
className="p-2 bg-primary-100 border border-primary-300 rounded cursor-pointer hover:bg-primary-200 transition-colors"
onClick={() => setSelectedShift(`${emp}-${dayIdx}`)}
>
<div className="text-xs font-medium text-primary-900">{shift.start} - {shift.end}</div>
<div className="text-xs text-primary-700">{shift.role}</div>
</div>
) : (
<div className="p-2 border-2 border-dashed border-gray-200 rounded cursor-pointer hover:border-primary-300 transition-colors text-center text-xs text-gray-400">
Click to add
</div>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Sidebar */}
<div className="w-64 bg-white rounded-lg shadow-sm border border-gray-200 p-4">
<h3 className="font-semibold text-gray-900 mb-4">Unassigned Shifts</h3>
<div className="space-y-2">
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm font-medium">Monday 8:00 AM</p>
<p className="text-xs text-gray-600">Cashier needed</p>
</div>
</div>
<div className="mt-6">
<h3 className="font-semibold text-gray-900 mb-4">Coverage</h3>
<div className="space-y-2">
{weekDays.map((day, idx) => (
<div key={idx} className="flex items-center justify-between p-2 bg-gray-50 rounded">
<span className="text-xs text-gray-600">{format(day, 'EEE')}</span>
<span className="text-xs font-medium text-green-600"> Covered</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* Shift Edit Drawer */}
{selectedShift && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold text-gray-900 mb-4">Edit Shift</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start Time</label>
<input type="time" className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue="08:00" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End Time</label>
<input type="time" className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue="17:00" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Role</label>
<select className="w-full px-3 py-2 border border-gray-300 rounded-lg">
<option>Cashier</option>
<option>Stock Associate</option>
<option>Manager</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Location</label>
<input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-lg" defaultValue="Main Store" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Notes</label>
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-lg" rows={3}></textarea>
</div>
<div>
<label className="flex items-center gap-2">
<input type="checkbox" className="rounded" />
<span className="text-sm text-gray-700">Repeat weekly</span>
</label>
</div>
</div>
<div className="flex gap-2 mt-6">
<button
onClick={() => setSelectedShift(null)}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
>
Cancel
</button>
<button className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700">
Save
</button>
<button className="px-4 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
)}
</div>
</Layout>
);
};

189
src/pages/TeamTimecards.tsx Normal file
View File

@ -0,0 +1,189 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { CheckCircle, XCircle, AlertCircle, Clock, Filter } from 'lucide-react';
import { format } from 'date-fns';
export const TeamTimecards: React.FC = () => {
const [selectedEmployees, setSelectedEmployees] = useState<string[]>([]);
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'flagged'>('all');
const employees = [
{ id: '1', name: 'John Doe', status: 'clocked_in', clockIn: '8:02 AM' },
{ id: '2', name: 'Jane Smith', status: 'clocked_in', clockIn: '8:00 AM' },
{ id: '3', name: 'Bob Johnson', status: 'on_break', clockIn: '8:15 AM' },
];
const timecards = [
{ id: '1', employeeId: '1', employeeName: 'John Doe', date: '2023-12-11', clockIn: '8:02 AM', clockOut: '5:14 PM', hours: 8, status: 'approved' },
{ id: '2', employeeId: '2', employeeName: 'Jane Smith', date: '2023-12-11', clockIn: '8:00 AM', clockOut: '5:00 PM', hours: 8, status: 'pending' },
{ id: '3', employeeId: '3', employeeName: 'Bob Johnson', date: '2023-12-11', clockIn: '8:15 AM', clockOut: null, hours: 4, status: 'flagged' },
];
const toggleEmployee = (id: string) => {
setSelectedEmployees(prev =>
prev.includes(id) ? prev.filter(e => e !== id) : [...prev, id]
);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'approved':
return <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-medium flex items-center gap-1"><CheckCircle className="w-3 h-3" /> Approved</span>;
case 'pending':
return <span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs font-medium flex items-center gap-1"><AlertCircle className="w-3 h-3" /> Pending</span>;
case 'flagged':
return <span className="px-2 py-1 bg-red-100 text-red-800 rounded text-xs font-medium flex items-center gap-1"><XCircle className="w-3 h-3" /> Flagged</span>;
default:
return null;
}
};
const filteredTimecards = filterStatus === 'all'
? timecards
: timecards.filter(tc => tc.status === filterStatus);
return (
<Layout>
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-6">Team Timecards</h1>
{/* Live Status */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Currently Clocked In</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{employees.map(emp => (
<div key={emp.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
<div className={`w-3 h-3 rounded-full ${
emp.status === 'clocked_in' ? 'bg-green-500' :
emp.status === 'on_break' ? 'bg-yellow-500' :
'bg-gray-400'
}`}></div>
<div className="flex-1">
<p className="font-medium text-gray-900">{emp.name}</p>
<p className="text-sm text-gray-600">Clocked in: {emp.clockIn}</p>
</div>
</div>
))}
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex items-center gap-4">
<Filter className="w-5 h-5 text-gray-400" />
<div className="flex gap-2">
{(['all', 'pending', 'approved', 'flagged'] as const).map(status => (
<button
key={status}
onClick={() => setFilterStatus(status)}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
filterStatus === status
? 'bg-primary-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</button>
))}
</div>
</div>
</div>
{/* Timecards Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left">
<input type="checkbox" className="rounded" />
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Employee</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock In</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clock Out</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Hours</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredTimecards.map(tc => (
<tr key={tc.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<input
type="checkbox"
checked={selectedEmployees.includes(tc.id)}
onChange={() => toggleEmployee(tc.id)}
className="rounded"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{tc.employeeName}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{format(new Date(tc.date), 'MMM d, yyyy')}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{tc.clockIn}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{tc.clockOut || 'Missing'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{tc.hours}h</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getStatusBadge(tc.status)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-primary-600 hover:text-primary-900 mr-4">Approve</button>
<button className="text-red-600 hover:text-red-900">Deny</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Bulk Actions */}
{selectedEmployees.length > 0 && (
<div className="mt-6 bg-primary-50 border border-primary-200 rounded-lg p-4 flex items-center justify-between">
<span className="text-sm font-medium text-primary-900">
{selectedEmployees.length} timecard(s) selected
</span>
<div className="flex gap-2">
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700">
Approve Selected
</button>
<button className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700">
Deny Selected
</button>
</div>
</div>
)}
{/* Alerts */}
<div className="mt-6 space-y-2">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<AlertCircle className="w-5 h-5 text-red-600" />
<span className="font-medium text-red-900">Missing Clock-Out</span>
</div>
<p className="text-sm text-red-800">Bob Johnson - December 11, 2023</p>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-5 h-5 text-yellow-600" />
<span className="font-medium text-yellow-900">Excessive Overtime</span>
</div>
<p className="text-sm text-yellow-800">John Doe - 12 hours on December 10, 2023</p>
</div>
</div>
</div>
</Layout>
);
};

406
src/pages/TimeClock.tsx Normal file
View File

@ -0,0 +1,406 @@
import React, { useState, useEffect } from 'react';
import { Clock, Lock } from 'lucide-react';
import { format } from 'date-fns';
interface EmployeeStatus {
id: string;
name: string;
email: string;
currentStatus: 'clocked_in' | 'clocked_out' | 'on_break';
clockInTime?: string;
clockOutTime?: string;
breakStartTime?: string;
scheduledStart?: string;
scheduledEnd?: string;
isLate?: boolean;
leftEarly?: boolean;
}
export const TimeClock: React.FC = () => {
const [employees, setEmployees] = useState<EmployeeStatus[]>([]);
const [selectedEmployee, setSelectedEmployee] = useState<EmployeeStatus | null>(null);
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [currentTime, setCurrentTime] = useState(new Date());
// Update current time every second
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
// Fetch employees and their status
useEffect(() => {
fetchEmployees();
const interval = setInterval(fetchEmployees, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
const fetchEmployees = async () => {
try {
const response = await fetch('http://localhost:3001/api/timeclock/status');
if (response.ok) {
const data = await response.json();
setEmployees(data);
}
} catch (error) {
console.error('Error fetching employee status:', error);
}
};
const handleEmployeeClick = (employee: EmployeeStatus) => {
setSelectedEmployee(employee);
setPassword('');
setError('');
};
const handleClockAction = async () => {
if (!selectedEmployee || !password) {
setError('Please enter your password');
return;
}
setLoading(true);
setError('');
try {
// Determine action based on current status
let action: string;
if (selectedEmployee.currentStatus === 'clocked_out') {
action = 'clock_in';
} else if (selectedEmployee.currentStatus === 'on_break') {
action = 'break_end'; // End break first, then they can clock out
} else {
action = 'clock_out';
}
const response = await fetch('http://localhost:3001/api/timeclock/action', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
employeeId: selectedEmployee.id,
password,
action,
}),
});
const data = await response.json();
if (response.ok) {
setSelectedEmployee(null);
setPassword('');
// Small delay to ensure database is updated
setTimeout(async () => {
await fetchEmployees(); // Refresh status
}, 500);
} else {
setError(data.error || 'Authentication failed');
}
} catch (error) {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
};
const getTimeBarPosition = (time: string | undefined): number => {
if (!time) return 0;
// Handle both "HH:mm" and "HH:mm:ss" formats
const timeOnly = time.includes(' ') ? time.split(' ')[1] : time;
const [hours, minutes] = timeOnly.split(':').map(Number);
const totalMinutes = hours * 60 + (minutes || 0);
const startMinutes = 8 * 60; // 8:00 AM
const endMinutes = 18 * 60; // 6:00 PM
const totalRange = endMinutes - startMinutes;
if (totalMinutes < startMinutes) return 0;
if (totalMinutes > endMinutes) return 100;
return ((totalMinutes - startMinutes) / totalRange) * 100;
};
const getCurrentTimePosition = (): number => {
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
const totalMinutes = hours * 60 + minutes;
const startMinutes = 8 * 60;
const endMinutes = 18 * 60;
const totalRange = endMinutes - startMinutes;
if (totalMinutes < startMinutes) return 0;
if (totalMinutes > endMinutes) return 100;
return ((totalMinutes - startMinutes) / totalRange) * 100;
};
const getStatusColor = (employee: EmployeeStatus): string => {
if (employee.currentStatus === 'clocked_in') return 'bg-green-500';
if (employee.currentStatus === 'on_break') return 'bg-yellow-500';
return 'bg-red-500';
};
const getStatusText = (employee: EmployeeStatus): string => {
if (employee.currentStatus === 'clocked_in') return 'Clocked In';
if (employee.currentStatus === 'on_break') return 'On Break';
return 'Clocked Out';
};
const formatTime = (time: string | undefined): string => {
if (!time) return '';
try {
const [h, m] = time.split(':').map(Number);
const date = new Date();
date.setHours(h, m || 0, 0);
return format(date, 'h:mm a');
} catch {
return time;
}
};
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-4xl font-bold text-gray-900 mb-2">Time Clock</h1>
<p className="text-lg text-gray-600">
{format(currentTime, 'EEEE, MMMM d, yyyy')} {format(currentTime, 'h:mm:ss a')}
</p>
</div>
<div className="flex items-center gap-3 text-3xl font-bold text-primary-600">
<Clock className="w-10 h-10" />
</div>
</div>
</div>
{/* Employee Grid - Large, clear blocks */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{employees.map((employee) => {
const isClockedIn = employee.currentStatus === 'clocked_in' || employee.currentStatus === 'on_break';
const currentPosition = getCurrentTimePosition();
const clockInPosition = employee.clockInTime ? getTimeBarPosition(employee.clockInTime) : 100;
const clockOutPosition = employee.clockOutTime ? getTimeBarPosition(employee.clockOutTime) : 100;
return (
<div
key={employee.id}
onClick={() => handleEmployeeClick(employee)}
className="bg-white rounded-xl shadow-lg border-2 border-gray-300 hover:border-primary-500 hover:shadow-xl transition-all cursor-pointer p-6"
>
{/* Employee Name - Large and prominent */}
<div className="mb-4">
<h3 className="text-2xl font-bold text-gray-900 mb-2">{employee.name}</h3>
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded-full ${getStatusColor(employee)}`}></div>
<span className="text-base font-semibold text-gray-700">{getStatusText(employee)}</span>
</div>
</div>
{/* Time Bar Container - Large and clear */}
<div className="relative h-16 bg-red-500 rounded-lg overflow-hidden border-2 border-gray-300">
{/* Hour markers */}
<div className="absolute inset-0 flex">
{Array.from({ length: 11 }, (_, i) => (
<div
key={i}
className="flex-1 border-r-2 border-red-600"
style={{ width: `${100 / 11}%` }}
></div>
))}
</div>
{/* Hour labels at bottom */}
<div className="absolute bottom-0 left-0 right-0 flex text-xs font-medium text-white bg-red-600 py-1">
{Array.from({ length: 11 }, (_, i) => (
<div
key={i}
className="flex-1 text-center"
style={{ width: `${100 / 11}%` }}
>
{8 + i}
</div>
))}
</div>
{/* Green bar for clocked in period - always show if they clocked in today */}
{employee.clockInTime && (
<>
{employee.clockOutTime ? (
// Employee clocked in and out - green from clock in to clock out (historical)
<div
className="absolute top-0 bottom-6 bg-green-500 opacity-90 z-5"
style={{
left: `${clockInPosition}%`,
width: `${clockOutPosition - clockInPosition}%`,
minWidth: '1%',
}}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-black opacity-10"></div>
</div>
) : (
// Employee clocked in but not out - green from clock in to current time
<div
className="absolute top-0 bottom-6 bg-green-500 opacity-90 z-5"
style={{
left: `${clockInPosition}%`,
width: `${currentPosition - clockInPosition}%`,
minWidth: '2%',
}}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-black opacity-10"></div>
</div>
)}
</>
)}
{/* Current time indicator */}
<div
className="absolute top-0 bottom-0 w-1 bg-gray-900 z-10"
style={{ left: `${currentPosition}%` }}
>
<div className="absolute -top-2 left-1/2 transform -translate-x-1/2 w-4 h-4 bg-gray-900 rounded-full border-2 border-white"></div>
</div>
{/* Late indicator */}
{employee.isLate && employee.scheduledStart && (
<div
className="absolute top-0 bottom-6 w-2 bg-red-700 z-20 border-l-2 border-red-900"
style={{ left: `${getTimeBarPosition(employee.scheduledStart)}%` }}
title="Late arrival"
></div>
)}
</div>
{/* Status Info */}
<div className="mt-4 space-y-1 text-sm">
{employee.clockInTime && (
<div className="flex justify-between">
<span className="text-gray-600">Clocked In:</span>
<span className="font-semibold text-gray-900">{formatTime(employee.clockInTime)}</span>
</div>
)}
{employee.clockOutTime && (
<div className="flex justify-between">
<span className="text-gray-600">Clocked Out:</span>
<span className="font-semibold text-gray-900">{formatTime(employee.clockOutTime)}</span>
</div>
)}
{employee.scheduledStart && (
<div className={`flex justify-between ${employee.isLate ? 'text-red-600 font-semibold' : 'text-gray-600'}`}>
<span>Scheduled:</span>
<span className="font-semibold">{formatTime(employee.scheduledStart)}</span>
</div>
)}
{employee.isLate && (
<div className="text-red-600 font-semibold text-center mt-2">
Late Arrival
</div>
)}
</div>
{/* Click hint */}
<div className="mt-4 pt-4 border-t border-gray-200 text-center">
<span className="text-xs text-gray-500">
Click to {
employee.currentStatus === 'clocked_out' ? 'Clock In' :
employee.currentStatus === 'on_break' ? 'End Break' :
'Clock Out'
}
</span>
</div>
</div>
);
})}
</div>
{/* Password Modal */}
{selectedEmployee && (
<div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-2xl p-8 w-full max-w-md">
<div className="mb-6">
<h2 className="text-3xl font-bold text-gray-900 mb-2">
{selectedEmployee.currentStatus === 'clocked_out' ? 'Clock In' :
selectedEmployee.currentStatus === 'on_break' ? 'End Break' :
'Clock Out'}
</h2>
<p className="text-xl text-gray-600">
{selectedEmployee.name}
</p>
</div>
{error && (
<div className="mb-4 p-4 bg-red-50 border-2 border-red-200 rounded-lg text-red-800 font-medium">
{error}
</div>
)}
<div className="mb-6">
<label className="block text-lg font-semibold text-gray-700 mb-3">
Enter Your Password
</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 transform -translate-y-1/2 w-6 h-6 text-gray-400" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleClockAction()}
autoFocus
className="w-full pl-12 pr-4 py-4 text-lg border-2 border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none"
placeholder="Enter your password"
/>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setSelectedEmployee(null);
setPassword('');
setError('');
}}
className="flex-1 px-6 py-3 bg-gray-200 text-gray-700 rounded-lg font-semibold hover:bg-gray-300 text-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleClockAction}
disabled={loading || !password}
className="flex-1 px-6 py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-lg transition-colors"
>
{loading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
Processing...
</>
) : (
selectedEmployee.currentStatus === 'clocked_out' ? 'Clock In' :
selectedEmployee.currentStatus === 'on_break' ? 'End Break' :
'Clock Out'
)}
</button>
</div>
</div>
</div>
)}
{/* Empty state */}
{employees.length === 0 && (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
<Clock className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<p className="text-xl text-gray-600">No employees found</p>
<p className="text-sm text-gray-500 mt-2">Make sure the backend server is running</p>
</div>
)}
</div>
</div>
);
};

179
src/pages/Timecards.tsx Normal file
View File

@ -0,0 +1,179 @@
import React, { useState } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Clock, CheckCircle, XCircle, AlertCircle, Calendar } from 'lucide-react';
import { format, startOfWeek, addDays, isSameDay } from 'date-fns';
export const Timecards: React.FC = () => {
const [view, setView] = useState<'weekly' | 'monthly'>('weekly');
const [selectedDate] = useState(new Date());
const timecards = [
{ date: new Date(2023, 11, 11), clockIn: '8:02 AM', clockOut: '5:14 PM', breakTime: '1h', totalHours: 8, status: 'approved' },
{ date: new Date(2023, 11, 12), clockIn: '8:00 AM', clockOut: '5:00 PM', breakTime: '1h', totalHours: 8, status: 'approved' },
{ date: new Date(2023, 11, 13), clockIn: '8:15 AM', clockOut: '5:30 PM', breakTime: '1h', totalHours: 8.25, status: 'pending' },
{ date: new Date(2023, 11, 14), clockIn: '8:00 AM', clockOut: null, breakTime: '0h', totalHours: 4, status: 'flagged' },
];
const weekStart = startOfWeek(selectedDate);
const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
const getStatusColor = (status: string) => {
switch (status) {
case 'approved':
return 'bg-green-100 text-green-800 border-green-200';
case 'pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'flagged':
return 'bg-red-100 text-red-800 border-red-200';
default:
return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'approved':
return <CheckCircle className="w-4 h-4" />;
case 'pending':
return <AlertCircle className="w-4 h-4" />;
case 'flagged':
return <XCircle className="w-4 h-4" />;
default:
return null;
}
};
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">My Timecards</h1>
<div className="flex gap-2">
<button
onClick={() => setView('weekly')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
view === 'weekly' ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Weekly
</button>
<button
onClick={() => setView('monthly')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
view === 'monthly' ? 'bg-primary-600 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Monthly
</button>
</div>
</div>
{/* Weekly Summary Card */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Weekly Summary</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-gray-600 mb-1">Total Hours</p>
<p className="text-2xl font-bold text-gray-900">32.0</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Overtime</p>
<p className="text-2xl font-bold text-orange-600">0.0</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Break Time</p>
<p className="text-2xl font-bold text-gray-900">5.0h</p>
</div>
<div>
<p className="text-sm text-gray-600 mb-1">Approval Status</p>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-sm font-medium">Pending</span>
</div>
</div>
</div>
</div>
{view === 'weekly' ? (
<>
{/* Daily Breakdown */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Daily Breakdown</h2>
<div className="space-y-4">
{timecards.map((tc, idx) => (
<div key={idx} className={`p-4 rounded-lg border ${getStatusColor(tc.status)}`}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span className="font-medium">{format(tc.date, 'EEEE, MMM d, yyyy')}</span>
{getStatusIcon(tc.status)}
</div>
<button className="text-sm text-primary-600 hover:text-primary-700 font-medium">
Request Fix
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-3">
<div>
<p className="text-xs text-gray-600 mb-1">Clock In</p>
<p className="font-medium">{tc.clockIn}</p>
</div>
<div>
<p className="text-xs text-gray-600 mb-1">Break</p>
<p className="font-medium">{tc.breakTime}</p>
</div>
<div>
<p className="text-xs text-gray-600 mb-1">Clock Out</p>
<p className="font-medium">{tc.clockOut || 'Missing'}</p>
</div>
<div>
<p className="text-xs text-gray-600 mb-1">Total Hours</p>
<p className="font-medium">{tc.totalHours}h</p>
</div>
</div>
</div>
))}
</div>
</div>
{/* Calendar View */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Calendar View</h2>
<div className="grid grid-cols-7 gap-2">
{weekDays.map((day, idx) => {
const tc = timecards.find(t => isSameDay(t.date, day));
return (
<div
key={idx}
className={`p-3 rounded-lg border ${
tc
? getStatusColor(tc.status)
: 'bg-gray-50 border-gray-200'
}`}
>
<p className="text-xs font-medium mb-1">{format(day, 'EEE')}</p>
<p className="text-lg font-bold mb-2">{format(day, 'd')}</p>
{tc && (
<div className="text-xs">
<p className="font-medium">{tc.totalHours}h</p>
{tc.status === 'approved' && <CheckCircle className="w-3 h-3 mt-1" />}
{tc.status === 'pending' && <AlertCircle className="w-3 h-3 mt-1" />}
{tc.status === 'flagged' && <XCircle className="w-3 h-3 mt-1" />}
</div>
)}
</div>
);
})}
</div>
</div>
</>
) : (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Monthly View</h2>
<p className="text-gray-600">Monthly calendar view would be displayed here</p>
</div>
)}
</div>
</Layout>
);
};

View File

@ -0,0 +1,258 @@
import React, { useState, useEffect } from 'react';
import { Layout } from '../components/Layout/Layout';
import { Users, Edit, Trash2, Plus, Search, Filter, Save, X } from 'lucide-react';
import { mockDatabase } from '../data/mockDatabase';
import { User, UserRole } from '../types';
import { useAuth } from '../context/AuthContext';
export const UserManagement: React.FC = () => {
const { user: currentUser } = useAuth();
const [users, setUsers] = useState<User[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [showForm, setShowForm] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [formData, setFormData] = useState({
name: '',
email: '',
role: 'employee' as UserRole,
});
useEffect(() => {
loadUsers();
}, []);
const loadUsers = () => {
setUsers(mockDatabase.getUsers());
};
const handleCreate = () => {
setEditingUser(null);
setFormData({ name: '', email: '', role: 'employee' });
setShowForm(true);
};
const handleEdit = (user: User) => {
setEditingUser(user);
setFormData({ name: user.name, email: user.email, role: user.role });
setShowForm(true);
};
const handleDelete = (id: string) => {
if (window.confirm('Are you sure you want to delete this user?')) {
if (mockDatabase.deleteUser(id)) {
loadUsers();
}
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (editingUser) {
// Update existing user
const updated = mockDatabase.updateUser(editingUser.id, formData);
if (updated) {
loadUsers();
setShowForm(false);
setEditingUser(null);
}
} else {
// Create new user
const newUser = mockDatabase.createUser(formData);
if (newUser) {
loadUsers();
setShowForm(false);
setFormData({ name: '', email: '', role: 'employee' });
}
}
};
const filteredUsers = users.filter(u =>
u.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
u.email.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<Layout>
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold text-gray-900">User & Role Management</h1>
<button
onClick={handleCreate}
className="px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center gap-2"
>
<Plus className="w-4 h-4" />
New User
</button>
</div>
{/* Search and Filters */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
<div className="flex items-center gap-4">
<div className="flex-1 flex items-center gap-2 bg-gray-100 rounded-lg px-4 py-2">
<Search className="w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-transparent border-none outline-none flex-1"
/>
</div>
<button className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300 flex items-center gap-2">
<Filter className="w-4 h-4" />
Filter
</button>
</div>
</div>
{/* Users Table */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">{user.name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{user.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 rounded text-xs font-medium ${
user.role === 'admin' ? 'bg-purple-100 text-purple-800' :
user.role === 'hr' ? 'bg-blue-100 text-blue-800' :
user.role === 'payroll' ? 'bg-green-100 text-green-800' :
user.role === 'manager' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{user.role.toUpperCase()}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div className="flex items-center gap-2">
<button
onClick={() => handleEdit(user)}
className="text-primary-600 hover:text-primary-900"
title="Edit"
>
<Edit className="w-4 h-4" />
</button>
{user.id !== currentUser?.id && (
<button
onClick={() => handleDelete(user.id)}
className="text-red-600 hover:text-red-900"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Create/Edit Form Modal */}
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900">
{editingUser ? 'Edit User' : 'Create New User'}
</h2>
<button
onClick={() => {
setShowForm(false);
setEditingUser(null);
}}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Full Name
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Role
</label>
<select
value={formData.role}
onChange={(e) => setFormData({ ...formData, role: e.target.value as UserRole })}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="employee">Employee</option>
<option value="manager">Manager</option>
<option value="hr">HR</option>
<option value="payroll">Payroll</option>
<option value="admin">Admin</option>
</select>
</div>
<div className="flex gap-2 pt-4">
<button
type="button"
onClick={() => {
setShowForm(false);
setEditingUser(null);
}}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
>
Cancel
</button>
<button
type="submit"
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 flex items-center justify-center gap-2"
>
<Save className="w-4 h-4" />
{editingUser ? 'Update' : 'Create'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
</Layout>
);
};

70
src/types.ts Normal file
View File

@ -0,0 +1,70 @@
export type UserRole = 'employee' | 'manager' | 'hr' | 'payroll' | 'admin';
export type ClockStatus = 'clocked_out' | 'clocked_in' | 'on_break';
export interface User {
id: string;
name: string;
email: string;
role: UserRole;
avatar?: string;
}
export interface Timecard {
id: string;
employeeId: string;
date: string;
clockIn?: string;
clockOut?: string;
breakStart?: string;
breakEnd?: string;
totalHours: number;
overtime: number;
status: 'pending' | 'approved' | 'flagged';
}
export interface Shift {
id: string;
employeeId: string;
date: string;
startTime: string;
endTime: string;
role: string;
location: string;
notes?: string;
}
export interface DisciplinaryAction {
id: string;
employeeId: string;
date: string;
details: string;
severity: 'low' | 'medium' | 'high' | 'critical';
status: 'draft' | 'pending_approval' | 'finalized';
documentUrl?: string;
}
export interface Receipt {
id: string;
vendor: string;
amount: number;
date: string;
tax: number;
category: string;
notes?: string;
status: 'pending' | 'needs_review' | 'approved';
ocrConfidence?: number;
imageUrl?: string;
}
export interface PayrollRun {
id: string;
payPeriod: string;
status: 'preview' | 'pending' | 'completed';
totalHours: number;
totalGross: number;
totalNet: number;
errors: number;
}

28
tailwind.config.js Normal file
View File

@ -0,0 +1,28 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
},
},
},
plugins: [],
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

12
tsconfig.node.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

8
vite.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})