| import { Component, OnInit, OnDestroy } from '@angular/core'; |
| import { Router } from '@angular/router'; |
| import { CaseStoreService, PoliceCase } from '../shared/case-store.service'; |
| import { InfopageComponent } from '../infopage/infopage.component'; |
| import { Subscription } from 'rxjs'; |
|
|
| @Component({ |
| selector: 'app-recordpage', |
| templateUrl: './recordpage.component.html', |
| styleUrls: ['./recordpage.component.css'] |
| }) |
| export class RecordpageComponent implements OnInit, OnDestroy { |
| cases: PoliceCase[] = []; |
| private casesSub?: Subscription; |
|
|
| |
| dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']); |
| dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']); |
|
|
| |
| currentPage: number = 1; |
| pageSize: number = 5; |
| get totalPages(): number { |
| return Math.ceil(this.filteredCases.length / this.pageSize) || 1; |
| } |
| get pagedCases(): PoliceCase[] { |
| const start = (this.currentPage - 1) * this.pageSize; |
| return this.filteredCases.slice(start, start + this.pageSize); |
| } |
| setPage(page: number) { |
| if (page < 1 || page > this.totalPages) return; |
| this.currentPage = page; |
| } |
| nextPage() { this.setPage(this.currentPage + 1); } |
| prevPage() { this.setPage(this.currentPage - 1); } |
|
|
| |
| showDetails = false; |
| selectedCase: PoliceCase | null = null; |
| selectedIndex = -1; |
|
|
| |
| q = ''; |
|
|
| |
| sortKey: 'caseId' | 'crime' | 'dateTime' | 'location' | 'status' | 'Investigation Officer' = 'dateTime'; |
| sortDir: 'asc' | 'desc' = 'desc'; |
|
|
| objectKeys = Object.keys; |
|
|
| |
| selectedSubgroup: any = { |
| crime: 'Identification & Timing', |
| suspect: 'Identity', |
| notes: 'Investigation Notes' |
| }; |
|
|
| |
| 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 { |
| |
| const fieldMap: { [key: string]: string | string[] } = { |
| |
| '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 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'], |
| |
| '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'], |
| |
| 'Created By': 'createdBy', |
| 'Creation Date': 'creationDate', |
| 'Last Updated': 'lastUpdated', |
| 'Verified By': 'verifiedBy' |
| }; |
|
|
| const path = fieldMap[field] || field; |
| let value: any = undefined; |
| if (Array.isArray(path)) { |
| let v = sc; |
| for (const p of path) { |
| if (v && v[p] !== undefined) v = v[p]; |
| else { v = undefined; break; } |
| } |
| value = v; |
| } else { |
| value = sc && sc[path] !== undefined ? sc[path] : undefined; |
| } |
|
|
| |
| if (value === null || value === undefined || value === '') { |
| try { |
| const fd = this.getFormDataArray(sc); |
| const norm = (s: any) => { |
| if (s === null || s === undefined) return ''; |
| let t = String(s).toLowerCase(); |
| t = t.replace(/&/g, ''); |
| t = t.replace(/and/g, ''); |
| t = t.replace(/entry/g, ''); |
| t = t.replace(/\s+/g, ''); |
| return t.replace(/[^a-z0-9]/g, ''); |
| }; |
|
|
| if (fd && fd.length) { |
| let kv = fd.find(k => k && String(k.key).toLowerCase() === String(field).toLowerCase()); |
| if (kv) value = kv.value; |
| if (value === null || value === undefined || value === '') { |
| const fieldNorm = norm(field); |
| const pathName = Array.isArray(path) ? path[path.length -1] : String(path); |
| const pathNorm = norm(pathName); |
| kv = fd.find(k => k && (norm(k.key) === fieldNorm || norm(k.key) === pathNorm || norm(k.key).includes(fieldNorm) || fieldNorm.includes(norm(k.key)))); |
| if (kv) value = kv.value; |
| } |
| } |
|
|
| if ((value === null || value === undefined || value === '') && sc && sc.formData && typeof sc.formData === 'object') { |
| if (sc.formData[field] !== undefined) value = sc.formData[field]; |
| else { |
| const fieldNorm = (s: any) => s === null || s === undefined ? '' : String(s).toLowerCase().replace(/[^a-z0-9]/g, ''); |
| const target = fieldNorm(field); |
| for (const k of Object.keys(sc.formData)) { |
| if (fieldNorm(k) === target || k.toLowerCase() === field.toLowerCase() || k.toLowerCase().includes(field.toLowerCase())) { |
| value = sc.formData[k]; |
| break; |
| } |
| } |
| } |
| } |
| } catch (e) { |
| |
| } |
| } |
|
|
| |
| if (value === null || value === undefined || value === '') return '—'; |
|
|
| |
| if ((this.dateTimeFields && this.dateTimeFields.has(field)) || (this.dateFields && this.dateFields.has(field))) { |
| const d = new Date(value); |
| if (!isNaN(d.getTime())) { |
| if (this.dateFields && this.dateFields.has(field)) return d.toISOString().slice(0,10); |
| return d.toLocaleString(); |
| } |
| } |
|
|
| if (typeof value === 'object') return this.formatFormValue(value); |
| return value; |
| } |
|
|
| getValue(obj: any, key: string): any { |
| const v = obj && obj[key] !== undefined ? obj[key] : undefined; |
| if (v === null || v === undefined || v === '') return '—'; |
| if (typeof v === 'object') return this.formatFormValue(v); |
| if ((this.dateTimeFields && this.dateTimeFields.has(key)) || (this.dateFields && this.dateFields.has(key))) { |
| const d = new Date(v); |
| if (!isNaN(d.getTime())) { |
| if (this.dateFields && this.dateFields.has(key)) return d.toISOString().slice(0,10); |
| return d.toLocaleString(); |
| } |
| } |
| return v; |
| } |
|
|
| |
| isFormDataArray(fd: any): boolean { |
| return Array.isArray(fd) && fd.length >0 && fd.every((item: any) => item && Object.prototype.hasOwnProperty.call(item, 'key')); |
| } |
|
|
| getFormDataArray(caseObj: any): Array<{ key: string; value: any }> { |
| if (!caseObj || !caseObj.formData) return []; |
| const fd = caseObj.formData; |
| if (this.isFormDataArray(fd)) return fd as Array<{ key: string; value: any }>; |
| if (typeof fd === 'object') return Object.keys(fd).map(k => ({ key: k, value: fd[k] })); |
| return [{ key: 'value', value: fd }]; |
| } |
|
|
| formatFormValue(value: any): string { |
| if (value === null || value === undefined || value === '') return '—'; |
| if (typeof value === 'object') { |
| try { return JSON.stringify(value, null,2); } catch { return String(value); } |
| } |
| return String(value); |
| } |
|
|
| constructor(private caseStore: CaseStoreService, private router: Router) { } |
|
|
| |
| filterCrimeType: string = ''; |
| filterStatus: string = ''; |
| filterLocation: string = ''; |
| filterOfficer: string = ''; |
|
|
| crimeTypes: string[] = []; |
| statusTypes: string[] = []; |
| locations: string[] = []; |
| officers: string[] = []; |
|
|
| filteredCases: PoliceCase[] = []; |
|
|
| |
| allSelected: boolean = false; |
|
|
| ngOnInit(): void { |
| |
| if (typeof this.caseStore.getCases$ === 'function') { |
| this.casesSub = this.caseStore.getCases$().subscribe((cases: PoliceCase[]) => { |
| this.cases = cases || []; |
| this.cases.forEach((c: any) => { if (c.selected === undefined) c.selected = false; }); |
| this.populateFilterOptions(); |
| this.applyFilters(); |
| this.applySort(); |
| }); |
| } else { |
| |
| this.load(); |
| this.populateFilterOptions(); |
| this.applyFilters(); |
| this.applySort(); |
| } |
| } |
|
|
| ngOnDestroy(): void { |
| if (this.casesSub) this.casesSub.unsubscribe(); |
| } |
|
|
| load(): void { |
| this.cases = this.caseStore.getPoliceCases(); |
| this.cases.forEach((c: any) => { if (c.selected === undefined) c.selected = false; }); |
| this.populateFilterOptions(); |
| this.applyFilters(); |
| } |
|
|
| toggleSelectAll(event: Event): void { |
| const checked = (event.target as HTMLInputElement).checked; |
| this.allSelected = checked; |
| this.rows.forEach((c: any) => c.selected = checked); |
| } |
|
|
| 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() { |
| |
| let filtered = 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) |
| ); |
| |
| const s = (this.q || '').toLowerCase(); |
| if (s) { |
| filtered = filtered.filter(c => |
| (c.caseId || '').toString().toLowerCase().includes(s) || |
| (c.crime || '').toLowerCase().includes(s) || |
| (c.police?.address || '').toLowerCase().includes(s) || |
| (c.status || '').toLowerCase().includes(s) || |
| (c.police?.name || '').toLowerCase().includes(s) |
| ); |
| } |
| this.filteredCases = filtered; |
| this.currentPage =1; |
| this.applySort(); |
| } |
|
|
| resetFilters() { |
| this.filterCrimeType = ''; |
| this.filterStatus = ''; |
| this.filterLocation = ''; |
| this.filterOfficer = ''; |
| this.applyFilters(); |
| } |
|
|
| |
| get filtered(): PoliceCase[] { |
| const s = (this.q || '').toLowerCase(); |
| if (!s) return this.cases; |
| return this.cases.filter(c => |
| (c.caseId || '').toString().toLowerCase().includes(s) || |
| (c.crime || '').toLowerCase().includes(s) || |
| (c.police?.address || '').toLowerCase().includes(s) || |
| (c.status || '').toLowerCase().includes(s) || |
| (c.police?.name || '').toLowerCase().includes(s) |
| ); |
| } |
|
|
| |
| setSort(key: typeof this.sortKey) { |
| if (this.sortKey === key) { |
| this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc'; |
| } else { |
| this.sortKey = key; |
| this.sortDir = key === 'dateTime' ? 'desc' : 'asc'; |
| } |
| this.applySort(); |
| } |
| isAsc(key: typeof this.sortKey) { |
| return this.sortKey === key && this.sortDir === 'asc'; |
| } |
| isDesc(key: typeof this.sortKey) { |
| return this.sortKey === key && this.sortDir === 'desc'; |
| } |
| ariaSort(key: typeof this.sortKey) { |
| return this.sortKey === key ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none'; |
| } |
|
|
| applySort() { |
| const key = this.sortKey; |
| const dir = this.sortDir; |
| this.filteredCases.sort((a, b) => { |
| let aVal: any, bVal: any; |
| switch (key) { |
| case 'caseId': |
| aVal = a.caseId || ''; |
| bVal = b.caseId || ''; |
| break; |
| case 'crime': |
| aVal = a.crime || ''; |
| bVal = b.crime || ''; |
| break; |
| case 'dateTime': |
| aVal = a.dateTime ? new Date(a.dateTime).getTime() :0; |
| bVal = b.dateTime ? new Date(b.dateTime).getTime() :0; |
| break; |
| case 'location': |
| aVal = a.police?.address || ''; |
| bVal = b.police?.address || ''; |
| break; |
| case 'status': |
| aVal = a.status || ''; |
| bVal = b.status || ''; |
| break; |
| case 'Investigation Officer': |
| aVal = a.police?.name || ''; |
| bVal = b.police?.name || ''; |
| break; |
| default: |
| aVal = ''; |
| bVal = ''; |
| } |
| if (aVal < bVal) return dir === 'asc' ? -1 :1; |
| if (aVal > bVal) return dir === 'asc' ?1 : -1; |
| return 0; |
| }); |
| } |
|
|
| |
| get rows(): PoliceCase[] { |
| return this.pagedCases; |
| } |
|
|
| 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 { |
| |
| const prefill: Record<string, any> = {}; |
| try { |
| |
| const fd = (c as any).formData; |
| if (fd && Array.isArray(fd)) { |
| (fd as Array<any>).forEach(kv => { if (kv && kv.key) prefill[kv.key] = kv.value; }); |
| } else if (fd && typeof fd === 'object') { |
| Object.assign(prefill, fd as Record<string, any>); |
| } |
| |
| if (c.caseId) prefill['Case ID'] = c.caseId; |
| if (c.crime) prefill['Crime Type'] = c.crime; |
| if (c.dateTime) prefill['Date & Time (Entry)'] = c.dateTime; |
| if (c.police && c.police.address) prefill['Location'] = c.police.address; |
| if (c.police && c.police.name) prefill['Investigating Officer'] = c.police.name; |
| if (c.accused && c.accused.name) prefill['Suspect Name'] = c.accused.name; |
| } catch (e) { |
| |
| } |
|
|
| this.router.navigate(['/infopage', c.caseId], { state: { from: 'record', returnId: c.caseId, prefillFormData: prefill, case: c } }); |
| } |
|
|
| |
| private resolveOrigin(): string { |
| try { |
| const cur = this.router.url || ''; |
| if (cur.includes('/case-details')) return 'case-details'; |
| if (cur.includes('/record')) return 'record'; |
| } catch {} |
| return 'record'; |
| } |
|
|
| navigateToCaseDetails(c: PoliceCase): void { |
| if (!c || !c.caseId) return; |
| const origin = this.resolveOrigin(); |
| |
| const from = origin; |
| this.router.navigate(['/case-details-summary-page', c.caseId], { queryParams: { from, returnId: c.caseId }, state: { case: c, from, returnId: c.caseId } }); |
| } |
|
|
| viewSummary(caseId: string) { |
| const origin = this.resolveOrigin(); |
| this.router.navigate(['/case-details-summary-page', caseId], { queryParams: { from: origin, returnId: caseId }, state: { from: origin, returnId: caseId } }); |
| } |
|
|
| onModernSearch() { |
| |
| |
| return false; |
| } |
|
|
| navigateHome(): void { |
| this.router.navigate(['/']); |
| } |
|
|
| onNewRecord(): void { |
| |
| 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'; |
| 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; |
| } |
|
|
| getPagination(): (number | string)[] { |
| const pages: (number | string)[] = []; |
| const total = this.totalPages; |
| if (total <= 7) { |
| for (let i = 1; i <= total; i++) pages.push(i); |
| } else { |
| if (this.currentPage <= 4) { |
| for (let i = 1; i <= 5; i++) pages.push(i); |
| pages.push('...'); |
| pages.push(total); |
| } else if (this.currentPage >= total - 3) { |
| pages.push(1); |
| pages.push('...'); |
| for (let i = total - 4; i <= total; i++) pages.push(i); |
| } else { |
| pages.push(1); |
| pages.push('...'); |
| for (let i = this.currentPage - 1; i <= this.currentPage + 1; i++) pages.push(i); |
| pages.push('...'); |
| pages.push(total); |
| } |
| } |
| return pages; |
| } |
|
|
| goToPage(page: string | number) { |
| if (typeof page === 'number') { |
| this.setPage(page); |
| } |
| } |
|
|
| pageSizeOptions: number[] = [5, 10, 20, 50]; |
| onPageSizeChange(size: number) { |
| this.pageSize = size; |
| this.currentPage = 1; |
| } |
| get resultsStart(): number { |
| return this.filteredCases.length === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1; |
| } |
| get resultsEnd(): number { |
| return Math.min(this.currentPage * this.pageSize, this.filteredCases.length); |
| } |
| get resultsTotal(): number { |
| return this.filteredCases.length; |
| } |
|
|
| goToDetect(caseId: string): void { |
| this.router.navigate(['/py-detect'], { state: { caseId } }); |
| } |
|
|
| navigateBackToInfoPage(): void { |
| this.router.navigate(['/infopage']); |
| } |
|
|
| logout(): void { |
| |
| |
| window.location.href = '/'; |
| } |
| } |
|
|