|
|
|
|
|
<div class="site-header"> |
|
|
<div class="header-inner"> |
|
|
<div class="logo-cluster"> |
|
|
<span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;"> |
|
|
<img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" /> |
|
|
</span> |
|
|
<div class="py-detect-title-header"> |
|
|
<span class="py-letter p">P</span> |
|
|
<span class="py-letter y">Y</span> |
|
|
<span class="py-shape"></span> |
|
|
<span class="py-letter d">D</span> |
|
|
<span class="py-letter e">E</span> |
|
|
<span class="py-letter t">T</span> |
|
|
<span class="py-letter e2">E</span> |
|
|
<span class="py-letter c">C</span> |
|
|
<span class="py-letter t2">T</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="header-actions-right"> |
|
|
<button class="back-small" *ngIf="selectedCase" (click)="closeAndReturn()">← Back to {{ getReturnLabel() }}</button> |
|
|
|
|
|
|
|
|
<div #profileDisplay class="profile-display" [class.open]="showProfileMenu" style="margin-left:12px; display:flex;align-items:center; cursor:pointer; position:relative;" (click)="toggleProfileMenu($event)"> |
|
|
<div class="profile-avatar" style="width:2.2em; height:2.2em; border-radius:50%; background: linear-gradient(135deg, #1E3A8A, #2563eb); color: #fff; display: flex; align-items: center; justify-content: center; font-weight:700; font-size:0.95em; overflow: hidden;"> |
|
|
<img *ngIf="currentUser?.avatarUrl" [src]="currentUser?.avatarUrl" alt="avatar" style="width:100%;height:100%;object-fit:cover;" /> |
|
|
<span *ngIf="!currentUser?.avatarUrl">{{ getUserInitials() }}</span> |
|
|
</div> |
|
|
<div *ngIf="showProfileMenu" class="profile-menu" [ngStyle]="profileMenuStyle" style="background:#222;color:#fff;border-radius:8px;padding:8px10px;min-width:220px;box-shadow:06px18px rgba(0,0,0,0.25);"> |
|
|
<div style="display:flex;align-items:center;gap:10px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:8px;"> |
|
|
<div style="width:42px;height:42px;border-radius:50%;background:#2b6ea6;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;overflow:hidden;"> |
|
|
<img *ngIf="currentUser?.avatarUrl" [src]="currentUser?.avatarUrl" alt="avatar" style="width:100%;height:100%;object-fit:cover;" /> |
|
|
<span *ngIf="!currentUser?.avatarUrl">{{ getUserInitials() }}</span> |
|
|
</div> |
|
|
<div style="font-size:0.95em;"> |
|
|
<div>{{ currentUser?.name }}</div> |
|
|
<div style="font-size:0.8em;opacity:0.9;">{{ currentUser?.email }}</div> |
|
|
</div> |
|
|
</div> |
|
|
<button (click)="logout()" style="width:100%;display:flex;align-items:center;gap:8px;padding:8px10px;border:none;border-radius:6px;background:transparent;color:#fff;cursor:pointer;"> |
|
|
<i class="fas fa-sign-out-alt" style="width:18px;text-align:center;"></i> |
|
|
Logout |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button class="logout-btn" (click)="logout()"> |
|
|
<span class="logout-icon">⎋</span> Logout |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="main-content"> |
|
|
<div class="content-container"> |
|
|
|
|
|
<div class="record-card"> |
|
|
<div class="analytics-panel"> |
|
|
<div class="analytics-blue"> |
|
|
<div class="record-header"> |
|
|
<div class="record-title-group"> |
|
|
<span class="record-title"><i class="fas fa-database"></i> Police Investigation Records</span> |
|
|
</div> |
|
|
<div class="record-header-actions"> |
|
|
<span style="position:relative;"> |
|
|
<i class="fas fa-search" style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#b0b0b0;"></i> |
|
|
<input class="record-search" type="text" [(ngModel)]="q" (ngModelChange)="applyFilters()" placeholder="Search this list..." style="padding-left:32px;" /> |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="analytics-cards"> |
|
|
<div class="summary-card total"> |
|
|
<div class="summary-left"> |
|
|
<div class="summary-label">Total Cases</div> |
|
|
<div class="summary-value blue">{{ totalCases }}</div> |
|
|
<div class="summary-sub"> </div> |
|
|
</div> |
|
|
<div class="summary-icon icon-indigo"><i class="fas fa-folder-open fa-bounce"></i></div> |
|
|
</div> |
|
|
<div class="summary-card open"> |
|
|
<div class="summary-left"> |
|
|
<div class="summary-label">Open</div> |
|
|
<div class="summary-value green">{{ openCases }}</div> |
|
|
<div class="summary-sub"> </div> |
|
|
</div> |
|
|
<div class="summary-icon icon-blue"><i class="fas fa-exclamation-circle fa-beat"></i></div> |
|
|
</div> |
|
|
<div class="summary-card closed"> |
|
|
<div class="summary-left"> |
|
|
<div class="summary-label">Closed</div> |
|
|
<div class="summary-value red">{{ closedCases }}</div> |
|
|
<div class="summary-sub"> </div> |
|
|
</div> |
|
|
<div class="summary-icon icon-green"><i class="fas fa-check-circle fa-spin"></i></div> |
|
|
</div> |
|
|
<div class="summary-card review"> |
|
|
<div class="summary-left"> |
|
|
<div class="summary-label">Pending Review</div> |
|
|
<div class="summary-value blue">{{ reviewCases }}</div> |
|
|
<div class="summary-sub"> </div> |
|
|
</div> |
|
|
<div class="summary-icon icon-yellow"><i class="fas fa-hourglass-half"></i></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="record-meta" style="padding:8px24px024px; color: #6b7280; font-size:0.98em;"> |
|
|
{{ filteredCases.length }} items • Updated a few seconds ago |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="filter-bar"> |
|
|
<span class="filter-icon"><i class="fas fa-filter"></i></span> |
|
|
<select [(ngModel)]="filterStatus"> |
|
|
<option value="">Status</option> |
|
|
<option *ngFor="let status of statusTypes">{{ status }}</option> |
|
|
</select> |
|
|
<select [(ngModel)]="filterCrimeType"> |
|
|
<option value="">Crime Type</option> |
|
|
<option *ngFor="let type of crimeTypes">{{ type }}</option> |
|
|
</select> |
|
|
<button (click)="applyFilters()">Apply</button> |
|
|
<button (click)="resetFilters()">Reset</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="table-container"> |
|
|
<div class="table-wrapper"> |
|
|
<table class="record-table"> |
|
|
<thead> |
|
|
<tr> |
|
|
<th>#</th> |
|
|
<th (click)="setSort('caseId')" [attr.aria-sort]="ariaSort('caseId')" style="cursor:pointer;"> |
|
|
Case ID |
|
|
<span class="sort" [ngClass]="{'asc': isAsc('caseId'), 'desc': isDesc('caseId'), 'neutral': !isAsc('caseId') && !isDesc('caseId')}"></span> |
|
|
</th> |
|
|
<th (click)="setSort('priority')" [attr.aria-sort]="ariaSort('priority')" style="cursor:pointer;"> |
|
|
Priority |
|
|
<span class="sort" [ngClass]="{'asc': isAsc('priority'), 'desc': isDesc('priority'), 'neutral': !isAsc('priority') && !isDesc('priority')}"></span> |
|
|
</th> |
|
|
<th (click)="setSort('status')" [attr.aria-sort]="ariaSort('status')" style="cursor:pointer;"> |
|
|
Status |
|
|
<span class="sort" [ngClass]="{'asc': isAsc('status'), 'desc': isDesc('status'), 'neutral': !isAsc('status') && !isDesc('status')}"></span> |
|
|
</th> |
|
|
<th (click)="setSort('crime')" [attr.aria-sort]="ariaSort('crime')" style="cursor:pointer;"> |
|
|
Crime Type |
|
|
<span class="sort" [ngClass]="{'asc': isAsc('crime'), 'desc': isDesc('crime'), 'neutral': !isAsc('crime') && !isDesc('crime')}"></span> |
|
|
</th> |
|
|
<th (click)="setSort('dateTime')" [attr.aria-sort]="ariaSort('dateTime')" style="cursor:pointer;"> |
|
|
Date & Time |
|
|
<span class="sort" [ngClass]="{'asc': isAsc('dateTime'), 'desc': isDesc('dateTime'), 'neutral': !isAsc('dateTime') && !isDesc('dateTime')}"></span> |
|
|
</th> |
|
|
<th>Location</th> |
|
|
<th>Suspect Name</th> |
|
|
<th>Last Updated</th> |
|
|
<th class="progress-col">Progress</th> |
|
|
<th class="next-action-col">Next Action</th> |
|
|
<th class="actions">Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
<tr *ngFor="let c of rows, let i = index"> |
|
|
<td>{{ (currentPage -1) * pageSize + i +1 }}</td> |
|
|
<td class="mono"> |
|
|
<a (click)="openDetails(c)" style="cursor:pointer; color:#2563eb; text-decoration:underline;"> |
|
|
{{ c.caseId || '—' }} |
|
|
</a> |
|
|
</td> |
|
|
<td> |
|
|
<ng-container [ngSwitch]="getCasePriority(c)"> |
|
|
<span *ngSwitchCase="'High'" class="priority-pill priority-high" title="High Priority"> |
|
|
🔴 High |
|
|
</span> |
|
|
<span *ngSwitchCase="'Medium'" class="priority-pill priority-medium" title="Medium Priority"> |
|
|
🟡 Medium |
|
|
</span> |
|
|
<span *ngSwitchCase="'Low'" class="priority-pill priority-low" title="Low Priority"> |
|
|
🟢 Low |
|
|
</span> |
|
|
<span *ngSwitchDefault style="color:#6b7280;">—</span> |
|
|
</ng-container> |
|
|
</td> |
|
|
<td> |
|
|
<span class="status-label" |
|
|
[ngClass]="{ |
|
|
'status-open': c.status === 'Open', |
|
|
'status-under': c.status === 'Under Investigation', |
|
|
'status-closed': c.status === 'Closed' |
|
|
}"> |
|
|
{{ c.status || '—' }} |
|
|
</span> |
|
|
</td> |
|
|
<td>{{ c.crime || '—' }}</td> |
|
|
<td>{{ c.dateTime ? (c.dateTime | date:'M/d/yyyy HH:mm') : '—' }}</td> |
|
|
<td>{{ c.police?.address || '—' }}</td> |
|
|
<td>{{ c.accused?.name || '—' }}</td> |
|
|
<td>{{ c.lastUpdated ? (c.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</td> |
|
|
<td class="progress-col"> |
|
|
<ng-container [ngSwitch]="getProgressValue(c)"> |
|
|
<ng-container *ngSwitchCase="100"> |
|
|
<span class="progress-check">✔</span> |
|
|
<span class="progress-value">100%</span> |
|
|
</ng-container> |
|
|
<ng-container *ngSwitchCase="75"> |
|
|
<span class="progress-dot green"></span> |
|
|
<span class="progress-value">75%</span> |
|
|
</ng-container> |
|
|
<ng-container *ngSwitchDefault> |
|
|
<span class="progress-dot blue"></span> |
|
|
<span class="progress-value">{{ getProgressValue(c) }}%</span> |
|
|
</ng-container> |
|
|
</ng-container> |
|
|
</td> |
|
|
<td class="next-action-col"> |
|
|
{{ getNextActionMessage(c) }} |
|
|
</td> |
|
|
<td class="actions"> |
|
|
<button type="button" class="icon-btn view" (click)="openDetails(c)" title="View Case Details" aria-label="View Case Details"> |
|
|
<i class="fas fa-eye"></i> |
|
|
</button> |
|
|
<button type="button" class="detect-btn" |
|
|
[disabled]="!c.caseId" |
|
|
(click)="c.caseId && goToDetectWithMetadata(c)" |
|
|
title="Go Detect" aria-label="Go Detect"> |
|
|
Go Detect |
|
|
</button> |
|
|
<button *ngIf="isInvestigator()" type="button" class="icon-btn upload" (click)="showEvidencePanel(c)" title="Upload Evidence" aria-label="Upload Evidence"> |
|
|
<i class="fas fa-upload"></i> |
|
|
</button> |
|
|
</td> |
|
|
</tr> |
|
|
<tr *ngIf="rows.length ===0"> |
|
|
<td colspan="12" class="empty">No records found.</td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bottom-controls"> |
|
|
<div class="results-summary"> |
|
|
<div class="results-info"> |
|
|
<i class="fas fa-list-ol"></i> |
|
|
<span>Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span> |
|
|
</div> |
|
|
<div class="page-size-selector"> |
|
|
<span>Show:</span> |
|
|
<select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)"> |
|
|
<option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="pagination-controls"> |
|
|
<button class="page-btn prev" (click)="prevPage()" [disabled]="currentPage ===1"> |
|
|
<i class="fas fa-chevron-left"></i> |
|
|
</button> |
|
|
<div class="page-numbers"> |
|
|
<ng-container *ngFor="let page of getPagination()"> |
|
|
<button *ngIf="page !== '...'" |
|
|
class="page-number" |
|
|
[class.active]="currentPage === page" |
|
|
(click)="goToPage(page)"> |
|
|
{{ page }} |
|
|
</button> |
|
|
<span *ngIf="page === '...'" class="page-ellipsis">...</span> |
|
|
</ng-container> |
|
|
</div> |
|
|
<button class="page-btn next" (click)="nextPage()" [disabled]="currentPage === totalPages"> |
|
|
<i class="fas fa-chevron-right"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div *ngIf="isInvestigator() && selectedCase" class="evidence-upload-section"> |
|
|
<h3>Upload Evidence for Case: {{ selectedCase.caseId }}</h3> |
|
|
<input type="file" multiple (change)="onEvidenceUpload($event)" /> |
|
|
<div class="evidence-list" *ngIf="uploadedEvidence.length"> |
|
|
<div *ngFor="let file of uploadedEvidence" class="evidence-file"> |
|
|
<i class="fas fa-file-upload"></i> {{ file.name }} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div *ngIf="isInvestigator() && evidencePanelCase && evidencePanelCase.caseId" class="evidence-upload-section"> |
|
|
<hr class="evidence-hr" /> |
|
|
<div class="evidence-title"> |
|
|
<i class="fas fa-folder-open evidence-folder"></i> |
|
|
Evidence Upload for Case: {{ evidencePanelCase.caseId }} |
|
|
</div> |
|
|
<hr class="evidence-hr" /> |
|
|
<div class="evidence-type-tabs"> |
|
|
<button [class.active]="evidenceType === 'Document'" (click)="setEvidenceType('Document')">Document</button> |
|
|
<button [class.active]="evidenceType === 'Photo'" (click)="setEvidenceType('Photo')">Photo</button> |
|
|
</div> |
|
|
<div class="evidence-actions"> |
|
|
<label class="evidence-file-label"> |
|
|
[Choose File] |
|
|
<input type="file" multiple (change)="onEvidenceFileSelectType($event)" style="display:none;" /> |
|
|
</label> |
|
|
</div> |
|
|
<hr class="evidence-hr" /> |
|
|
<div class="evidence-list-block"> |
|
|
<div class="evidence-list-title">Uploaded Evidence ({{ evidenceType }}):</div> |
|
|
<div *ngFor="let file of evidenceFiles[evidencePanelCase.caseId][evidenceType]" class="evidence-file-row"> |
|
|
<i [ngClass]="getEvidenceIcon(file.name)" class="evidence-file-icon"></i> |
|
|
<span class="evidence-file-name">{{ file.name }}</span> |
|
|
<a class="evidence-view-link" href="#" (click)="viewEvidenceFile(file)">(View)</a> |
|
|
</div> |
|
|
</div> |
|
|
<hr class="evidence-hr" /> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div *ngIf="selectedCase" class="fullpage-popup-overlay"> |
|
|
<div class="fullpage-popup-content"> |
|
|
<div class="case-details-title">Case Details</div> |
|
|
<div class="details-sections"> |
|
|
<ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']"> |
|
|
<div class="details-section-card"> |
|
|
<div class="section-title">{{ sections[sectionKey].title }}</div> |
|
|
<div class="subgroup-pills"> |
|
|
<button *ngFor="let subgroup of getSubgroups(sectionKey)" |
|
|
[class.active]="selectedSubgroup[sectionKey] === subgroup" |
|
|
(click)="selectSubgroup(sectionKey, subgroup)"> |
|
|
{{ subgroup }} |
|
|
</button> |
|
|
</div> |
|
|
<div class="fields-table-2col"> |
|
|
<div class="fields-col fields-col-labels"> |
|
|
<div class="field-label" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])"> |
|
|
{{ field }} |
|
|
</div> |
|
|
</div> |
|
|
<div class="fields-col fields-col-values"> |
|
|
<div class="field-value" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])"> |
|
|
{{ getFieldValue(selectedCase, sectionKey, field) }} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</ng-container> |
|
|
|
|
|
<div class="details-section-card"> |
|
|
<div style="display:flex;align-items:center;justify-content:space-between;"> |
|
|
<div class="section-title">All Entered Fields (Raw Form Data)</div> |
|
|
<div style="display:flex;gap:8px;align-items:center;"> |
|
|
<button class="small-btn" (click)="copyFormData()" title="Copy JSON">Copy JSON</button> |
|
|
<span *ngIf="copySuccess" style="color:green;font-weight:600;">Copied!</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="raw-formdata-table" style="max-height:420px;overflow:auto;padding:8px;"> |
|
|
<div *ngIf="getFormDataArray(selectedCase)?.length; else noRaw"> |
|
|
<table style="width:100%;border-collapse:collapse;"> |
|
|
<tr *ngFor="let kv of getFormDataArray(selectedCase)"> |
|
|
<td style="padding:8px;border-bottom:1px solid #eee;font-weight:600;width:35%;vertical-align:top;">{{ kv.key }}</td> |
|
|
<td style="padding:8px;border-bottom:1px solid #eee;vertical-align:top;"> |
|
|
<pre style="white-space:pre-wrap;margin:0;font-family:inherit;">{{ formatFormValue(kv.value) }}</pre> |
|
|
</td> |
|
|
</tr> |
|
|
</table> |
|
|
</div> |
|
|
<ng-template #noRaw> |
|
|
<div style="color:#888;">No raw form data saved for this case.</div> |
|
|
</ng-template> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
<div style="display:flex;gap:8px;position:relative;"> |
|
|
<button class="btn edit-btn" (click)="editCase()" title="Edit Case">Edit</button> |
|
|
<button class="btn close-btn-bottom" (click)="closeAndReturn()" title="Close">×</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<footer> |
|
|
<p>©2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p> |
|
|
</footer> |
|
|
|
|
|
|