diff --git "a/src/app/infopage/infopage.component.ts" "b/src/app/infopage/infopage.component.ts" --- "a/src/app/infopage/infopage.component.ts" +++ "b/src/app/infopage/infopage.component.ts" @@ -1,1228 +1,1385 @@ 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 { Subject } from 'rxjs'; +import { debounceTime, takeUntil } from 'rxjs/operators'; +import { Router, ActivatedRoute } from '@angular/router'; import { CaseStoreService } from '../shared/case-store.service'; @Component({ - selector: 'app-infopage', - templateUrl: './infopage.component.html', - 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 })) - ]), - transition(':leave', [ - animate('300ms ease-in', - style({ transform: 'translateY(20px)', opacity:0 })) - ]) - ]), - // Field animation - trigger('fieldAnimation', [ - transition(':enter', [ - style({ opacity:0, transform: 'translateY(10px)' }), - animate('200ms ease-out', - style({ opacity:1, transform: 'translateY(0)' })) - ]), - transition(':leave', [ - animate('150ms ease-out', style({ opacity:0, transform: 'translateY(10px)' })) - ]) - ]), - // 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)' })) - ]) - ]) - ] + selector: 'app-infopage', + templateUrl: './infopage.component.html', + 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 })) + ]), + transition(':leave', [ + animate('300ms ease-in', style({ transform: 'translateY(20px)', opacity: 0 })) + ]) + ]), + // Field animation + trigger('fieldAnimation', [ + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(10px)' }), + animate('200ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })) + ]), + transition(':leave', [ + animate('150ms ease-out', style({ opacity: 0, transform: 'translateY(10px)' })) + ]) + ]), + // 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, AfterViewInit, OnDestroy { - showRemarkModal: boolean = false; - showSubmitPopup: boolean = false; - showMicPopup: boolean = false; - isRecording: 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; - showViewRecordsTooltip: 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', 'Physical Evidence', 'Evidence Storage Reference', 'Photos / Video?', 'CCTV Present?', 'CCTV Sources / IDs', 'Forensic Tests Required', 'Chain of Custody?', 'Scene Condition', 'Digital Evidence'], - '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)', 'Tattoo Details', 'Hair Color', 'Scar Details', 'Distinguishing Marks', 'Build', 'Eye Color', '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 { - // Set up autosave - this.autoSave$.pipe( - debounceTime(2000), - takeUntil(this.destroy$) - ).subscribe(() => { - this.performAutoSave(); - }); - - // Do NOT auto-load saved form data on page load/refresh to keep the form empty by default. - // Load only field selections (UI prefs) - this.loadFieldSelections(); - - // If navigation state contains prefillFormData (when editing a case), merge it into formData. - try { - const navState = (history && (history as any).state) || null; - const statePrefill = navState && navState.prefillFormData ? navState.prefillFormData : null; - if (statePrefill && typeof statePrefill === 'object') { - // Merge saved values into current formData - this.formData = { ...(this.formData || {}), ...statePrefill }; - // Restore cascading dropdown selections if present - if (this.formData['Country']) this.selectedValues['Country'] = this.formData['Country']; - if (this.formData['State']) this.selectedValues['State'] = this.formData['State']; - if (this.formData['District']) this.selectedValues['District'] = this.formData['District']; - // Recompute completion status - this.updateCompletionStatus(); - } else { - // Ensure form is empty when arriving without prefill (fresh/new case or refresh) - this.formData = {}; - this.completedFields.clear(); - this.completedSubgroups.clear(); - this.completedSections.clear(); - } - } catch (e) { - // ignore - } - } - - 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 between1 and120'; - } 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; - } - } - - // 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(); - } - - 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); - } - } - - 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 (first10 or all if less than10) - 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'; - },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') { - if (this.selectedValues['State'] === 'Tamil Nadu') { - return this.tamilNaduDistricts; - } else if (this.selectedValues['State']) { - return []; - } else { - return []; - } - } - 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; - } - - 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 subgroups = this.getSubgroups(); - const currentIndex = subgroups.indexOf(this.currentSubgroup); - if (currentIndex < subgroups.length -1) { - return subgroups[currentIndex +1]; - } - // If last subgroup, return first subgroup of next section if exists - const sectionIndex = this.sectionKeys.indexOf(this.currentSection); - if (sectionIndex < this.sectionKeys.length -1) { - const nextSection = this.sectionKeys[sectionIndex +1]; - return Object.keys(this.sections[nextSection].subgroups)[0]; - } - return ''; - } - - isLastSubgroup(): boolean { - const list = this.getSubgroups(); - return list.indexOf(this.currentSubgroup) === list.length -1; - } - - canNextSubgroup(): boolean { - // Enable Next on last subgroup if not notes section - if (this.isLastSubgroup()) { - return !(this.currentSection === 'notes' && this.currentSubgroup === 'Remark'); - } - return true; - } - - nextSubgroup(): void { - const subgroups = this.getSubgroups(); - const currentIndex = subgroups.indexOf(this.currentSubgroup); - // If not last subgroup, go to next subgroup - if (currentIndex < subgroups.length -1) { - this.setSubgroup(subgroups[currentIndex +1]); - return; - } - // If last subgroup, go to first subgroup of next section (if exists) - const sectionIndex = this.sectionKeys.indexOf(this.currentSection); - if (sectionIndex < this.sectionKeys.length -1) { - const nextSection = this.sectionKeys[sectionIndex +1]; - this.currentSection = nextSection; - this.currentSubgroup = Object.keys(this.sections[nextSection].subgroups)[0]; - this.showHelpFor = null; - this.triggerAutoSave(); - } - } - - prevSubgroup(): void { - const subgroups = this.getSubgroups(); - const currentIndex = subgroups.indexOf(this.currentSubgroup); - if (currentIndex >0) { - this.setSubgroup(subgroups[currentIndex -1]); - return; - } - // If first subgroup, go to last subgroup of previous section (if exists) - const sectionIndex = this.sectionKeys.indexOf(this.currentSection); - if (sectionIndex >0) { - const prevSection = this.sectionKeys[sectionIndex -1]; - const prevSubgroups = Object.keys(this.sections[prevSection].subgroups); - this.currentSection = prevSection; - this.currentSubgroup = prevSubgroups[prevSubgroups.length -1]; - this.showHelpFor = null; - this.triggerAutoSave(); - } - } - - 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; - } - - this.performAutoSave(); - - // Map flat formData to nested structure expected by addOrUpdateFromInfoForm - 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'] || '', - briefDescription: this.formData['Brief Description'] || '', - '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'] || '', - verifiedBy: this.formData['Verified By'] || '' - }; - const legal = { - witnessStatements: this.formData['Witness Statements'] || '', - confessions: this.formData['Confessions'] || '', - evidence: this.uploadedFiles['Evidence Files'] || [] - }; - - // Pass the full flat formData object so CaseStoreService can save raw inputs - this.caseStore.addOrUpdateFromInfoForm({ crime, suspect, notes, legal, formData: this.formData }); - // 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); - } - } - - // 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 = {}; - } - } - - // Track by function for ngFor optimization - trackByField(index: number, field: string): string { - return field; - } - - toggleRecording() { - this.isRecording = !this.isRecording; - // Add actual recording logic here if needed - // For now, just toggles the state - } - - goToRecords(): void { - this.router.navigate(['/record']); - } + showRemarkModal: boolean = false; + showSubmitPopup: boolean = false; + showMicPopup: boolean = false; + isRecording: boolean = false; + // New per-field recording state + recordingField: string | null = null; + showMicPopupField: string | null = null; + + // Current logged-in user profile (display only) + currentUser: { name: string; email?: string; avatarUrl?: string } | null = null; + // Profile dropdown visibility + showProfileMenu: boolean = false; + + // NEW: Case ID duplicate popup state + showCaseIdExistsPopup: boolean = false; + caseIdExistsMessage: string = ''; + showNoDataPopup: boolean = false; // <-- New property for no data popup + + // When editing an existing case, store its ID to avoid false duplicate detection + editingCaseId: string | null = null; + + constructor(private router: Router, private route: ActivatedRoute, 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; + showViewRecordsTooltip: 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 + readonly sectionKeys: ('crime' | 'suspect' | 'notes')[] = ['crime', 'suspect', 'notes']; + readonly maxFieldsPerCard = 8; + readonly maxFieldsPerSecondaryCard = 8; + readonly maxFieldsPerCardIdentificationTiming = 6; + readonly maxFieldsPerSecondaryCardIdentificationTiming = 6; + + @ViewChild('formCard1') formCard1!: ElementRef; + @ViewChild('formCard2') formCard2!: ElementRef; + @ViewChild('formCard3') formCard3!: ElementRef; + // Reference to profile display for positioning menu + @ViewChild('profileDisplay', { read: ElementRef }) profileDisplayRef!: ElementRef; + // Dynamic inline styles for positioned profile menu + profileMenuStyle: Record = {}; + + // 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', 'Investigative 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' + ]); + + 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': '*' + }; + + selectedValues: Record = {}; + + dateTimeFields = new Set(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered', 'Scene Secured Time']); + dateFields = new Set(['Follow-up Date', 'Next Hearing Date']); + + 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' + ]; + + 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'], + 'risk Level': ['Low', 'Medium', 'High', 'Critical'], + 'Confidentiality': ['Internal', 'Restricted', 'Sensitive', 'Sealed'], + 'Initial Actions Taken': ['Scene Secured', 'Medical Aid', 'Evidence Logged', 'Witness Statements', 'Suspect Detained', 'General', 'Other'], + 'Investigative 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'], + 'Skin Tone': ['Fair', 'Medium', 'Olive', 'Dark', 'Very Dark', 'Unknown'], + 'Facial Hair': ['Clean Shaven', 'Mustache', 'Beard', 'Goatee', 'Stubble', 'None'], + 'Handedness': ['Left-handed', 'Right-handed', 'Ambidextrous'], + 'Voice Characteristics': ['Deep', 'High-pitched', 'Normal', 'Raspy', 'Stutter'], + '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 + 'Arrest Count': ['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 = {}; + // Transient confirmation messages per upload field + uploadConfirmations: 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 & Involved Persons': ['Location', 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Reporter Phone', 'Reporter Email', 'Witness Count', 'Witness Phone', 'Witness Email', 'Victim Name', 'Victim Phone', 'Victim Email', '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', 'Physical Evidence List', 'Evidence Storage Reference', 'Evidence Collected By', 'Evidence Bag / Seal Number', 'Scene Secured Time', 'Photos / Video?', 'CCTV Present?', 'CCTV Sources / IDs', 'Forensic Tests Required', 'Chain of Custody?', 'Scene Condition'], + 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'risk Level', 'Confidentiality'], + 'Status & Linkage': ['Biometric / Forensic IDs', 'DNA Ref ID', 'Fingerprint ID', 'Investigative Status', 'Linked Cases', 'Arrest Count', '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', 'Suspect Email', 'Suspect Phone', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'], + 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Build', 'Skin Tone', 'Hair Color', 'Eye Color', 'Facial Hair', 'Handedness', 'Tattoo Details', 'Scar Details', 'Distinguishing Marks', 'Voice Characteristics', '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', 'Digital Evidence'], + '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.', + 'Reporter Phone': 'Primary phone number for the reporting party (include country code if known).', + 'Reporter Email': 'Email address for the reporting party, if available.', + 'Witness Count': 'Number of identified witnesses so far.', + 'Witness Phone': 'Primary phone number for the witness (include country code if known).', + 'Witness Email': 'Email address for the witness, if available.', + 'Victim Name': 'Primary victim name (or placeholder if protected).', + 'Victim Phone': 'Primary phone number for the victim (include country code if known).', + 'Victim Email': 'Email address for the victim, if available.', + '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.', + 'Evidence Collected By': 'Name / identifier of the person who collected the evidence.', + 'Evidence Bag / Seal Number': 'Chain of custody bag or seal identifier assigned to collected evidence.', + 'Scene Secured Time': 'Timestamp when the scene was first secured and access restricted.', + + // 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.', + 'risk Level': '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.', + 'Investigative Status': 'Lifecycle status (Open / Active / Closed etc.).', + 'Linked Cases': 'Related or associated case identifiers.', + 'Arrest Count': '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.', + 'Suspect Email': 'Primary email address for the suspect, if available.', + 'Suspect Phone': 'Primary phone number for the suspect, including country code if known.', + '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.', + 'Skin Tone': 'Approximate skin tone (e.g., Fair, Medium, Olive, Dark).', + 'Facial Hair': 'Facial hair description (Mustache, Beard, Goatee, etc.).', + 'Handedness': 'Primary dominant hand (Left/Right/Ambidextrous).', + 'Voice Characteristics': 'Notable voice traits (Deep, Raspy, Stutter, etc.).', + + // 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.' + }; + + // update subgroup icons mapping + subgroupIcons: any = { + 'Identification & Timing': 'fas fa-clock', + 'Location & Involved Persons': '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 { + // Load current user profile (if available) + this.loadCurrentUser(); + + // Set up autosave + this.autoSave$.pipe(debounceTime(2000), takeUntil(this.destroy$)).subscribe(() => { + this.performAutoSave(); + }); + + // Load field selections + this.loadFieldSelections(); + + // Initialize formData empty + this.formData = {}; + this.completedFields.clear(); + this.completedSubgroups.clear(); + this.completedSections.clear(); + + // Handle navigation state/params to prefill when editing a case + this.handleNavigationPrefill(); + } + + ngAfterViewInit(): void { } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + if (this.submitInterval) { + clearInterval(this.submitInterval); + } + } + + // Progress + 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 & Involved Persons + private isLocationAndPeoplePage(): boolean { + return this.currentSubgroup === 'Location & Involved Persons'; + } + + // 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 + } + + // Check if current page is Physical Description + isPhysicalDescriptionPage(): boolean { + return this.currentSection === 'suspect' && this.currentSubgroup === 'Physical Description'; + } + + // 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(); + } + + 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 === 'Initial Findings') return 'Summary of first observations...'; + if (field === 'Detailed Notes') return 'Detailed analysis and observations...'; + 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 between1 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 if (field === 'Case ID') { + // Check for duplicate Case ID (compare numeric parts when possible) + const input = String(value).trim(); + const inputNumeric = this.extractNumericId(input); + const existing = this.caseStore.getPoliceCases().find(c => { + const existingVal = c.caseId || ''; + const existingNumeric = this.extractNumericId(existingVal); + if (inputNumeric && existingNumeric) { + return inputNumeric === existingNumeric; // numeric match + } + // fallback to full string match (case-insensitive) + return existingVal.toLowerCase() === input.toLowerCase(); + }); + + // If editing an existing case, ignore match against the same case + if (existing && existing.caseId !== this.editingCaseId) { + hasError = true; + message = `Case ID \"${input}\" already exists`; + this.caseIdExistsMessage = message; + this.showCaseIdExistsPopup = true; + } else { + isValid = true; + } + } else { + // Generic validation for other fields (extend as needed) + isValid = true; + } + } + + // Update field validation state + this.fieldValidation[field] = { hasError, isValid, message }; + + // Update overall form completion status + this.updateCompletionStatus(); + } + + // Extract first numeric sequence from a case id string (e.g., 'CASE-001' -> '001', '001' -> '001') + private extractNumericId(val: string): string | null { + if (!val) return null; + const m = String(val).match(/(\d+)/); + return m ? m[1].replace(/^0+(?=\d)/, '') : null; // strip leading zeros but keep '0' if that's all + } + + // Close duplicate popup + closeCaseIdPopup(): void { + this.showCaseIdExistsPopup = false; + } + + // Close no-data popup + closeNoDataPopup(): void { + this.showNoDataPopup = false; + } + + 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(); + } + + 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); + } + } + + 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); + } + + getSelectedFieldCount(): number { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + if (!selections) return this.getCurrentFields().length; + return selections.length; + } + + isSelectionLimitReached(): boolean { + return this.getSelectedFieldCount() >= this.maxSelectableFields; + } + + 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(); + } + + 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(); + } + + 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(); + } + + getDynamicMaxSelectable(): number { + const totalFields = this.getTotalAvailableFieldsCount(); + return Math.min(this.maxSelectableFields, totalFields); + } + + areAllFieldsSelected(): boolean { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + const totalFields = this.getCurrentFields().length; + if (!selections) return true; + return selections.length === totalFields; + } + + areNoFieldsSelected(): boolean { + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + const selections = this.selectedFields[subgroupKey]; + if (!selections) return false; + return selections.length === 0; + } + + getSubgroups(): string[] { + return Object.keys(this.sections[this.currentSection].subgroups); + } + + showSection(section: 'crime' | 'suspect' | 'notes'): void { + this.closeFieldSelector(); + this.currentSection = section; + this.currentSubgroup = Object.keys(this.sections[this.currentSection].subgroups)[0]; + this.showHelpFor = null; + this.triggerAutoSave(); + } + + @HostListener('document:click', ['$event']) + handleDoc(event: Event): void { + this.showHelpFor = null; + const target = event.target as HTMLElement; + if (this.showFieldSelector && !target.closest('.field-selector-container')) this.closeFieldSelector(); + if (this.showProfileMenu && !target.closest('.profile-display')) this.showProfileMenu = false; + } + + setSubgroup(key: string): void { + this.closeFieldSelector(); + this.currentSubgroup = key; + this.showHelpFor = null; + this.triggerAutoSave(); + } + + isSectionCompleted(section: string): boolean { + return this.completedSections.has(section); + } + + isSubgroupCompleted(subgroup: string): boolean { + return this.completedSubgroups.has(subgroup); + } + + getSectionDescription(section: string): string { + const descriptions = { + crime: 'Capture comprehensive crime intelligence, including timing, location, evidence, and operational context. Use predefined dropdown values for consistency and upload supporting documents and materials.', + suspect: 'Document a comprehensive suspect profile, including identity, physical characteristics, background, associations, and criminal history. Include recent photographs when available.', + notes: 'Maintain detailed investigative records, including findings, evidence files, reference materials, and final recommendations, with proper version control.' + }; + return descriptions[section as keyof typeof descriptions] || ''; + } + + onFieldChange(field: string): void { + this.validateField(field); + this.triggerAutoSave(); + } + + private triggerAutoSave(): void { + this.autoSave$.next(); + } + + private performAutoSave(): void { + this.isAutoSaving = true; + this.autoSaveStatus = 'Saving...'; + this.saveFormData(); + setTimeout(() => { + this.isAutoSaving = false; + this.autoSaveStatus = 'Saved'; + setTimeout(() => { + this.autoSaveStatus = 'Auto-save'; + }, 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'; + } + } + + 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') { + if (this.selectedValues['State'] === 'Tamil Nadu') return this.tamilNaduDistricts; + return []; + } + 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; + 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(); + } + + toggleFieldInfo(field: string, ev: MouseEvent): void { + ev.stopPropagation(); + this.showHelpFor = this.showHelpFor === field ? null : field; + } + + closeFieldInfo(): void { + this.showHelpFor = null; + } + + 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(); + const total = this.uploadedFiles[field].length; + const message = total > 1 ? `${total} files uploaded` : 'Upload successful'; + this.uploadConfirmations[field] = message; + setTimeout(() => delete this.uploadConfirmations[field], 4000); + } + } + + 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(); + const total = this.uploadedFiles[field].length; + const message = total > 1 ? `${total} files uploaded` : 'Upload successful'; + this.uploadConfirmations[field] = message; + setTimeout(() => delete this.uploadConfirmations[field], 4000); + } + } + + 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] || '*'; + } + + getAcceptedDisplay(field: string): string { + const accept = (this.fileTypeConfig && this.fileTypeConfig[field]) || ''; + if (!accept || accept === '*') return 'Any file type'; + if (accept.includes('.') || accept.includes(',')) { + const parts = accept.split(',').map(p => p.trim()).filter(Boolean); + return parts.map(p => p.startsWith('.') ? p : (p.includes('/') ? this._mimeToExtList(p) : '.' + p)).flat().join(', '); + } + if (accept.includes('image/')) return '.jpg, .jpeg, .png, .gif'; + if (accept.includes('video/')) return '.mp4, .mov, .avi'; + if (accept.includes('audio/')) return '.mp3, .wav'; + return accept; + } + + private _mimeToExtList(mime: string): string[] { + mime = mime.trim(); + if (mime === 'image/*') return ['.jpg', '.jpeg', '.png', '.gif']; + if (mime === 'video/*') return ['.mp4', '.mov', '.avi']; + if (mime === 'audio/*') return ['.mp3', '.wav']; + if (mime === '.pdf' || mime === 'application/pdf') return ['.pdf']; + if (mime === '.doc' || mime === '.docx' || mime === 'application/msword' || mime === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') return ['.doc', '.docx']; + return [mime]; + } + + 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); } + } + + 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 = {}; + } + } + + closeFieldSelector(): void { this.showFieldSelector = null; } + + goToRecords(): void { this.router.navigate(['/record']); } + + toggleFieldSelector(event?: Event): void { + if (event) { event.preventDefault(); event.stopPropagation(); } + const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; + this.showFieldSelector = this.showFieldSelector === subgroupKey ? null : subgroupKey; + } + + trackByField(index: number, field: string): string { return field; } + + toggleRecording(field?: string) { + if (field) { + if (this.recordingField === field) { this.recordingField = null; this.isRecording = false; } + else { this.recordingField = field; this.isRecording = true; } + } else { + this.isRecording = !this.isRecording; + if (!this.isRecording) this.recordingField = null; + } + } + + getUploadZoneIcon(field: string): string { + const imageFields = new Set(['Photo Upload', 'Evidence Photos']); + const videoFields = new Set(['Evidence Videos']); + const documentFields = new Set(['Evidence Documents']); + if (imageFields.has(field)) return 'fas fa-file-image'; + if (videoFields.has(field)) return 'fas fa-file-video'; + if (documentFields.has(field)) return 'fas fa-file-alt'; + const accept = (this.fileTypeConfig && this.fileTypeConfig[field]) || ''; + if (accept.includes('image/')) return 'fas fa-file-image'; + if (accept.includes('video/')) return 'fas fa-file-video'; + if (accept.includes('.pdf') || accept.includes('.doc')) return 'fas fa-file-alt'; + return 'fas fa-cloud-upload-alt'; + } + + 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'; + } + } + + isLastSubgroup(): boolean { const list = this.getSubgroups(); return list.indexOf(this.currentSubgroup) === list.length - 1; } + + canNextSubgroup(): boolean { + if (this.isLastSubgroup()) return !(this.currentSection === 'notes' && this.currentSubgroup === 'Remark'); + return true; + } + + canPrevSubgroup(): boolean { + const subgroups = this.getSubgroups(); + const currentIndex = subgroups.indexOf(this.currentSubgroup); + if (currentIndex > 0) return true; + const sectionIndex = this.sectionKeys.indexOf(this.currentSection); + return sectionIndex > 0; + } + + previousSubgroup(): void { + const subgroups = this.getSubgroups(); + const currentIndex = subgroups.indexOf(this.currentSubgroup); + if (currentIndex > 0) { this.setSubgroup(subgroups[currentIndex - 1]); return; } + const sectionIndex = this.sectionKeys.indexOf(this.currentSection); + if (sectionIndex > 0) { + const prevSection = this.sectionKeys[sectionIndex - 1]; + this.currentSection = prevSection; + const prevSubgroups = Object.keys(this.sections[prevSection].subgroups); + this.currentSubgroup = prevSubgroups[prevSubgroups.length - 1]; + this.showHelpFor = null; + this.triggerAutoSave(); + } + } + + nextSubgroup(): void { + const subgroups = this.getSubgroups(); + const currentIndex = subgroups.indexOf(this.currentSubgroup); + if (currentIndex < subgroups.length - 1) { this.setSubgroup(subgroups[currentIndex + 1]); return; } + const sectionIndex = this.sectionKeys.indexOf(this.currentSection); + if (sectionIndex < this.sectionKeys.length - 1) { + const nextSection = this.sectionKeys[sectionIndex + 1]; + this.currentSection = nextSection; + this.currentSubgroup = Object.keys(this.sections[nextSection].subgroups)[0]; + this.showHelpFor = null; + this.triggerAutoSave(); + } + } + + isSubmitting: boolean = false; + submitProgress: number = 0; + submitInterval: any; + + submitCurrentSection(): void { + // Prevent submission if no data entered at all + const hasFormData = Object.keys(this.formData).some(k => { + const v = this.formData[k]; + return v !== null && v !== undefined && String(v).trim() !== ''; + }); + const hasUploadedFiles = Object.keys(this.uploadedFiles).some(k => (this.uploadedFiles[k] || []).length >0); + if (!hasFormData && !hasUploadedFiles) { + // Show non-blocking popup instead of alert + this.showNoDataPopup = true; + return; + } + + 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; + } + // Prevent submission if Case ID duplicate exists + if (this.fieldValidation['Case ID'] && this.fieldValidation['Case ID'].hasError) { + this.showCaseIdExistsPopup = true; + return; + } + + // Start loading state + this.isSubmitting = true; + this.submitProgress =0; + + // Simulate progress (you can adjust timing as needed) + this.submitInterval = setInterval(() => { + this.submitProgress += Math.random() *30; + if (this.submitProgress >=100) { + this.submitProgress =100; + clearInterval(this.submitInterval); + + // Simulate processing delay + setTimeout(() => { + this.finalizeSubmission(); + },500); + } + },200); + } + + // Add a new method to finalize submission after loading: + private finalizeSubmission(): void { + // Save data first + this.performAutoSave(); + + + 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'] || '', + briefDescription: this.formData['Brief Description'] || '', + '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['Investigative Status'] || this.formData['Status'] || 'Open', + officerInCharge: this.formData['Investigating Officer'] || '', + initialFindings: this.formData['Initial Findings'] || '', + verifiedBy: this.formData['Verified By'] || '' + }; + + const legal = { + witnessStatements: this.formData['Witness Statements'] || '', + confessions: this.formData['Confessions'] || '', + evidence: this.uploadedFiles['Evidence Files'] || [] + }; + this.caseStore.addOrUpdateFromInfoForm({ crime, suspect, notes, legal, formData: this.formData }); + /* this.showSubmitPopup = true;*/ + // Hide loader and show success popup + this.isSubmitting = false; + this.submitProgress = 0; + this.showSubmitPopup = true; + } + + + onSubmitPopupClose(): void { + this.showSubmitPopup = false; + this.router.navigate(['/record'], { state: { formData: this.formData } }); + } + + // Load current user profile from localStorage key 'pydetect-current-user' + private loadCurrentUser(): void { + try { + const raw = localStorage.getItem('pydetect-current-user'); + if (raw) this.currentUser = JSON.parse(raw); + else this.currentUser = { name: 'Admin User', email: 'admin@gmail.com' }; + } catch (e) { + this.currentUser = { name: 'Admin User' }; + } + } + + getUserInitials(): string { + if (!this.currentUser || !this.currentUser.name) return 'A'; + const parts = this.currentUser.name.trim().split(/\s+/); + const first = parts[0] ? parts[0].charAt(0).toUpperCase() : 'A'; + const second = parts.length > 1 ? (parts[1].charAt(0) || '').toUpperCase() : ''; + return `${first}${second}`; + } + + // Toggle profile menu (attached to template) + toggleProfileMenu(ev?: Event): void { + if (ev) ev.stopPropagation(); + this.showProfileMenu = !this.showProfileMenu; + if (this.showProfileMenu) { + try { + const el = this.profileDisplayRef && this.profileDisplayRef.nativeElement; + if (el) { + const rect = el.getBoundingClientRect(); + const menuWidth = 260; // desired menu width + const gap = 12; // gap between icon bottom and menu + // Align menu right edge with icon right edge, clamp within viewport + const leftUnclamped = Math.round(rect.right - menuWidth); + const left = Math.min(Math.max(leftUnclamped, 8), Math.max(window.innerWidth - menuWidth - 8, 8)); + const top = Math.round(rect.bottom + gap); + this.profileMenuStyle = { + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + 'min-width': `${menuWidth}px`, + 'z-index': '9999', + padding: '10px' + }; + } + } catch (e) { /* ignore */ } + } else { + this.profileMenuStyle = {}; + } + } + + logout(): void { + try { localStorage.removeItem('pydetect-current-user'); } catch { } + this.showProfileMenu = false; + this.router.navigate(['/']); + } + + // Handle navigation prefilling when routed from record page + private handleNavigationPrefill(): void { + try { + // First try router navigation extras state (works during direct navigation) + const nav = (this.router as any).getCurrentNavigation && (this.router as any).getCurrentNavigation(); + const state = nav && nav.extras && nav.extras.state ? nav.extras.state : (history && (history.state as any) ? (history.state as any) : null); + + const routeId = this.route.snapshot.paramMap.get('id') || this.route.snapshot.paramMap.get('caseId'); + + if (state && state.prefillFormData) { + // Use provided prefillFormData from recordpage + this.formData = { ...(state.prefillFormData || {}) }; + if (state.case && state.case.caseId) this.editingCaseId = state.case.caseId; + this.currentSection = 'crime'; + this.currentSubgroup = 'Identification & Timing'; + this.updateCompletionStatus(); + } else if (routeId) { + // Try to load case by id from store + const cases = this.caseStore.getPoliceCases(); + const found = cases.find((c: any) => (c.caseId || '') === routeId); + if (found) { + this.populateFromCaseObject(found); + } + } else if (state && state.case) { + // fallback if only case object passed + this.populateFromCaseObject(state.case); + } + } catch (e) { + // ignore + } + } + + // Populate formData from a case object structure + private populateFromCaseObject(c: any): void { + try { + this.formData = {}; + // If formData is already an object, copy values + if (c.formData && typeof c.formData === 'object' && !Array.isArray(c.formData)) { + Object.assign(this.formData, c.formData); + } + + // If formData is an array of key/value pairs + if (c.formData && Array.isArray(c.formData)) { + (c.formData as Array).forEach(kv => { if (kv && kv.key) this.formData[kv.key] = kv.value; }); + } + + // Map some top-level fields into formData for editing convenience + if (c.caseId) this.formData['Case ID'] = c.caseId; + if (c.crime) this.formData['Crime Type'] = c.crime; + if (c.dateTime) this.formData['Date & Time (Entry)'] = c.dateTime; + if (c.police && c.police.address) this.formData['Location'] = c.police.address; + if (c.police && c.police.name) this.formData['Investigating Officer'] = c.police.name; + if (c.accused && c.accused.name) this.formData['Suspect Name'] = c.accused.name; + + // mark editing id to avoid duplicate detection against itself + if (c.caseId) this.editingCaseId = c.caseId; + + // Update selected values for Country/State/District if present + if (this.formData['Country']) this.selectedValues['Country'] = this.formData['Country']; + if (this.formData['State']) this.selectedValues['State'] = this.formData['State']; + if (this.formData['District']) this.selectedValues['District'] = this.formData['District']; + + // Update completion state and validations + this.updateCompletionStatus(); + for (const f of Object.keys(this.formData)) { + this.validateField(f); + } + } catch (e) { + // ignore + } + } } +