|
|
import { Component, HostListener, ElementRef, ViewChild, AfterViewInit, OnInit, OnDestroy } from '@angular/core'; |
|
|
import { trigger, state, style, transition, animate } from '@angular/animations'; |
|
|
import { Subject, debounceTime, takeUntil } from 'rxjs'; |
|
|
import { Router } from '@angular/router'; |
|
|
import { CaseStoreService } from '../shared/case-store.service'; |
|
|
|
|
|
@Component({ |
|
|
selector: 'app-infopage', |
|
|
templateUrl: './infopage.component.html', |
|
|
styleUrls: ['./infopage.component.css'], |
|
|
animations: [ |
|
|
|
|
|
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 })) |
|
|
]) |
|
|
]), |
|
|
|
|
|
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)' })) |
|
|
]) |
|
|
]), |
|
|
|
|
|
trigger('fadeIn', [ |
|
|
transition(':enter', [ |
|
|
style({ opacity:0 }), |
|
|
animate('200ms ease-in', style({ opacity:1 })) |
|
|
]), |
|
|
transition(':leave', [ |
|
|
animate('150ms ease-out', style({ opacity:0 })) |
|
|
]) |
|
|
]), |
|
|
|
|
|
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) {} |
|
|
|
|
|
currentSection: 'crime' | 'suspect' | 'notes' = 'crime'; |
|
|
currentSubgroup: string = 'Identification & Timing'; |
|
|
showHelpFor: string | null = null; |
|
|
|
|
|
|
|
|
isAutoSaving: boolean = false; |
|
|
autoSaveStatus: string = 'Saved'; |
|
|
isDragOver: boolean = false; |
|
|
showViewRecordsTooltip: boolean = false; |
|
|
|
|
|
|
|
|
isCardMinimized = { |
|
|
primary: false, |
|
|
secondary: false, |
|
|
tertiary: false |
|
|
}; |
|
|
|
|
|
|
|
|
formData: Record<string, any> = {}; |
|
|
fieldValidation: Record<string, { hasError: boolean, isValid: boolean, message: string }> = {}; |
|
|
completedFields: Set<string> = new Set(); |
|
|
completedSubgroups: Set<string> = new Set(); |
|
|
completedSections: Set<string> = new Set(); |
|
|
|
|
|
|
|
|
private destroy$ = new Subject<void>(); |
|
|
private autoSave$ = new Subject<void>(); |
|
|
|
|
|
|
|
|
readonly sectionKeys: ('crime' | 'suspect' | 'notes')[] = ['crime', 'suspect', 'notes']; |
|
|
readonly maxFieldsPerCard =8; |
|
|
readonly maxFieldsPerSecondaryCard =8; |
|
|
readonly maxFieldsPerCardIdentificationTiming =6; |
|
|
readonly maxFieldsPerSecondaryCardIdentificationTiming =6; |
|
|
|
|
|
@ViewChild('formCard1') formCard1!: ElementRef<HTMLDivElement>; |
|
|
@ViewChild('formCard2') formCard2!: ElementRef<HTMLDivElement>; |
|
|
@ViewChild('formCard3') formCard3!: ElementRef<HTMLDivElement>; |
|
|
|
|
|
|
|
|
readonly requiredFields = new Set<string>([ |
|
|
'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<string>([ |
|
|
'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<string>([ |
|
|
'Age', 'Height (cm)', 'Weight (kg)', 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count' |
|
|
]); |
|
|
|
|
|
|
|
|
readonly fileTypeConfig: Record<string, string> = { |
|
|
'Photo Upload': 'image/*', |
|
|
'Evidence Photos': 'image/*', |
|
|
'Evidence Videos': 'video/*', |
|
|
'Evidence Documents': '.pdf,.doc,.docx,.txt', |
|
|
'Evidence Files': '*', |
|
|
'Upload Evidence Files': '*', |
|
|
'Digital Evidence': '*' |
|
|
}; |
|
|
|
|
|
|
|
|
selectedValues: Record<string, string> = {}; |
|
|
|
|
|
|
|
|
dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']); |
|
|
dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']); |
|
|
|
|
|
|
|
|
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<string, string[]> = { |
|
|
'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'], |
|
|
|
|
|
'arrestCount': ['0', '1', '2', '3', '4', '5+'], |
|
|
'Linked Cases': [], |
|
|
'Suspect Link': [], |
|
|
'Government ID': ['Aadhaar Card', 'PAN Card', 'Driving License', 'Passport', 'Voter ID', 'Other'], |
|
|
'Family Connections': ['Spouse', 'Parent', 'Child', 'Sibling', 'Relative', 'Friend', 'Other'], |
|
|
'Social Media Handles': [], |
|
|
'Version History / Updates': [] |
|
|
}; |
|
|
|
|
|
|
|
|
fileFields = new Set<string>([ |
|
|
'Photo Upload', 'Evidence Photos', 'Evidence Videos', 'Evidence Documents', |
|
|
'Evidence Files', 'Upload Evidence Files', 'Digital Evidence' |
|
|
]); |
|
|
|
|
|
uploadedFiles: Record<string, File[]> = {}; |
|
|
|
|
|
|
|
|
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'] |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
fieldDescriptions: Record<string, string> = { |
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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 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).', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'Criminal History': 'Summary of prior criminal involvement.', |
|
|
'Prior Arrests': 'Number / list of previous arrests.', |
|
|
'Probation/Parole Status': 'Current supervision / release status.', |
|
|
|
|
|
|
|
|
'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.', |
|
|
|
|
|
|
|
|
'Evidence Photos': 'Photographic evidence references.', |
|
|
'Evidence Videos': 'Video evidence references.', |
|
|
'Evidence Documents': 'Document / PDF evidence references.', |
|
|
|
|
|
|
|
|
'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 { |
|
|
|
|
|
this.autoSave$.pipe( |
|
|
debounceTime(2000), |
|
|
takeUntil(this.destroy$) |
|
|
).subscribe(() => { |
|
|
this.performAutoSave(); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this.loadFieldSelections(); |
|
|
|
|
|
|
|
|
try { |
|
|
const navState = (history && (history as any).state) || null; |
|
|
const statePrefill = navState && navState.prefillFormData ? navState.prefillFormData : null; |
|
|
if (statePrefill && typeof statePrefill === 'object') { |
|
|
|
|
|
this.formData = { ...(this.formData || {}), ...statePrefill }; |
|
|
|
|
|
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']; |
|
|
|
|
|
this.updateCompletionStatus(); |
|
|
} else { |
|
|
|
|
|
this.formData = {}; |
|
|
this.completedFields.clear(); |
|
|
this.completedSubgroups.clear(); |
|
|
this.completedSections.clear(); |
|
|
} |
|
|
} catch (e) { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
ngAfterViewInit(): void { |
|
|
|
|
|
} |
|
|
|
|
|
ngOnDestroy(): void { |
|
|
this.destroy$.next(); |
|
|
this.destroy$.complete(); |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
private isIdentificationAndTimingPage(): boolean { |
|
|
return this.currentSubgroup === 'Identification & Timing'; |
|
|
} |
|
|
|
|
|
|
|
|
private isLocationAndPeoplePage(): boolean { |
|
|
return this.currentSubgroup === 'Location & People'; |
|
|
} |
|
|
|
|
|
|
|
|
private needsCompactLayout(): boolean { |
|
|
return true; |
|
|
} |
|
|
|
|
|
|
|
|
showSecondaryCard(): boolean { |
|
|
return false; |
|
|
} |
|
|
|
|
|
showTertiaryCard(): boolean { |
|
|
return false; |
|
|
} |
|
|
|
|
|
getPrimaryFields(): string[] { |
|
|
|
|
|
return this.getSelectedFieldsForDisplay(); |
|
|
} |
|
|
|
|
|
getSecondaryFields(): string[] { |
|
|
|
|
|
return []; |
|
|
} |
|
|
|
|
|
getTertiaryFields(): string[] { |
|
|
|
|
|
return []; |
|
|
} |
|
|
|
|
|
private getCurrentFields(): string[] { |
|
|
return this.sections[this.currentSection].subgroups[this.currentSubgroup] || []; |
|
|
} |
|
|
|
|
|
toggleCardMinimize(card: 'primary' | 'secondary' | 'tertiary'): void { |
|
|
this.isCardMinimized[card] = !this.isCardMinimized[card]; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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() !== '') { |
|
|
|
|
|
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') { |
|
|
|
|
|
const emailPattern = /\S+@\S+\.\S+/; |
|
|
isValid = emailPattern.test(value); |
|
|
if (!isValid) { |
|
|
hasError = true; |
|
|
message = 'Invalid email address format'; |
|
|
} |
|
|
} else { |
|
|
|
|
|
isValid = true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.fieldValidation[field] = { hasError, isValid, message }; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
selectedFields: Record<string, string[]> = {}; |
|
|
showFieldSelector: string | null = null; |
|
|
readonly maxSelectableFields =50; |
|
|
|
|
|
|
|
|
getAvailableFields(): string[] { |
|
|
return this.getCurrentFields(); |
|
|
} |
|
|
|
|
|
|
|
|
getTotalAvailableFieldsCount(): number { |
|
|
return this.getAvailableFields().length; |
|
|
} |
|
|
|
|
|
|
|
|
getSelectedFieldsForDisplay(): string[] { |
|
|
const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; |
|
|
if (this.selectedFields[subgroupKey] && this.selectedFields[subgroupKey].length >=0) { |
|
|
return this.selectedFields[subgroupKey]; |
|
|
} |
|
|
|
|
|
return this.getCurrentFields(); |
|
|
} |
|
|
|
|
|
|
|
|
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]) { |
|
|
|
|
|
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) { |
|
|
|
|
|
currentSelection.splice(fieldIndex,1); |
|
|
console.log('Removed field:', field); |
|
|
} else { |
|
|
|
|
|
if (currentSelection.length < this.maxSelectableFields) { |
|
|
currentSelection.push(field); |
|
|
console.log('Added field:', field); |
|
|
} else { |
|
|
console.log('Selection limit reached, cannot add:', field); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.selectedFields = { ...this.selectedFields }; |
|
|
this.saveFieldSelections(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
isFieldSelected(field: string): boolean { |
|
|
const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; |
|
|
const selections = this.selectedFields[subgroupKey]; |
|
|
if (!selections) { |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 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] || ''; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} 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; |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
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'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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]; |
|
|
} |
|
|
|
|
|
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 { |
|
|
|
|
|
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 (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(); |
|
|
} |
|
|
} |
|
|
|
|
|
prevSubgroup(): 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]; |
|
|
const prevSubgroups = Object.keys(this.sections[prevSection].subgroups); |
|
|
this.currentSection = prevSection; |
|
|
this.currentSubgroup = prevSubgroups[prevSubgroups.length -1]; |
|
|
this.showHelpFor = null; |
|
|
this.triggerAutoSave(); |
|
|
} |
|
|
} |
|
|
|
|
|
submitCurrentSection(): void { |
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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'] || [] |
|
|
}; |
|
|
|
|
|
|
|
|
this.caseStore.addOrUpdateFromInfoForm({ crime, suspect, notes, legal, formData: this.formData }); |
|
|
|
|
|
this.showSubmitPopup = true; |
|
|
} |
|
|
|
|
|
onSubmitPopupClose(): void { |
|
|
this.showSubmitPopup = false; |
|
|
this.router.navigate(['/record'], { state: { formData: this.formData } }); |
|
|
} |
|
|
|
|
|
|
|
|
@HostListener('document:keydown', ['$event']) |
|
|
handleKeydown(event: KeyboardEvent): void { |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
toggleFieldSelector(event?: Event): void { |
|
|
if (event) { |
|
|
event.preventDefault(); |
|
|
event.stopPropagation(); |
|
|
} |
|
|
|
|
|
const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`; |
|
|
this.showFieldSelector = this.showFieldSelector === subgroupKey ? null : subgroupKey; |
|
|
} |
|
|
|
|
|
|
|
|
closeFieldSelector(): void { |
|
|
this.showFieldSelector = null; |
|
|
} |
|
|
|
|
|
|
|
|
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 = {}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
trackByField(index: number, field: string): string { |
|
|
return field; |
|
|
} |
|
|
|
|
|
toggleRecording() { |
|
|
this.isRecording = !this.isRecording; |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
goToRecords(): void { |
|
|
this.router.navigate(['/record']); |
|
|
} |
|
|
|
|
|
} |
|
|
|