diff --git a/obj/Debug/Py-Detect.esproj.FileListAbsolute.txt b/obj/Debug/Py-Detect.esproj.FileListAbsolute.txt index f2427ac815576fb1f4e65512bdfc9c72c7320567..ec45a64874726f74ea5789e0b61e84f8c1b16c12 100644 --- a/obj/Debug/Py-Detect.esproj.FileListAbsolute.txt +++ b/obj/Debug/Py-Detect.esproj.FileListAbsolute.txt @@ -1 +1,2 @@ C:\Users\Admin\Desktop\Py-Detect\obj\Debug\Py-Detect.esproj.CoreCompileInputs.cache +C:\Users\Admin\Desktop\deployment-pydetect\Py-detect\obj\Debug\Py-Detect.esproj.CoreCompileInputs.cache diff --git a/obj/Debug/package.g.props b/obj/Debug/package.g.props index 2988bd8926d940d634e867e485aa80d96bdf2321..e5534d82bcfdf6810e53e1b569095e90ee50316d 100644 --- a/obj/Debug/package.g.props +++ b/obj/Debug/package.g.props @@ -17,6 +17,7 @@ ^16.1.0 ^16.1.0 ^16.1.0 + ^0.0.3 * ~7.8.0 ^2.3.0 diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 38f1b2d27e4b23d85d467ee54fd7029b94619142..81564d07b93af0b1b32c96f088027428675b3365 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,21 +1,26 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { HomepageComponent } from './homepage/homepage.component'; -import { PyDetectComponent } from './py-detect/py-detect.component'; import { InfopageComponent } from './infopage/infopage.component'; import { ValidationpageComponent } from './validationpage/validationpage.component'; import { RecordpageComponent } from './recordpage/recordpage.component'; import { CaseDetailsPageComponent } from './case-details-page/case-details-page.component'; const routes: Routes = [ - { path: '', component: HomepageComponent }, + { + path: '', + loadComponent: () => import('./homepage/homepage.component').then(m => m.HomepageComponent) + }, { path: 'infopage', component: InfopageComponent }, { path: 'infopage/:id', component: InfopageComponent }, - { path: 'py-detect', component: PyDetectComponent }, + { + path: 'py-detect', + loadComponent: () => import('./py-detect/py-detect.component').then(m => m.PyDetectComponent) + }, { path: 'validationpage', component: ValidationpageComponent }, { path: 'record', component: RecordpageComponent }, { path: 'case-details', component: CaseDetailsPageComponent }, { path: 'case-details/:id', component: CaseDetailsPageComponent }, + { path: '', redirectTo: '/case-detail', pathMatch: 'full' }, { path: 'auth/signin', loadComponent: () => diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f953279ca6a45ca5769c5cce9869f675e7f03478..d48de4ba875ffe1ed223790794ad91a3b0af8b0d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,8 +1,10 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { InfopageComponent } from './infopage/infopage.component'; import { AppRoutingModule } from './app-routing.module'; @@ -10,8 +12,8 @@ import { ValidationpageComponent } from './validationpage/validationpage.compone import { SignInComponent } from './homepage/sign-in/sign-in.component'; import { SignUpComponent } from './homepage/sign-up/sign-up.component'; import { RecordpageComponent } from './recordpage/recordpage.component'; -import { HomepageComponent } from './homepage/homepage.component'; import { CaseDetailsPageComponent } from './case-details-page/case-details-page.component'; +import { MatCardModule } from '@angular/material/card'; @NgModule({ declarations: [ @@ -23,14 +25,14 @@ import { CaseDetailsPageComponent } from './case-details-page/case-details-page. ], imports: [ BrowserModule, + BrowserAnimationsModule, ReactiveFormsModule, FormsModule, CommonModule, AppRoutingModule, HttpClientModule, - HomepageComponent, - SignInComponent, - SignUpComponent + RouterModule, + MatCardModule ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/auth.service.spec.ts b/src/app/auth.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1251cacf9dda3b3bd9dc5222583fafa110ffe8f --- /dev/null +++ b/src/app/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/auth.service.ts b/src/app/auth.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a65940a8fa9e9af39db314fe08f12349657cb96d --- /dev/null +++ b/src/app/auth.service.ts @@ -0,0 +1,50 @@ +// src/app/auth.service.ts +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' // This makes the service available globally in the app +}) +export class AuthService { + + constructor() { } + + // Sign-up method to handle user registration + signUp(name: string, role: string, email: string, password: string): Promise { + return new Promise((resolve) => { + // Simulating storing user data in localStorage + const user = { name, role, email, password }; + localStorage.setItem('user', JSON.stringify(user)); // Save user data in localStorage + localStorage.setItem('userRole', role); // Save only the role in localStorage (for later use) + resolve('User signed up'); + }); + } + + // Sign-in method to handle user login + signIn(email: string, password: string): Promise { + return new Promise((resolve, reject) => { + const user = JSON.parse(localStorage.getItem('user') || '{}'); + // Check if email and password match the stored data + if (user.email === email && user.password === password) { + resolve('User signed in'); + } else { + reject('Invalid credentials'); + } + }); + } + + // Helper methods to manage user session and check if the user is logged in + isLoggedIn(): boolean { + return !!localStorage.getItem('user'); + } + + // Get the user's role from localStorage + getUserRole(): string | null { + return localStorage.getItem('userRole'); + } + + // Logout function to clear the session + logout(): void { + localStorage.removeItem('user'); + localStorage.removeItem('userRole'); + } +} diff --git a/src/app/case-details-page/case-details-page.component.css b/src/app/case-details-page/case-details-page.component.css index c9736bdc5cce22f6ca03ede5cff0ba674f462c98..cc99704e87588fab109ab1eef58a6f018a54e339 100644 --- a/src/app/case-details-page/case-details-page.component.css +++ b/src/app/case-details-page/case-details-page.component.css @@ -1,5 +1,119 @@ @import '../recordpage/recordpage.component.css'; +body, html { + overflow: auto !important; +} + +body, main.content { + background: #f4f6fa; + min-height: 100vh; + margin: 0; + padding: 0; + font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif; +} + + +/* Modern UI header styles from infopage */ +.site-header { + background: #011329; + box-shadow: 0 2px 12px #38bdf844; + margin-bottom: 0; + position: relative; + z-index: 10; + padding-bottom: 0; +} + +.header-inner { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 18px 32px 0 32px; + position: relative; +} + +.logo-cluster { + display: flex; + align-items: center; + gap: 18px; +} + +.logo-img-header { + width: 54px; + height: 54px; + border-radius: 50%; + background: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.18); + padding: 4px; + margin-top: -6px; + margin-bottom: 1vh; +} + +.py-detect-title-header { + font-size: 2.1rem; + font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif; + font-weight: 900; + letter-spacing: 6px; + color: #38bdf8; + display: flex; + align-items: center; + gap: 2px; + margin-bottom: 1.5vh; +} + + .py-detect-title-header .py-letter.p { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.y { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-shape { + color: #e3f6ff; + background: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff; + border: 2px solid #23272b; + width: 18px; + height: 4px; + display: inline-block; + margin: 0 8px; + border-radius: 2px; + } + + .py-detect-title-header .py-letter.d { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.e { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.t { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.e2 { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.c { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.t2 { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .case-details-list { width: 100%; max-width: 1700px; @@ -143,13 +257,23 @@ hr { } .detail-row span { - color: #64748b; - font-weight: 500; + color: #1e293b; + font-weight: 700; + min-width: 180px; + font-size: 1.08em; + margin-right: 32px; + text-align: left; + display: inline-block; } .detail-row b { - color: #23272b; - font-weight: 500; + color: #2563eb; + font-weight: 700; + word-break: break-word; + font-size: 1.13em; + margin-left: 8px; + text-align: left; + display: inline-block; } .bold-value { @@ -172,18 +296,19 @@ hr { /* Modal for case details (match record page style) */ .modal-blur-overlay { position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; + inset: 0; + z-index: 199; + background: rgba(255,255,255,0.45); + backdrop-filter: blur(8px); + pointer-events: none; + display: block; } .modal { - background: #fefefe; - border-radius: 8px; - box-shadow: 0 4px 32px rgba(30, 41, 59, 0.12); + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 8px #0001, 0 1.5px 0 #e5e7eb; + border: 1.5px solid #e5e7eb; overflow: hidden; position: fixed; top: 50%; @@ -198,8 +323,9 @@ hr { } .modal-header { - background: #f5fafd; - border-bottom: 1px solid #e2e8f0; + background: #f8fafc; + border-radius: 10px 10px 0 0; + border-bottom: 1.5px solid #e5e7eb; padding: 16px; display: flex; justify-content: space-between; @@ -218,11 +344,13 @@ hr { background: #fff; flex: 1; overflow-y: auto; + color: #23272b; } .modal-footer { - background: #f5fafd; - border-top: 1px solid #e2e8f0; + background: #f8fafc; + border-radius: 0 0 10px 10px; + border-top: 1.5px solid #e5e7eb; padding: 12px; text-align: right; } @@ -242,47 +370,47 @@ hr { background: #34b3e0; } -@keyframes fadeIn { - from { - opacity: 0; - transform: translate(-50%, -48%); - } - to { - opacity: 1; - transform: translate(-50%, -50%); - } -} - -.active-cases-label { - font-size: 1.8rem; - font-weight: 800; - color: #38bdf8; - margin-bottom: 16px; - position: relative; - text-align: left; -} - .actions { display: flex; flex-direction: row; justify-content: center; align-items: center; - gap: 16px; + gap: 18px; flex-wrap: nowrap; width: 100%; } -.actions .btn { - min-width: 150px; - max-width: 200px; - padding: 12px 20px; +.btn.view { + background: #64748b; + color: #fff; + border: none; + border-radius: 12px; + font-weight: 600; font-size: 1.08rem; - white-space: nowrap; - border-radius: 8px; - box-sizing: border-box; - overflow: visible; - text-overflow: unset; - margin: 0 2px; + padding: 12px 32px; + box-shadow: 0 2px 8px #2563eb22; + transition: background 0.2s, box-shadow 0.2s; +} +.btn.view:hover { + background: #1d4ed8; + box-shadow: 0 4px 16px #2563eb44; +} + +.btn.edit { + background: #8b5cf6; + color: #fff; + border: none; + border-radius: 12px; + font-weight: 600; + font-size: 1.08rem; + padding: 12px 32px; + box-shadow: 0 2px 8px #8b5cf622; + transition: background 0.2s, box-shadow 0.2s; + margin-left: 0; +} +.btn.edit:hover { + background: #7c3aed; + box-shadow: 0 4px 16px #8b5cf644; } .actions-col { @@ -292,11 +420,20 @@ hr { .records th.actions-col, .records td.actions { - width: 28% !important; - min-width: 260px; + width: 24% !important; + min-width: 313px; max-width: 320px; } +.next-action-col { + text-align: left; + min-width: 120px; + padding-left: 0; + padding-right: 0; + font-weight: 500; + color: #2563eb; +} + @media (max-width: 700px) { .actions .btn { min-width: 120px; @@ -312,19 +449,422 @@ hr { } } -/* Make all table columns equal width */ -.records { - table-layout: fixed; +@media (max-width: 900px) { + .btn.view, .btn.edit { + padding: 10px 16px; + font-size: 1rem; + } + .records th.actions-col, + .records td.actions { + min-width: 120px; + max-width: 180px; + width: 36% !important; + } +} + +/* table layout for case details page */ +.record-table { width: 100%; + table-layout: auto; + border-collapse: collapse; + overflow-x: hidden; + box-sizing: border-box; } -.records th, -.records td { - width: 16.66%; /* 6 columns, 100/6 = 16.66% */ - text-align: center; + +.record-table th, .record-table td { + padding-left: 12px; + padding-right: 12px; + word-break: break-word; + white-space: normal; + box-sizing: border-box; + border-bottom: 1px solid #e5e7eb; /* consistent row border */ +} + +.record-table tr:last-child td { + border-bottom: none; +} + +th.actions, td.actions { + text-align: left; + padding-left: 0; + padding-right: 0; +} + +td.actions { vertical-align: middle; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } +.icon-btn.view { + margin: 0; + padding: 0; + background: none; + border: none; + cursor: pointer; + color: #2563eb; + font-size: 1.4em; + display: flex; + align-items: center; + justify-content: flex-start; +} + +.custom-active-cases-label { + font-weight: 800; + color: #2196f3; + font-size: 2rem; + margin-bottom: 18px; + margin-top: 0; + text-align: left; + position: relative; + right: 32vw; +} + +.filter-bar select, +.filter-bar .filter-date { + padding: 6px 18px; + border-radius: 6px; + border: 1.5px solid #cbd5e1; + font-size: 1em; + color: #2563eb; + background: #fff; + font-weight: 600; + outline: none; + transition: border 0.15s; + min-width: 160px; + margin-right: 4px; +} +.filter-bar select:focus, +.filter-bar .filter-date:focus { + border: 1.5px solid #38bdf8; +} +.date-group { + display: inline-flex; + align-items: center; + font-family: inherit; + color: #a86a00; + font-size: 1.08em; + font-weight: 500; + margin-right: 8px; + gap: 2px; +} +.date-label { + margin: 0 4px 0 4px; + display: inline-flex; + align-items: center; + gap: 2px; +} +.calendar-ico { + font-size: 1.1em; + margin-left: 2px; + vertical-align: middle; +} +.filter-date { + border: none; + background: transparent; + color: #a86a00; + font-size: 1em; + font-weight: 600; + outline: none; + min-width: 90px; + margin-left: 2px; +} +.filter-date::-webkit-input-placeholder { color: #a86a00; } +.filter-date:-moz-placeholder { color: #a86a00; } +.filter-date::-moz-placeholder { color: #a86a00; } +.filter-date:-ms-input-placeholder { color: #a86a00; } + +.fullpage-details { + width: 100vw; + min-height: 100vh; + background: #f8fafc; + padding: 32px 0 32px 0; + position: relative; + z-index: 1; +} +.details-content { + max-width: 1200px; + margin: 0 auto; + background: #fff; + border-radius: 18px; + box-shadow: 0 8px 32px rgba(30,41,59,0.12); + padding: 32px 48px; +} +.btn.back-btn { + margin: 0 0 24px 0; + background: #64748b; + color: #fff; + border-radius: 8px; + font-weight: 600; + font-size: 1.1rem; + padding: 10px 28px; + border: none; + cursor: pointer; +} + +.fullpage-popup-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + width: 100vw; + height: 100vh; + background: #f8fafc; + z-index: 9999; + display: flex; + align-items: flex-start; + justify-content: center; + overflow-y: auto; + overscroll-behavior: contain; +} +body.popup-open { + overflow: hidden !important; +} + +.fullpage-popup-content { + background: #fff; + border-radius: 18px; + box-shadow: 0 8px 32px rgba(30,41,59,0.18); + margin: 40px 0; + max-width: 1200px; + width: 90vw; + min-height: 80vh; + padding: 32px 48px; + position: relative; +} + +/* --- Section Card Styles --- */ +.details-section-card { + background: #fff; + border-radius: 22px; + box-shadow: 0 4px 24px rgba(30,41,59,0.10); + border-left: 6px solid #38bdf8; + border-right: 2px solid #e0f2fe; + padding: 32px 36px 32px 36px; + margin-bottom: 0; + margin-top: 32px; + animation: fadeInUp 0.6s cubic-bezier(.23,1.01,.32,1) both; +} +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(32px); } + to { opacity: 1; transform: translateY(0); } +} + +.section-title { + font-size: 1.5rem; + font-weight: 700; + color: #2563eb; + margin-bottom: 18px; + letter-spacing: 0.5px; +} + +.subgroup-pills { + display: flex; + gap: 18px; + margin-bottom: 28px; + flex-wrap: wrap; +} +.subgroup-pills button { + background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%); + color: #fff; + font-weight: 700; + border: none; + border-radius: 22px; + padding: 10px 32px; + font-size: 1.08em; + box-shadow: 0 2px 12px rgba(56,189,248,0.13); + cursor: pointer; + transition: background 0.18s, box-shadow 0.18s, transform 0.18s; + outline: none; + margin-bottom: 4px; +} +.subgroup-pills button.active, +.subgroup-pills button:focus { + background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%); + box-shadow: 0 4px 16px rgba(56,189,248,0.18); + transform: translateY(-2px) scale(1.04); +} + +.fields-table-2col { + display: flex; + flex-direction: row; + gap: 0; + margin-top: 8px; + margin-bottom: 8px; +} +.fields-col { + display: flex; + flex-direction: column; + gap: 6px; +} +.fields-col-labels { + min-width: 260px; + text-align: left; +} +.fields-col-values { + min-width: 220px; + text-align: right; +} +.field-label { + color: #22223b; + font-weight: 500; + font-size: 1.08em; + padding: 2px 0; +} +.field-value { + color: #22223b; + font-weight: 700; + font-size: 1.08em; + padding: 2px 0; + letter-spacing: 0.5px; +} + +/* --- Card Container and Title --- */ +.case-details-title { + font-size: 2.2rem; + font-weight: 700; + color: #22223b; + margin-bottom: 32px; + margin-left: 8px; + display: flex; + align-items: center; + justify-content: space-between; + position: relative; +} + +.btn.close-btn { + background: none; + color: #64748b; + border: none; + font-size: 2.2rem; + font-weight: 700; + cursor: pointer; + position: absolute; + right: 0; + top: 0; + line-height: 1; + padding: 0 16px; + transition: color 0.15s; +} +.btn.close-btn:hover { + color: #2563eb; +} + +.btn.close-btn-bottom { + position: absolute; + right: 32px; + top: 32px; + background: #2563eb; + color: #fff; + border: none; + border-radius: 50%; + width: 48px; + height: 48px; + font-size: 2.2rem; + font-weight: 700; + box-shadow: 0 4px 16px rgba(56,189,248,0.18); + cursor: pointer; + z-index: 10001; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.18s, color 0.18s, box-shadow 0.18s; +} + .btn.close-btn-bottom:hover { + background: #38bdf8; + color: #22223b; + } + +.progress-col { + text-align: left; + min-width: 90px; + padding-left: 0; + padding-right: 0; +} + +.progress-dot { + width: 18px; + height: 18px; + border-radius: 50%; + display: inline-block; + margin-right: 6px; + vertical-align: middle; + box-shadow: 0 2px 8px rgba(56,189,248,0.10); +} +.progress-dot.blue { + background: linear-gradient(135deg, #2563eb 0%, #38bdf8 100%); +} +.progress-dot.green { + background: linear-gradient(135deg, #34d399 0%, #6ee7b7 100%); +} +.progress-check { + font-size: 1.2em; + color: #34d399; + background: #d1fae5; + border-radius: 4px; + padding: 2px 4px; + display: inline-block; + margin-right: 6px; + vertical-align: middle; +} +.progress-value { + font-weight: 700; + font-size: 1em; + color: #22223b; + vertical-align: middle; +} + +.priority-pill { + display: inline-flex; + align-items: center; + font-weight: bold; + font-size: 0.98em; + padding: 2px 12px 2px 8px; + border-radius: 16px; + margin-right: 2px; + letter-spacing: 0.02em; +} +.priority-high { + background: #fee2e2; + color: #b91c1c; +} +.priority-medium { + background: #fef9c3; + color: #ca8a04; +} +.priority-low { + background: #dcfce7; + color: #15803d; +} + +.evidence-upload-section { + margin-top: 32px; + padding: 16px; + background: #f3f4f6; + border-radius: 12px; + box-shadow: 0 1px 4px rgba(0,0,0,0.04); +} +.evidence-upload-section h3 { + margin-bottom: 12px; + font-size: 1.1em; + color: #2563eb; +} +.evidence-list { + margin-top: 10px; +} +.evidence-file { + display: flex; + align-items: center; + font-size: 0.98em; + margin-bottom: 6px; + color: #374151; +} +.evidence-file i { + margin-right: 8px; + color: #2563eb; +} + + + + + + + + diff --git a/src/app/case-details-page/case-details-page.component.html b/src/app/case-details-page/case-details-page.component.html index a94b1af231370cf33607d7d9cde3821ab2ecbe34..098f702747048a323c6321323f0e5285b62cda9e 100644 --- a/src/app/case-details-page/case-details-page.component.html +++ b/src/app/case-details-page/case-details-page.component.html @@ -1,107 +1,234 @@ -
-
- - - - - - - -
-
- -
- PyDetect Logo -
- P - Y - - D - E - T - E - C - T + + -
+
+
+
+ Your Assigned Cases +
+
+ +
+
-
-
-
Welcome User 👮
-
Welcome, {{ username }}👮
-
Your Active Cases
-
- - - - - - - - - - - - - - - - - - - - - - - - -
Case IDCrimeDate & TimeLocationStatusActions
{{ c.caseId || '—' }}{{ c.crime || '—' }}{{ c.dateTime ? (c.dateTime | date:'yyyy-MM-dd HH:mm') : '—' }}{{ c.police.address || '—' }} - - {{ c.status || '—' }} - - - - -
No records found.
+
+
+
Total Cases
+
{{ totalCases }}
+
+
Open
+
{{ openCases }}
+
+
+
Closed
+
{{ closedCases }}
+
+
+
Pending Review
+
{{ reviewCases }}
+
+
- - - - - - -
+
+ diff --git a/src/app/infopage/infopage.component.ts b/src/app/infopage/infopage.component.ts index 4d5b259a5a4ca394cef1a286b181f31ba9c0f9ef..9f5c3a15c6540d1e744dc187f40b43efdc753566 100644 --- a/src/app/infopage/infopage.component.ts +++ b/src/app/infopage/infopage.component.ts @@ -1,223 +1,1145 @@ -import { Component, OnInit } from '@angular/core'; -import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; -import { CaseStoreService, PoliceCase } from '../case-store.service'; - -type CrimeType = 'Theft' | 'Assault' | 'Fraud' | 'Murder' | 'Cybercrime' | 'Other'; -type Gender = 'Male' | 'Female' | 'Other'; -type CaseStatus = 'Open' | 'Under Investigation' | 'Closed'; +import { Component, HostListener, ElementRef, ViewChild, AfterViewInit, OnInit, OnDestroy } from '@angular/core'; +import { trigger, state, style, transition, animate } from '@angular/animations'; +import { Subject, debounceTime, takeUntil } from 'rxjs'; +import { Router } from '@angular/router'; +import { CaseStoreService } from '../case-store.service'; @Component({ selector: 'app-infopage', templateUrl: './infopage.component.html', - styleUrls: ['./infopage.component.css'] + styleUrls: ['./infopage.component.css'], + animations: [ + // Simple card animation + trigger('cardSlide', [ + transition(':enter', [ + style({ transform: 'translateY(20px)', opacity: 0 }), + animate('300ms ease-out', + style({ transform: 'translateY(0)', opacity: 1 })) + ]) + ]), + // Field animation + trigger('fieldAnimation', [ + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(10px)' }), + animate('200ms ease-out', + style({ opacity: 1, transform: 'translateY(0)' })) + ]) + ]), + // Simple fade animation + trigger('fadeIn', [ + transition(':enter', [ + style({ opacity: 0 }), + animate('200ms ease-in', style({ opacity: 1 })) + ]), + transition(':leave', [ + animate('150ms ease-out', style({ opacity: 0 })) + ]) + ]), + // Help animation + trigger('helpAnimation', [ + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(-10px)' }), + animate('200ms ease-out', + style({ opacity: 1, transform: 'translateY(0)' })) + ]), + transition(':leave', [ + animate('150ms ease-in', + style({ opacity: 0, transform: 'translateY(-10px)' })) + ]) + ]) + ] }) -export class InfopageComponent implements OnInit { - step = 1; - - crimeTypes: CrimeType[] = ['Theft', 'Assault', 'Fraud', 'Murder', 'Cybercrime', 'Other']; - genders: Gender[] = ['Male', 'Female', 'Other']; - statuses: CaseStatus[] = ['Open', 'Under Investigation', 'Closed']; - - // If navigating here via Edit, we store the index to update. - private editIndex: number | null = null; - - form: FormGroup = this.fb.group({ - crime: this.fb.group({ - caseId: [''], - crimeType: ['', Validators.required], - dateTime: ['', Validators.required], - location: ['', Validators.required], - reportedByName: [''], - reportedByContact: [''], - description: ['', [Validators.required, Validators.minLength(10)]], - }), - - suspect: this.fb.group({ - suspectId: [''], - fullName: ['', Validators.required], - alias: [''], - dob: [''], - age: ['', [Validators.min(0)]], - gender: ['Male' as Gender, Validators.required], - address: [''], - knownLocations: this.fb.array([]), - contact: this.fb.group({ - phone: [''], - email: ['', Validators.email], - }), - physical: this.fb.group({ - heightCm: [''], - weightKg: [''], - build: [''], - hairColor: [''], - eyeColor: [''], - marks: [''], - }), - photo: [null], // File - }), - - notes: this.fb.group({ - initialFindings: [''], - officerInCharge: [''], - status: ['Open' as CaseStatus, Validators.required], - attachments: this.fb.array([]), // Files - }), - }); - - constructor( - private fb: FormBuilder, - private router: Router, - private route: ActivatedRoute, - private caseStore: CaseStoreService - ) { } - - // ----------------- lifecycle ----------------- +export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy { + showRemarkModal: boolean = false; + showSubmitPopup: boolean = false; + constructor(private router: Router, private caseStore: CaseStoreService) {} + // Core state + currentSection: 'crime' | 'suspect' | 'notes' = 'crime'; + currentSubgroup: string = 'Identification & Timing'; + showHelpFor: string | null = null; + + // UI state + isAutoSaving: boolean = false; + autoSaveStatus: string = 'Saved'; + isDragOver: boolean = false; + + // Card state + isCardMinimized = { + primary: false, + secondary: false, + tertiary: false + }; + + // Form data and validation + formData: Record = {}; + fieldValidation: Record = {}; + completedFields: Set = new Set(); + completedSubgroups: Set = new Set(); + completedSections: Set = new Set(); + + // Subjects for reactive programming + private destroy$ = new Subject(); + private autoSave$ = new Subject(); + + // Constants - Reverted to original values except for Identification & Timing + readonly sectionKeys: ('crime' | 'suspect' | 'notes')[] = ['crime', 'suspect', 'notes']; + readonly maxFieldsPerCard = 8; // Reverted to original + readonly maxFieldsPerSecondaryCard = 8; // Reverted to original + readonly maxFieldsPerCardIdentificationTiming = 6; // Special for Identification & Timing + readonly maxFieldsPerSecondaryCardIdentificationTiming = 6; // Special for Identification & Timing + + @ViewChild('formCard1') formCard1!: ElementRef; + @ViewChild('formCard2') formCard2!: ElementRef; + @ViewChild('formCard3') formCard3!: ElementRef; + + // Enhanced field definitions with validation rules + readonly requiredFields = new Set([ + 'Case ID', 'Crime Type', 'Date & Time (Entry)', 'Location', 'Suspect Name', 'Age', 'Gender', + 'FIR / Ref #', 'Case Category', 'Occurred From', 'Country', 'State', 'District', + 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Case Status', 'Investigating Officer' + ]); + + readonly compactFields = new Set([ + 'Age', 'Gender', 'Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color', + 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count', 'Case Priority', + 'Photos / Video?', 'CCTV Present?', 'Arrest Made', 'risk Level', 'Confidentiality' + ]); + + readonly numericFields = new Set([ + 'Age', 'Height (cm)', 'Weight (kg)', 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count' + ]); + + // File type configurations + readonly fileTypeConfig: Record = { + 'Photo Upload': 'image/*', + 'Evidence Photos': 'image/*', + 'Evidence Videos': 'video/*', + 'Evidence Documents': '.pdf,.doc,.docx,.txt', + 'Evidence Files': '*', + 'Upload Evidence Files': '*', + 'Digital Evidence': '*' + }; + + // Track selected values (cascading dropdown logic) + selectedValues: Record = {}; + + // Date field groups + dateTimeFields = new Set(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']); + dateFields = new Set(['Follow-up Date', 'Next Hearing Date']); + + // Country/State/District data + countries = ['India']; + indiaStates = [ + 'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chhattisgarh', 'Goa', 'Gujarat', + 'Haryana', 'Himachal Pradesh', 'Jharkhand', 'Karnataka', 'Kerala', 'Madhya Pradesh', + 'Maharashtra', 'Manipur', 'Meghalaya', 'Mizoram', 'Nagaland', 'Odisha', 'Punjab', + 'Rajasthan', 'Sikkim', 'Tamil Nadu', 'Telangana', 'Tripura', 'Uttar Pradesh', + 'Uttarakhand', 'West Bengal' + ]; + + tamilNaduDistricts = [ + 'Ariyalur', 'Chengalpattu', 'Chennai', 'Coimbatore', 'Cuddalore', 'Dharmapuri', 'Dindigul', + 'Erode', 'Kallakurichi', 'Kanchipuram', 'Kanyakumari', 'Karur', 'Krishnagiri', 'Madurai', + 'Mayiladuthurai', 'Nagapattinam', 'Namakkal', 'Nilgiris', 'Perambalur', 'Pudukkottai', + 'Ramanathapuram', 'Ranipet', 'Salem', 'Sivaganga', 'Tenkasi', 'Thanjavur', 'Theni', + 'Thoothukudi (Tuticorin)', 'Tiruchirappalli', 'Tirunelveli', 'Tirupathur', 'Tiruppur', + 'Tiruvallur', 'Tiruvannamalai', 'Tiruvarur', 'Vellore', 'Viluppuram', 'Virudhunagar' + ]; + + // Enhanced select options - Added missing field options + selectOptions: Record = { + 'Crime Type': ['Theft', 'Assault', 'Homicide', 'Cybercrime', 'Fraud', 'Narcotics', 'Arson', 'Kidnapping', 'General', 'Other'], + 'Case Category': ['Property', 'Violent', 'Cyber', 'Financial', 'Public Order', 'Narcotics', 'Organized', 'General', 'Other'], + 'Number of Victims': ['0', '1', '2', '3', '4', '5+'], + 'Jurisdiction / PS': ['Central PS', 'East Division', 'West Division', 'Rural Unit', 'Cyber Cell', 'General'], + 'Scene Type': ['Residential', 'Commercial', 'Public Space', 'Vehicle', 'Rural', 'Online', 'General', 'Other'], + 'Witness Count': ['0', '1', '2', '3', '4', '5+'], + 'Victim Summary': ['Stable', 'Injured', 'Critical', 'Deceased', 'Unknown'], + 'Suspected Offender Known?': ['Yes', 'No', 'Unknown'], + 'Offence Category': ['Minor', 'Serious', 'Organized', 'Cyber', 'Financial', 'Violent', 'General', 'Other'], + 'Suspected Motive': ['Financial Gain', 'Revenge', 'Jealousy', 'Ideological', 'Political', 'Personal Dispute', 'Unknown', 'General', 'Other'], + 'Confirmed Motive': ['Financial Gain', 'Revenge', 'Jealousy', 'Ideological', 'Political', 'Personal Dispute', 'Unknown', 'General', 'Other'], + 'Weapon Involved': ['None', 'Knife', 'Firearm', 'Blunt Object', 'Explosive', 'Chemical', 'Other', 'Unknown', 'General'], + 'Property Loss / Damage': ['None', 'Minor', 'Moderate', 'Major', 'Severe', 'Unknown'], + 'Photos / Video?': ['Yes', 'No'], + 'CCTV Present?': ['Yes', 'No'], + 'Scene Condition': ['Intact', 'Disturbed', 'Contaminated', 'Secured', 'Compromised', 'General'], + 'Chain of Custody?': ['Initiated', 'Ongoing', 'Complete', 'Not Started'], + 'Forensic Tests Required': ['None', 'DNA', 'Fingerprints', 'Ballistics', 'Toxicology', 'Digital Forensics', 'Trace', 'General', 'Other'], + 'Arrest Made': ['Yes', 'No'], + 'riskLevel': ['Low', 'Medium', 'High', 'Critical'], + 'Confidentiality': ['Internal', 'Restricted', 'Sensitive', 'Sealed'], + 'Initial Actions Taken': ['Scene Secured', 'Medical Aid', 'Evidence Logged', 'Witness Statements', 'Suspect Detained', 'General', 'Other'], + 'Case Status': ['Open', 'Active', 'Suspended', 'Closed', 'Archived'], + 'Case Priority': ['Low', 'Normal', 'High', 'Urgent', 'Critical'], + 'Gender': ['Male', 'Female', 'Other'], + 'Nationality': ['India'], + 'Languages': ['English', 'Hindi', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Bengali', 'Marathi', 'Gujarati', 'Other'], + 'Build': ['Slim', 'Average', 'Athletic', 'Heavy', 'Obese'], + 'Hair Color': ['Black', 'Brown', 'Blonde', 'Red', 'Grey', 'White', 'Dyed / Other', 'Unknown'], + 'Eye Color': ['Brown', 'Blue', 'Green', 'Hazel', 'Grey', 'Black', 'Unknown'], + 'Employment': ['Employed', 'Unemployed', 'Self-Employed', 'Student', 'Retired', 'Unknown'], + 'Education': ['None', 'Primary', 'Secondary', 'Diploma', 'Bachelor', 'Master', 'Doctorate', 'Other'], + 'Marital Status': ['Single', 'Married', 'Divorced', 'Separated', 'Widowed', 'Unknown'], + 'Known Habits': ['Smoking', 'Alcohol', 'Substance Use', 'Gambling', 'None', 'Unknown'], + 'Occupation': ['Unskilled', 'Skilled Labour', 'Professional', 'Executive', 'Military', 'Law Enforcement', 'IT', 'Healthcare', 'Education', 'Finance', 'Other'], + 'Known Financial Details': ['None', 'Low Income', 'Moderate Income', 'High Income', 'Wealthy', 'Unknown'], + 'Gang Affiliation': ['None', 'Local', 'Regional', 'International', 'Unknown'], + 'Criminal History': ['None', 'Minor', 'Multiple', 'Serious'], + 'Prior Arrests': ['0', '1', '2', '3', '4', '5+'], + 'Probation/Parole Status': ['None', 'On Probation', 'On Parole', 'Completed', 'Unknown'], + 'Status': ['Draft', 'In Progress', 'Completed', 'Archived'], + // Additional field options for complete coverage + 'arrestCount': ['0', '1', '2', '3', '4', '5+'], + 'Linked Cases': [], // Will be populated dynamically with existing case IDs + 'Suspect Link': [], // Will be populated dynamically with existing suspect IDs + 'Government ID': ['Aadhaar Card', 'PAN Card', 'Driving License', 'Passport', 'Voter ID', 'Other'], + 'Family Connections': ['Spouse', 'Parent', 'Child', 'Sibling', 'Relative', 'Friend', 'Other'], + 'Social Media Handles': [], // Text input field for multiple handles + 'Version History / Updates': [] // Text area for version tracking + }; + + // File upload fields + fileFields = new Set([ + 'Photo Upload', 'Evidence Photos', 'Evidence Videos', 'Evidence Documents', + 'Evidence Files', 'Upload Evidence Files', 'Digital Evidence' + ]); + + uploadedFiles: Record = {}; + + // Section icons + sectionIcons = { + crime: 'fas fa-gavel', + suspect: 'fas fa-user-secret', + notes: 'fas fa-sticky-note' + }; + + sections: any = { + crime: { + title: 'Crime Details', + subgroups: { + 'Identification & Timing': ['Case ID', 'FIR / Ref #', 'Crime Type', 'Case Category', 'Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered', 'Country', 'State', 'District', 'Number of Victims', 'Brief Description'], + 'Location & People': ['Location', 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Reported Contact', 'Witness Count', 'Victim Name', 'Victim Contact', 'Victim Summary', 'Suspected Offender Known?', 'Suspect Link'], + 'Offence & Context': ['Legal Sections / Charges', 'Offence Category', 'Offence Description', 'Suspected Motive', 'Confirmed Motive', 'Weapon Involved', 'Property Loss / Damage'], + 'Evidence & Scene': ['Evidence Collected', 'Forensic Tests Required', 'Scene Condition', 'Photos / Video?', 'CCTV Present?', 'CCTV Sources / IDs', 'Physical Evidence (list)', 'Chain of Custody?', 'Digital Evidence', 'Evidence Storage Reference'], + 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'riskLevel', 'Confidentiality'], + 'Status & Linkage': ['Biometric / Forensic IDs', 'DNA Ref ID', 'Fingerprint ID', 'Case Status', 'Linked Cases', 'arrestCount', 'Case Priority', 'Follow-up Date', 'Court Case ID', 'Next Hearing Date', 'Final Summary'], + 'Remark': ['Remark'] + } + }, + suspect: { + title: 'Suspect Details', + subgroups: { + 'Identity': ['Suspect ID', 'Suspect Name', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'], + 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color', 'Distinguishing Marks', 'Tattoo Details', 'Scar Details', 'Photo Upload'], + 'Background': ['Employment', 'Education', 'Occupation', 'Company', 'Workplace Address', 'Marital Status', 'Known Habits', 'Known Financial Details'], + 'Known Associates': ['Associate Names', 'Gang Affiliation', 'Family Connections', 'Social Media Handles'], + 'Prior Records': ['Criminal History', 'Prior Arrests', 'Probation/Parole Status'], + 'Remark': ['Remark'] + } + }, + notes: { + title: 'Evidence and Documents', + subgroups: { + 'Investigation Notes': ['Initial Findings', 'Detailed Notes', 'Status', 'Version History / Updates'], + 'Evidence Files': ['Evidence Photos', 'Evidence Videos', 'Evidence Documents'], + 'Links and Recommendation': ['Links to Evidence', 'Final Recommendations'], + 'Remark': ['Remark'] + } + } + }; + + // Complete field descriptions + fieldDescriptions: Record = { + // Crime: Identification & Timing + 'Case ID': 'Unique internal tracking identifier for this case.', + 'FIR / Ref #': 'Official First Information Report or reference number.', + 'Crime Type': 'Primary legal / investigative classification of the offence.', + 'Case Category': 'Broader grouping used for analytics and reporting.', + 'Date & Time (Entry)': 'Timestamp when the case was first registered in the system.', + 'Occurred From': 'Start of the known / suspected offence time window.', + 'Occurred To': 'End of the known / suspected offence time window.', + 'Time Reported': 'When it was first reported to authorities.', + 'Time Discovered': 'When the incident was first discovered (may differ from reported).', + 'Country': 'Country where the offence occurred.', + 'State': 'State / province of occurrence.', + 'District': 'Administrative district of occurrence (Tamil Nadu districts supported).', + 'Number of Victims': 'Total count of direct victims involved.', + 'Brief Description': 'Short narrative summary for quick reference.', + + // Crime: Location & People + 'Location': 'Exact address / geo description of the scene.', + 'Jurisdiction / PS': 'Police Station or jurisdiction handling the investigation.', + 'Scene Type': 'Type of environment where the offence occurred.', + 'Reported By': 'Name of the reporting individual / entity.', + 'Reported Contact': 'Contact details for the reporting party.', + 'Witness Count': 'Number of identified witnesses so far.', + 'Victim Name': 'Primary victim name (or placeholder if protected).', + 'Victim Contact': 'Phone / email / other contact channel for victim.', + 'Victim Summary': 'Short summary of victim condition or status.', + 'Suspected Offender Known?': 'Whether victim / witnesses know the offender.', + 'Suspect Link': 'Internal reference to related suspect record.', + + // Crime: Offence & Context + 'Legal Sections / Charges': 'Applicable statutory sections / penal codes.', + 'Offence Category': 'Higher level grouping (e.g., violent, cyber).', + 'Offence Description': 'Detailed narrative of what occurred.', + 'Suspected Motive': 'Preliminary perceived motive (subject to change).', + 'Confirmed Motive': 'Validated motive after evidence review.', + 'Weapon Involved': 'Weapon(s) used or suspected; choose Unknown if unclear.', + 'Property Loss / Damage': 'Summary / valuation of property loss or damage.', + + // Crime: Evidence & Scene + 'Evidence Collected': 'General list of all evidentiary items gathered.', + 'Forensic Tests Required': 'Pending or requested forensic examinations.', + 'Scene Condition': 'Condition of scene upon first secure entry.', + 'Photos / Video?': 'Whether any media was captured.', + 'CCTV Present?': 'If relevant CCTV sources exist.', + 'CCTV Sources / IDs': 'Identifiers / locations for each CCTV source.', + 'Physical Evidence (list)': 'Individual tangible exhibits (bagged / tagged).', + 'Chain of Custody?': 'Status of formal evidence transfer logging.', + 'Digital Evidence': 'Electronic sources: phones, email dumps, logs, socials.', + 'Evidence Storage Reference': 'Locker / repository / digital vault reference ID.', + + // Crime: Operational Notes + 'Investigating Officer': 'Lead officer responsible for case progress.', + 'Duty Person': 'Officer / staff who received the report.', + 'Supervising Officer': 'Oversight / escalation point for the case.', + 'Patrol Notes': 'First responder observations / scene notes.', + 'Arrest Made': 'Indicates whether an arrest has occurred.', + 'Arrest Location': 'Location at which arrest was executed.', + 'Initial Actions Taken': 'Immediate remedial or containment actions.', + 'riskLevel': 'Risk classification influencing priority.', + 'Confidentiality': 'Access / visibility level of case records.', + + // Crime: Status & Linkage + 'Biometric / Forensic IDs': 'External forensic system identifiers (AFIS, DNA DB).', + 'DNA Ref ID': 'Laboratory DNA reference identifier.', + 'Fingerprint ID': 'Fingerprint database reference.', + 'Case Status': 'Lifecycle status (Open / Active / Closed etc.).', + 'Linked Cases': 'Related or associated case identifiers.', + 'arrestCount': 'Total arrests associated with this case.', + 'Case Priority': 'Operational prioritisation level.', + 'Follow-up Date': 'Next scheduled investigative review date.', + 'Court Case ID': 'Judicial / docket identifier once filed.', + 'Next Hearing Date': 'Date of next scheduled court proceeding.', + 'Final Summary': 'Closure narrative entered at completion.', + + // Suspect: Identity + 'Suspect ID': 'Internal unique suspect identifier.', + 'Suspect Name': 'Full legal or recorded name.', + 'Alias / Nickname': 'Commonly used alternative names.', + 'Age': 'Approximate or confirmed age.', + 'Gender': 'Recorded gender descriptor.', + 'Nationality': 'Country of citizenship.', + 'Nationality ID / Passport Number': 'Official national ID / passport number.', + 'Languages': 'Languages spoken or understood by suspect.', + 'Address': 'Primary last known address.', + 'Known Aliases': 'Additional identity variations.', + 'Government ID': 'Government issued identification (license / ID card).', + + // Suspect: Physical Description + 'Height (cm)': 'Height in centimetres measured or estimated.', + 'Weight (kg)': 'Weight in kilograms measured or estimated.', + 'Build': 'General body build classification.', + 'Hair Color': 'Observed or recorded hair colour.', + 'Eye Color': 'Observed or recorded eye colour.', + 'Distinguishing Marks': 'Unique visible physical markers.', + 'Tattoo Details': 'Location and description of tattoos.', + 'Scar Details': 'Location and description of scars.', + 'Photo Upload': 'Most recent or relevant facial photograph.', + + // Suspect: Background + 'Employment': 'Current employment status.', + 'Education': 'Highest completed education level.', + 'Occupation': 'Primary occupation / role.', + 'Company': 'Employer / organisation name.', + 'Workplace Address': 'Physical address of workplace.', + 'Marital Status': 'Current marital / relationship status.', + 'Known Habits': 'Behavioural patterns (substances, gambling, etc.).', + 'Known Financial Details': 'Financial profile relevant to investigation.', + + // Suspect: Known Associates + 'Associate Names': 'Key associate individuals linked to suspect.', + 'Gang Affiliation': 'Known gang or group membership.', + 'Family Connections': 'Notable family relational links.', + 'Social Media Handles': 'Identifiers used on social platforms.', + + // Suspect: Prior Records + 'Criminal History': 'Summary of prior criminal involvement.', + 'Prior Arrests': 'Number / list of previous arrests.', + 'Probation/Parole Status': 'Current supervision / release status.', + + // Notes: Investigation Notes + 'Initial Findings': 'Early observations at investigation start.', + 'Detailed Notes': 'Progressive narrative & analytical details.', + 'Status': 'Progress state category for notes.', + 'Version History / Updates': 'Chronological changes & authorship log.', + + // Notes: Evidence Files + 'Evidence Photos': 'Photographic evidence references.', + 'Evidence Videos': 'Video evidence references.', + 'Evidence Documents': 'Document / PDF evidence references.', + + // Notes: Links and Recommendation + 'Links to Evidence': 'External or internal reference links to sources.', + 'Final Recommendations': 'Closing recommendations / actions summary.' + }; + + subgroupIcons: any = { + 'Identification & Timing': 'fas fa-clock', + 'Location & People': 'fas fa-map-marker-alt', + 'Offence & Context': 'fas fa-gavel', + 'Evidence & Scene': 'fas fa-search', + 'Operational Notes': 'fas fa-clipboard', + 'Status & Linkage': 'fas fa-link', + 'Identity': 'fas fa-id-card', + 'Physical Description': 'fas fa-user', + 'Background': 'fas fa-user-graduate', + 'Known Associates': 'fas fa-users', + 'Prior Records': 'fas fa-file-alt', + 'Investigation Notes': 'fas fa-sticky-note', + 'Evidence Files': 'fas fa-folder', + 'Links and Recommendation': 'fas fa-link', + 'Recommendations': 'fas fa-thumbs-up' + }; + ngOnInit(): void { - // If navigated with router state from Record page Edit action - const state: any = history.state || {}; - if (typeof state.editIndex === 'number' && state.editCase) { - this.editIndex = state.editIndex; - this.patchFromCase(state.editCase as PoliceCase); - return; + // Set up autosave + this.autoSave$.pipe( + debounceTime(2000), + takeUntil(this.destroy$) + ).subscribe(() => { + this.performAutoSave(); + }); + + // Load saved form data + this.loadFormData(); + + // Load field selections + this.loadFieldSelections(); + } + + ngAfterViewInit(): void { + // No special scroll handling needed anymore + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + // Progress Calculation + get progressPercentage(): number { + const totalFields = this.getAllFields().length; + const completedFields = this.completedFields.size; + return totalFields > 0 ? Math.round((completedFields / totalFields) * 100) : 0; + } + + private getAllFields(): string[] { + let allFields: string[] = []; + for (const section of this.sectionKeys) { + for (const subgroup of Object.keys(this.sections[section].subgroups)) { + allFields = allFields.concat(this.sections[section].subgroups[subgroup]); + } + } + return allFields; + } + + // Helper method to check if we're in Identification & Timing + private isIdentificationAndTimingPage(): boolean { + return this.currentSubgroup === 'Identification & Timing'; + } + + // Helper method to check if we're in Location & People + private isLocationAndPeoplePage(): boolean { + return this.currentSubgroup === 'Location & People'; + } + + // Helper method to check if we need compact layout (applies to all pages now) + private needsCompactLayout(): boolean { + return true; // Apply compact layout to all pages to prevent main page scroll + } + + // Card Management - Updated for single card layout across ALL pages + showSecondaryCard(): boolean { + return false; // No secondary card for any page - single card layout for all + } + + showTertiaryCard(): boolean { + return false; // No tertiary card for any page - single card layout for all + } + + getPrimaryFields(): string[] { + // Return selected fields for display instead of all fields + return this.getSelectedFieldsForDisplay(); + } + + getSecondaryFields(): string[] { + // Return empty array for single card layout across all pages + return []; + } + + getTertiaryFields(): string[] { + // Return empty array for single card layout across all pages + return []; + } + + private getCurrentFields(): string[] { + return this.sections[this.currentSection].subgroups[this.currentSubgroup] || []; + } + + toggleCardMinimize(card: 'primary' | 'secondary' | 'tertiary'): void { + this.isCardMinimized[card] = !this.isCardMinimized[card]; + } + + // Field Management + isFieldRequired(field: string): boolean { + return this.requiredFields.has(field); + } + + isCompactField(field: string): boolean { + return this.compactFields.has(field); + } + + getInputType(field: string): string { + if (this.numericFields.has(field)) return 'number'; + if (this.dateTimeFields.has(field)) return 'datetime-local'; + if (this.dateFields.has(field)) return 'date'; + if (field.toLowerCase().includes('email')) return 'email'; + if (field.toLowerCase().includes('phone') || field.toLowerCase().includes('contact')) return 'tel'; + if (field.toLowerCase().includes('url') || field.toLowerCase().includes('link')) return 'url'; + if (field.toLowerCase().includes('description')) return 'textarea'; + return 'text'; + } + + getFieldPlaceholder(field: string): string { + if (field === 'Age') return 'Enter age (18-99)'; + if (field === 'Height (cm)') return 'Height in cm'; + if (field === 'Weight (kg)') return 'Weight in kg'; + if (this.dateTimeFields.has(field)) return 'dd-mm-yyyy --:--'; + if (this.dateFields.has(field)) return 'dd-mm-yyyy'; + if (field.toLowerCase().includes('email')) return 'Enter email address'; + if (field.toLowerCase().includes('phone')) return 'Enter phone number'; + if (field.toLowerCase().includes('description')) return 'Enter detailed description...'; + return `Enter ${field.toLowerCase()}`; + } + + getMaxLength(field: string): number { + if (field === 'Age') return 2; + if (field === 'Gender') return 10; + if (field === 'Height (cm)') return 3; + if (field === 'Weight (kg)') return 3; + if (this.compactFields.has(field)) return 20; + return 500; + } + + // Validation + validateField(field: string): void { + const value = this.formData[field]; + let hasError = false; + let isValid = false; + let message = ''; + + if (this.isFieldRequired(field) && (!value || value.toString().trim() === '')) { + hasError = true; + message = `${field} is required`; + } else if (value && value.toString().trim() !== '') { + // Field-specific validation + if (field === 'Age') { + const age = parseInt(value); + if (isNaN(age) || age < 1 || age > 120) { + hasError = true; + message = 'Age must be a valid number between 1 and 120'; + } else { + isValid = true; + } + } else if (field === 'Email') { + // Basic email pattern; improve with regex if needed + const emailPattern = /\S+@\S+\.\S+/; + isValid = emailPattern.test(value); + if (!isValid) { + hasError = true; + message = 'Invalid email address format'; + } + } else { + // Generic validation for other fields (extend as needed) + isValid = true; + } } - // If navigated via /infopage/:id, load by caseId - const caseId = this.route.snapshot.paramMap.get('id'); - if (caseId) { - const cases = this.caseStore.getPoliceCases(); - const idx = cases.findIndex(c => c.caseId == caseId); - if (idx !== -1) { - this.editIndex = idx; - this.patchFromCase(cases[idx]); + + // Update field validation state + this.fieldValidation[field] = { hasError, isValid, message }; + + // Update overall form completion status + this.updateCompletionStatus(); + } + + private updateCompletionStatus(): void { + this.completedFields.clear(); + + for (const field of Object.keys(this.formData)) { + if (this.formData[field] !== null && this.formData[field] !== undefined && this.formData[field] !== '') { + this.completedFields.add(field); } } + + // Update completed subgroups and sections + this.updateCompletedGroupsAndSections(); } - // ----------------- getters ----------------- - get crimeGroup() { return this.form.get('crime') as FormGroup; } - get suspectGroup() { return this.form.get('suspect') as FormGroup; } - get notesGroup() { return this.form.get('notes') as FormGroup; } - - get knownLocations(): FormArray { - return this.suspectGroup.get('knownLocations') as FormArray; - } - get attachments(): FormArray { - return this.notesGroup.get('attachments') as FormArray; - } - - // ----------------- helpers (arrays) ----------------- - addKnownLocation() { this.knownLocations.push(new FormControl('')); } - removeKnownLocation(index: number) { this.knownLocations.removeAt(index); } - - // ----------------- file helpers ----------------- - setPhoto(file: File | null) { this.suspectGroup.get('photo')?.setValue(file || null); } - addAttachment(file: File) { this.attachments.push(new FormControl(file)); } - - onPhotoChange(event: Event): void { - const input = event.target as HTMLInputElement | null; - let file: File | null = null; - if (input && input.files && input.files.length > 0) file = input.files[0]; - this.setPhoto(file); - if (input) input.value = ''; - } - - onAttachmentsChange(event: Event): void { - const input = event.target as HTMLInputElement | null; - if (!input || !input.files || input.files.length === 0) return; - for (let i = 0; i < input.files.length; i++) { - const f = input.files.item(i); - if (f) this.addAttachment(f); - } - input.value = ''; - } - - // ----------------- wizard ----------------- - next() { - if (this.step === 1 && this.crimeGroup.invalid) { this.crimeGroup.markAllAsTouched(); return; } - if (this.step === 2 && this.suspectGroup.invalid) { this.suspectGroup.markAllAsTouched(); return; } - this.step = Math.min(3, this.step + 1); - } - back() { this.step = Math.max(1, this.step - 1); } - - // ----------------- edit support ----------------- - private patchFromCase(c: PoliceCase): void { - // Patch only fields guaranteed by your current PoliceCase type - // (do not read c.caseId / c.dateTime here to avoid TS errors) - this.form.patchValue({ - crime: { - caseId: '', // optional: leave as is or derive from elsewhere - crimeType: c.crime || '', - dateTime: '', // optional: leave as is - location: c.police?.address || '', - reportedByName: '', - reportedByContact: '', - description: c.police?.information || '' - }, - suspect: { - suspectId: '', - fullName: c.accused?.name || '', - alias: c.accused?.occupation || '', - dob: '', - age: c.accused?.age || '', - gender: c.accused?.gender || '', - address: c.accused?.address || '' - }, - notes: { - initialFindings: c.police?.information || '', - officerInCharge: c.police?.name || '', - status: 'Open', + private updateCompletedGroupsAndSections(): void { + this.completedSubgroups.clear(); + this.completedSections.clear(); + + for (const section of this.sectionKeys) { + const subgroups = Object.keys(this.sections[section].subgroups); + for (const subgroup of subgroups) { + const fields = this.sections[section].subgroups[subgroup]; + const allFieldsCompleted = fields.every((field: string) => this.completedFields.has(field)); + + if (allFieldsCompleted) { + this.completedSubgroups.add(subgroup); + } } - }); - // Reset arrays (optional) - this.knownLocations.clear(); - this.attachments.clear(); - this.step = 1; - } - - private mapToPoliceCase(): PoliceCase { - const v: any = this.form.value; - // Build an object that matches your current PoliceCase type (no caseId/dateTime) - return { - crime: v.crime?.crimeType || 'Unknown', - police: { - name: v.notes?.officerInCharge || '—', - station: '—', - address: v.crime?.location || '—', - pincode: '', - dutyPerson: v.notes?.officerInCharge || '—', - modeOfCrime: v.crime?.crimeType || '—', - information: v.notes?.initialFindings || '' - }, - accused: { - name: v.suspect?.fullName || '—', - age: v.suspect?.age || '—', - gender: v.suspect?.gender || '—', - address: v.suspect?.address || '—', - occupation: v.suspect?.alias || '' + + if (this.completedSubgroups.size === subgroups.length) { + this.completedSections.add(section); } + } + } + + // Field selection functionality - Updated to allow all fields by default + selectedFields: Record = {}; // Store selected fields per subgroup + showFieldSelector: string | null = null; // Track which field selector is open + readonly maxSelectableFields = 50; // Increased limit to allow more fields + + // Get all available fields for current subgroup + getAvailableFields(): string[] { + return this.getCurrentFields(); + } + + // Get total available fields count dynamically + getTotalAvailableFieldsCount(): number { + return this.getAvailableFields().length; + } + + // Get currently selected fields for display (default to ALL fields if none selected) + getSelectedFieldsForDisplay(): string[] { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + if (this.selectedFields[subgroupKey] && this.selectedFields[subgroupKey].length >= 0) { + return this.selectedFields[subgroupKey]; + } + // Default to ALL fields if no selection made + return this.getCurrentFields(); + } + + // Toggle field selection with enhanced debugging + toggleFieldSelection(field: string, event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + console.log('Toggling field selection for:', field); + + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + if (!this.selectedFields[subgroupKey]) { + // Initialize with all fields selected by default + this.selectedFields[subgroupKey] = [...this.getCurrentFields()]; + } + + const currentSelection = this.selectedFields[subgroupKey]; + const fieldIndex = currentSelection.indexOf(field); + + console.log('Current selection:', currentSelection); + console.log('Field index:', fieldIndex); + + if (fieldIndex > -1) { + // Remove field if already selected + currentSelection.splice(fieldIndex, 1); + console.log('Removed field:', field); + } else { + // Add field if not selected and under limit + if (currentSelection.length < this.maxSelectableFields) { + currentSelection.push(field); + console.log('Added field:', field); + } else { + console.log('Selection limit reached, cannot add:', field); + } + } + + // Trigger change detection + this.selectedFields = { ...this.selectedFields }; + this.saveFieldSelections(); + // Do NOT close the popup here; just update selection and save + } + + // Check if field is currently selected + isFieldSelected(field: string): boolean { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + if (!selections) { + // If no selections made, all fields are selected by default + return true; + } + return selections.includes(field); + } + + // Get count of selected fields + getSelectedFieldCount(): number { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + if (!selections) { + return this.getCurrentFields().length; // All fields selected by default + } + return selections.length; + } + + // Check if selection limit is reached + isSelectionLimitReached(): boolean { + return this.getSelectedFieldCount() >= this.maxSelectableFields; + } + + // Reset field selection for current subgroup (clear all) + resetFieldSelection(event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + this.selectedFields[subgroupKey] = []; + this.selectedFields = { ...this.selectedFields }; + this.saveFieldSelections(); + // Do NOT close the popup here; keep it open for further selection + } + + // Select all fields + selectAllFields(event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const allFields = this.getCurrentFields(); + this.selectedFields[subgroupKey] = [...allFields]; + this.selectedFields = { ...this.selectedFields }; + this.saveFieldSelections(); + } + + // Select default fields (first 10 or all if less than 10) - Renamed for clarity + selectDefaultFields(event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const allFields = this.getCurrentFields(); + this.selectedFields[subgroupKey] = allFields.slice(0, Math.min(10, allFields.length)); + this.selectedFields = { ...this.selectedFields }; + this.saveFieldSelections(); + } + + // Get dynamic max selectable based on available fields + getDynamicMaxSelectable(): number { + const totalFields = this.getTotalAvailableFieldsCount(); + return Math.min(this.maxSelectableFields, totalFields); + } + + // Check if all fields are selected + areAllFieldsSelected(): boolean { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + const totalFields = this.getCurrentFields().length; + + if (!selections) { + return true; // All fields selected by default + } + + return selections.length === totalFields; + } + + // Check if no fields are selected + areNoFieldsSelected(): boolean { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + + if (!selections) { + return false; // All fields selected by default + } + + return selections.length === 0; + } + + // Navigation and section management methods + getSubgroups(): string[] { + return Object.keys(this.sections[this.currentSection].subgroups); + } + + showSection(section: 'crime' | 'suspect' | 'notes'): void { + // Close field selector when changing sections + this.closeFieldSelector(); + this.currentSection = section; + this.currentSubgroup = Object.keys(this.sections[this.currentSection].subgroups)[0]; + this.showHelpFor = null; + this.triggerAutoSave(); + } + + // Enhanced document click handler + @HostListener('document:click', ['$event']) + handleDoc(event: Event): void { + this.showHelpFor = null; + // Only close field selector if click is outside the popup + const target = event.target as HTMLElement; + if (this.showFieldSelector && !target.closest('.field-selector-container')) { + this.closeFieldSelector(); + } + } + + // Handle section/subgroup changes to refresh field selector + setSubgroup(key: string): void { + // Close field selector when changing subgroups + this.closeFieldSelector(); + this.currentSubgroup = key; + this.showHelpFor = null; + this.triggerAutoSave(); + } + + // Section and subgroup completion status + isSectionCompleted(section: string): boolean { + return this.completedSections.has(section); + } + + isSubgroupCompleted(subgroup: string): boolean { + return this.completedSubgroups.has(subgroup); + } + + // Section descriptions + getSectionDescription(section: string): string { + const descriptions = { + crime: 'Capture complete crime intelligence: timing, location, evidence, and operational context. Use dropdown values for consistency and upload supporting materials.', + suspect: 'Document comprehensive suspect profile: identity, physical characteristics, background, associations, and criminal history. Include recent photographs where available.', + notes: 'Maintain detailed investigative records: findings, evidence files, reference materials, and final recommendations with proper version control.' + }; + return descriptions[section as keyof typeof descriptions] || ''; + } + + // Field handling and validation + onFieldChange(field: string): void { + this.validateField(field); + this.triggerAutoSave(); + } + + // Auto-save functionality + private triggerAutoSave(): void { + this.autoSave$.next(); + } + + private performAutoSave(): void { + this.isAutoSaving = true; + this.autoSaveStatus = 'Saving...'; + + // Save to localStorage + this.saveFormData(); + + // Simulate save delay + setTimeout(() => { + this.isAutoSaving = false; + this.autoSaveStatus = 'Saved'; + + // Reset status after a moment + setTimeout(() => { + this.autoSaveStatus = 'Auto-save enabled'; + }, 2000); + }, 500); + } + + private saveFormData(): void { + const saveData = { + formData: this.formData, + completedFields: Array.from(this.completedFields), + completedSubgroups: Array.from(this.completedSubgroups), + completedSections: Array.from(this.completedSections), + currentSection: this.currentSection, + currentSubgroup: this.currentSubgroup }; + localStorage.setItem('pydetect-form-data', JSON.stringify(saveData)); + } + + private loadFormData(): void { + const savedData = localStorage.getItem('pydetect-form-data'); + if (savedData) { + const data = JSON.parse(savedData); + this.formData = data.formData || {}; + this.completedFields = new Set(data.completedFields || []); + this.completedSubgroups = new Set(data.completedSubgroups || []); + this.completedSections = new Set(data.completedSections || []); + this.currentSection = data.currentSection || 'crime'; + this.currentSubgroup = data.currentSubgroup || 'Identification & Timing'; + } + } + + // Dropdown options and cascading logic + getOptions(field: string): string[] | undefined { + if (field === 'Country') return this.countries; + if (field === 'State') return (this.selectedValues['Country'] === 'India' || !this.selectedValues['Country']) ? this.indiaStates : []; + if (field === 'District') return this.selectedValues['State'] === 'Tamil Nadu' ? this.tamilNaduDistricts : []; + return this.selectOptions[field]; + } + + onSelectChange(field: string, event: Event): void { + const value = (event.target as HTMLSelectElement).value; + this.selectedValues[field] = value; + this.formData[field] = value; + + // Clear dependent fields + if (field === 'Country') { + delete this.selectedValues['State']; + delete this.selectedValues['District']; + delete this.formData['State']; + delete this.formData['District']; + } + if (field === 'State') { + delete this.selectedValues['District']; + delete this.formData['District']; + } + + this.validateField(field); + this.triggerAutoSave(); + } + + // Field help functionality + toggleFieldInfo(field: string, ev: MouseEvent): void { + ev.stopPropagation(); + this.showHelpFor = this.showHelpFor === field ? null : field; } - // ----------------- submit ----------------- - submit() { - if (this.form.invalid) { - this.form.markAllAsTouched(); + closeFieldInfo(): void { + this.showHelpFor = null; + } + + // File upload functionality + onFileChange(field: string, event: Event): void { + const input = event.target as HTMLInputElement; + const files = input.files ? Array.from(input.files) : []; + if (files.length) { + this.uploadedFiles[field] = (this.uploadedFiles[field] || []).concat(files); + this.validateField(field); + this.triggerAutoSave(); + } + } + + onDragOver(event: DragEvent): void { + event.preventDefault(); + this.isDragOver = true; + } + + onDragLeave(event: DragEvent): void { + event.preventDefault(); + this.isDragOver = false; + } + + onFileDrop(field: string, event: DragEvent): void { + event.preventDefault(); + this.isDragOver = false; + + const files = event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : []; + if (files.length) { + this.uploadedFiles[field] = (this.uploadedFiles[field] || []).concat(files); + this.validateField(field); + this.triggerAutoSave(); + } + } + + removeFile(field: string, file: File): void { + if (this.uploadedFiles[field]) { + this.uploadedFiles[field] = this.uploadedFiles[field].filter(f => f !== file); + this.triggerAutoSave(); + } + } + + getAcceptedFileTypes(field: string): string { + return this.fileTypeConfig[field] || '*'; + } + + getFileIcon(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase(); + switch (ext) { + case 'pdf': return 'fas fa-file-pdf'; + case 'doc': + case 'docx': return 'fas fa-file-word'; + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': return 'fas fa-file-image'; + case 'mp4': + case 'avi': + case 'mov': return 'fas fa-file-video'; + default: return 'fas fa-file'; + } + } + + // Navigation helper methods for floating buttons + getPreviousSubgroup(): string { + const list = this.getSubgroups(); + const currentIndex = list.indexOf(this.currentSubgroup); + return currentIndex > 0 ? list[currentIndex - 1] : ''; + } + + getNextSubgroup(): string { + const list = this.getSubgroups(); + const currentIndex = list.indexOf(this.currentSubgroup); + return currentIndex < list.length - 1 ? list[currentIndex + 1] : ''; + } + + isLastSubgroup(): boolean { + const list = this.getSubgroups(); + return list.indexOf(this.currentSubgroup) === list.length - 1; + } + + isFirstSubgroup(): boolean { + return this.getSubgroups().indexOf(this.currentSubgroup) === 0; + } + + canPrevSubgroup(): boolean { + return !this.isFirstSubgroup(); + } + + canNextSubgroup(): boolean { + return !this.isLastSubgroup(); + } + + prevSubgroup(): void { + if (!this.canPrevSubgroup()) return; + const list = this.getSubgroups(); + const i = list.indexOf(this.currentSubgroup); + this.setSubgroup(list[i - 1]); + } + + nextSubgroup(): void { + if (!this.canNextSubgroup()) return; + const list = this.getSubgroups(); + const i = list.indexOf(this.currentSubgroup); + this.setSubgroup(list[i + 1]); + } + + submitCurrentSection(): void { + // Perform final validation + const currentFields = this.getCurrentFields(); + const requiredFields = currentFields.filter(f => this.isFieldRequired(f)); + const missingFields = requiredFields.filter(f => !this.completedFields.has(f)); + + if (missingFields.length > 0) { + alert(`Please complete the following required fields: ${missingFields.join(', ')}`); return; } - if (this.editIndex !== null) { - // Update existing record (service must have updatePoliceCaseAt; see earlier message) - this.caseStore.updatePoliceCaseAt(this.editIndex, this.mapToPoliceCase() as PoliceCase); - } else { - // Add new record using your existing service mapping - this.caseStore.addFromInfoForm(this.form.value); + this.performAutoSave(); + + // Map flat formData to nested structure expected by addFromInfoForm + const crime = { + caseId: this.formData['Case ID'] || '', + dateTime: this.formData['Date & Time (Entry)'] || '', + crimeType: this.formData['Crime Type'] || '', + location: this.formData['Location'] || '', + victimName: this.formData['Victim Name'] || '', + caseCategory: this.formData['Case Category'] || '', + reportedBy: this.formData['Reported By'] || '', + 'FIR / Ref #': this.formData['FIR / Ref #'] || '', + 'Occurred From': this.formData['Occurred From'] || '', + 'Occurred To': this.formData['Occurred To'] || '', + 'Jurisdiction / PS': this.formData['Jurisdiction / PS'] || '', + 'Scene Type': this.formData['Scene Type'] || '' + }; + const suspect = { + fullName: this.formData['Suspect Name'] || '', + age: this.formData['Age'] || '', + gender: this.formData['Gender'] || '', + address: this.formData['Address'] || '', + alias: this.formData['Alias / Nickname'] || '' + }; + const notes = { + status: this.formData['Case Status'] || this.formData['Status'] || 'Open', + officerInCharge: this.formData['Investigating Officer'] || '', + initialFindings: this.formData['Initial Findings'] || '' + }; + const legal = { + witnessStatements: this.formData['Witness Statements'] || '', + confessions: this.formData['Confessions'] || '', + evidence: this.uploadedFiles['Evidence Files'] || [] + }; + + this.caseStore.addFromInfoForm({ crime, suspect, notes, legal }); + // Show popup first + this.showSubmitPopup = true; + } + + onSubmitPopupClose(): void { + this.showSubmitPopup = false; + this.router.navigate(['/record'], { state: { formData: this.formData } }); + } + + // Keyboard navigation + @HostListener('document:keydown', ['$event']) + handleKeydown(event: KeyboardEvent): void { + // Keyboard navigation + if (event.ctrlKey && event.key === 'ArrowRight') { + event.preventDefault(); + this.nextSubgroup(); + } else if (event.ctrlKey && event.key === 'ArrowLeft') { + event.preventDefault(); + this.prevSubgroup(); + } else if (event.ctrlKey && event.key === 's') { + event.preventDefault(); + this.performAutoSave(); + } else if (event.key === 'Escape') { + this.closeFieldInfo(); + } + } + + // Toggle field selector visibility + toggleFieldSelector(event?: Event): void { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + this.showFieldSelector = this.showFieldSelector === subgroupKey ? null : subgroupKey; + } + + // Close field selector + closeFieldSelector(): void { + this.showFieldSelector = null; + } + + // Save field selections to localStorage + private saveFieldSelections(): void { + try { + localStorage.setItem('pydetect-field-selections', JSON.stringify(this.selectedFields)); + } catch (error) { + console.warn('Could not save field selections to localStorage:', error); } + } - // Navigate to record page - this.router.navigate(['/record']); + // Load field selections from localStorage + private loadFieldSelections(): void { + try { + const savedSelections = localStorage.getItem('pydetect-field-selections'); + if (savedSelections) { + this.selectedFields = JSON.parse(savedSelections); + } + } catch (error) { + console.warn('Could not load field selections from localStorage:', error); + this.selectedFields = {}; + } } - navigateHome(): void { - this.router.navigate(['/']); + // Track by function for ngFor optimization + trackByField(index: number, field: string): string { + return field; } } diff --git a/src/app/recordpage/recordpage.component.css b/src/app/recordpage/recordpage.component.css index 47cedaede7a5fc5ff40b3d7a5953c2cae3e8a5e2..3fb197c247e0cadd310c34c1b44bbafcf9a5e1ce 100644 --- a/src/app/recordpage/recordpage.component.css +++ b/src/app/recordpage/recordpage.component.css @@ -38,16 +38,22 @@ /* Add gap between logo and browser border */ .logo-img { width: 6vw; - height: 6vw; - border-radius: 50%; - box-shadow: 0 0 15px rgba(255, 255, 255, 0.8); position: fixed; - top: 18px; /* Add gap from top */ - left: 18px; /* Add gap from left */ + top: 21px; + left: 36px; z-index: 300; margin: 0; + background: #ffffff; + max-width: 4.2vw; + min-width: 56px; + height: auto; + border-radius: 50%; + padding: 4px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25); + transition: transform .25s ease; } +/* Move Py-Detect text further down by increasing top value of .logo-title-row */ .logo-title-row { display: flex; flex-direction: row; @@ -63,10 +69,10 @@ .py-detect-title { position: fixed; - margin-left: 132px; - margin-top: -208px; + margin-left: 97px; + margin-top: -228px; text-align: left; - font-size: 4vw; + font-size: 3vw; color: #38bdf8; font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif; font-weight: 900; @@ -97,35 +103,48 @@ border: 2px solid #23272b; } - .py-detect-title .py-letter.d { - color: #e3f6ff; - text-shadow: 0 0 6px #38bdf8; - } +.py-shape { + display: inline-block; + width: 18px; + height: 4px; + background: #38bdf8; + margin: 0 8px; + vertical-align: middle; + border-radius: 2px; + box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff; + border: 2px solid #23272b; +} - .py-detect-title .py-letter.e { - color: #38bdf8; - text-shadow: 0 0 6px #38bdf8; - } +.py-detect-title .py-letter.d { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; +} - .py-detect-title .py-letter.t { - color: #e3f6ff; - text-shadow: 0 0 6px #38bdf8; - } +.py-detect-title .py-letter.e { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; +} - .py-detect-title .py-letter.e2 { - color: #38bdf8; - text-shadow: 0 0 6px #38bdf8; - } +.py-detect-title .py-letter.t { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; +} - .py-detect-title .py-letter.c { - color: #e3f6ff; - text-shadow: 0 0 6px #38bdf8; - } +.py-detect-title .py-letter.e2 { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; +} + +.py-detect-title .py-letter.c { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; +} + +.py-detect-title .py-letter.t2 { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; +} - .py-detect-title .py-letter.t2 { - color: #38bdf8; - text-shadow: 0 0 6px #38bdf8; - } .py-shape { display: inline-block; @@ -201,11 +220,32 @@ margin: 0 auto; } +.record-table th, +.record-table td { + white-space: nowrap; + min-width: 90px; +} + +.record-table td { + vertical-align: middle; +} + +.record-table th { + vertical-align: middle; +} + +.record-table td a { + white-space: nowrap; + display: inline-block; + min-width: 80px; +} + @media (max-width: 1800px) { .records { max-width: 100vw; min-width: 1000px; } + .table-wrap { max-width: 100vw; min-height: 400px; @@ -218,13 +258,16 @@ width: 100%; max-width: 100vw; } + .table-wrap { min-height: 200px; } + .padded-table-wrap { padding-left: 4px; padding-right: 4px; } + .records-header-row { flex-direction: column; align-items: stretch; @@ -232,10 +275,12 @@ padding-left: 4px; padding-right: 4px; } + .page-title { text-align: center; margin-right: 0; } + .toolbar { position: static; transform: none; @@ -250,6 +295,21 @@ border-bottom: 1px solid #edf2f7; text-align: left; white-space: nowrap; + min-width: 90px; +} + +.records td { + vertical-align: middle; +} + +.records th { + vertical-align: middle; +} + +.records td a { + white-space: nowrap; + display: inline-block; + min-width: 80px; } .records thead th { @@ -270,6 +330,11 @@ .records th.actions-col, .records td.actions { color: #fff; + text-align: left; + padding-left: 18px; + padding-right: 18px; + width: 1%; + white-space: nowrap; } .records tbody tr:last-child td { @@ -341,6 +406,34 @@ color: #991b1b; } +.status-label { + display: inline-block; + padding: 3px 16px; + border-radius: 12px; + font-weight: 700; + font-size: 1em; + letter-spacing: 0.5px; + min-width: 70px; + text-align: center; + background: #e5e7eb; + color: #22223b; +} + +.status-open { + background: #d1fae5; + color: #059669; +} + +.status-under { + background: #dbeafe; + color: #2563eb; +} + +.status-closed { + background: #fee2e2; + color: #dc2626; +} + .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } @@ -377,6 +470,86 @@ border-color: #805ad5; } + .btn.delete { + background: #ef4444; + color: #fff; + border-color: #ef4444; + box-shadow: 0 2px 8px #ef444422; + transition: background 0.18s, color 0.18s; + } + + .btn.delete:hover { + background: #991b1b; + color: #fff; + } + +/* Icon buttons */ +.icon-btn { + background: none; + border: none; + padding: 6px 10px; + margin: 0 2px; + border-radius: 6px; + cursor: pointer; + font-size: 1.18em; + transition: background 0.15s, color 0.15s, box-shadow 0.15s; + vertical-align: middle; + outline: none; + display: inline-flex; + align-items: center; + justify-content: center; +} + + .icon-btn.view { + margin: 0; + padding: 0 8px; + background: none; + border: none; + cursor: pointer; + color: #2563eb; + font-size: 1.2em; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .icon-btn.edit { + color: #7c3aed; + } + + .icon-btn.delete { + color: #ef4444; + } + + .icon-btn:hover { + background: #f0f7ff; + box-shadow: 0 2px 8px #2563eb22; + } + + .icon-btn.delete:hover { + background: #fff0f0; + color: #b91c1c; + } + + .icon-btn.edit:hover { + background: #f3e8ff; + color: #5b21b6; + } + + .icon-btn.view:hover { + background: #e0f2fe; + color: #0ea5e9; + } + + .icon-btn.verify { + color: #22c55e; + } + + .icon-btn.verify:hover { + background: #e0ffe6; + color: #15803d; + } + .empty { text-align: center; color: #718096; @@ -392,47 +565,104 @@ .modal { position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%,-50%); - width: min(720px, 94vw); + top: 0; + left: 0; + width: 100vw; + height: 100vh; + min-width: 0; + min-height: 0; + max-width: 100vw; + max-height: 100vh; background: #fff; - border-radius: 12px; - box-shadow: 0 20px 60px rgba(0,0,0,.2); - overflow: hidden; + border-radius: 0; + box-shadow: none; + overflow-y: auto; + z-index: 2000; + padding: 0; + font-size: 1.13rem; + animation: none; + display: flex; + flex-direction: column; } .modal-header, .modal-footer { - padding: 12px 16px; - background: #f7fafc; + padding: 24px 40px 18px 40px; + background: #f8fafc; + font-weight: 700; + font-size: 1.18rem; + color: #23272b; } .modal-body { - padding: 16px; + flex: 1 1 auto; + padding: 32px 48px 32px 48px; + background: #fff; + color: #23272b; + font-size: 1.08rem; + overflow-y: auto; } - - .detail-row { display: grid; - grid-template-columns: 160px 1fr; - gap: 8px; - padding: 6px 0; + grid-template-columns: 180px 1fr; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid #e5e7eb; } + .detail-row:last-child { + border-bottom: none; + } + .detail-row span { - color: #4a5568; + color: #2563eb; + font-weight: 600; + font-size: 1.07em; } -.detail-block .label { - font-weight: 700; - margin-bottom: 6px; + .detail-row b { + color: #23272b; + font-weight: 500; + font-size: 1.07em; + } + +.detail-block { + margin-top: 18px; + margin-bottom: 8px; } + .detail-block .label { + font-weight: 700; + color: #2563eb; + margin-bottom: 6px; + font-size: 1.09em; + } + .explain { white-space: pre-wrap; + color: #23272b; + font-size: 1.07em; +} + +.modal-footer { + text-align: right; +} + +.btn { + padding: 8px 18px; + border: 1px solid #cbd5e0; + border-radius: 8px; + background: #f7fafc; + cursor: pointer; + font-size: 1.05em; + font-weight: 600; + margin-left: 8px; } + .btn:hover { + background: #e0e7ef; + } + .records-center { display: flex; flex-direction: column; @@ -530,11 +760,11 @@ z-index: 10; } -.modern-searchbar-container.compact { - width: auto; - margin: 0 0 0 24px; - align-items: center; -} + .modern-searchbar-container.compact { + width: auto; + margin: 0 0 0 24px; + align-items: center; + } .modern-searchbar-form { display: flex; @@ -548,12 +778,12 @@ position: relative; } -.modern-searchbar-form.compact { - height: 44px; - min-width: 260px; - width: 320px; - padding: 0 8px 0 10px; -} + .modern-searchbar-form.compact { + height: 44px; + min-width: 260px; + width: 320px; + padding: 0 8px 0 10px; + } .modern-searchbar-icon { display: flex; @@ -574,11 +804,11 @@ height: 32px; } -.modern-searchbar-input::placeholder { - color: #e0e7ef; - opacity: 1; - font-weight: 400; -} + .modern-searchbar-input::placeholder { + color: #e0e7ef; + opacity: 1; + font-weight: 400; + } .modern-searchbar-btn { background: #fff; @@ -595,10 +825,10 @@ transition: background 0.18s, color 0.18s; } -.modern-searchbar-btn:hover { - background: #ede9fe; - color: #4f46e5; -} + .modern-searchbar-btn:hover { + background: #ede9fe; + color: #4f46e5; + } .modern-searchbar-form.white-bg { background: #fff !important; @@ -610,10 +840,10 @@ color: #23272b !important; } -.modern-searchbar-input.white-bg::placeholder { - color: #64748b !important; - opacity: 1; -} + .modern-searchbar-input.white-bg::placeholder { + color: #64748b !important; + opacity: 1; + } .modern-searchbar-icon svg { stroke: #64748b !important; @@ -634,21 +864,35 @@ transition: background 0.18s, color 0.18s; } -.modern-searchbar-btn:hover { - background: #38bdf8; - color: #fff; -} + .modern-searchbar-btn:hover { + background: #38bdf8; + color: #fff; + } + +.back-colo { + background: #011329; + width: 100%; + height: 7vw; + position: fixed; +} /* Search bar at top right corner */ .searchbar-topright { position: fixed; - top: 54px; + top: 20px; right: 48px; z-index: 1001; display: flex; align-items: center; } +.back-colo { + background: #011329; + width: 100%; + height: 7vw; + position: fixed; +} + @media (max-width: 900px) { .searchbar-topright { top: 12px; @@ -656,10 +900,559 @@ width: 95vw; justify-content: flex-end; } + .modern-searchbar-form.compact { width: 100%; min-width: 0; } } +/* Modern UI header styles from infopage */ +.site-header { + background: #011329; + box-shadow: 0 2px 12px #38bdf844; + margin-bottom: 0; + position: relative; + z-index: 10; + padding-bottom: 0; +} + +.header-inner { + display: flex; + align-items: center; + justify-content: flex-start; + padding: 18px 32px 0 32px; + position: relative; +} + +.logo-cluster { + display: flex; + align-items: center; + gap: 18px; +} + +.logo-img-header { + width: 54px; + height: 54px; + border-radius: 50%; + background: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.18); + padding: 4px; + margin-top: -6px; + margin-bottom: 1vh; +} + +.py-detect-title-header { + font-size: 2.1rem; + font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif; + font-weight: 900; + letter-spacing: 6px; + color: #38bdf8; + display: flex; + align-items: center; + gap: 2px; + margin-bottom: 1.5vh; +} + + .py-detect-title-header .py-letter.p { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.y { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-shape { + color: #e3f6ff; + background: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff; + border: 2px solid #23272b; + width: 18px; + height: 4px; + display: inline-block; + margin: 0 8px; + border-radius: 2px; + } + + .py-detect-title-header .py-letter.d { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.e { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.t { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.e2 { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.c { + color: #e3f6ff; + text-shadow: 0 0 6px #38bdf8; + } + + .py-detect-title-header .py-letter.t2 { + color: #38bdf8; + text-shadow: 0 0 6px #38bdf8; + } + +body, main.content { + background: #f4f6fa; + min-height: 100vh; + margin: 0; + padding: 0; + font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif; +} + +.record-card { + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 8px #0001, 0 1.5px 0 #e5e7eb; + border: 1.5px solid #e5e7eb; + margin: 40px auto 0 auto; + max-width: 98vw; + width: 98vw; + min-width: 320px; + padding: 0; + overflow-x: auto; +} + +.record-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 24px 8px 24px; + border-bottom: 1.5px solid #e5e7eb; + background: #f8fafc; + border-radius: 10px 10px 0 0; +} + +.record-title-group { + display: flex; + align-items: center; + gap: 12px; +} + +.record-title { + font-size: 1.25rem; + font-weight: 700; + color: #222b45; +} + +.record-dropdown { + font-size: 1.05rem; + color: #222b45; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 6px; + padding: 4px 12px; + margin-left: 8px; +} + +.record-header-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.record-search { + border: 1.5px solid #e5e7eb; + border-radius: 6px; + padding: 6px 12px; + font-size: 1rem; + background: #fff; + margin-right: 8px; + min-width: 220px; +} +.record-new-btn { + background: #2563eb; + color: #fff; + font-weight: 600; + border: none; + border-radius: 6px; + padding: 7px 22px; + font-size: 1rem; + box-shadow: 0 2px 8px #2563eb22; + cursor: pointer; + transition: background 0.18s; +} + + .record-new-btn:hover { + background: #1e40af; + } + +.record-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + background: #fff; +} + + .record-table th, .record-table td { + border-bottom: 1.5px solid #e5e7eb; + padding: 12px 16px; + text-align: left; + font-size: 1.05rem; + } + + .record-table th { + background: #f4f6fa; + color: #222b45; + font-weight: 700; + } + + .record-table tr:last-child td { + border-bottom: none; + } + + .record-table tr { + transition: background 0.15s; + } + + .record-table tr:hover { + background: #f1f5f9; + } + + .record-table a { + color: #2563eb; + text-decoration: underline; + cursor: pointer; + } + + .record-table .record-status { + font-weight: 500; + border-radius: 6px; + padding: 2px 10px; + background: #f1f5f9; + color: #2563eb; + font-size: 0.98em; + } + +.vertical-sections { + display: flex; + flex-direction: column; + gap: 32px; +} + +.horizontal-sections { + display: flex; + flex-direction: column; + gap: 36px; + background: linear-gradient(120deg, #f0f7ff 0%, #e0f2fe 100%); + border-radius: 24px; + box-shadow: 0 8px 32px #2563eb22; + padding: 32px 0 32px 0; +} + +.section-block { + background: rgba(255,255,255,0.98); + border-radius: 18px; + box-shadow: 0 4px 24px #38bdf822; + padding: 28px 38px 18px 38px; + margin-bottom: 0; + display: flex; + flex-direction: column; + gap: 22px; + border-left: 6px solid #38bdf8; + transition: box-shadow 0.2s, border 0.2s; +} + + .section-block:hover { + box-shadow: 0 8px 32px #2563eb33; + border-left: 6px solid #2563eb; + } + +.section-title { + font-size: 1.32em; + font-weight: 900; + color: #1d4ed8; + margin-bottom: 12px; + letter-spacing: 1px; + text-shadow: 0 1px 0 #fff; + display: flex; + align-items: center; + gap: 10px; +} + +.subgroup-row { + display: flex; + flex-direction: row; + gap: 36px; + overflow-x: auto; +} + +.subgroup-block { + min-width: 260px; + flex: 1 1 0; + background: rgba(248,250,252,0.95); + border-radius: 14px; + box-shadow: 0 2px 8px #38bdf822; + padding: 18px 20px 10px 20px; + margin-bottom: 0; + margin-top: 8px; + display: flex; + flex-direction: column; + gap: 0; + border: 1.5px solid #e0e7ef; + transition: box-shadow 0.18s, border 0.18s, background 0.18s; +} + + .subgroup-block:hover { + box-shadow: 0 4px 16px #38bdf855; + border: 1.5px solid #38bdf8; + background: #e0f2fe; + } + +.subgroup-title { + font-size: 1.11em; + font-weight: 700; + color: #0ea5e9; + margin-bottom: 12px; + letter-spacing: 0.5px; + text-shadow: 0 1px 0 #fff; + display: flex; + align-items: center; + gap: 8px; +} + +.fields-table { + display: flex; + flex-direction: column; + gap: 0; + background: none; + box-shadow: none; + border-radius: 0; + padding: 0; +} + +.field-row { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 14px 0 14px 0; + border-bottom: 1px solid #e0e7ef; + font-size: 1.09em; + background: none; + box-shadow: none; + border-radius: 0; + transition: background 0.18s; + gap: 32px; +} + + .field-row:last-child { + border-bottom: none; + } + + .field-row span { + color: #1e293b; + font-weight: 700; + letter-spacing: 0.2px; + min-width: 180px; + font-size: 1.08em; + margin-right: 32px; + text-align: left; + display: inline-block; + } + + .field-row b { + color: #2563eb; + font-weight: 700; + word-break: break-word; + letter-spacing: 0.1px; + font-size: 1.13em; + margin-left: 8px; + text-align: left; + display: inline-block; + } + + .field-row b:hover { + color: #0ea5e9; + } + +/* Stylish pills for subgroups */ +.subgroup-pills { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 18px; +} + + .subgroup-pills button { + background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%); + border: none; + border-radius: 20px; + padding: 7px 22px; + font-size: 1em; + color: #fff; + font-weight: 700; + cursor: pointer; + box-shadow: 0 2px 8px #2563eb22; + transition: background 0.18s, color 0.18s, box-shadow 0.18s; + outline: none; + } + + .subgroup-pills button.active, + .subgroup-pills button:focus { + background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%); + color: #fff; + box-shadow: 0 4px 16px #38bdf855; + } + + .subgroup-pills button:hover { + background: linear-gradient(90deg, #0ea5e9 0%, #2563eb 100%); + color: #fff; + } + +.field-card { + display: flex; + width: 30%; + gap: 9vw; + justify-content: space-between; +} + +/* Responsive tweaks */ +@media (max-width: 900px) { + .section-block { + padding: 18px 8vw 12px 8vw; + } + + .subgroup-block { + min-width: 180px; + padding: 12px 8px 8px 8px; + } + + .field-row span { + min-width: 120px; + } +} + +@media (max-width: 600px) { + .section-block { + padding: 10px 2vw 8px 2vw; + } + + .subgroup-block { + min-width: 120px; + padding: 8px 2px 6px 2px; + } + + .field-row span { + min-width: 80px; + font-size: 0.98em; + } + + .field-row b { + font-size: 1em; + } +} + +/* Modern styles for the filter bar, dropdowns, and buttons */ +.filter-bar { + display: flex; + gap: 16px; + align-items: center; + margin: 24px 0 12px 0; + padding: 12px 18px; + background: #f8fafc; + border-radius: 12px; + box-shadow: 0 2px 8px #2563eb11; +} + + .filter-bar select { + padding: 6px 18px; + border-radius: 6px; + border: 1.5px solid #cbd5e1; + font-size: 1em; + color: #2563eb; + background: #fff; + font-weight: 600; + outline: none; + transition: border 0.15s; + } + + .filter-bar select:focus { + border: 1.5px solid #38bdf8; + } + + .filter-bar button { + padding: 6px 18px; + border-radius: 6px; + border: none; + font-size: 1em; + font-weight: 700; + cursor: pointer; + background: #2563eb; + color: #fff; + transition: background 0.15s; + } + + .filter-bar button:hover { + background: #0ea5e9; + } + + .filter-bar button:last-child { + background: #f87171; + color: #fff; + margin-left: 4px; + } + + .filter-bar button:last-child:hover { + background: #ef4444; + } + +.analytics-summary { + display: flex; + gap: 32px; + margin: 18px 0 8px 0; +} + +.summary-card { + background: #f8fafc; + border-radius: 12px; + box-shadow: 0 2px 8px #2563eb11; + padding: 18px 32px; + min-width: 120px; + text-align: center; +} + + .summary-card .summary-label { + font-size: 1em; + color: #64748b; + font-weight: 600; + margin-bottom: 6px; + } + + .summary-card .summary-value { + font-size: 2em; + font-weight: 900; + color: #2563eb; + } + + .summary-card.open .summary-value { + color: #059669; + } + + .summary-card.closed .summary-value { + color: #dc2626; + } + +.record-meta { + color: #64748b; + font-size: 0.98em; + margin: 0 0 10px 0; + padding-left: 2px; + font-weight: 500; + letter-spacing: 0.1px; +} diff --git a/src/app/recordpage/recordpage.component.html b/src/app/recordpage/recordpage.component.html index b6769d3232b193d62908f8fda447159ccbf4f93b..42d091b1556846e0c2db38b57dbb6a7414a5b6db 100644 --- a/src/app/recordpage/recordpage.component.html +++ b/src/app/recordpage/recordpage.component.html @@ -1,117 +1,179 @@ - -
-
- - - - - -
-
- - -
- PyDetect Logo -
- P - Y - - D - E - T - E - C - T + + -
- -
-
- -

Police Investigation Records

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Case ID Crime Date & Time Location Status Investigation Officer Actions
{{ c.caseId || '—' }}{{ c.crime || '—' }}{{ c.dateTime ? (c.dateTime | date:'yyyy-MM-dd HH:mm') : '—' }}{{ c.police.address || '—' }} - - {{ c.status || '—' }} - - {{ c.police.name || '—' }} - - -
No records found.
+ +
+
+
+ Police Investigation Records +
+
+ +
+
- - - - - - -
+ +
diff --git a/src/app/recordpage/recordpage.component.ts b/src/app/recordpage/recordpage.component.ts index b0e3937f71c7c891a43c3976f17c87afdd5d11fb..483173b7a756703deed1d51aa8096daa7b7b43e9 100644 --- a/src/app/recordpage/recordpage.component.ts +++ b/src/app/recordpage/recordpage.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { CaseStoreService, PoliceCase } from '../case-store.service'; +import { InfopageComponent } from '../infopage/infopage.component'; @Component({ selector: 'app-recordpage', @@ -22,14 +23,218 @@ export class RecordpageComponent implements OnInit { sortKey: 'caseId' | 'crime' | 'dateTime' | 'location' | 'status' | 'Investigation Officer' = 'dateTime'; sortDir: 'asc' | 'desc' = 'desc'; + objectKeys = Object.keys; + + // For modal subgroup pills + selectedSubgroup: any = { + crime: 'Identification & Timing', + suspect: 'Identity', + notes: 'Investigation Notes' + }; + + // Reference infopage section/subgroup/fields structure + sections = new InfopageComponent(null as any, null as any).sections; + + getSubgroups(sectionKey: string): string[] { + return Object.keys(this.sections[sectionKey].subgroups); + } + + getFieldsForSubgroup(sectionKey: string, subgroup: string): string[] { + return this.sections[sectionKey].subgroups[subgroup] || []; + } + + selectSubgroup(sectionKey: string, subgroup: string) { + this.selectedSubgroup[sectionKey] = subgroup; + } + + getFieldValue(sc: any, sectionKey: string, field: string): any { + // Map field labels to PoliceCase property paths + const fieldMap: { [key: string]: string | string[] } = { + // Crime Details + 'Case ID': 'caseId', + 'FIR / Ref #': 'firRef', + 'Crime Type': 'crime', + 'Case Category': 'caseCategory', + 'Date & Time (Entry)': 'dateTime', + 'Occurred From': 'occurredFrom', + 'Occurred To': 'occurredTo', + 'Time Reported': 'timeReported', + 'Time Discovered': 'timeDiscovered', + 'Country': 'country', + 'State': 'state', + 'District': 'district', + 'Number of Victims': 'numberOfVictims', + 'Brief Description': 'briefDescription', + 'Location': ['police', 'address'], + 'Jurisdiction / PS': 'jurisdiction', + 'Scene Type': 'sceneType', + 'Reported By': 'reportedBy', + 'Reported Contact': 'reportedContact', + 'Witness Count': 'witnessCount', + 'Victim Name': 'victimName', + 'Victim Contact': 'victimContact', + 'Victim Summary': 'victimSummary', + 'Suspected Offender Known?': 'suspectedOffenderKnown', + 'Suspect Link': 'suspectLink', + 'Legal Sections / Charges': 'legalSections', + 'Offence Category': 'offenceCategory', + 'Offence Description': 'offenceDescription', + 'Suspected Motive': 'suspectedMotive', + 'Confirmed Motive': 'confirmedMotive', + 'Weapon Involved': 'weaponInvolved', + 'Property Loss / Damage': 'propertyLoss', + 'Evidence Collected': 'evidenceCollected', + 'Forensic Tests Required': 'forensicTestsRequired', + 'Scene Condition': 'sceneCondition', + 'Photos / Video?': 'photosVideo', + 'CCTV Present?': 'cctvPresent', + 'CCTV Sources / IDs': 'cctvSources', + 'Physical Evidence (list)': 'physicalEvidence', + 'Chain of Custody?': 'chainOfCustody', + 'Digital Evidence': 'digitalEvidence', + 'Evidence Storage Reference': 'evidenceStorageReference', + 'Investigating Officer': ['police', 'name'], + 'Duty Person': ['police', 'dutyPerson'], + 'Supervising Officer': ['police', 'supervisingOfficer'], + 'Patrol Notes': ['police', 'patrolNotes'], + 'Arrest Made': 'arrestMade', + 'Arrest Location': 'arrestLocation', + 'Initial Actions Taken': 'initialActionsTaken', + 'riskLevel': 'riskLevel', + 'Confidentiality': 'confidentiality', + 'Biometric / Forensic IDs': 'biometricIds', + 'DNA Ref ID': 'dnaRefId', + 'Fingerprint ID': 'fingerprintId', + 'Case Status': 'status', + 'Linked Cases': 'linkedCases', + 'arrestCount': 'arrestCount', + 'Case Priority': 'casePriority', + 'Follow-up Date': 'followUpDate', + 'Court Case ID': 'courtCaseId', + 'Next Hearing Date': 'nextHearingDate', + 'Final Summary': 'finalSummary', + 'Remark': 'remark', + // Suspect Details + 'Suspect ID': ['accused', 'suspectId'], + 'Suspect Name': ['accused', 'name'], + 'Alias / Nickname': ['accused', 'alias'], + 'Age': ['accused', 'age'], + 'Gender': ['accused', 'gender'], + 'Nationality': ['accused', 'nationality'], + 'Nationality ID / Passport Number': ['accused', 'passportNumber'], + 'Languages': ['accused', 'languages'], + 'Address': ['accused', 'address'], + 'Known Aliases': ['accused', 'knownAliases'], + 'Government ID': ['accused', 'governmentId'], + 'Height (cm)': ['accused', 'height'], + 'Weight (kg)': ['accused', 'weight'], + 'Build': ['accused', 'build'], + 'Hair Color': ['accused', 'hairColor'], + 'Eye Color': ['accused', 'eyeColor'], + 'Distinguishing Marks': ['accused', 'distinguishingMarks'], + 'Tattoo Details': ['accused', 'tattooDetails'], + 'Scar Details': ['accused', 'scarDetails'], + 'Photo Upload': ['accused', 'photoUpload'], + 'Employment': ['accused', 'employment'], + 'Education': ['accused', 'education'], + 'Occupation': ['accused', 'occupation'], + 'Company': ['accused', 'company'], + 'Workplace Address': ['accused', 'workplaceAddress'], + 'Marital Status': ['accused', 'maritalStatus'], + 'Known Habits': ['accused', 'knownHabits'], + 'Known Financial Details': ['accused', 'knownFinancialDetails'], + 'Associate Names': ['accused', 'associateNames'], + 'Gang Affiliation': ['accused', 'gangAffiliation'], + 'Family Connections': ['accused', 'familyConnections'], + 'Social Media Handles': ['accused', 'socialMediaHandles'], + 'Criminal History': ['accused', 'criminalHistory'], + 'Prior Arrests': ['accused', 'priorArrests'], + 'Probation/Parole Status': ['accused', 'probationStatus'], + // Notes/Evidence + 'Initial Findings': ['police', 'information'], + 'Detailed Notes': ['notes', 'detailedNotes'], + 'Status': 'status', + 'Version History / Updates': ['notes', 'versionHistory'], + 'Evidence Photos': ['legal', 'evidencePhotos'], + 'Evidence Videos': ['legal', 'evidenceVideos'], + 'Evidence Documents': ['legal', 'evidenceDocuments'], + 'Links to Evidence': ['legal', 'linksToEvidence'], + 'Final Recommendations': ['legal', 'finalRecommendations'], + 'Witness Statements': ['legal', 'witnessStatements'], + 'Confessions': ['legal', 'confessions'], + // Audit Fields + 'Created By': 'createdBy', + 'Creation Date': 'creationDate', + 'Last Updated': 'lastUpdated', + 'Verified By': 'verifiedBy' + }; + + const path = fieldMap[field] || field; + if (Array.isArray(path)) { + let value = sc; + for (const p of path) { + if (value && value[p] !== undefined) value = value[p]; + else return '—'; + } + return value !== undefined && value !== null && value !== '' ? value : '—'; + } else { + return sc[path] !== undefined && sc[path] !== null && sc[path] !== '' ? sc[path] : '—'; + } + } + + getValue(obj: any, key: string): any { + return obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== '' ? obj[key] : '—'; + } + constructor(private caseStore: CaseStoreService, private router: Router) { } + // Filter state + filterCrimeType: string = ''; + filterStatus: string = ''; + filterLocation: string = ''; + filterOfficer: string = ''; + + crimeTypes: string[] = []; + statusTypes: string[] = []; + locations: string[] = []; + officers: string[] = []; + + filteredCases: PoliceCase[] = []; + ngOnInit(): void { this.load(); + this.populateFilterOptions(); + this.applyFilters(); } load(): void { this.cases = this.caseStore.getPoliceCases(); + this.populateFilterOptions(); + this.applyFilters(); + } + + populateFilterOptions() { + this.crimeTypes = [...new Set(this.cases.map(c => c.crime).filter(Boolean))] as string[]; + this.statusTypes = [...new Set(this.cases.map(c => c.status).filter(Boolean))] as string[]; + this.locations = [...new Set(this.cases.map(c => c.police?.address).filter(Boolean))] as string[]; + this.officers = [...new Set(this.cases.map(c => c.police?.name).filter(Boolean))] as string[]; + } + + applyFilters() { + this.filteredCases = this.cases.filter(c => + (!this.filterCrimeType || c.crime === this.filterCrimeType) && + (!this.filterStatus || c.status === this.filterStatus) && + (!this.filterLocation || c.police?.address === this.filterLocation) && + (!this.filterOfficer || c.police?.name === this.filterOfficer) + ); + } + + resetFilters() { + this.filterCrimeType = ''; + this.filterStatus = ''; + this.filterLocation = ''; + this.filterOfficer = ''; + this.applyFilters(); } // search over visible columns (no accused) @@ -71,27 +276,23 @@ export class RecordpageComponent implements OnInit { } } + // Update your table to use filteredCases instead of rows get rows(): PoliceCase[] { - const out = this.filtered.slice(); - const key = this.sortKey, dir = this.sortDir === 'asc' ? 1 : -1; - out.sort((a, b) => { - const va = this.cell(a, key), vb = this.cell(b, key); - if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir; - return va.toString().localeCompare(vb.toString()) * dir; - }); - return out; + return this.filteredCases; } openDetails(c: PoliceCase, i: number): void { this.selectedCase = c; this.selectedIndex = i; this.showDetails = true; + document.body.classList.add('modal-open'); } closeDetails(): void { this.showDetails = false; this.selectedCase = null; this.selectedIndex = -1; + document.body.classList.remove('modal-open'); } editCase(c: PoliceCase, i: number): void { @@ -108,4 +309,33 @@ export class RecordpageComponent implements OnInit { navigateHome(): void { this.router.navigate(['/']); } + + onNewRecord(): void { + // Placeholder: implement record creation logic or modal here + alert('New record functionality coming soon!'); + } + + deleteCase(index: number): void { + if (confirm('Are you sure you want to delete this case?')) { + this.caseStore.deletePoliceCaseAt(index); + this.load(); + } + } + + verifyCase(index: number): void { + const adminName = 'Admin'; // Replace with actual admin name if available + const updated = { ...this.cases[index], verifiedBy: adminName, lastUpdated: new Date().toISOString() }; + this.caseStore.updatePoliceCaseAt(index, updated); + this.load(); + } + + get totalCases(): number { + return this.cases.length; + } + get openCases(): number { + return this.cases.filter(c => c.status === 'Open').length; + } + get closedCases(): number { + return this.cases.filter(c => c.status === 'Closed').length; + } } diff --git a/src/app/shared/case-store.service.ts b/src/app/shared/case-store.service.ts index c47867714eac86ab0e4949e293ad868a500604e0..dfa4d0a26f58071ca74ae95c487ad364cb2d3ed0 100644 --- a/src/app/shared/case-store.service.ts +++ b/src/app/shared/case-store.service.ts @@ -22,6 +22,9 @@ export interface PoliceCase { address: string; occupation?: string; }; + lastUpdated?: string; + nextAction?: string; + casePriority?: 'High' | 'Medium' | 'Low'; } @Injectable({ providedIn: 'root' }) diff --git a/src/app/shared/truncate.pipe.ts b/src/app/shared/truncate.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/assets/1-old.JPG b/src/assets/1-old.JPG new file mode 100644 index 0000000000000000000000000000000000000000..f1ba6b6e979f2eced769297df2cd14e310052908 --- /dev/null +++ b/src/assets/1-old.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f1b4dfea042dab76916683485000d63160b27d683654731fd72e72788170b5d +size 122786 diff --git a/src/assets/2.JPG b/src/assets/2.JPG new file mode 100644 index 0000000000000000000000000000000000000000..0e6316fee10970a13923b80111473d5308ac58c9 --- /dev/null +++ b/src/assets/2.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28a45e50f5d64d9b04d1dc4587cb2a3ed5b1a1ea160e3eb760679c4051050730 +size 20112 diff --git a/src/assets/3.JPG b/src/assets/3.JPG new file mode 100644 index 0000000000000000000000000000000000000000..64a1d2cd5e8c500f46e2562b242e838a9420b832 --- /dev/null +++ b/src/assets/3.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d33bddc759b3afb011c827bfdba6d625a44bcf7913fc625715af52b393b3c2e0 +size 25851 diff --git a/src/assets/4.JPG b/src/assets/4.JPG new file mode 100644 index 0000000000000000000000000000000000000000..841c138562bd04785ca138651ff4b9fef17ec4da --- /dev/null +++ b/src/assets/4.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d86fed9a3d8d98d86473b595847b5125fb3116c1d2cfc20cc78d70496559961 +size 17043 diff --git a/src/assets/5.JPG b/src/assets/5.JPG new file mode 100644 index 0000000000000000000000000000000000000000..ee92f751bbdaf0a4c579338bb3456d9683d4774f --- /dev/null +++ b/src/assets/5.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c49a044008ebac42ef09eb3eccc1a7ea3a5f30fbae6a68f805872df6a6fd8e29 +size 27009 diff --git a/src/assets/6.JPG b/src/assets/6.JPG new file mode 100644 index 0000000000000000000000000000000000000000..c573c977111afc3f808a768e0882a8a7f796e163 --- /dev/null +++ b/src/assets/6.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bad6b2ee1a997c3087464338a019051c2f9891042d9713d55b9d3e9b983b6a59 +size 17333 diff --git a/src/assets/7.JPG b/src/assets/7.JPG new file mode 100644 index 0000000000000000000000000000000000000000..39cf094f2cdf9f91cfca4fa6dc8f092b15afa062 --- /dev/null +++ b/src/assets/7.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5e516318830ae7f14768744c6f68d088d94f094f65b7f773132bb76ab828da2 +size 22003 diff --git a/src/assets/8.JPG b/src/assets/8.JPG new file mode 100644 index 0000000000000000000000000000000000000000..162d53fc9f3fc020a0263207bcad3cf1221baa62 --- /dev/null +++ b/src/assets/8.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bfe9727ff52a357f202e42d0099033f7a353d8dc4b4b516c2345baafd328340 +size 16047 diff --git a/src/assets/9.JPG b/src/assets/9.JPG new file mode 100644 index 0000000000000000000000000000000000000000..8572983f435cb6eff2f006e01e5e3ef18cb78544 --- /dev/null +++ b/src/assets/9.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b38734f3ad661443854decea4a3af3344c6132182b6eef6b785708608b9a1157 +size 176460 diff --git a/src/assets/background-2.jpg b/src/assets/background-2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a0d44ffb04aaab9891e58d0b2d03129542bb686b --- /dev/null +++ b/src/assets/background-2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb3629b8b1cc4f1d22cdc586b1c704e82686ec5b8772d23448245038aaab6be +size 4073811 diff --git a/src/assets/background.jpg b/src/assets/background.jpg index 570ac40a28446d89831f3812c24e25f1500713a1..a0d44ffb04aaab9891e58d0b2d03129542bb686b 100644 --- a/src/assets/background.jpg +++ b/src/assets/background.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92f913300dfd5ef30b16121cf273e13d43f96cbdb442d79ac450281e643456b9 -size 587188 +oid sha256:edb3629b8b1cc4f1d22cdc586b1c704e82686ec5b8772d23448245038aaab6be +size 4073811 diff --git a/src/assets/background1234.jpg b/src/assets/background1234.jpg new file mode 100644 index 0000000000000000000000000000000000000000..570ac40a28446d89831f3812c24e25f1500713a1 --- /dev/null +++ b/src/assets/background1234.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92f913300dfd5ef30b16121cf273e13d43f96cbdb442d79ac450281e643456b9 +size 587188 diff --git a/src/assets/font/EBGaramond-Medium.ttf b/src/assets/font/EBGaramond-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..38af72ca620ec75361c1eaa36149b8f17caee040 --- /dev/null +++ b/src/assets/font/EBGaramond-Medium.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e2105ea6bbb11a408667c03e5f0fa19f69fffe417fffe9b915baee4bda9c77f +size 561508 diff --git a/src/assets/font/EBGaramond-Regular.ttf b/src/assets/font/EBGaramond-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e860ce459bdbec5cf8153c93fccb63372ff9b504 --- /dev/null +++ b/src/assets/font/EBGaramond-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a45a5040e1a328d056871d42aa70f10f7ca3bf5c447887b80720542feeead5f2 +size 558640 diff --git a/src/assets/font/Roliana-Regular.otf b/src/assets/font/Roliana-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..0b14e591d06162d54a6f37a4865af18099263164 --- /dev/null +++ b/src/assets/font/Roliana-Regular.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1f3947d7128de625e8815bc66bd3ece11329410519e8c0a3eef04f9bf03ae41 +size 110052 diff --git a/src/assets/hero-jpg.jpg b/src/assets/hero-jpg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6ee7dd783dfca141691797c512ecc07eb13d3ee9 --- /dev/null +++ b/src/assets/hero-jpg.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a8f6e92b08b67f73540db6b998140eb666b2734668adb05612479748b46c79c +size 120843 diff --git a/src/assets/hero-old.png b/src/assets/hero-old.png new file mode 100644 index 0000000000000000000000000000000000000000..7295733b43a2553f644f3bd89113f8616ff1a65c --- /dev/null +++ b/src/assets/hero-old.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21a7fb629c07d487a166c58d33145b2a5e525680deb33f977ba2a19ab0571bd3 +size 1222843 diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..4a704fe488cc86450173bb10783e062ea42cca8a --- /dev/null +++ b/src/assets/hero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5a30310076faae7dc54e0c1f646956beb819c17e3cd620be7821908b39cbb45 +size 1478287 diff --git a/src/assets/home.png b/src/assets/home.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec021ea4066cc5ae930255f0540d17ac4729cf3 --- /dev/null +++ b/src/assets/home.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:743f3ffc55716e7e11aacae879790d9cdde1bf597257ca3f345cbd70eccd19a8 +size 1475352 diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/index.html b/src/index.html index d7cc26abe703d1ae2c63d049b154d3d7340b6f0b..99ed506f42b1f982fe380fad70a437e697141bac 100644 --- a/src/index.html +++ b/src/index.html @@ -5,11 +5,13 @@ Py-Detect - + + + + - diff --git a/src/pykara-favicon.ico b/src/pykara-favicon.ico deleted file mode 100644 index b9f179fad6b2e0c7c764ef99eaa93fc5646a5642..0000000000000000000000000000000000000000 Binary files a/src/pykara-favicon.ico and /dev/null differ diff --git a/src/styles.css b/src/styles.css index b46d4643d8595fc7d264719c9b7096e98b03f50e..9b657b6826fe6f661e8d3ab838a04c4a9e5d5930 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,12 +1,125 @@ body, html { margin: 0; padding: 0; - font-family: Arial, sans-serif; + font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; background: url('/assets/background.jpg') no-repeat center center fixed; background-size: cover; + transition: background-color 0.3s ease, color 0.3s ease; } +/* Theme Support */ +body.dark-theme { + background-color: #0a0e16; + color: #ffffff; +} + +body.light-theme { + background-color: #f8fafc; + color: #1e293b; +} + +/* Homepage specific background */ body.homepage-bg { - background: url('/assets/background.jpg') no-repeat center center fixed; + background: url('/assets/hero.png') no-repeat center center fixed; background-size: cover; } + +@font-face { + font-family: 'Roliana'; + src: url('/assets/font/Roliana-Regular.otf') format('opentype'); + font-weight: normal; + font-style: normal; +} + +/* Enhanced Typography */ +h1, h2, h3, h4, h5, h6 { + font-family: 'Inter', sans-serif; + font-weight: 700; + line-height: 1.2; + margin: 0; +} + +/* Code Elements */ +code, pre, .code { + font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Consolas', monospace; +} + +/* Smooth Scrolling */ +html { + scroll-behavior: smooth; +} + +/* Selection Styling */ +::selection { + background: rgba(0, 212, 255, 0.3); + color: #ffffff; +} + +::-moz-selection { + background: rgba(0, 212, 255, 0.3); + color: #ffffff; +} + +/* Focus Outline Override */ +*:focus { + outline: 2px solid #00d4ff; + outline-offset: 2px; +} + +/* Enhanced Scrollbar for WebKit browsers */ +::-webkit-scrollbar { + width: 12px; +} + +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 6px; +} + +::-webkit-scrollbar-thumb { + background: linear-gradient(135deg, #00d4ff 0%, #ff6b35 100%); + border-radius: 6px; + border: 2px solid transparent; + background-clip: content-box; +} + +::-webkit-scrollbar-thumb:hover { + background: linear-gradient(135deg, #0099cc 0%, #e55d2b 100%); + background-clip: content-box; +} + +/* Firefox Scrollbar */ +html { + scrollbar-width: thin; + scrollbar-color: #00d4ff rgba(0, 0, 0, 0.1); +} + +/* Material Design Compatibility */ +.mat-typography { + font-family: 'Inter', sans-serif; +} + +/* Utility Classes */ +.visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +/* Animation Preferences */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +}