| 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 })) |
| ]) |
| ]), |
| |
| trigger('fieldAnimation', [ |
| transition(':enter', [ |
| style({ opacity: 0, transform: 'translateY(10px)' }), |
| animate('200ms ease-out', |
| style({ opacity: 1, transform: 'translateY(0)' })) |
| ]) |
| ]), |
| |
| 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; |
| |
| |
| 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', 'Forensic Tests Required', 'Scene Condition', 'Photos / Video?', 'CCTV Present?', 'CCTV Sources / IDs', 'Physical Evidence (list)', 'Chain of Custody?', 'Digital Evidence', 'Evidence Storage Reference'], |
| 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'riskLevel', 'Confidentiality'], |
| 'Status & Linkage': ['Biometric / Forensic IDs', 'DNA Ref ID', 'Fingerprint ID', 'Case Status', 'Linked Cases', 'arrestCount', 'Case Priority', 'Follow-up Date', 'Court Case ID', 'Next Hearing Date', 'Final Summary'], |
| 'Remark': ['Remark'] |
| } |
| }, |
| suspect: { |
| title: 'Suspect Details', |
| subgroups: { |
| 'Identity': ['Suspect ID', 'Suspect Name', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'], |
| 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color', 'Distinguishing Marks', 'Tattoo Details', 'Scar Details', 'Photo Upload'], |
| 'Background': ['Employment', 'Education', 'Occupation', 'Company', 'Workplace Address', 'Marital Status', 'Known Habits', 'Known Financial Details'], |
| 'Known Associates': ['Associate Names', 'Gang Affiliation', 'Family Connections', 'Social Media Handles'], |
| 'Prior Records': ['Criminal History', 'Prior Arrests', 'Probation/Parole Status'], |
| 'Remark': ['Remark'] |
| } |
| }, |
| notes: { |
| title: 'Evidence and Documents', |
| subgroups: { |
| 'Investigation Notes': ['Initial Findings', 'Detailed Notes', 'Status', 'Version History / Updates'], |
| 'Evidence Files': ['Evidence Photos', 'Evidence Videos', 'Evidence Documents'], |
| 'Links and Recommendation': ['Links to Evidence', 'Final Recommendations'], |
| 'Remark': ['Remark'] |
| } |
| } |
| }; |
|
|
| |
| 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.loadFormData(); |
| |
| |
| this.loadFieldSelections(); |
| } |
|
|
| 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 between 1 and 120'; |
| } 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 enabled'; |
| }, 2000); |
| }, 500); |
| } |
|
|
| private saveFormData(): void { |
| const saveData = { |
| formData: this.formData, |
| completedFields: Array.from(this.completedFields), |
| completedSubgroups: Array.from(this.completedSubgroups), |
| completedSections: Array.from(this.completedSections), |
| currentSection: this.currentSection, |
| currentSubgroup: this.currentSubgroup |
| }; |
| localStorage.setItem('pydetect-form-data', JSON.stringify(saveData)); |
| } |
|
|
| private loadFormData(): void { |
| const savedData = localStorage.getItem('pydetect-form-data'); |
| if (savedData) { |
| const data = JSON.parse(savedData); |
| this.formData = data.formData || {}; |
| this.completedFields = new Set(data.completedFields || []); |
| this.completedSubgroups = new Set(data.completedSubgroups || []); |
| this.completedSections = new Set(data.completedSections || []); |
| this.currentSection = data.currentSection || 'crime'; |
| this.currentSubgroup = data.currentSubgroup || 'Identification & Timing'; |
| } |
| } |
|
|
| |
| 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 }); |
| |
| 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']); |
| } |
|
|
| } |
|
|