diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..cf1c099
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,142 @@
+# BioStream - AI Coding Agent Instructions
+
+## Project Overview
+BioStream is an Angular 19 standalone application with TypeScript, featuring role-based authentication and multi-user workflows. The app supports five user roles: User, Administrator, FieldAgent, FinanceUser, and ITSupportUser.
+
+## Architecture & Key Patterns
+
+### Component Architecture
+- **Standalone Components**: All components use Angular 19's standalone API (no NgModules)
+- **Signal-based State**: Uses Angular signals for reactive state management instead of RxJS BehaviorSubject where possible
+- **Route-level Lazy Loading**: Components are lazy-loaded via `loadComponent` in routes
+
+### Authentication Flow
+1. **Registration** → Email verification link sent
+2. **Email Verification** → Account activated
+3. **Login** → OTP sent to email (check console for demo OTP)
+4. **OTP Verification** → User authenticated and redirected to role-based dashboard
+
+### User Roles & Dashboards
+Each role has a dedicated dashboard route:
+- `Administrator` → `/admin/dashboard`
+- `FieldAgent` → `/field-agent/dashboard`
+- `FinanceUser` → `/finance/dashboard`
+- `ITSupportUser` → `/it-support/dashboard`
+- `User` → `/user/dashboard`
+
+### Guards & Security
+- `authGuard`: Protects authenticated routes
+- `guestGuard`: Prevents authenticated users from accessing login/register
+- `roleGuard(roles)`: Enforces role-based access control
+
+## Key Files & Locations
+
+### Core Services
+- `src/app/services/auth.service.ts` - Authentication, session management, mock user database
+- `src/app/services/email.service.ts` - Email templates (OTP, verification, welcome emails)
+
+### Models
+- `src/app/models/user.model.ts` - User interface, UserRole enum, auth request/response types
+
+### Guards
+- `src/app/guards/auth.guard.ts` - All route protection logic
+
+### Components Structure
+```
+src/app/components/
+├── login/ # Login with demo credentials
+├── register/ # Multi-role registration
+├── verify-otp/ # OTP verification with resend
+├── verify-email/ # Email verification handler
+└── dashboards/ # Role-specific landing pages
+ ├── admin-dashboard.component.ts
+ ├── field-agent-dashboard.component.ts
+ ├── finance-dashboard.component.ts
+ ├── it-support-dashboard.component.ts
+ └── user-dashboard.component.ts
+```
+
+## Development Patterns
+
+### Component Style
+- Use inline templates and styles for components
+- Leverage `@if`, `@for`, `@defer` control flow syntax (not *ngIf, *ngFor)
+- Import `CommonModule` and `FormsModule` as needed
+
+### State Management
+```typescript
+// Prefer signals over observables for local state
+private loading = signal(false);
+private errorMessage = signal('');
+```
+
+### Authentication Service Usage
+```typescript
+// Check authentication
+if (authService.isAuthenticated()) { }
+
+// Check role
+if (authService.hasRole(UserRole.Administrator)) { }
+
+// Get current user
+const user = authService.getCurrentUser();
+
+// Get role dashboard route
+const route = authService.getRoleDashboardRoute(user.role);
+```
+
+## Mock Data & Demo Accounts
+
+The app includes pre-configured demo accounts (see `auth.service.ts` → `initializeMockUsers`):
+- **Admin**: admin@biostream.com / admin123
+- **Field Agent**: field@biostream.com / field123
+- **Finance**: finance@biostream.com / finance123
+- **IT Support**: it@biostream.com / it123
+- **User**: user@biostream.com / user123
+
+OTPs are logged to console during development.
+
+## Build & Run
+
+```bash
+# Development server
+npm start
+
+# Production build
+npm run build
+
+# Run tests
+npm test
+```
+
+## Adding New Features
+
+### Adding a New User Role
+1. Add role to `UserRole` enum in `user.model.ts`
+2. Create dashboard component in `components/dashboards/`
+3. Add route in `app.routes.ts` with `roleGuard`
+4. Update `getRoleDashboardRoute()` in `auth.service.ts`
+
+### Adding Protected Routes
+```typescript
+{
+ path: 'protected-page',
+ loadComponent: () => import('./component').then(m => m.Component),
+ canActivate: [roleGuard([UserRole.Administrator, UserRole.User])]
+}
+```
+
+## Common Patterns to Follow
+
+- **Lazy load routes**: Always use `loadComponent` for route components
+- **Use signals for component state**: Prefer `signal()` over class properties for reactive data
+- **Inline auth checks in templates**: Use `@if (authService.isAuthenticated())`
+- **Role-based UI**: Show/hide features based on `authService.hasRole()` or `hasAnyRole()`
+- **Logout everywhere**: Include logout button in all dashboards
+
+## Important Notes
+
+- This is a **mock authentication system** - no backend integration yet
+- OTP and verification tokens are stored in localStorage (client-side only)
+- All email sends are simulated and logged to console
+- Session persistence uses localStorage for demo purposes
diff --git a/angular.json b/angular.json
index 8e608be..58791e9 100644
--- a/angular.json
+++ b/angular.json
@@ -2,7 +2,8 @@
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
- "packageManager": "npm"
+ "packageManager": "npm",
+ "analytics": "b939cba9-d047-4104-9335-993c60a34d95"
},
"newProjectRoot": "projects",
"projects": {
diff --git a/package-lock.json b/package-lock.json
index b7526a7..f4cd008 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -460,6 +460,7 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.2.tgz",
"integrity": "sha512-dOi7w0dsUCJ5ZFnXD2eR/8LWy9/XAzXuo9zU6zu7qP4vimjTQRs11IawnuC+jaAQtCFiySshzEPPsuAw9bPkOA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -476,6 +477,7 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.2.tgz",
"integrity": "sha512-Rs69yqT1M+l0DqAAZcGDt2TntKAPyldEViq3GQHbkM1W4f/hoRgBRsE6StxvP6wszW6VVHH3uQQdyeZV8Z4rpw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -489,6 +491,7 @@
"integrity": "sha512-+6lyvDV0rY1qbc9+rzFCBZDGCfJU0ah3p+4Tu0YYgKRbpbwvqj/O4cG1mLknEuQ2G61Y/tTKnTa4ng1XNtqVyw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/core": "7.28.4",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -521,6 +524,7 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.2.tgz",
"integrity": "sha512-jj2lYmwMKYY7tmZ7ml8rXJRKwkVMJamFIf6VQuIlSFK79Pmn6AeUhZwDlrAmK7sY9kakEKUmslSg0XLL3bfiyw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -565,6 +569,7 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.2.tgz",
"integrity": "sha512-Qygk215mRK2S1tvD6B5dy3ekMidGmmLktxr5i01YC8synHYcex7HK18JcWuCrFbY6NbCnHsMD3bYi0mwhag+Sg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -686,6 +691,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -1036,6 +1042,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -1079,6 +1086,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1795,6 +1803,7 @@
"integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@inquirer/checkbox": "^4.3.0",
"@inquirer/confirm": "^5.1.19",
@@ -3885,7 +3894,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@tufjs/canonical-json": {
"version": "2.0.0",
@@ -4384,6 +4394,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -4549,6 +4560,7 @@
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
@@ -5247,6 +5259,7 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -5951,6 +5964,7 @@
"integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.23",
"@asamuzakjp/dom-selector": "^6.7.4",
@@ -6051,6 +6065,7 @@
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cli-truncate": "^5.0.0",
"colorette": "^2.0.20",
@@ -7528,6 +7543,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -8152,7 +8168,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
+ "license": "0BSD",
+ "peer": true
},
"node_modules/tuf-js": {
"version": "4.0.0",
@@ -8190,6 +8207,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8322,6 +8340,7 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -8881,6 +8900,7 @@
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.0.15",
"@vitest/mocker": "4.0.15",
@@ -9318,6 +9338,7 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/app/app.html b/src/app/app.html
index e0118a1..67e7bd4 100644
--- a/src/app/app.html
+++ b/src/app/app.html
@@ -1,342 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Hello, {{ title() }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index dc39edb..d5f1333 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -1,3 +1,58 @@
import { Routes } from '@angular/router';
+import { authGuard, guestGuard, roleGuard } from './guards/auth.guard';
+import { UserRole } from './models/user.model';
-export const routes: Routes = [];
+export const routes: Routes = [
+ {
+ path: '',
+ redirectTo: '/login',
+ pathMatch: 'full'
+ },
+ {
+ path: 'login',
+ loadComponent: () => import('./components/login/login.component').then(m => m.LoginComponent),
+ canActivate: [guestGuard]
+ },
+ {
+ path: 'register',
+ loadComponent: () => import('./components/register/register.component').then(m => m.RegisterComponent),
+ canActivate: [guestGuard]
+ },
+ {
+ path: 'verify-otp',
+ loadComponent: () => import('./components/verify-otp/verify-otp.component').then(m => m.VerifyOtpComponent)
+ },
+ {
+ path: 'verify-email',
+ loadComponent: () => import('./components/verify-email/verify-email.component').then(m => m.VerifyEmailComponent)
+ },
+ {
+ path: 'admin/dashboard',
+ loadComponent: () => import('./components/dashboards/admin-dashboard.component').then(m => m.AdminDashboardComponent),
+ canActivate: [roleGuard([UserRole.Administrator])]
+ },
+ {
+ path: 'field-agent/dashboard',
+ loadComponent: () => import('./components/dashboards/field-agent-dashboard.component').then(m => m.FieldAgentDashboardComponent),
+ canActivate: [roleGuard([UserRole.FieldAgent])]
+ },
+ {
+ path: 'finance/dashboard',
+ loadComponent: () => import('./components/dashboards/finance-dashboard.component').then(m => m.FinanceDashboardComponent),
+ canActivate: [roleGuard([UserRole.FinanceUser])]
+ },
+ {
+ path: 'it-support/dashboard',
+ loadComponent: () => import('./components/dashboards/it-support-dashboard.component').then(m => m.ITSupportDashboardComponent),
+ canActivate: [roleGuard([UserRole.ITSupportUser])]
+ },
+ {
+ path: 'user/dashboard',
+ loadComponent: () => import('./components/dashboards/user-dashboard.component').then(m => m.UserDashboardComponent),
+ canActivate: [roleGuard([UserRole.User])]
+ },
+ {
+ path: '**',
+ redirectTo: '/login'
+ }
+];
diff --git a/src/app/components/dashboards/admin-dashboard.component.ts b/src/app/components/dashboards/admin-dashboard.component.ts
new file mode 100644
index 0000000..8814fbc
--- /dev/null
+++ b/src/app/components/dashboards/admin-dashboard.component.ts
@@ -0,0 +1,222 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { User } from '../../models/user.model';
+
+@Component({
+ selector: 'app-admin-dashboard',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!
+
Administrator
+
+
+
+
+
👥
+
User Management
+
Manage all system users and permissions
+
+ 125
+ Total Users
+
+
+
+
+
⚙️
+
System Settings
+
Configure system-wide settings and parameters
+
+ 15
+ Active Modules
+
+
+
+
+
📊
+
Analytics
+
View system analytics and reports
+
+ 98%
+ System Uptime
+
+
+
+
+
🔐
+
Security
+
Monitor security logs and access controls
+
+ 0
+ Security Alerts
+
+
+
+
+
💾
+
Database
+
Database management and backups
+
+ 2.4 GB
+ Storage Used
+
+
+
+
+
📧
+
Communications
+
Email templates and notifications
+
+ 450
+ Emails Sent Today
+
+
+
+
+ `,
+ styles: [`
+ .dashboard {
+ padding: 20px;
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ .dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+ }
+
+ .dashboard-header h1 {
+ margin: 0;
+ color: #333;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-danger {
+ background-color: #dc3545;
+ color: white;
+ }
+
+ .btn-danger:hover {
+ background-color: #c82333;
+ }
+
+ .welcome-section {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 30px;
+ border-radius: 10px;
+ margin-bottom: 30px;
+ }
+
+ .welcome-section h2 {
+ margin: 0 0 10px 0;
+ }
+
+ .role-badge {
+ display: inline-block;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .role-badge.admin {
+ background-color: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ }
+
+ .card {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+
+ .card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ }
+
+ .card-icon {
+ font-size: 48px;
+ margin-bottom: 15px;
+ }
+
+ .admin-icon {
+ filter: hue-rotate(250deg);
+ }
+
+ .card h3 {
+ margin: 0 0 10px 0;
+ color: #333;
+ }
+
+ .card p {
+ color: #666;
+ margin: 0 0 20px 0;
+ }
+
+ .card-stats {
+ display: flex;
+ flex-direction: column;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+ }
+
+ .stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: #667eea;
+ margin-bottom: 5px;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ }
+ `]
+})
+export class AdminDashboardComponent {
+ user = signal(null);
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {
+ this.user.set(this.authService.getCurrentUser());
+ }
+
+ logout(): void {
+ this.authService.logout();
+ }
+}
diff --git a/src/app/components/dashboards/field-agent-dashboard.component.ts b/src/app/components/dashboards/field-agent-dashboard.component.ts
new file mode 100644
index 0000000..83abdcd
--- /dev/null
+++ b/src/app/components/dashboards/field-agent-dashboard.component.ts
@@ -0,0 +1,218 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { User } from '../../models/user.model';
+
+@Component({
+ selector: 'app-field-agent-dashboard',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!
+
Field Agent
+
+
+
+
+
📍
+
Site Visits
+
Manage and track site visits
+
+ 12
+ Scheduled Today
+
+
+
+
+
📋
+
Sample Collection
+
Record and manage biological samples
+
+ 45
+ Samples This Week
+
+
+
+
+
📊
+
Field Reports
+
Submit and view field reports
+
+ 3
+ Pending Reports
+
+
+
+
+
🗺️
+
Route Planning
+
Optimize your field routes
+
+ 8
+ Locations Today
+
+
+
+
+
📸
+
Photo Documentation
+
Upload and manage site photos
+
+ 127
+ Photos This Month
+
+
+
+
+
⏱️
+
Time Tracking
+
Log field work hours
+
+ 32.5
+ Hours This Week
+
+
+
+
+ `,
+ styles: [`
+ .dashboard {
+ padding: 20px;
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ .dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+ }
+
+ .dashboard-header h1 {
+ margin: 0;
+ color: #333;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-danger {
+ background-color: #dc3545;
+ color: white;
+ }
+
+ .btn-danger:hover {
+ background-color: #c82333;
+ }
+
+ .welcome-section {
+ color: white;
+ padding: 30px;
+ border-radius: 10px;
+ margin-bottom: 30px;
+ }
+
+ .welcome-section.field-agent {
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+ }
+
+ .welcome-section h2 {
+ margin: 0 0 10px 0;
+ }
+
+ .role-badge {
+ display: inline-block;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ background-color: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ }
+
+ .card {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+
+ .card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ }
+
+ .card-icon {
+ font-size: 48px;
+ margin-bottom: 15px;
+ }
+
+ .card h3 {
+ margin: 0 0 10px 0;
+ color: #333;
+ }
+
+ .card p {
+ color: #666;
+ margin: 0 0 20px 0;
+ }
+
+ .card-stats {
+ display: flex;
+ flex-direction: column;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+ }
+
+ .stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: #11998e;
+ margin-bottom: 5px;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ }
+ `]
+})
+export class FieldAgentDashboardComponent {
+ user = signal(null);
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {
+ this.user.set(this.authService.getCurrentUser());
+ }
+
+ logout(): void {
+ this.authService.logout();
+ }
+}
diff --git a/src/app/components/dashboards/finance-dashboard.component.ts b/src/app/components/dashboards/finance-dashboard.component.ts
new file mode 100644
index 0000000..de7ef45
--- /dev/null
+++ b/src/app/components/dashboards/finance-dashboard.component.ts
@@ -0,0 +1,218 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { User } from '../../models/user.model';
+
+@Component({
+ selector: 'app-finance-dashboard',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!
+
Finance User
+
+
+
+
+
💰
+
Revenue
+
Track revenue and income
+
+ $125,430
+ This Month
+
+
+
+
+
💸
+
Expenses
+
Monitor expenses and costs
+
+ $45,200
+ This Month
+
+
+
+
+
📈
+
Profit Margin
+
View profit margins and trends
+
+ 64%
+ Current Margin
+
+
+
+
+
🧾
+
Invoices
+
Manage invoices and billing
+
+ 23
+ Pending Invoices
+
+
+
+
+
💳
+
Payments
+
Process and track payments
+
+ 15
+ Pending Payments
+
+
+
+
+
📊
+
Financial Reports
+
Generate financial reports
+
+ 8
+ Reports Generated
+
+
+
+
+ `,
+ styles: [`
+ .dashboard {
+ padding: 20px;
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ .dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+ }
+
+ .dashboard-header h1 {
+ margin: 0;
+ color: #333;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-danger {
+ background-color: #dc3545;
+ color: white;
+ }
+
+ .btn-danger:hover {
+ background-color: #c82333;
+ }
+
+ .welcome-section {
+ color: white;
+ padding: 30px;
+ border-radius: 10px;
+ margin-bottom: 30px;
+ }
+
+ .welcome-section.finance {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ }
+
+ .welcome-section h2 {
+ margin: 0 0 10px 0;
+ }
+
+ .role-badge {
+ display: inline-block;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ background-color: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ }
+
+ .card {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+
+ .card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ }
+
+ .card-icon {
+ font-size: 48px;
+ margin-bottom: 15px;
+ }
+
+ .card h3 {
+ margin: 0 0 10px 0;
+ color: #333;
+ }
+
+ .card p {
+ color: #666;
+ margin: 0 0 20px 0;
+ }
+
+ .card-stats {
+ display: flex;
+ flex-direction: column;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+ }
+
+ .stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: #f5576c;
+ margin-bottom: 5px;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ }
+ `]
+})
+export class FinanceDashboardComponent {
+ user = signal(null);
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {
+ this.user.set(this.authService.getCurrentUser());
+ }
+
+ logout(): void {
+ this.authService.logout();
+ }
+}
diff --git a/src/app/components/dashboards/it-support-dashboard.component.ts b/src/app/components/dashboards/it-support-dashboard.component.ts
new file mode 100644
index 0000000..0cdefdc
--- /dev/null
+++ b/src/app/components/dashboards/it-support-dashboard.component.ts
@@ -0,0 +1,218 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { User } from '../../models/user.model';
+
+@Component({
+ selector: 'app-it-support-dashboard',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!
+
IT Support User
+
+
+
+
+
🎫
+
Support Tickets
+
Manage support requests
+
+ 18
+ Open Tickets
+
+
+
+
+
💻
+
System Status
+
Monitor system health
+
+ 99.9%
+ Uptime
+
+
+
+
+
🔧
+
Maintenance
+
Schedule and track maintenance
+
+ 3
+ Scheduled Tasks
+
+
+
+
+
🖥️
+
Hardware
+
Manage hardware inventory
+
+ 85
+ Active Devices
+
+
+
+
+
📦
+
Software Licenses
+
Track software licenses
+
+ 42
+ Active Licenses
+
+
+
+
+
📞
+
Help Desk
+
User support and assistance
+
+ 127
+ Calls This Week
+
+
+
+
+ `,
+ styles: [`
+ .dashboard {
+ padding: 20px;
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ .dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+ }
+
+ .dashboard-header h1 {
+ margin: 0;
+ color: #333;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-danger {
+ background-color: #dc3545;
+ color: white;
+ }
+
+ .btn-danger:hover {
+ background-color: #c82333;
+ }
+
+ .welcome-section {
+ color: white;
+ padding: 30px;
+ border-radius: 10px;
+ margin-bottom: 30px;
+ }
+
+ .welcome-section.it-support {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ }
+
+ .welcome-section h2 {
+ margin: 0 0 10px 0;
+ }
+
+ .role-badge {
+ display: inline-block;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ background-color: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ }
+
+ .card {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+
+ .card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ }
+
+ .card-icon {
+ font-size: 48px;
+ margin-bottom: 15px;
+ }
+
+ .card h3 {
+ margin: 0 0 10px 0;
+ color: #333;
+ }
+
+ .card p {
+ color: #666;
+ margin: 0 0 20px 0;
+ }
+
+ .card-stats {
+ display: flex;
+ flex-direction: column;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+ }
+
+ .stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: #4facfe;
+ margin-bottom: 5px;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ }
+ `]
+})
+export class ITSupportDashboardComponent {
+ user = signal(null);
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {
+ this.user.set(this.authService.getCurrentUser());
+ }
+
+ logout(): void {
+ this.authService.logout();
+ }
+}
diff --git a/src/app/components/dashboards/user-dashboard.component.ts b/src/app/components/dashboards/user-dashboard.component.ts
new file mode 100644
index 0000000..814cec8
--- /dev/null
+++ b/src/app/components/dashboards/user-dashboard.component.ts
@@ -0,0 +1,218 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { User } from '../../models/user.model';
+
+@Component({
+ selector: 'app-user-dashboard',
+ standalone: true,
+ imports: [CommonModule],
+ template: `
+
+
+
+
+
Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!
+
User
+
+
+
+
+
📊
+
My Data
+
View your personal data and statistics
+
+ 24
+ Records
+
+
+
+
+
📝
+
My Submissions
+
Track your submissions
+
+ 8
+ Pending Review
+
+
+
+
+
📧
+
Messages
+
View your messages and notifications
+
+ 5
+ Unread Messages
+
+
+
+
+
⚙️
+
Settings
+
Manage your account settings
+
+ ✓
+ Profile Complete
+
+
+
+
+
📅
+
Calendar
+
View your schedule and events
+
+ 3
+ Events Today
+
+
+
+
+
📄
+
Documents
+
Access your documents and files
+
+ 42
+ Total Documents
+
+
+
+
+ `,
+ styles: [`
+ .dashboard {
+ padding: 20px;
+ max-width: 1400px;
+ margin: 0 auto;
+ }
+
+ .dashboard-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+ }
+
+ .dashboard-header h1 {
+ margin: 0;
+ color: #333;
+ }
+
+ .btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-danger {
+ background-color: #dc3545;
+ color: white;
+ }
+
+ .btn-danger:hover {
+ background-color: #c82333;
+ }
+
+ .welcome-section {
+ color: white;
+ padding: 30px;
+ border-radius: 10px;
+ margin-bottom: 30px;
+ }
+
+ .welcome-section.user {
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
+ }
+
+ .welcome-section h2 {
+ margin: 0 0 10px 0;
+ }
+
+ .role-badge {
+ display: inline-block;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 14px;
+ font-weight: 500;
+ background-color: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ }
+
+ .dashboard-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ }
+
+ .card {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+
+ .card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ }
+
+ .card-icon {
+ font-size: 48px;
+ margin-bottom: 15px;
+ }
+
+ .card h3 {
+ margin: 0 0 10px 0;
+ color: #333;
+ }
+
+ .card p {
+ color: #666;
+ margin: 0 0 20px 0;
+ }
+
+ .card-stats {
+ display: flex;
+ flex-direction: column;
+ padding-top: 15px;
+ border-top: 1px solid #eee;
+ }
+
+ .stat-number {
+ font-size: 28px;
+ font-weight: 700;
+ color: #fa709a;
+ margin-bottom: 5px;
+ }
+
+ .stat-label {
+ font-size: 12px;
+ color: #999;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ }
+ `]
+})
+export class UserDashboardComponent {
+ user = signal(null);
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {
+ this.user.set(this.authService.getCurrentUser());
+ }
+
+ logout(): void {
+ this.authService.logout();
+ }
+}
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts
new file mode 100644
index 0000000..e633178
--- /dev/null
+++ b/src/app/components/login/login.component.ts
@@ -0,0 +1,283 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router, RouterLink } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+
+@Component({
+ selector: 'app-login',
+ standalone: true,
+ imports: [CommonModule, FormsModule, RouterLink],
+ template: `
+
+
+
+
+ @if (errorMessage()) {
+
+ {{ errorMessage() }}
+
+ }
+
+ @if (successMessage()) {
+
+ {{ successMessage() }}
+
+ }
+
+
+
+
+
+
+
Demo Accounts:
+
+ - Admin: admin@biostream.com / admin123
+ - Field Agent: field@biostream.com / field123
+ - Finance: finance@biostream.com / finance123
+ - IT Support: it@biostream.com / it123
+ - User: user@biostream.com / user123
+
+
+
+
+ `,
+ styles: [`
+ .auth-container {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 20px;
+ }
+
+ .auth-card {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ padding: 40px;
+ max-width: 450px;
+ width: 100%;
+ }
+
+ .auth-header {
+ text-align: center;
+ margin-bottom: 30px;
+ }
+
+ .auth-header h1 {
+ color: #667eea;
+ margin: 0 0 10px 0;
+ font-size: 32px;
+ }
+
+ .auth-header h2 {
+ color: #333;
+ margin: 0;
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ .alert {
+ padding: 12px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+ }
+
+ .alert-error {
+ background-color: #fee;
+ color: #c33;
+ border: 1px solid #fcc;
+ }
+
+ .alert-success {
+ background-color: #efe;
+ color: #3c3;
+ border: 1px solid #cfc;
+ }
+
+ .form-group {
+ margin-bottom: 20px;
+ }
+
+ .form-group label {
+ display: block;
+ margin-bottom: 5px;
+ color: #555;
+ font-weight: 500;
+ }
+
+ .form-group input {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ box-sizing: border-box;
+ }
+
+ .form-group input:focus {
+ outline: none;
+ border-color: #667eea;
+ }
+
+ .form-group input:disabled {
+ background-color: #f5f5f5;
+ cursor: not-allowed;
+ }
+
+ .btn {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-primary {
+ background-color: #667eea;
+ color: white;
+ }
+
+ .btn-primary:hover:not(:disabled) {
+ background-color: #5568d3;
+ }
+
+ .btn-primary:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+ }
+
+ .btn-block {
+ width: 100%;
+ }
+
+ .auth-footer {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ .auth-footer p {
+ color: #666;
+ margin: 0;
+ }
+
+ .auth-footer a {
+ color: #667eea;
+ text-decoration: none;
+ font-weight: 500;
+ }
+
+ .auth-footer a:hover {
+ text-decoration: underline;
+ }
+
+ .demo-credentials {
+ margin-top: 30px;
+ padding: 15px;
+ background-color: #f8f9fa;
+ border-radius: 5px;
+ border: 1px solid #e0e0e0;
+ }
+
+ .demo-credentials h4 {
+ margin: 0 0 10px 0;
+ color: #555;
+ font-size: 14px;
+ }
+
+ .demo-credentials ul {
+ margin: 0;
+ padding-left: 20px;
+ font-size: 12px;
+ color: #666;
+ }
+
+ .demo-credentials li {
+ margin-bottom: 5px;
+ }
+ `]
+})
+export class LoginComponent {
+ email = '';
+ password = '';
+ loading = signal(false);
+ errorMessage = signal('');
+ successMessage = signal('');
+
+ constructor(
+ private authService: AuthService,
+ private router: Router
+ ) {}
+
+ onSubmit(): void {
+ this.loading.set(true);
+ this.errorMessage.set('');
+ this.successMessage.set('');
+
+ this.authService.login({ email: this.email, password: this.password }).subscribe({
+ next: (response) => {
+ this.loading.set(false);
+ if (response.requiresOTP) {
+ this.successMessage.set('OTP sent to your email. Check console for demo OTP.');
+ setTimeout(() => {
+ this.router.navigate(['/verify-otp'], { queryParams: { email: this.email } });
+ }, 2000);
+ } else {
+ this.router.navigate([this.authService.getRoleDashboardRoute(response.user.role)]);
+ }
+ },
+ error: (error) => {
+ this.loading.set(false);
+ this.errorMessage.set(error.message || 'Login failed. Please try again.');
+ }
+ });
+ }
+}
diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts
new file mode 100644
index 0000000..c12b06a
--- /dev/null
+++ b/src/app/components/register/register.component.ts
@@ -0,0 +1,323 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router, RouterLink } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { EmailService } from '../../services/email.service';
+import { UserRole } from '../../models/user.model';
+
+@Component({
+ selector: 'app-register',
+ standalone: true,
+ imports: [CommonModule, FormsModule, RouterLink],
+ template: `
+
+
+
+
+ @if (errorMessage()) {
+
+ {{ errorMessage() }}
+
+ }
+
+ @if (successMessage()) {
+
+ {{ successMessage() }}
+
+ }
+
+
+
+
+
+
+ `,
+ styles: [`
+ .auth-container {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 20px;
+ }
+
+ .auth-card {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ padding: 40px;
+ max-width: 500px;
+ width: 100%;
+ }
+
+ .auth-header {
+ text-align: center;
+ margin-bottom: 30px;
+ }
+
+ .auth-header h1 {
+ color: #667eea;
+ margin: 0 0 10px 0;
+ font-size: 32px;
+ }
+
+ .auth-header h2 {
+ color: #333;
+ margin: 0;
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ .alert {
+ padding: 12px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+ }
+
+ .alert-error {
+ background-color: #fee;
+ color: #c33;
+ border: 1px solid #fcc;
+ }
+
+ .alert-success {
+ background-color: #efe;
+ color: #3c3;
+ border: 1px solid #cfc;
+ }
+
+ .form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 15px;
+ }
+
+ .form-group {
+ margin-bottom: 20px;
+ }
+
+ .form-group label {
+ display: block;
+ margin-bottom: 5px;
+ color: #555;
+ font-weight: 500;
+ }
+
+ .form-group input,
+ .form-group select {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ box-sizing: border-box;
+ }
+
+ .form-group input:focus,
+ .form-group select:focus {
+ outline: none;
+ border-color: #667eea;
+ }
+
+ .form-group input:disabled,
+ .form-group select:disabled {
+ background-color: #f5f5f5;
+ cursor: not-allowed;
+ }
+
+ .btn {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-primary {
+ background-color: #667eea;
+ color: white;
+ }
+
+ .btn-primary:hover:not(:disabled) {
+ background-color: #5568d3;
+ }
+
+ .btn-primary:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+ }
+
+ .btn-block {
+ width: 100%;
+ }
+
+ .auth-footer {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ .auth-footer p {
+ color: #666;
+ margin: 0;
+ }
+
+ .auth-footer a {
+ color: #667eea;
+ text-decoration: none;
+ font-weight: 500;
+ }
+
+ .auth-footer a:hover {
+ text-decoration: underline;
+ }
+ `]
+})
+export class RegisterComponent {
+ firstName = '';
+ lastName = '';
+ email = '';
+ password = '';
+ role = '';
+ loading = signal(false);
+ errorMessage = signal('');
+ successMessage = signal('');
+
+ roles = [
+ { value: UserRole.User, label: 'User' },
+ { value: UserRole.Administrator, label: 'Administrator' },
+ { value: UserRole.FieldAgent, label: 'Field Agent' },
+ { value: UserRole.FinanceUser, label: 'Finance User' },
+ { value: UserRole.ITSupportUser, label: 'IT Support User' }
+ ];
+
+ constructor(
+ private authService: AuthService,
+ private emailService: EmailService,
+ private router: Router
+ ) {}
+
+ onSubmit(): void {
+ this.loading.set(true);
+ this.errorMessage.set('');
+ this.successMessage.set('');
+
+ this.authService.register({
+ email: this.email,
+ password: this.password,
+ firstName: this.firstName,
+ lastName: this.lastName,
+ role: this.role as UserRole
+ }).subscribe({
+ next: (response) => {
+ this.loading.set(false);
+ this.successMessage.set(response.message);
+
+ // Simulate sending verification email
+ const verificationLink = `${window.location.origin}/verify-email?token=${btoa(this.email + ':' + Date.now())}`;
+ this.emailService.sendVerificationEmail(this.email, verificationLink).subscribe();
+
+ // Redirect to login after 3 seconds
+ setTimeout(() => {
+ this.router.navigate(['/login']);
+ }, 3000);
+ },
+ error: (error) => {
+ this.loading.set(false);
+ this.errorMessage.set(error.message || 'Registration failed. Please try again.');
+ }
+ });
+ }
+}
diff --git a/src/app/components/verify-email/verify-email.component.ts b/src/app/components/verify-email/verify-email.component.ts
new file mode 100644
index 0000000..3cd12ab
--- /dev/null
+++ b/src/app/components/verify-email/verify-email.component.ts
@@ -0,0 +1,203 @@
+import { Component, signal, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router, ActivatedRoute, RouterLink } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+import { EmailService } from '../../services/email.service';
+
+@Component({
+ selector: 'app-verify-email',
+ standalone: true,
+ imports: [CommonModule, RouterLink],
+ template: `
+
+
+
+
+ @if (loading()) {
+
+
+
Verifying your email...
+
+ }
+
+ @if (errorMessage()) {
+
+
Verification Failed
+
{{ errorMessage() }}
+
+ }
+
+ @if (successMessage()) {
+
+
✓ Email Verified!
+
{{ successMessage() }}
+
+ }
+
+
+
+
+ `,
+ styles: [`
+ .auth-container {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 20px;
+ }
+
+ .auth-card {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ padding: 40px;
+ max-width: 450px;
+ width: 100%;
+ }
+
+ .auth-header {
+ text-align: center;
+ margin-bottom: 30px;
+ }
+
+ .auth-header h1 {
+ color: #667eea;
+ margin: 0 0 10px 0;
+ font-size: 32px;
+ }
+
+ .auth-header h2 {
+ color: #333;
+ margin: 0;
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ .loading-spinner {
+ text-align: center;
+ padding: 40px 0;
+ }
+
+ .spinner {
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #667eea;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 20px;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ .loading-spinner p {
+ color: #666;
+ margin: 0;
+ }
+
+ .alert {
+ padding: 20px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+ text-align: center;
+ }
+
+ .alert h3 {
+ margin: 0 0 10px 0;
+ font-size: 20px;
+ }
+
+ .alert p {
+ margin: 0;
+ }
+
+ .alert-error {
+ background-color: #fee;
+ color: #c33;
+ border: 1px solid #fcc;
+ }
+
+ .alert-success {
+ background-color: #efe;
+ color: #3c3;
+ border: 1px solid #cfc;
+ }
+
+ .auth-footer {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ .btn {
+ display: inline-block;
+ padding: 12px 24px;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ text-decoration: none;
+ transition: all 0.3s;
+ }
+
+ .btn-primary {
+ background-color: #667eea;
+ color: white;
+ }
+
+ .btn-primary:hover {
+ background-color: #5568d3;
+ }
+ `]
+})
+export class VerifyEmailComponent implements OnInit {
+ loading = signal(true);
+ errorMessage = signal('');
+ successMessage = signal('');
+
+ constructor(
+ private authService: AuthService,
+ private emailService: EmailService,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {}
+
+ ngOnInit(): void {
+ const token = this.route.snapshot.queryParams['token'];
+ if (!token) {
+ this.loading.set(false);
+ this.errorMessage.set('Invalid verification link');
+ return;
+ }
+
+ this.verifyEmail(token);
+ }
+
+ private verifyEmail(token: string): void {
+ this.authService.verifyEmail({ token }).subscribe({
+ next: (response) => {
+ this.loading.set(false);
+ this.successMessage.set(response.message);
+
+ // Auto-redirect to login after 3 seconds
+ setTimeout(() => {
+ this.router.navigate(['/login']);
+ }, 3000);
+ },
+ error: (error) => {
+ this.loading.set(false);
+ this.errorMessage.set(error.message || 'Email verification failed. The link may be invalid or expired.');
+ }
+ });
+ }
+}
diff --git a/src/app/components/verify-otp/verify-otp.component.ts b/src/app/components/verify-otp/verify-otp.component.ts
new file mode 100644
index 0000000..1c880ad
--- /dev/null
+++ b/src/app/components/verify-otp/verify-otp.component.ts
@@ -0,0 +1,332 @@
+import { Component, signal, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router, ActivatedRoute, RouterLink } from '@angular/router';
+import { AuthService } from '../../services/auth.service';
+
+@Component({
+ selector: 'app-verify-otp',
+ standalone: true,
+ imports: [CommonModule, FormsModule, RouterLink],
+ template: `
+
+
+
+
+ @if (errorMessage()) {
+
+ {{ errorMessage() }}
+
+ }
+
+ @if (successMessage()) {
+
+ {{ successMessage() }}
+
+ }
+
+
+
+
+
Didn't receive the code?
+
+
+
+
+
+
+ `,
+ styles: [`
+ .auth-container {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 20px;
+ }
+
+ .auth-card {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+ padding: 40px;
+ max-width: 450px;
+ width: 100%;
+ }
+
+ .auth-header {
+ text-align: center;
+ margin-bottom: 30px;
+ }
+
+ .auth-header h1 {
+ color: #667eea;
+ margin: 0 0 10px 0;
+ font-size: 32px;
+ }
+
+ .auth-header h2 {
+ color: #333;
+ margin: 0 0 10px 0;
+ font-size: 24px;
+ font-weight: 500;
+ }
+
+ .subtitle {
+ color: #666;
+ font-size: 14px;
+ margin: 0;
+ }
+
+ .alert {
+ padding: 12px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+ }
+
+ .alert-error {
+ background-color: #fee;
+ color: #c33;
+ border: 1px solid #fcc;
+ }
+
+ .alert-success {
+ background-color: #efe;
+ color: #3c3;
+ border: 1px solid #cfc;
+ }
+
+ .form-group {
+ margin-bottom: 20px;
+ }
+
+ .form-group label {
+ display: block;
+ margin-bottom: 5px;
+ color: #555;
+ font-weight: 500;
+ }
+
+ .otp-input {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 24px;
+ text-align: center;
+ letter-spacing: 10px;
+ font-weight: 600;
+ box-sizing: border-box;
+ }
+
+ .otp-input:focus {
+ outline: none;
+ border-color: #667eea;
+ }
+
+ .otp-input:disabled {
+ background-color: #f5f5f5;
+ cursor: not-allowed;
+ }
+
+ .btn {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .btn-primary {
+ background-color: #667eea;
+ color: white;
+ }
+
+ .btn-primary:hover:not(:disabled) {
+ background-color: #5568d3;
+ }
+
+ .btn-primary:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+ }
+
+ .btn-block {
+ width: 100%;
+ }
+
+ .btn-link {
+ background: none;
+ color: #667eea;
+ padding: 0;
+ font-size: 14px;
+ }
+
+ .btn-link:hover:not(:disabled) {
+ text-decoration: underline;
+ }
+
+ .btn-link:disabled {
+ color: #999;
+ cursor: not-allowed;
+ }
+
+ .resend-section {
+ text-align: center;
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid #eee;
+ }
+
+ .resend-section p {
+ color: #666;
+ margin: 0 0 10px 0;
+ font-size: 14px;
+ }
+
+ .auth-footer {
+ text-align: center;
+ margin-top: 20px;
+ }
+
+ .auth-footer p {
+ color: #666;
+ margin: 0;
+ }
+
+ .auth-footer a {
+ color: #667eea;
+ text-decoration: none;
+ font-weight: 500;
+ }
+
+ .auth-footer a:hover {
+ text-decoration: underline;
+ }
+ `]
+})
+export class VerifyOtpComponent implements OnInit {
+ email = signal('');
+ otp = '';
+ loading = signal(false);
+ resendLoading = signal(false);
+ resendCooldown = signal(0);
+ errorMessage = signal('');
+ successMessage = signal('');
+
+ constructor(
+ private authService: AuthService,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {}
+
+ ngOnInit(): void {
+ const email = this.route.snapshot.queryParams['email'];
+ if (!email) {
+ this.router.navigate(['/login']);
+ return;
+ }
+ this.email.set(email);
+ }
+
+ onSubmit(): void {
+ this.loading.set(true);
+ this.errorMessage.set('');
+ this.successMessage.set('');
+
+ this.authService.verifyOTP({ email: this.email(), otp: this.otp }).subscribe({
+ next: (response) => {
+ this.loading.set(false);
+ this.successMessage.set('OTP verified successfully! Redirecting...');
+ setTimeout(() => {
+ this.router.navigate([this.authService.getRoleDashboardRoute(response.user.role)]);
+ }, 1500);
+ },
+ error: (error) => {
+ this.loading.set(false);
+ this.errorMessage.set(error.message || 'OTP verification failed. Please try again.');
+ }
+ });
+ }
+
+ resendOTP(): void {
+ this.resendLoading.set(true);
+ this.errorMessage.set('');
+ this.successMessage.set('');
+
+ this.authService.resendOTP(this.email()).subscribe({
+ next: (response) => {
+ this.resendLoading.set(false);
+ this.successMessage.set(response.message);
+ this.startCooldown();
+ },
+ error: (error) => {
+ this.resendLoading.set(false);
+ this.errorMessage.set(error.message || 'Failed to resend OTP. Please try again.');
+ }
+ });
+ }
+
+ private startCooldown(): void {
+ this.resendCooldown.set(60);
+ const interval = setInterval(() => {
+ const current = this.resendCooldown();
+ if (current <= 1) {
+ clearInterval(interval);
+ this.resendCooldown.set(0);
+ } else {
+ this.resendCooldown.set(current - 1);
+ }
+ }, 1000);
+ }
+}
diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts
new file mode 100644
index 0000000..f875dbf
--- /dev/null
+++ b/src/app/guards/auth.guard.ts
@@ -0,0 +1,77 @@
+import { inject } from '@angular/core';
+import { Router, CanActivateFn, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { AuthService } from '../services/auth.service';
+import { UserRole } from '../models/user.model';
+
+export const authGuard: CanActivateFn = (
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+) => {
+ const authService = inject(AuthService);
+ const router = inject(Router);
+
+ if (!authService.isAuthenticated()) {
+ router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
+ return false;
+ }
+
+ // Check if route requires specific roles
+ const requiredRoles = route.data['roles'] as UserRole[] | undefined;
+
+ if (requiredRoles && requiredRoles.length > 0) {
+ if (!authService.hasAnyRole(requiredRoles)) {
+ // Redirect to user's appropriate dashboard
+ const user = authService.getCurrentUser();
+ if (user) {
+ router.navigate([authService.getRoleDashboardRoute(user.role)]);
+ } else {
+ router.navigate(['/unauthorized']);
+ }
+ return false;
+ }
+ }
+
+ return true;
+};
+
+export const guestGuard: CanActivateFn = (
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot
+) => {
+ const authService = inject(AuthService);
+ const router = inject(Router);
+
+ if (authService.isAuthenticated()) {
+ const user = authService.getCurrentUser();
+ if (user) {
+ router.navigate([authService.getRoleDashboardRoute(user.role)]);
+ }
+ return false;
+ }
+
+ return true;
+};
+
+export const roleGuard = (roles: UserRole[]): CanActivateFn => {
+ return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
+ const authService = inject(AuthService);
+ const router = inject(Router);
+
+ if (!authService.isAuthenticated()) {
+ router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
+ return false;
+ }
+
+ if (!authService.hasAnyRole(roles)) {
+ const user = authService.getCurrentUser();
+ if (user) {
+ router.navigate([authService.getRoleDashboardRoute(user.role)]);
+ } else {
+ router.navigate(['/unauthorized']);
+ }
+ return false;
+ }
+
+ return true;
+ };
+};
diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts
new file mode 100644
index 0000000..d055ee3
--- /dev/null
+++ b/src/app/models/user.model.ts
@@ -0,0 +1,55 @@
+export enum UserRole {
+ User = 'User',
+ Administrator = 'Administrator',
+ FieldAgent = 'FieldAgent',
+ FinanceUser = 'FinanceUser',
+ ITSupportUser = 'ITSupportUser'
+}
+
+export interface User {
+ id: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ role: UserRole;
+ isEmailVerified: boolean;
+ isActive: boolean;
+ createdAt: Date;
+ lastLogin?: Date;
+}
+
+export interface RegisterRequest {
+ email: string;
+ password: string;
+ firstName: string;
+ lastName: string;
+ role: UserRole;
+}
+
+export interface LoginRequest {
+ email: string;
+ password: string;
+}
+
+export interface LoginResponse {
+ user: User;
+ token: string;
+ requiresOTP: boolean;
+}
+
+export interface OTPVerificationRequest {
+ email: string;
+ otp: string;
+}
+
+export interface EmailVerificationRequest {
+ token: string;
+}
+
+export interface AuthState {
+ user: User | null;
+ token: string | null;
+ isAuthenticated: boolean;
+ requiresOTP: boolean;
+ pendingEmail?: string;
+}
diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts
new file mode 100644
index 0000000..227de21
--- /dev/null
+++ b/src/app/services/auth.service.ts
@@ -0,0 +1,376 @@
+import { Injectable, signal } from '@angular/core';
+import { Router } from '@angular/router';
+import { Observable, of, throwError, delay } from 'rxjs';
+import {
+ User,
+ UserRole,
+ RegisterRequest,
+ LoginRequest,
+ LoginResponse,
+ OTPVerificationRequest,
+ EmailVerificationRequest,
+ AuthState
+} from '../models/user.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuthService {
+ private authState = signal({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+ requiresOTP: false
+ });
+
+ // Mock OTP storage (in production, this would be server-side)
+ private otpStore = new Map();
+
+ // Mock user database
+ private users: Map = new Map();
+
+ constructor(private router: Router) {
+ this.loadAuthState();
+ this.initializeMockUsers();
+ }
+
+ private initializeMockUsers() {
+ // Create default users for each role
+ const defaultUsers = [
+ {
+ id: '1',
+ email: 'admin@biostream.com',
+ password: 'admin123',
+ firstName: 'Admin',
+ lastName: 'User',
+ role: UserRole.Administrator,
+ isEmailVerified: true,
+ isActive: true,
+ createdAt: new Date()
+ },
+ {
+ id: '2',
+ email: 'field@biostream.com',
+ password: 'field123',
+ firstName: 'Field',
+ lastName: 'Agent',
+ role: UserRole.FieldAgent,
+ isEmailVerified: true,
+ isActive: true,
+ createdAt: new Date()
+ },
+ {
+ id: '3',
+ email: 'finance@biostream.com',
+ password: 'finance123',
+ firstName: 'Finance',
+ lastName: 'User',
+ role: UserRole.FinanceUser,
+ isEmailVerified: true,
+ isActive: true,
+ createdAt: new Date()
+ },
+ {
+ id: '4',
+ email: 'it@biostream.com',
+ password: 'it123',
+ firstName: 'IT',
+ lastName: 'Support',
+ role: UserRole.ITSupportUser,
+ isEmailVerified: true,
+ isActive: true,
+ createdAt: new Date()
+ },
+ {
+ id: '5',
+ email: 'user@biostream.com',
+ password: 'user123',
+ firstName: 'Regular',
+ lastName: 'User',
+ role: UserRole.User,
+ isEmailVerified: true,
+ isActive: true,
+ createdAt: new Date()
+ }
+ ];
+
+ defaultUsers.forEach(user => {
+ this.users.set(user.email, user);
+ });
+ }
+
+ getAuthState() {
+ return this.authState.asReadonly();
+ }
+
+ getCurrentUser(): User | null {
+ return this.authState().user;
+ }
+
+ isAuthenticated(): boolean {
+ return this.authState().isAuthenticated;
+ }
+
+ hasRole(role: UserRole): boolean {
+ return this.authState().user?.role === role;
+ }
+
+ hasAnyRole(roles: UserRole[]): boolean {
+ const userRole = this.authState().user?.role;
+ return userRole ? roles.includes(userRole) : false;
+ }
+
+ register(request: RegisterRequest): Observable<{ message: string; email: string }> {
+ // Simulate API delay
+ return new Observable(observer => {
+ setTimeout(() => {
+ // Check if user already exists
+ if (this.users.has(request.email)) {
+ observer.error({ message: 'User already exists' });
+ return;
+ }
+
+ // Create new user
+ const newUser: User & { password: string } = {
+ id: Math.random().toString(36).substring(7),
+ email: request.email,
+ password: request.password,
+ firstName: request.firstName,
+ lastName: request.lastName,
+ role: request.role,
+ isEmailVerified: false,
+ isActive: false,
+ createdAt: new Date()
+ };
+
+ this.users.set(request.email, newUser);
+
+ // Generate verification token (in production, this would be a proper JWT)
+ const verificationToken = btoa(request.email + ':' + Date.now());
+
+ // Store token for verification (in production, this would be server-side)
+ localStorage.setItem(`verification_${verificationToken}`, request.email);
+
+ observer.next({
+ message: 'Registration successful. Please check your email for verification link.',
+ email: request.email
+ });
+ observer.complete();
+ }, 1000);
+ });
+ }
+
+ login(request: LoginRequest): Observable {
+ return new Observable(observer => {
+ setTimeout(() => {
+ const user = this.users.get(request.email);
+
+ if (!user || user.password !== request.password) {
+ observer.error({ message: 'Invalid email or password' });
+ return;
+ }
+
+ if (!user.isEmailVerified) {
+ observer.error({ message: 'Please verify your email before logging in' });
+ return;
+ }
+
+ if (!user.isActive) {
+ observer.error({ message: 'Your account is not active. Please contact support.' });
+ return;
+ }
+
+ // Generate OTP
+ const otp = this.generateOTP();
+ const expires = Date.now() + 5 * 60 * 1000; // 5 minutes
+ this.otpStore.set(request.email, { otp, expires });
+
+ console.log(`OTP for ${request.email}: ${otp}`); // In production, send via email
+
+ // Return response requiring OTP
+ const { password, ...userWithoutPassword } = user;
+ observer.next({
+ user: userWithoutPassword,
+ token: '',
+ requiresOTP: true
+ });
+
+ // Update auth state for OTP verification
+ this.authState.set({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+ requiresOTP: true,
+ pendingEmail: request.email
+ });
+
+ observer.complete();
+ }, 1000);
+ });
+ }
+
+ verifyOTP(request: OTPVerificationRequest): Observable {
+ return new Observable(observer => {
+ setTimeout(() => {
+ const storedOTP = this.otpStore.get(request.email);
+
+ if (!storedOTP) {
+ observer.error({ message: 'OTP not found. Please login again.' });
+ return;
+ }
+
+ if (Date.now() > storedOTP.expires) {
+ this.otpStore.delete(request.email);
+ observer.error({ message: 'OTP has expired. Please login again.' });
+ return;
+ }
+
+ if (storedOTP.otp !== request.otp) {
+ observer.error({ message: 'Invalid OTP' });
+ return;
+ }
+
+ // OTP verified successfully
+ this.otpStore.delete(request.email);
+ const user = this.users.get(request.email)!;
+
+ // Update last login
+ user.lastLogin = new Date();
+
+ // Generate token (in production, this would be a proper JWT)
+ const token = btoa(request.email + ':' + Date.now());
+
+ const { password, ...userWithoutPassword } = user;
+
+ // Update auth state
+ this.authState.set({
+ user: userWithoutPassword,
+ token,
+ isAuthenticated: true,
+ requiresOTP: false
+ });
+
+ this.saveAuthState();
+
+ observer.next({
+ user: userWithoutPassword,
+ token,
+ requiresOTP: false
+ });
+
+ observer.complete();
+ }, 1000);
+ });
+ }
+
+ verifyEmail(request: EmailVerificationRequest): Observable<{ message: string }> {
+ return new Observable(observer => {
+ setTimeout(() => {
+ const email = localStorage.getItem(`verification_${request.token}`);
+
+ if (!email) {
+ observer.error({ message: 'Invalid or expired verification token' });
+ return;
+ }
+
+ const user = this.users.get(email);
+
+ if (!user) {
+ observer.error({ message: 'User not found' });
+ return;
+ }
+
+ // Mark email as verified and activate user
+ user.isEmailVerified = true;
+ user.isActive = true;
+
+ // Clean up token
+ localStorage.removeItem(`verification_${request.token}`);
+
+ observer.next({ message: 'Email verified successfully. You can now login.' });
+ observer.complete();
+ }, 1000);
+ });
+ }
+
+ resendOTP(email: string): Observable<{ message: string }> {
+ return new Observable(observer => {
+ setTimeout(() => {
+ const user = this.users.get(email);
+
+ if (!user) {
+ observer.error({ message: 'User not found' });
+ return;
+ }
+
+ // Generate new OTP
+ const otp = this.generateOTP();
+ const expires = Date.now() + 5 * 60 * 1000;
+ this.otpStore.set(email, { otp, expires });
+
+ console.log(`New OTP for ${email}: ${otp}`); // In production, send via email
+
+ observer.next({ message: 'OTP has been resent to your email' });
+ observer.complete();
+ }, 1000);
+ });
+ }
+
+ logout(): void {
+ this.authState.set({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+ requiresOTP: false
+ });
+ this.clearAuthState();
+ this.router.navigate(['/login']);
+ }
+
+ private generateOTP(): string {
+ return Math.floor(100000 + Math.random() * 900000).toString();
+ }
+
+ private saveAuthState(): void {
+ const state = this.authState();
+ if (state.isAuthenticated && state.user && state.token) {
+ localStorage.setItem('auth_user', JSON.stringify(state.user));
+ localStorage.setItem('auth_token', state.token);
+ }
+ }
+
+ private loadAuthState(): void {
+ const userStr = localStorage.getItem('auth_user');
+ const token = localStorage.getItem('auth_token');
+
+ if (userStr && token) {
+ try {
+ const user = JSON.parse(userStr);
+ this.authState.set({
+ user,
+ token,
+ isAuthenticated: true,
+ requiresOTP: false
+ });
+ } catch (e) {
+ this.clearAuthState();
+ }
+ }
+ }
+
+ private clearAuthState(): void {
+ localStorage.removeItem('auth_user');
+ localStorage.removeItem('auth_token');
+ }
+
+ getRoleDashboardRoute(role: UserRole): string {
+ const routes: Record = {
+ [UserRole.Administrator]: '/admin/dashboard',
+ [UserRole.FieldAgent]: '/field-agent/dashboard',
+ [UserRole.FinanceUser]: '/finance/dashboard',
+ [UserRole.ITSupportUser]: '/it-support/dashboard',
+ [UserRole.User]: '/user/dashboard'
+ };
+ return routes[role];
+ }
+}
diff --git a/src/app/services/email.service.ts b/src/app/services/email.service.ts
new file mode 100644
index 0000000..01938eb
--- /dev/null
+++ b/src/app/services/email.service.ts
@@ -0,0 +1,116 @@
+import { Injectable } from '@angular/core';
+import { Observable, of, delay } from 'rxjs';
+
+export interface EmailOptions {
+ to: string;
+ subject: string;
+ body: string;
+ isHTML?: boolean;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EmailService {
+ // In production, this would integrate with a real email service (SendGrid, AWS SES, etc.)
+
+ sendEmail(options: EmailOptions): Observable<{ success: boolean; message: string }> {
+ console.log('=== EMAIL SENT ===');
+ console.log(`To: ${options.to}`);
+ console.log(`Subject: ${options.subject}`);
+ console.log(`Body:\n${options.body}`);
+ console.log('==================');
+
+ // Simulate email sending delay
+ return of({
+ success: true,
+ message: 'Email sent successfully'
+ }).pipe(delay(500));
+ }
+
+ sendOTPEmail(email: string, otp: string): Observable<{ success: boolean; message: string }> {
+ const body = `
+ BioStream - Your OTP Code
+ Hello,
+ Your One-Time Password (OTP) for logging into BioStream is:
+ ${otp}
+ This code will expire in 5 minutes.
+ If you didn't request this code, please ignore this email.
+
+ Best regards,
BioStream Team
+ `;
+
+ return this.sendEmail({
+ to: email,
+ subject: 'BioStream - Your OTP Code',
+ body,
+ isHTML: true
+ });
+ }
+
+ sendVerificationEmail(email: string, verificationLink: string): Observable<{ success: boolean; message: string }> {
+ const body = `
+ BioStream - Email Verification
+ Hello,
+ Thank you for registering with BioStream!
+ Please click the link below to verify your email address:
+ Verify Email
+ Or copy and paste this link into your browser:
+ ${verificationLink}
+ This link will expire in 24 hours.
+ If you didn't create an account, please ignore this email.
+
+ Best regards,
BioStream Team
+ `;
+
+ return this.sendEmail({
+ to: email,
+ subject: 'BioStream - Verify Your Email',
+ body,
+ isHTML: true
+ });
+ }
+
+ sendWelcomeEmail(email: string, firstName: string, role: string): Observable<{ success: boolean; message: string }> {
+ const body = `
+ Welcome to BioStream!
+ Hello ${firstName},
+ Your email has been verified successfully!
+ You are now registered as a ${role}.
+ You can now log in to your account and start using BioStream.
+ Login Now
+
+ Best regards,
BioStream Team
+ `;
+
+ return this.sendEmail({
+ to: email,
+ subject: 'Welcome to BioStream!',
+ body,
+ isHTML: true
+ });
+ }
+
+ sendPasswordResetEmail(email: string, resetLink: string): Observable<{ success: boolean; message: string }> {
+ const body = `
+ BioStream - Password Reset
+ Hello,
+ We received a request to reset your password.
+ Click the link below to reset your password:
+ Reset Password
+ Or copy and paste this link into your browser:
+ ${resetLink}
+ This link will expire in 1 hour.
+ If you didn't request a password reset, please ignore this email.
+
+ Best regards,
BioStream Team
+ `;
+
+ return this.sendEmail({
+ to: email,
+ subject: 'BioStream - Password Reset',
+ body,
+ isHTML: true
+ });
+ }
+}
diff --git a/src/styles.css b/src/styles.css
index 90d4ee0..cdaa2ae 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1 +1,14 @@
/* You can add global styles to this file, and also import other style files */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background-color: #f5f5f5;
+ color: #333;
+ line-height: 1.6;
+}