import { Component, OnInit, OnDestroy } from '@angular/core'; import { Router } from '@angular/router'; import { CaseStoreService, PoliceCase } from '../shared/case-store.service'; import { INFOPAGE_SECTIONS } from '../shared/infopage-sections'; 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; // Date field groups for formatting dateTimeFields = new Set(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']); dateFields = new Set(['Follow-up Date', 'Next Hearing Date']); // Pagination currentPage: number = 1; pageSize: number = 5; pageSizeOptions: number[] = [5, 10, 20, 50]; 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); } // Modal state showDetails = false; selectedCase: PoliceCase | null = null; selectedIndex = -1; // Search query q = ''; // Sorting sortKey: 'caseId' | 'crime' | 'dateTime' | 'status' | 'Investigation Officer' = 'dateTime'; sortDir: 'asc' | 'desc' = 'desc'; // Filter state filterCrimeType: string = ''; filterStatus: string = ''; filterLocation: string = ''; filterOfficer: string = ''; crimeTypes: string[] = []; statusTypes: string[] = []; locations: string[] = []; officers: string[] = []; filteredCases: PoliceCase[] = []; // Selection state allSelected: boolean = false; // For modal subgroup pills selectedSubgroup: any = { crime: 'Identification & Timing', suspect: 'Identity', notes: 'Investigation Notes' }; // Reference infopage section/subgroup/fields structure sections = INFOPAGE_SECTIONS; constructor(private caseStore: CaseStoreService, private router: Router) { } 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(); } // Helper methods 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; } // Case statistics 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; } // Table rows getter get rows(): PoliceCase[] { return this.pagedCases; } // Pagination results 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; } // Navigation methods navigateHome(): void { this.router.navigate(['/']); } navigateBackToInfoPage(): void { this.router.navigate(['/infopage']); } logout(): void { // Implement proper logout logic localStorage.clear(); sessionStorage.clear(); this.router.navigate(['/login']); } // Data loading and filtering load(): void { this.cases = this.caseStore.getPoliceCases(); this.cases.forEach((c: any) => { if (c.selected === undefined) c.selected = false; }); 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() { // Filter by dropdowns 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) ); // Filter by search query 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.q = ''; this.applyFilters(); } // Sorting methods 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 '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; }); } // Pagination methods 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); } 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); } } onPageSizeChange(size: number) { this.pageSize = size; this.currentPage = 1; } // Modal methods openDetails(c: PoliceCase, i: number): void { this.selectedCase = c; this.selectedIndex = i; this.showDetails = true; document.body.style.overflow = 'hidden'; } closeDetails(): void { this.showDetails = false; this.selectedCase = null; this.selectedIndex = -1; document.body.style.overflow = 'auto'; } // Case actions editCase(c: PoliceCase, i: number): void { const prefill: Record = {}; try { const fd = (c as any).formData; if (fd && Array.isArray(fd)) { (fd as Array).forEach(kv => { if (kv && kv.key) prefill[kv.key] = kv.value; }); } else if (fd && typeof fd === 'object') { Object.assign(prefill, fd as Record); } // Include mapped top-level properties 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?.address) prefill['Location'] = c.police.address; if (c.police?.name) prefill['Investigating Officer'] = c.police.name; if (c.accused?.name) prefill['Suspect Name'] = c.accused.name; } catch (e) { console.error('Error preparing edit data:', e); } this.router.navigate(['/infopage', c.caseId], { state: { from: 'record', returnId: c.caseId, prefillFormData: prefill, case: c } }); } deleteCase(index: number): void { if (confirm('Are you sure you want to delete this case?')) { this.caseStore.deletePoliceCaseAt(index); this.load(); } } navigateToCaseDetails(c: PoliceCase): void { if (!c || !c.caseId) return; const origin = this.router.url.includes('/case-details') ? 'case-details' : 'record'; this.router.navigate(['/case-details-summary-page', c.caseId], { queryParams: { from: origin, returnId: c.caseId }, state: { case: c, from: origin, returnId: c.caseId } }); } // Field value getters for modal objectKeys = Object.keys; getFieldValue(sc: any, sectionKey: string, field: string): any { 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; 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; } // Try to find in formData 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)); 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]; } } catch (e) { // ignore } } // Normalize and format if (value === null || value === undefined || value === '') return 'Not Assigned'; // Date formatting if (this.dateTimeFields.has(field) || this.dateFields.has(field)) { const d = new Date(value); if (!isNaN(d.getTime())) { if (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 'Not Assigned'; if (typeof v === 'object') return this.formatFormValue(v); if (this.dateTimeFields.has(key) || this.dateFields.has(key)) { const d = new Date(v); if (!isNaN(d.getTime())) { if (this.dateFields.has(key)) return d.toISOString().slice(0, 10); return d.toLocaleString(); } } return v; } // Form data helpers 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 'Not Assigned'; if (typeof value === 'object') { try { return JSON.stringify(value, null, 2); } catch { return String(value); } } return String(value); } // Other methods toggleSelectAll(event: Event): void { const checked = (event.target as HTMLInputElement).checked; this.allSelected = checked; this.rows.forEach((c: any) => c.selected = checked); } onModernSearch() { return false; } goToDetect(caseId: string): void { this.router.navigate(['/py-detect'], { state: { caseId } }); } }