pykara commited on
Commit
73566f6
·
1 Parent(s): c1ed399
Files changed (50) hide show
  1. src/app/app-routing.module.ts +8 -0
  2. src/app/app.module.ts +7 -1
  3. src/app/case-details-page/case-details-page.component.css +69 -9
  4. src/app/case-details-page/case-details-page.component.html +343 -272
  5. src/app/case-details-page/case-details-page.component.ts +342 -14
  6. src/app/case-details-summary-page/case-details-summary-page.component.css +481 -0
  7. src/app/case-details-summary-page/case-details-summary-page.component.html +87 -0
  8. src/app/case-details-summary-page/case-details-summary-page.component.ts +446 -0
  9. src/app/data/case-data.ts +30 -0
  10. src/app/homepage/auth-card/auth-card.component.css +91 -0
  11. src/app/homepage/auth-card/auth-card.component.html +18 -0
  12. src/app/homepage/auth-card/auth-card.component.ts +19 -0
  13. src/app/homepage/auth-wrapper.component.css +13 -0
  14. src/app/homepage/auth-wrapper.component.ts +36 -0
  15. src/app/homepage/homepage.component.css +61 -0
  16. src/app/homepage/homepage.component.html +9 -2
  17. src/app/homepage/homepage.component.ts +5 -0
  18. src/app/homepage/sign-in/sign-in.component.css +955 -8
  19. src/app/homepage/sign-in/sign-in.component.html +191 -43
  20. src/app/homepage/sign-in/sign-in.component.ts +109 -25
  21. src/app/homepage/sign-in/sign-in.service.ts +1 -1
  22. src/app/homepage/sign-up/sign-up.component.css +560 -322
  23. src/app/homepage/sign-up/sign-up.component.html +133 -66
  24. src/app/homepage/sign-up/sign-up.component.ts +159 -21
  25. src/app/homepage/sign-up/sign-up.service.ts +1 -1
  26. src/app/infopage/infopage.component.css +6 -24
  27. src/app/infopage/infopage.component.html +12 -7
  28. src/app/infopage/infopage.component.ts +1217 -1186
  29. src/app/py-detect/py-detect.component.css +7 -6
  30. src/app/py-detect/py-detect.component.html +78 -22
  31. src/app/py-detect/py-detect.component.ts +543 -387
  32. src/app/py-detect/test-video.component.html +2 -0
  33. src/app/question-data.service.ts +21 -0
  34. src/app/question-summary-page/question-summary-page.component.css +821 -0
  35. src/app/question-summary-page/question-summary-page.component.html +131 -0
  36. src/app/question-summary-page/question-summary-page.component.ts +266 -0
  37. src/app/recordpage/recordpage.component.css +613 -62
  38. src/app/recordpage/recordpage.component.html +269 -228
  39. src/app/recordpage/recordpage.component.ts +252 -27
  40. src/app/services/pydetect.service.ts +486 -0
  41. src/app/shared/case-store.service.ts +150 -21
  42. src/app/validationpage/validationpage.component.css +1021 -732
  43. src/app/validationpage/validationpage.component.html +206 -189
  44. src/app/validationpage/validationpage.component.ts +199 -6
  45. src/app/view-details-page/view-details-page.component.css +750 -0
  46. src/app/view-details-page/view-details-page.component.html +320 -0
  47. src/app/view-details-page/view-details-page.component.ts +360 -0
  48. src/assets/google-logo.svg +9 -0
  49. src/environments/environment.ts +4 -0
  50. src/styles.css +1 -2
src/app/app-routing.module.ts CHANGED
@@ -4,6 +4,9 @@ import { InfopageComponent } from './infopage/infopage.component';
4
  import { ValidationpageComponent } from './validationpage/validationpage.component';
5
  import { RecordpageComponent } from './recordpage/recordpage.component';
6
  import { CaseDetailsPageComponent } from './case-details-page/case-details-page.component';
 
 
 
7
 
8
  const routes: Routes = [
9
  {
@@ -20,6 +23,11 @@ const routes: Routes = [
20
  { path: 'record', component: RecordpageComponent },
21
  { path: 'case-details', component: CaseDetailsPageComponent },
22
  { path: 'case-details/:id', component: CaseDetailsPageComponent },
 
 
 
 
 
23
  { path: '', redirectTo: '/case-detail', pathMatch: 'full' },
24
 
25
  {
 
4
  import { ValidationpageComponent } from './validationpage/validationpage.component';
5
  import { RecordpageComponent } from './recordpage/recordpage.component';
6
  import { CaseDetailsPageComponent } from './case-details-page/case-details-page.component';
7
+ import { QuestionSummaryPageComponent } from './question-summary-page/question-summary-page.component';
8
+ import { CaseDetailsSummaryPageComponent } from './case-details-summary-page/case-details-summary-page.component';
9
+ import { ViewDetailsPageComponent } from './view-details-page/view-details-page.component';
10
 
11
  const routes: Routes = [
12
  {
 
23
  { path: 'record', component: RecordpageComponent },
24
  { path: 'case-details', component: CaseDetailsPageComponent },
25
  { path: 'case-details/:id', component: CaseDetailsPageComponent },
26
+ { path: 'case-details-summary-page', component: CaseDetailsSummaryPageComponent },
27
+ { path: 'case-details-summary-page/:id', component: CaseDetailsSummaryPageComponent },
28
+ { path: 'question-summary', component: QuestionSummaryPageComponent },
29
+ { path: 'view-details', component: ViewDetailsPageComponent },
30
+ { path: 'view-details/:index', component: ViewDetailsPageComponent },
31
  { path: '', redirectTo: '/case-detail', pathMatch: 'full' },
32
 
33
  {
src/app/app.module.ts CHANGED
@@ -14,6 +14,9 @@ import { SignUpComponent } from './homepage/sign-up/sign-up.component';
14
  import { RecordpageComponent } from './recordpage/recordpage.component';
15
  import { CaseDetailsPageComponent } from './case-details-page/case-details-page.component';
16
  import { MatCardModule } from '@angular/material/card';
 
 
 
17
 
18
  @NgModule({
19
  declarations: [
@@ -21,7 +24,10 @@ import { MatCardModule } from '@angular/material/card';
21
  InfopageComponent,
22
  ValidationpageComponent,
23
  RecordpageComponent,
24
- CaseDetailsPageComponent
 
 
 
25
  ],
26
  imports: [
27
  BrowserModule,
 
14
  import { RecordpageComponent } from './recordpage/recordpage.component';
15
  import { CaseDetailsPageComponent } from './case-details-page/case-details-page.component';
16
  import { MatCardModule } from '@angular/material/card';
17
+ import { QuestionSummaryPageComponent } from './question-summary-page/question-summary-page.component';
18
+ import { CaseDetailsSummaryPageComponent } from './case-details-summary-page/case-details-summary-page.component';
19
+ import { ViewDetailsPageComponent } from './view-details-page/view-details-page.component';
20
 
21
  @NgModule({
22
  declarations: [
 
24
  InfopageComponent,
25
  ValidationpageComponent,
26
  RecordpageComponent,
27
+ CaseDetailsPageComponent,
28
+ QuestionSummaryPageComponent,
29
+ CaseDetailsSummaryPageComponent,
30
+ ViewDetailsPageComponent
31
  ],
32
  imports: [
33
  BrowserModule,
src/app/case-details-page/case-details-page.component.css CHANGED
@@ -1,7 +1,7 @@
1
  @import '../recordpage/recordpage.component.css';
2
 
3
  body, html {
4
- overflow: auto !important;
5
  }
6
 
7
  body, main.content {
@@ -26,7 +26,7 @@ body, main.content {
26
  .header-inner {
27
  display: flex;
28
  align-items: center;
29
- justify-content: flex-start;
30
  padding: 18px 32px 0 32px;
31
  position: relative;
32
  }
@@ -462,6 +462,8 @@ hr {
462
  }
463
  }
464
 
 
 
465
  /* table layout for case details page */
466
  .record-table {
467
  width: 100%;
@@ -581,7 +583,8 @@ td.actions {
581
  z-index: 1;
582
  }
583
  .details-content {
584
- max-width: 1200px;
 
585
  margin: 0 auto;
586
  background: #fff;
587
  border-radius: 18px;
@@ -747,9 +750,9 @@ body.popup-open {
747
  }
748
 
749
  .btn.close-btn-bottom {
750
- position: absolute;
751
- right: 32px;
752
- top: 32px;
753
  background: #2563eb;
754
  color: #fff;
755
  border: none;
@@ -867,11 +870,12 @@ footer {
867
  color: #fff;
868
  text-align: center;
869
  padding: 10px 0px;
870
- position: relative;
871
  bottom: 0;
872
  left: 0;
873
  width: 100%;
874
- margin-top: 40px;
 
875
  }
876
 
877
  /* Additional Styles for Go Detect button/icon */
@@ -936,7 +940,7 @@ footer {
936
  border-radius: 24px;
937
  font-weight: 700;
938
  font-size: 1.08em;
939
- padding: 10px 28px;
940
  box-shadow: 0 2px 12px rgba(25, 118, 210, 0.13);
941
  cursor: pointer;
942
  transition: background 0.18s, box-shadow 0.18s, transform 0.18s;
@@ -974,6 +978,62 @@ footer {
974
  100% { opacity: 0.9; }
975
  }
976
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
 
978
 
979
 
 
1
  @import '../recordpage/recordpage.component.css';
2
 
3
  body, html {
4
+ overflow-y: hidden !important;
5
  }
6
 
7
  body, main.content {
 
26
  .header-inner {
27
  display: flex;
28
  align-items: center;
29
+ justify-content: space-between;
30
  padding: 18px 32px 0 32px;
31
  position: relative;
32
  }
 
462
  }
463
  }
464
 
465
+
466
+
467
  /* table layout for case details page */
468
  .record-table {
469
  width: 100%;
 
583
  z-index: 1;
584
  }
585
  .details-content {
586
+ max-width:
587
+ ;
588
  margin: 0 auto;
589
  background: #fff;
590
  border-radius: 18px;
 
750
  }
751
 
752
  .btn.close-btn-bottom {
753
+ position: fixed;
754
+ right: 326px;
755
+ top: 46px;
756
  background: #2563eb;
757
  color: #fff;
758
  border: none;
 
870
  color: #fff;
871
  text-align: center;
872
  padding: 10px 0px;
873
+ position: fixed;
874
  bottom: 0;
875
  left: 0;
876
  width: 100%;
877
+ z-index: 100;
878
+ margin-top: 0;
879
  }
880
 
881
  /* Additional Styles for Go Detect button/icon */
 
940
  border-radius: 24px;
941
  font-weight: 700;
942
  font-size: 1.08em;
943
+ padding: 5px 28px;
944
  box-shadow: 0 2px 12px rgba(25, 118, 210, 0.13);
945
  cursor: pointer;
946
  transition: background 0.18s, box-shadow 0.18s, transform 0.18s;
 
978
  100% { opacity: 0.9; }
979
  }
980
 
981
+ .header-actions-right {
982
+ position: absolute;
983
+ right: 32px;
984
+ top: 27px;
985
+ display: flex;
986
+ align-items: center;
987
+ z-index: 100;
988
+ }
989
+
990
+ .logout-btn {
991
+ font-size: 0.85rem;
992
+ padding: 0.18rem 0.7rem;
993
+ border-radius: 5px;
994
+ min-width: unset;
995
+ min-height: unset;
996
+ box-shadow: 0 1px 4px #d1d5db22;
997
+ display: inline-flex;
998
+ align-items: center;
999
+ gap: 6px;
1000
+ }
1001
+ .logout-btn:hover {
1002
+ background: linear-gradient(90deg, #23272b 0%, #ef4444 100%);
1003
+ color: #fff;
1004
+ box-shadow: 0 2px 24px #ef444488;
1005
+ transform: scale(1.04);
1006
+ }
1007
+ .logout-icon {
1008
+ font-size: 1.2em;
1009
+ margin-right: 6px;
1010
+ }
1011
+
1012
+ .summary-value.blue { color: #2563eb; }
1013
+ .summary-value.green { color: #22c55e; }
1014
+ .summary-value.red { color: #ef4444; }
1015
+
1016
+ .sort {
1017
+ display: inline-block;
1018
+ width:0;
1019
+ height:0;
1020
+ border-left:4px solid transparent;
1021
+ border-right:4px solid transparent;
1022
+ margin-left:6px;
1023
+ vertical-align: middle;
1024
+ }
1025
+ .sort.asc {
1026
+ border-bottom:6px solid #4a5568;
1027
+ }
1028
+ .sort.desc {
1029
+ border-top:6px solid #4a5568;
1030
+ }
1031
+ .sort.neutral {
1032
+ border-top:6px solid #b0b0b0;
1033
+ border-bottom:6px solid #b0b0b0;
1034
+ opacity:0.5;
1035
+ }
1036
+
1037
 
1038
 
1039
 
src/app/case-details-page/case-details-page.component.html CHANGED
@@ -1,302 +1,373 @@
1
  <!-- Modern UI header with logo and PyDetect title -->
2
  <div class="site-header">
3
- <div class="header-inner">
4
- <div class="logo-cluster">
5
- <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
- <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
- </span>
8
- <div class="py-detect-title-header">
9
- <span class="py-letter p">P</span>
10
- <span class="py-letter y">Y</span>
11
- <span class="py-shape"></span>
12
- <span class="py-letter d">D</span>
13
- <span class="py-letter e">E</span>
14
- <span class="py-letter t">T</span>
15
- <span class="py-letter e2">E</span>
16
- <span class="py-letter c">C</span>
17
- <span class="py-letter t2">T</span>
18
- </div>
19
- </div>
20
- </div>
 
 
 
 
 
 
21
  </div>
22
 
23
  <div class="record-card">
24
- <div class="record-header">
25
- <div class="record-title-group">
26
- <span class="record-title">Your Assigned Cases</span>
27
- </div>
28
- <div class="record-header-actions">
29
- <input class="record-search" type="text" [(ngModel)]="q" placeholder="Search your cases..." />
30
- </div>
31
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- <div class="analytics-summary">
34
- <div class="summary-card total">
35
- <div class="summary-label">Total Cases</div>
36
- <div class="summary-value">{{ totalCases }}</div>
37
- </div>
38
- <div class="summary-card open">
39
- <div class="summary-label">Open</div>
40
- <div class="summary-value">{{ openCases }}</div>
41
- </div>
42
- <div class="summary-card closed">
43
- <div class="summary-label">Closed</div>
44
- <div class="summary-value">{{ closedCases }}</div>
45
- </div>
46
- <div class="summary-card review">
47
- <div class="summary-label">Pending Review</div>
48
- <div class="summary-value">{{ reviewCases }}</div>
49
- </div>
50
- </div>
51
 
52
- <div class="record-meta" style="padding: 8px 24px 0 24px; color: #6b7280; font-size: 0.98em;">
53
- {{ filteredCases.length }} items • Updated a few seconds ago
54
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- <div class="filter-bar">
57
- <select [(ngModel)]="filterStatus">
58
- <option value="">Status</option>
59
- <option *ngFor="let status of statusTypes">{{ status }}</option>
60
- </select>
61
- <select [(ngModel)]="filterCrimeType">
62
- <option value="">Crime Type</option>
63
- <option *ngFor="let type of crimeTypes">{{ type }}</option>
64
- </select>
65
- <button (click)="applyFilters()">Apply</button>
66
- <button (click)="resetFilters()">Reset</button>
67
- </div>
68
-
69
- <!-- Table/list view always visible -->
70
- <table class="record-table">
71
- <thead>
72
- <tr>
73
- <th>#</th>
74
- <th>Case ID</th>
75
- <th>Priority</th>
76
- <th>Status</th>
77
- <th>Crime Type</th>
78
- <th>Date &amp; Time</th>
79
- <th>Location</th>
80
- <th>Suspect Name</th>
81
- <th>Last Updated</th>
82
- <th class="progress-col">Progress</th>
83
- <th class="next-action-col">Next Action</th>
84
- <th class="actions">Actions</th>
85
- </tr>
86
- </thead>
87
- <tbody>
88
- <tr *ngFor="let c of rows, let i = index">
89
- <td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
90
- <td class="mono">
91
- <a (click)="openDetails(c)" style="cursor:pointer; color:#2563eb; text-decoration:underline;">
92
- {{ c.caseId || '—' }}
93
- </a>
94
- </td>
95
- <td>
96
- <ng-container [ngSwitch]="getCasePriority(c)">
97
- <span *ngSwitchCase="'High'" class="priority-pill priority-high" title="High Priority">
98
- 🔴 High
99
- </span>
100
- <span *ngSwitchCase="'Medium'" class="priority-pill priority-medium" title="Medium Priority">
101
- 🟡 Medium
102
- </span>
103
- <span *ngSwitchCase="'Low'" class="priority-pill priority-low" title="Low Priority">
104
- 🟢 Low
105
- </span>
106
- <span *ngSwitchDefault style="color:#6b7280;">—</span>
107
- </ng-container>
108
- </td>
109
- <td>
110
- <span class="status-label"
111
- [ngClass]="{
112
- 'status-open': c.status === 'Open',
113
- 'status-under': c.status === 'Under Investigation',
114
- 'status-closed': c.status === 'Closed'
115
- }">
116
- {{ c.status || '—' }}
117
- </span>
118
- </td>
119
- <td>{{ c.crime || '—' }}</td>
120
- <td>{{ c.dateTime ? (c.dateTime | date:'M/d/yyyy HH:mm') : '—' }}</td>
121
- <td>{{ c.police.address || '—' }}</td>
122
- <td>{{ c.accused.name || '—' }}</td>
123
- <td>{{ c.lastUpdated ? (c.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</td>
124
- <td class="progress-col">
125
- <ng-container [ngSwitch]="getProgressValue(c)">
126
- <ng-container *ngSwitchCase="100">
127
- <span class="progress-check">&#x2714;</span>
128
- <span class="progress-value">100%</span>
129
- </ng-container>
130
- <ng-container *ngSwitchCase="75">
131
- <span class="progress-dot green"></span>
132
- <span class="progress-value">75%</span>
133
- </ng-container>
134
- <ng-container *ngSwitchDefault>
135
- <span class="progress-dot blue"></span>
136
- <span class="progress-value">{{ getProgressValue(c) }}%</span>
137
- </ng-container>
138
- </ng-container>
139
- </td>
140
- <td class="next-action-col">
141
- {{ getNextActionMessage(c) }}
142
- </td>
143
- <td class="actions">
144
- <button type="button" class="icon-btn view" (click)="openDetails(c)" title="View Case Details" aria-label="View Case Details">
145
- <i class="fas fa-eye"></i>
146
- </button>
147
- <button type="button" class="detect-btn"
148
- [disabled]="!c.caseId"
149
- (click)="c.caseId && goToDetectWithMetadata(c)"
150
- title="Go Detect" aria-label="Go Detect">
151
- Go Detect
152
- </button>
153
- <button *ngIf="isInvestigator()" type="button" class="icon-btn upload" (click)="showEvidencePanel(c)" title="Upload Evidence" aria-label="Upload Evidence">
154
- <i class="fas fa-upload"></i>
155
- </button>
156
- </td>
157
- </tr>
158
- <tr *ngIf="rows.length === 0">
159
- <td colspan="12" class="empty">No records found.</td>
160
- </tr>
161
- </tbody>
162
- </table>
163
-
164
- <!-- Pagination Controls -->
165
- <div class="pagination-controls" style="display:flex;justify-content:center;align-items:center;margin:20px 0;gap:10px;">
166
- <style>
167
- .pagination-controls button {
168
- border: none;
169
- background: #f3f4f6;
170
- color: #333;
171
- border-radius: 8px;
172
- padding: 0 16px;
173
- min-width: 40px;
174
- min-height: 40px;
175
- font-size: 1.1em;
176
- font-weight: 500;
177
- box-shadow: 0 2px 8px rgba(0,0,0,0.04);
178
- transition: background 0.2s, color 0.2s, transform 0.2s;
179
- cursor: pointer;
180
- outline: none;
181
- }
182
- .pagination-controls button:hover:not(:disabled),
183
- .pagination-controls button:focus:not(:disabled) {
184
- background: #e3eafe;
185
- color: #1976d2;
186
- transform: scale(1.08);
187
- }
188
- .pagination-controls button.active {
189
- background: #1976d2;
190
- color: #fff;
191
- font-weight: bold;
192
- box-shadow: 0 0 0 2px #90caf9;
193
- animation: pulseActive 1s infinite;
194
- }
195
- @keyframes pulseActive {
196
- 0% { box-shadow: 0 0 0 2px #90caf9; }
197
- 50% { box-shadow: 0 0 0 6px #90caf9; }
198
- 100% { box-shadow: 0 0 0 2px #90caf9; }
199
- }
200
- .pagination-controls span {
201
- font-size: 1.2em;
202
- color: #888;
203
- padding: 0 8px;
204
- }
205
- </style>
206
- <button (click)="prevPage()" [disabled]="currentPage === 1">«</button>
207
- <ng-container *ngFor="let page of getPagination()">
208
- <button *ngIf="page !== '...'" (click)="goToPage(page)" [class.active]="currentPage === page">{{ page }}</button>
209
- <span *ngIf="page === '...'">...</span>
210
- </ng-container>
211
- <button (click)="nextPage()" [disabled]="currentPage === totalPages">»</button>
212
- </div>
213
  </div>
214
 
215
  <!-- Results summary and page size selector -->
216
  <div style="display:flex;align-items:center;justify-content:flex-start;gap:24px;margin-bottom:16px;">
217
- <span style="font-size:1.1em;">Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span>
218
- <select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)" style="padding:4px 12px;border-radius:8px;font-size:1em;">
219
- <option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option>
220
- </select>
221
  </div>
222
 
223
  <!-- Evidence Upload Section for Investigators only, below the table -->
224
  <div *ngIf="isInvestigator() && selectedCase" class="evidence-upload-section">
225
- <h3>Upload Evidence for Case: {{ selectedCase.caseId }}</h3>
226
- <input type="file" multiple (change)="onEvidenceUpload($event)" />
227
- <div class="evidence-list" *ngIf="uploadedEvidence.length">
228
- <div *ngFor="let file of uploadedEvidence" class="evidence-file">
229
- <i class="fas fa-file-upload"></i> {{ file.name }}
230
- </div>
231
- </div>
232
  </div>
233
 
234
  <!-- Evidence Upload Panel for selected case -->
235
  <div *ngIf="isInvestigator() && evidencePanelCase && evidencePanelCase.caseId" class="evidence-upload-section">
236
- <hr class="evidence-hr" />
237
- <div class="evidence-title">
238
- <i class="fas fa-folder-open evidence-folder"></i>
239
- Evidence Upload for Case: {{ evidencePanelCase.caseId }}
240
- </div>
241
- <hr class="evidence-hr" />
242
- <div class="evidence-type-tabs">
243
- <button [class.active]="evidenceType === 'Document'" (click)="setEvidenceType('Document')">Document</button>
244
- <button [class.active]="evidenceType === 'Photo'" (click)="setEvidenceType('Photo')">Photo</button>
245
- </div>
246
- <div class="evidence-actions">
247
- <label class="evidence-file-label">
248
- [Choose File]
249
- <input type="file" multiple (change)="onEvidenceFileSelectType($event)" style="display:none;" />
250
- </label>
251
- </div>
252
- <hr class="evidence-hr" />
253
- <div class="evidence-list-block">
254
- <div class="evidence-list-title">Uploaded Evidence ({{ evidenceType }}):</div>
255
- <div *ngFor="let file of evidenceFiles[evidencePanelCase.caseId][evidenceType]" class="evidence-file-row">
256
- <i [ngClass]="getEvidenceIcon(file.name)" class="evidence-file-icon"></i>
257
- <span class="evidence-file-name">{{ file.name }}</span>
258
- <a class="evidence-view-link" href="#" (click)="viewEvidenceFile(file)">(View)</a>
259
- </div>
260
- </div>
261
- <hr class="evidence-hr" />
262
  </div>
263
 
264
  <!-- Full-page overlay for details (no evidence upload here) -->
265
  <div *ngIf="selectedCase" class="fullpage-popup-overlay">
266
- <div class="fullpage-popup-content">
267
- <div class="case-details-title">Case Details</div>
268
- <div class="details-sections">
269
- <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
270
- <div class="details-section-card">
271
- <div class="section-title">{{ sections[sectionKey].title }}</div>
272
- <div class="subgroup-pills">
273
- <button *ngFor="let subgroup of getSubgroups(sectionKey)"
274
- [class.active]="selectedSubgroup[sectionKey] === subgroup"
275
- (click)="selectSubgroup(sectionKey, subgroup)">
276
- {{ subgroup }}
277
- </button>
278
- </div>
279
- <div class="fields-table-2col">
280
- <div class="fields-col fields-col-labels">
281
- <div class="field-label" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
282
- {{ field }}
283
- </div>
284
- </div>
285
- <div class="fields-col fields-col-values">
286
- <div class="field-value" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
287
- {{ getFieldValue(selectedCase, sectionKey, field) }}
288
- </div>
289
- </div>
290
- </div>
291
- </div>
292
- </ng-container>
293
- </div>
294
- <button class="btn close-btn-bottom" (click)="closeDetails()" title="Close">&times;</button>
295
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  </div>
297
 
298
  <!-- Footer from provided design -->
299
  <footer>
300
- <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
301
  </footer>
302
  <!-- End of record-card -->
 
1
  <!-- Modern UI header with logo and PyDetect title -->
2
  <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
+ </div>
20
+ <div class="header-actions-right">
21
+ <button class="back-small" *ngIf="selectedCase" (click)="closeAndReturn()">← Back to {{ getReturnLabel() }}</button>
22
+ <button class="logout-btn" (click)="logout()">
23
+ <span class="logout-icon">⎋</span> Logout
24
+ </button>
25
+ </div>
26
+ </div>
27
  </div>
28
 
29
  <div class="record-card">
30
+ <div class="analytics-panel">
31
+ <div class="analytics-blue">
32
+ <div class="record-header">
33
+ <div class="record-title-group">
34
+ <span class="record-title"><i class="fas fa-database"></i> Police Investigation Records</span>
35
+ </div>
36
+ <div class="record-header-actions">
37
+ <span style="position:relative;">
38
+ <i class="fas fa-search" style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#b0b0b0;"></i>
39
+ <input class="record-search" type="text" [(ngModel)]="q" (ngModelChange)="applyFilters()" placeholder="Search this list..." style="padding-left:32px;" />
40
+ </span>
41
+ </div>
42
+ </div>
43
+ <div class="analytics-cards">
44
+ <div class="summary-card total">
45
+ <div class="summary-left">
46
+ <div class="summary-label">Total Cases</div>
47
+ <div class="summary-value blue">{{ totalCases }}</div>
48
+ <div class="summary-sub">&nbsp;</div>
49
+ </div>
50
+ <div class="summary-icon icon-indigo"><i class="fas fa-folder-open fa-bounce"></i></div>
51
+ </div>
52
+ <div class="summary-card open">
53
+ <div class="summary-left">
54
+ <div class="summary-label">Open</div>
55
+ <div class="summary-value green">{{ openCases }}</div>
56
+ <div class="summary-sub">&nbsp;</div>
57
+ </div>
58
+ <div class="summary-icon icon-blue"><i class="fas fa-exclamation-circle fa-beat"></i></div>
59
+ </div>
60
+ <div class="summary-card closed">
61
+ <div class="summary-left">
62
+ <div class="summary-label">Closed</div>
63
+ <div class="summary-value red">{{ closedCases }}</div>
64
+ <div class="summary-sub">&nbsp;</div>
65
+ </div>
66
+ <div class="summary-icon icon-green"><i class="fas fa-check-circle fa-spin"></i></div>
67
+ </div>
68
+ <div class="summary-card review">
69
+ <div class="summary-left">
70
+ <div class="summary-label">Pending Review</div>
71
+ <div class="summary-value blue">{{ reviewCases }}</div>
72
+ <div class="summary-sub">&nbsp;</div>
73
+ </div>
74
+ <div class="summary-icon icon-yellow"><i class="fas fa-hourglass-half"></i></div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <div class="record-meta" style="padding:8px24px024px; color: #6b7280; font-size:0.98em;">
79
+ {{ filteredCases.length }} items • Updated a few seconds ago
80
+ </div>
81
+ </div>
82
 
83
+ <div class="filter-bar">
84
+ <select [(ngModel)]="filterStatus">
85
+ <option value="">Status</option>
86
+ <option *ngFor="let status of statusTypes">{{ status }}</option>
87
+ </select>
88
+ <select [(ngModel)]="filterCrimeType">
89
+ <option value="">Crime Type</option>
90
+ <option *ngFor="let type of crimeTypes">{{ type }}</option>
91
+ </select>
92
+ <button (click)="applyFilters()">Apply</button>
93
+ <button (click)="resetFilters()">Reset</button>
94
+ </div>
 
 
 
 
 
 
95
 
96
+ <!-- Table/list view always visible -->
97
+ <table class="record-table">
98
+ <thead>
99
+ <tr>
100
+ <th>#</th>
101
+ <th (click)="setSort('caseId')" [attr.aria-sort]="ariaSort('caseId')" style="cursor:pointer;">
102
+ Case ID
103
+ <span class="sort" [ngClass]="{'asc': isAsc('caseId'), 'desc': isDesc('caseId'), 'neutral': !isAsc('caseId') && !isDesc('caseId')}"></span>
104
+ </th>
105
+ <th (click)="setSort('priority')" [attr.aria-sort]="ariaSort('priority')" style="cursor:pointer;">
106
+ Priority
107
+ <span class="sort" [ngClass]="{'asc': isAsc('priority'), 'desc': isDesc('priority'), 'neutral': !isAsc('priority') && !isDesc('priority')}"></span>
108
+ </th>
109
+ <th (click)="setSort('status')" [attr.aria-sort]="ariaSort('status')" style="cursor:pointer;">
110
+ Status
111
+ <span class="sort" [ngClass]="{'asc': isAsc('status'), 'desc': isDesc('status'), 'neutral': !isAsc('status') && !isDesc('status')}"></span>
112
+ </th>
113
+ <th (click)="setSort('crime')" [attr.aria-sort]="ariaSort('crime')" style="cursor:pointer;">
114
+ Crime Type
115
+ <span class="sort" [ngClass]="{'asc': isAsc('crime'), 'desc': isDesc('crime'), 'neutral': !isAsc('crime') && !isDesc('crime')}"></span>
116
+ </th>
117
+ <th (click)="setSort('dateTime')" [attr.aria-sort]="ariaSort('dateTime')" style="cursor:pointer;">
118
+ Date &amp; Time
119
+ <span class="sort" [ngClass]="{'asc': isAsc('dateTime'), 'desc': isDesc('dateTime'), 'neutral': !isAsc('dateTime') && !isDesc('dateTime')}"></span>
120
+ </th>
121
+ <th>Location</th>
122
+ <th>Suspect Name</th>
123
+ <th>Last Updated</th>
124
+ <th class="progress-col">Progress</th>
125
+ <th class="next-action-col">Next Action</th>
126
+ <th class="actions">Actions</th>
127
+ </tr>
128
+ </thead>
129
+ <tbody>
130
+ <tr *ngFor="let c of rows, let i = index">
131
+ <td>{{ (currentPage -1) * pageSize + i +1 }}</td>
132
+ <td class="mono">
133
+ <a (click)="openDetails(c)" style="cursor:pointer; color:#2563eb; text-decoration:underline;">
134
+ {{ c.caseId || '—' }}
135
+ </a>
136
+ </td>
137
+ <td>
138
+ <ng-container [ngSwitch]="getCasePriority(c)">
139
+ <span *ngSwitchCase="'High'" class="priority-pill priority-high" title="High Priority">
140
+ 🔴 High
141
+ </span>
142
+ <span *ngSwitchCase="'Medium'" class="priority-pill priority-medium" title="Medium Priority">
143
+ 🟡 Medium
144
+ </span>
145
+ <span *ngSwitchCase="'Low'" class="priority-pill priority-low" title="Low Priority">
146
+ 🟢 Low
147
+ </span>
148
+ <span *ngSwitchDefault style="color:#6b7280;">—</span>
149
+ </ng-container>
150
+ </td>
151
+ <td>
152
+ <span class="status-label"
153
+ [ngClass]="{
154
+ 'status-open': c.status === 'Open',
155
+ 'status-under': c.status === 'Under Investigation',
156
+ 'status-closed': c.status === 'Closed'
157
+ }">
158
+ {{ c.status || '—' }}
159
+ </span>
160
+ </td>
161
+ <td>{{ c.crime || '—' }}</td>
162
+ <td>{{ c.dateTime ? (c.dateTime | date:'M/d/yyyy HH:mm') : '—' }}</td>
163
+ <td>{{ c.police?.address || '—' }}</td>
164
+ <td>{{ c.accused?.name || '—' }}</td>
165
+ <td>{{ c.lastUpdated ? (c.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</td>
166
+ <td class="progress-col">
167
+ <ng-container [ngSwitch]="getProgressValue(c)">
168
+ <ng-container *ngSwitchCase="100">
169
+ <span class="progress-check">&#x2714;</span>
170
+ <span class="progress-value">100%</span>
171
+ </ng-container>
172
+ <ng-container *ngSwitchCase="75">
173
+ <span class="progress-dot green"></span>
174
+ <span class="progress-value">75%</span>
175
+ </ng-container>
176
+ <ng-container *ngSwitchDefault>
177
+ <span class="progress-dot blue"></span>
178
+ <span class="progress-value">{{ getProgressValue(c) }}%</span>
179
+ </ng-container>
180
+ </ng-container>
181
+ </td>
182
+ <td class="next-action-col">
183
+ {{ getNextActionMessage(c) }}
184
+ </td>
185
+ <td class="actions">
186
+ <button type="button" class="icon-btn view" (click)="openDetails(c)" title="View Case Details" aria-label="View Case Details">
187
+ <i class="fas fa-eye"></i>
188
+ </button>
189
+ <button type="button" class="detect-btn"
190
+ [disabled]="!c.caseId"
191
+ (click)="c.caseId && goToDetectWithMetadata(c)"
192
+ title="Go Detect" aria-label="Go Detect">
193
+ Go Detect
194
+ </button>
195
+ <button *ngIf="isInvestigator()" type="button" class="icon-btn upload" (click)="showEvidencePanel(c)" title="Upload Evidence" aria-label="Upload Evidence">
196
+ <i class="fas fa-upload"></i>
197
+ </button>
198
+ </td>
199
+ </tr>
200
+ <tr *ngIf="rows.length ===0">
201
+ <td colspan="12" class="empty">No records found.</td>
202
+ </tr>
203
+ </tbody>
204
+ </table>
205
 
206
+ <!-- Pagination Controls -->
207
+ <div class="pagination-controls" style="display:flex;justify-content:center;align-items:center;margin:20px0;gap:10px;">
208
+ <style>
209
+ .pagination-controls button {
210
+ border: none;
211
+ background: #f3f4f6;
212
+ color: #333;
213
+ border-radius:8px;
214
+ padding:016px;
215
+ min-width:40px;
216
+ min-height:40px;
217
+ font-size:1.1em;
218
+ font-weight:500;
219
+ box-shadow:02px 8px rgba(0,0,0,0.04);
220
+ transition: background 0.2s, color 0.2s, transform 0.2s;
221
+ cursor: pointer;
222
+ outline: none;
223
+ }
224
+ .pagination-controls button:hover:not(:disabled),
225
+ .pagination-controls button:focus:not(:disabled) {
226
+ background: #e3eafe;
227
+ color: #1976d2;
228
+ transform: scale(1.08);
229
+ }
230
+ .pagination-controls button.active {
231
+ background: #1976d2;
232
+ color: #fff;
233
+ font-weight: bold;
234
+ box-shadow:0002px #90caf9;
235
+ animation: pulseActive1s infinite;
236
+ }
237
+ @keyframes pulseActive {
238
+ 0% { box-shadow:0002px #90caf9; }
239
+ 50% { box-shadow:0006px #90caf9; }
240
+ 100% { box-shadow:0002px #90caf9; }
241
+ }
242
+ .pagination-controls span {
243
+ font-size:1.2em;
244
+ color: #888;
245
+ padding:08px;
246
+ }
247
+ </style>
248
+ <button (click)="prevPage()" [disabled]="currentPage ===1">«</button>
249
+ <ng-container *ngFor="let page of getPagination()">
250
+ <button *ngIf="page !== '...'" (click)="goToPage(page)" [class.active]="currentPage === page">{{ page }}</button>
251
+ <span *ngIf="page === '...'">...</span>
252
+ </ng-container>
253
+ <button (click)="nextPage()" [disabled]="currentPage === totalPages">»</button>
254
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  </div>
256
 
257
  <!-- Results summary and page size selector -->
258
  <div style="display:flex;align-items:center;justify-content:flex-start;gap:24px;margin-bottom:16px;">
259
+ <span style="font-size:1.1em;">Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span>
260
+ <select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)" style="padding:4px 12px;border-radius:8px;font-size:1em;">
261
+ <option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option>
262
+ </select>
263
  </div>
264
 
265
  <!-- Evidence Upload Section for Investigators only, below the table -->
266
  <div *ngIf="isInvestigator() && selectedCase" class="evidence-upload-section">
267
+ <h3>Upload Evidence for Case: {{ selectedCase.caseId }}</h3>
268
+ <input type="file" multiple (change)="onEvidenceUpload($event)" />
269
+ <div class="evidence-list" *ngIf="uploadedEvidence.length">
270
+ <div *ngFor="let file of uploadedEvidence" class="evidence-file">
271
+ <i class="fas fa-file-upload"></i> {{ file.name }}
272
+ </div>
273
+ </div>
274
  </div>
275
 
276
  <!-- Evidence Upload Panel for selected case -->
277
  <div *ngIf="isInvestigator() && evidencePanelCase && evidencePanelCase.caseId" class="evidence-upload-section">
278
+ <hr class="evidence-hr" />
279
+ <div class="evidence-title">
280
+ <i class="fas fa-folder-open evidence-folder"></i>
281
+ Evidence Upload for Case: {{ evidencePanelCase.caseId }}
282
+ </div>
283
+ <hr class="evidence-hr" />
284
+ <div class="evidence-type-tabs">
285
+ <button [class.active]="evidenceType === 'Document'" (click)="setEvidenceType('Document')">Document</button>
286
+ <button [class.active]="evidenceType === 'Photo'" (click)="setEvidenceType('Photo')">Photo</button>
287
+ </div>
288
+ <div class="evidence-actions">
289
+ <label class="evidence-file-label">
290
+ [Choose File]
291
+ <input type="file" multiple (change)="onEvidenceFileSelectType($event)" style="display:none;" />
292
+ </label>
293
+ </div>
294
+ <hr class="evidence-hr" />
295
+ <div class="evidence-list-block">
296
+ <div class="evidence-list-title">Uploaded Evidence ({{ evidenceType }}):</div>
297
+ <div *ngFor="let file of evidenceFiles[evidencePanelCase.caseId][evidenceType]" class="evidence-file-row">
298
+ <i [ngClass]="getEvidenceIcon(file.name)" class="evidence-file-icon"></i>
299
+ <span class="evidence-file-name">{{ file.name }}</span>
300
+ <a class="evidence-view-link" href="#" (click)="viewEvidenceFile(file)">(View)</a>
301
+ </div>
302
+ </div>
303
+ <hr class="evidence-hr" />
304
  </div>
305
 
306
  <!-- Full-page overlay for details (no evidence upload here) -->
307
  <div *ngIf="selectedCase" class="fullpage-popup-overlay">
308
+ <div class="fullpage-popup-content">
309
+ <div class="case-details-title">Case Details</div>
310
+ <div class="details-sections">
311
+ <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
312
+ <div class="details-section-card">
313
+ <div class="section-title">{{ sections[sectionKey].title }}</div>
314
+ <div class="subgroup-pills">
315
+ <button *ngFor="let subgroup of getSubgroups(sectionKey)"
316
+ [class.active]="selectedSubgroup[sectionKey] === subgroup"
317
+ (click)="selectSubgroup(sectionKey, subgroup)">
318
+ {{ subgroup }}
319
+ </button>
320
+ </div>
321
+ <div class="fields-table-2col">
322
+ <div class="fields-col fields-col-labels">
323
+ <div class="field-label" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
324
+ {{ field }}
325
+ </div>
326
+ </div>
327
+ <div class="fields-col fields-col-values">
328
+ <div class="field-value" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
329
+ {{ getFieldValue(selectedCase, sectionKey, field) }}
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ </ng-container>
335
+
336
+ <div class="details-section-card">
337
+ <div style="display:flex;align-items:center;justify-content:space-between;">
338
+ <div class="section-title">All Entered Fields (Raw Form Data)</div>
339
+ <div style="display:flex;gap:8px;align-items:center;">
340
+ <button class="small-btn" (click)="copyFormData()" title="Copy JSON">Copy JSON</button>
341
+ <span *ngIf="copySuccess" style="color:green;font-weight:600;">Copied!</span>
342
+ </div>
343
+ </div>
344
+ <div class="raw-formdata-table" style="max-height:420px;overflow:auto;padding:8px;">
345
+ <div *ngIf="getFormDataArray(selectedCase)?.length; else noRaw">
346
+ <table style="width:100%;border-collapse:collapse;">
347
+ <tr *ngFor="let kv of getFormDataArray(selectedCase)">
348
+ <td style="padding:8px;border-bottom:1px solid #eee;font-weight:600;width:35%;vertical-align:top;">{{ kv.key }}</td>
349
+ <td style="padding:8px;border-bottom:1px solid #eee;vertical-align:top;">
350
+ <pre style="white-space:pre-wrap;margin:0;font-family:inherit;">{{ formatFormValue(kv.value) }}</pre>
351
+ </td>
352
+ </tr>
353
+ </table>
354
+ </div>
355
+ <ng-template #noRaw>
356
+ <div style="color:#888;">No raw form data saved for this case.</div>
357
+ </ng-template>
358
+ </div>
359
+ </div>
360
+
361
+ </div>
362
+ <div style="display:flex;gap:8px;position:relative;">
363
+ <button class="btn edit-btn" (click)="editCase()" title="Edit Case">Edit</button>
364
+ <button class="btn close-btn-bottom" (click)="closeAndReturn()" title="Close">&times;</button>
365
+ </div>
366
+ </div>
367
  </div>
368
 
369
  <!-- Footer from provided design -->
370
  <footer>
371
+ <p>©2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
372
  </footer>
373
  <!-- End of record-card -->
src/app/case-details-page/case-details-page.component.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { Component, OnInit } from '@angular/core';
2
- import { Router } from '@angular/router';
3
  import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
4
 
5
  type EvidenceType = 'Document' | 'Photo';
@@ -21,7 +21,38 @@ export class CaseDetailsPageComponent implements OnInit {
21
  }
22
  get pagedCases(): PoliceCase[] {
23
  const start = (this.currentPage - 1) * this.pageSize;
24
- return this.filteredCases.slice(start, start + this.pageSize);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
  setPage(page: number) {
27
  if (page < 1 || page > this.totalPages) return;
@@ -45,9 +76,15 @@ export class CaseDetailsPageComponent implements OnInit {
45
 
46
  showDetails = false;
47
  selectedCase: PoliceCase | null = null;
 
 
48
  username: string = '';
49
  q: string = '';
50
 
 
 
 
 
51
  filterStatus: string = '';
52
  filterCrimeType: string = '';
53
  filterDateFrom: string = '';
@@ -100,11 +137,54 @@ export class CaseDetailsPageComponent implements OnInit {
100
  evidenceType: EvidenceType = 'Document';
101
  evidenceFiles: { [caseId: string]: { Document: File[]; Photo: File[] } } = {};
102
 
103
- constructor(private caseStore: CaseStoreService, private router: Router) {}
 
 
 
 
 
 
 
 
104
 
105
  ngOnInit(): void {
 
106
  this.cases = this.caseStore.getPoliceCases();
107
  this.username = localStorage.getItem('username') || sessionStorage.getItem('username') || '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
  get filteredCases(): PoliceCase[] {
@@ -150,10 +230,31 @@ export class CaseDetailsPageComponent implements OnInit {
150
  return Array.from(new Set(this.cases.map(c => c.crime).filter(s => !!s))) as string[];
151
  }
152
 
153
- openDetails(c: PoliceCase): void {
154
- this.selectedCase = c;
155
- this.showDetails = true;
156
- document.body.classList.add('popup-open');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  }
158
 
159
  closeDetails(): void {
@@ -170,6 +271,7 @@ export class CaseDetailsPageComponent implements OnInit {
170
  const metadata = {
171
  caseId: c.caseId || '',
172
  crimeType: c.crime || '',
 
173
  dateTime: c.dateTime || '',
174
  location: c.police?.address || '',
175
  suspectName: c.accused?.name || '',
@@ -336,16 +438,95 @@ export class CaseDetailsPageComponent implements OnInit {
336
  };
337
 
338
  const path = fieldMap[field] || field;
 
339
  if (Array.isArray(path)) {
340
- let value = sc;
341
  for (const p of path) {
342
- if (value && value[p] !== undefined) value = value[p];
343
- else return '—';
 
 
 
344
  }
345
- return value !== undefined && value !== null && value !== '' ? value : '—';
346
  } else {
347
- return sc[path] !== undefined && sc[path] !== null && sc[path] !== '' ? sc[path] : '—';
348
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  }
350
 
351
  getProgressValue(caseObj: any): number {
@@ -381,10 +562,13 @@ export class CaseDetailsPageComponent implements OnInit {
381
  // Automatic logic based on crime type and status
382
  const highCrimes = ['Murder', 'Robbery'];
383
  const mediumCrimes = ['Theft', 'Assault'];
384
- if (highCrimes.includes(c.crime) || c.status === 'Open') {
 
 
 
385
  return 'High';
386
  }
387
- if (mediumCrimes.includes(c.crime) || c.status === 'Under Investigation') {
388
  return 'Medium';
389
  }
390
  return 'Low';
@@ -437,6 +621,27 @@ export class CaseDetailsPageComponent implements OnInit {
437
  return this.pagedCases;
438
  }
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  // Helper for pagination display
441
  getPagination(): (number | string)[] {
442
  const pages: (number | string)[] = [];
@@ -468,4 +673,127 @@ export class CaseDetailsPageComponent implements OnInit {
468
  this.setPage(page);
469
  }
470
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  }
 
1
  import { Component, OnInit } from '@angular/core';
2
+ import { Router, ActivatedRoute } from '@angular/router';
3
  import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
4
 
5
  type EvidenceType = 'Document' | 'Photo';
 
21
  }
22
  get pagedCases(): PoliceCase[] {
23
  const start = (this.currentPage - 1) * this.pageSize;
24
+ const sorted = [...this.filteredCases].sort((a: any, b: any) => {
25
+ let aVal: any, bVal: any;
26
+ switch (this.sortKey) {
27
+ case 'caseId':
28
+ aVal = a.caseId || '';
29
+ bVal = b.caseId || '';
30
+ break;
31
+ case 'priority':
32
+ aVal = this.getCasePriority(a) || '';
33
+ bVal = this.getCasePriority(b) || '';
34
+ break;
35
+ case 'crime':
36
+ aVal = a.crime || '';
37
+ bVal = b.crime || '';
38
+ break;
39
+ case 'dateTime':
40
+ aVal = a.dateTime ? new Date(a.dateTime).getTime() :0;
41
+ bVal = b.dateTime ? new Date(b.dateTime).getTime() :0;
42
+ break;
43
+ case 'status':
44
+ aVal = a.status || '';
45
+ bVal = b.status || '';
46
+ break;
47
+ default:
48
+ aVal = '';
49
+ bVal = '';
50
+ }
51
+ if (aVal < bVal) return this.sortDir === 'asc' ? -1 :1;
52
+ if (aVal > bVal) return this.sortDir === 'asc' ?1 : -1;
53
+ return 0;
54
+ });
55
+ return sorted.slice(start, start + this.pageSize);
56
  }
57
  setPage(page: number) {
58
  if (page < 1 || page > this.totalPages) return;
 
76
 
77
  showDetails = false;
78
  selectedCase: PoliceCase | null = null;
79
+ rawFormJson: string = '';
80
+ copySuccess: boolean = false;
81
  username: string = '';
82
  q: string = '';
83
 
84
+ // Date field groups used for formatting in details view
85
+ dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']);
86
+ dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']);
87
+
88
  filterStatus: string = '';
89
  filterCrimeType: string = '';
90
  filterDateFrom: string = '';
 
137
  evidenceType: EvidenceType = 'Document';
138
  evidenceFiles: { [caseId: string]: { Document: File[]; Photo: File[] } } = {};
139
 
140
+ returnTo: string = '/';
141
+ navHasFrom: boolean = false;
142
+ overlayOpenedLocally: boolean = false;
143
+
144
+ // Sorting state
145
+ sortKey: string = 'dateTime';
146
+ sortDir: 'asc' | 'desc' = 'desc';
147
+
148
+ constructor(private caseStore: CaseStoreService, private router: Router, private route: ActivatedRoute) {}
149
 
150
  ngOnInit(): void {
151
+ // Load cases first
152
  this.cases = this.caseStore.getPoliceCases();
153
  this.username = localStorage.getItem('username') || sessionStorage.getItem('username') || '';
154
+
155
+ // If navigated with a case in history state, use it (faster)
156
+ try {
157
+ const navState = (history && (history as any).state) || null;
158
+ const navCase = navState && navState.case ? navState.case : null;
159
+ if (navCase) {
160
+ this.selectedCase = navCase as PoliceCase;
161
+ this.showDetails = true;
162
+ // store origin to navigate back
163
+ this.returnTo = navState.from || '/record';
164
+ this.navHasFrom = !!navState.from;
165
+ this.overlayOpenedLocally = false;
166
+ // Prepare raw JSON for copy/inspection
167
+ try { this.rawFormJson = this.selectedCase && this.selectedCase.formData ? JSON.stringify(this.selectedCase.formData, null,2) : ''; } catch { this.rawFormJson = ''; }
168
+ return;
169
+ }
170
+ } catch {}
171
+
172
+ // React to route param changes so clicking any eye always goes through the same route
173
+ this.route.paramMap.subscribe(params => {
174
+ const id = params.get('id');
175
+ if (!id) return;
176
+ // refresh cases in case store changed
177
+ this.cases = this.caseStore.getPoliceCases();
178
+ const found = this.cases.find(c => c.caseId === id || String(c.caseId).toLowerCase() === id.toLowerCase());
179
+ if (found) {
180
+ this.selectedCase = found;
181
+ this.showDetails = true;
182
+ this.overlayOpenedLocally = false;
183
+ try { this.rawFormJson = this.selectedCase && this.selectedCase.formData ? JSON.stringify(this.selectedCase.formData, null,2) : ''; } catch { this.rawFormJson = ''; }
184
+ // default returnTo if not set
185
+ if (!this.returnTo) this.returnTo = '/record';
186
+ }
187
+ });
188
  }
189
 
190
  get filteredCases(): PoliceCase[] {
 
230
  return Array.from(new Set(this.cases.map(c => c.crime).filter(s => !!s))) as string[];
231
  }
232
 
233
+ openDetails(c: PoliceCase, fromRoute: string = ''): void {
234
+ if (!c || !c.caseId) return;
235
+ // Determine origin for navigation
236
+ let origin = fromRoute;
237
+ if (!origin) {
238
+ // Prefer explicit current router URL (if user is on case-details page)
239
+ try {
240
+ const cur = this.router.url || '';
241
+ if (cur.includes('/case-details')) origin = 'case-details';
242
+ if (cur.includes('/record')) origin = 'record';
243
+ } catch {}
244
+ }
245
+ if (!origin) {
246
+ // Fallback to role-based detection
247
+ origin = this.isInvestigator() ? 'case-details' : 'record';
248
+ }
249
+
250
+ console.log('openDetails: navigating to summary for', c.caseId, 'origin=', origin);
251
+ this.router.navigate(['/case-details-summary-page', c.caseId], { queryParams: { from: origin, returnId: c.caseId }, state: { case: c, from: origin, returnId: c.caseId } });
252
+ }
253
+
254
+ viewSummary(caseId: string) {
255
+ const origin = (this.router.url || '').includes('/case-details') ? 'case-details' : 'record';
256
+ console.log('viewSummary: caseId=', caseId, 'from=', origin);
257
+ this.router.navigate(['/case-details-summary-page', caseId], { queryParams: { from: origin, returnId: caseId }, state: { from: origin, returnId: caseId } });
258
  }
259
 
260
  closeDetails(): void {
 
271
  const metadata = {
272
  caseId: c.caseId || '',
273
  crimeType: c.crime || '',
274
+ briefDescription: c.briefDescription || '',
275
  dateTime: c.dateTime || '',
276
  location: c.police?.address || '',
277
  suspectName: c.accused?.name || '',
 
438
  };
439
 
440
  const path = fieldMap[field] || field;
441
+ let value: any = '—';
442
  if (Array.isArray(path)) {
443
+ let v = sc;
444
  for (const p of path) {
445
+ if (v && v[p] !== undefined) v = v[p];
446
+ else {
447
+ v = undefined;
448
+ break;
449
+ }
450
  }
451
+ value = v;
452
  } else {
453
+ value = sc && sc[path] !== undefined ? sc[path] : undefined;
454
  }
455
+
456
+ // If not found on mapped path, try raw formData saved with the case
457
+ if (value === null || value === undefined || value === '') {
458
+ try {
459
+ const fd = this.getFormDataArray(sc);
460
+ const norm = (s: any) => {
461
+ if (s === null || s === undefined) return '';
462
+ let t = String(s).toLowerCase();
463
+ t = t.replace(/&/g, ''); // remove ampersand
464
+ t = t.replace(/and/g, '');
465
+ t = t.replace(/entry/g, '');
466
+ t = t.replace(/\s+/g, '');
467
+ return t.replace(/[^a-z0-9]/g, '');
468
+ };
469
+
470
+ // Try exact key match first
471
+ if (fd && fd.length) {
472
+ let kv = fd.find(k => k && String(k.key).toLowerCase() === String(field).toLowerCase());
473
+ if (kv) value = kv.value;
474
+
475
+ // Try normalized matches: label -> stored key, and mapped path names
476
+ if (value === null || value === undefined || value === '') {
477
+ const fieldNorm = norm(field);
478
+ const pathName = Array.isArray(path) ? path[path.length -1] : String(path);
479
+ const pathNorm = norm(pathName);
480
+
481
+ kv = fd.find(k => k && (norm(k.key) === fieldNorm || norm(k.key) === pathNorm || norm(k.key).includes(fieldNorm) || fieldNorm.includes(norm(k.key))));
482
+ if (kv) value = kv.value;
483
+ }
484
+ }
485
+
486
+ // If still not found, check sc.formData object shape
487
+ if ((value === null || value === undefined || value === '') && sc && sc.formData && typeof sc.formData === 'object') {
488
+ // try direct property
489
+ if (sc.formData[field] !== undefined) value = sc.formData[field];
490
+ else {
491
+ // normalized search in object keys
492
+ const fieldNorm = (s: any) => s === null || s === undefined ? '' : String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
493
+ const target = fieldNorm(field);
494
+ for (const k of Object.keys(sc.formData)) {
495
+ if (fieldNorm(k) === target || k.toLowerCase() === field.toLowerCase() || k.toLowerCase().includes(field.toLowerCase())) {
496
+ value = sc.formData[k];
497
+ break;
498
+ }
499
+ }
500
+ }
501
+ }
502
+ } catch (e) {
503
+ // ignore
504
+ }
505
+ }
506
+
507
+ // Normalize and format
508
+ if (value === null || value === undefined || value === '') return '—';
509
+
510
+ // Special-case: date/time fields - try to format human-readable
511
+ if ((this.dateTimeFields && this.dateTimeFields.has(field)) || (this.dateFields && this.dateFields.has(field))) {
512
+ const d = new Date(value);
513
+ if (!isNaN(d.getTime())) {
514
+ if (this.dateFields && this.dateFields.has(field)) return d.toISOString().slice(0,10);
515
+ return d.toLocaleString();
516
+ }
517
+ }
518
+
519
+ if (typeof value === 'object') return this.formatFormValue(value);
520
+ return value;
521
+ }
522
+
523
+ // New helper: safely format display value for formData
524
+ formatFormValue(value: any): string {
525
+ if (value === null || value === undefined || value === '') return '—';
526
+ if (typeof value === 'object') {
527
+ try { return JSON.stringify(value, null,2); } catch { return String(value); }
528
+ }
529
+ return String(value);
530
  }
531
 
532
  getProgressValue(caseObj: any): number {
 
562
  // Automatic logic based on crime type and status
563
  const highCrimes = ['Murder', 'Robbery'];
564
  const mediumCrimes = ['Theft', 'Assault'];
565
+ const crimeStr = (c && c.crime) ? String(c.crime) : '';
566
+ const statusStr = (c && c.status) ? String(c.status) : '';
567
+
568
+ if (highCrimes.includes(crimeStr) || statusStr === 'Open') {
569
  return 'High';
570
  }
571
+ if (mediumCrimes.includes(crimeStr) || statusStr === 'Under Investigation') {
572
  return 'Medium';
573
  }
574
  return 'Low';
 
621
  return this.pagedCases;
622
  }
623
 
624
+ // Sorting methods
625
+ setSort(key: string) {
626
+ if (this.sortKey === key) {
627
+ this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
628
+ } else {
629
+ this.sortKey = key;
630
+ this.sortDir = key === 'dateTime' ? 'desc' : 'asc';
631
+ }
632
+ this.currentPage = 1;
633
+ }
634
+
635
+ isAsc(key: string) {
636
+ return this.sortKey === key && this.sortDir === 'asc';
637
+ }
638
+ isDesc(key: string) {
639
+ return this.sortKey === key && this.sortDir === 'desc';
640
+ }
641
+ ariaSort(key: string) {
642
+ return this.sortKey === key ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
643
+ }
644
+
645
  // Helper for pagination display
646
  getPagination(): (number | string)[] {
647
  const pages: (number | string)[] = [];
 
673
  this.setPage(page);
674
  }
675
  }
676
+
677
+ logout(): void {
678
+ // Implement your logout logic here (clear session, etc.)
679
+ // For now, just redirect to home/login
680
+ window.location.href = '/';
681
+ }
682
+
683
+ // New helper: return keys in saved formData (preserve order)
684
+ getFormDataKeys(caseObj: any): string[] {
685
+ if (!caseObj || !caseObj.formData) return [];
686
+ try {
687
+ const fd = caseObj.formData;
688
+ if (Array.isArray(fd)) {
689
+ return fd.map((kv: any) => kv && kv.key ? String(kv.key) : '');
690
+ }
691
+ return Object.keys(fd);
692
+ } catch {
693
+ return [];
694
+ }
695
+ }
696
+
697
+ // New helper: detect if formData is key/value array
698
+ isFormDataArray(fd: any): boolean {
699
+ return Array.isArray(fd) && fd.length >0 && fd.every((item: any) => item && Object.prototype.hasOwnProperty.call(item, 'key'));
700
+ }
701
+
702
+ // Return formData as array of {key,value}
703
+ getFormDataArray(caseObj: any): Array<{ key: string; value: any }> {
704
+ if (!caseObj || !caseObj.formData) return [];
705
+ const fd = caseObj.formData;
706
+ if (this.isFormDataArray(fd)) return fd as Array<{ key: string; value: any }>;
707
+ if (typeof fd === 'object') return Object.keys(fd).map(k => ({ key: k, value: fd[k] }));
708
+ return [{ key: 'value', value: fd }];
709
+ }
710
+
711
+ copyFormData(): void {
712
+ if (!this.selectedCase) return;
713
+ // Use the key/value array representation for copying so UI and clipboard match
714
+ const kv = this.getFormDataArray(this.selectedCase);
715
+ const json = JSON.stringify(kv, null,2);
716
+
717
+ const write = (text: string) => {
718
+ if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) {
719
+ return (navigator as any).clipboard.writeText(text);
720
+ }
721
+ return new Promise<void>((resolve, reject) => {
722
+ try {
723
+ const ta = document.createElement('textarea');
724
+ ta.value = text;
725
+ ta.style.position = 'fixed';
726
+ ta.style.left = '-9999px';
727
+ document.body.appendChild(ta);
728
+ ta.select();
729
+ document.execCommand('copy');
730
+ document.body.removeChild(ta);
731
+ resolve();
732
+ } catch (e) { reject(e); }
733
+ });
734
+ };
735
+
736
+ write(json).then(() => {
737
+ this.copySuccess = true;
738
+ setTimeout(() => this.copySuccess = false,2000);
739
+ }).catch(() => {
740
+ alert('Copy failed - your browser may not support programmatic clipboard access.');
741
+ });
742
+ }
743
+
744
+ editCase(): void {
745
+ if (!this.selectedCase) return;
746
+ // Send the stored key/value array back to Infopage as a flat object
747
+ const kv = this.getFormDataArray(this.selectedCase);
748
+ const flat: any = {};
749
+ kv.forEach(item => { flat[item.key] = item.value; });
750
+ // Navigate to infopage route with state carrying the flat form
751
+ this.router.navigate(['/infopage'], { state: { prefillFormData: flat } });
752
+ }
753
+
754
+ // Navigation back to origin
755
+ closeAndReturn(): void {
756
+ this.closeDetails();
757
+ // Use returnTo if available, otherwise default to '/record'
758
+ const target = this.returnTo || '/record';
759
+
760
+ // Debug info for runtime troubleshooting
761
+ console.log('closeAndReturn called. returnTo=', this.returnTo, 'history.state=', (history && (history as any).state) || {});
762
+
763
+ // Normalize and handle case-details with id if available in history.state
764
+ const hist = (history && (history as any).state) || {};
765
+ const returnId = hist && (hist.returnId || hist.caseId || hist.case) ? (hist.returnId || hist.caseId || (hist.case && hist.case.caseId)) : null;
766
+
767
+ try {
768
+ if (target.includes('case-details')) {
769
+ if (returnId) {
770
+ // navigate to specific case details page
771
+ this.router.navigate(['/case-details', returnId]);
772
+ return;
773
+ }
774
+ // navigate to case-details root
775
+ this.router.navigate(['/case-details']);
776
+ return;
777
+ }
778
+ } catch (e) {
779
+ // fallback to generic navigation below
780
+ }
781
+
782
+ // For other targets ensure absolute URL and navigate
783
+ const normalized = target.startsWith('/') ? target : ('/' + target);
784
+ // Use navigateByUrl to avoid relative navigation issues
785
+ this.router.navigateByUrl(normalized);
786
+ }
787
+
788
+ getReturnLabel(): string {
789
+ try {
790
+ if (!this.returnTo) return 'Previous Page';
791
+ const t = this.returnTo.toString();
792
+ if (t.includes('/record') || t === 'record') return 'Records';
793
+ if (t.includes('/case-details') || t === 'case-details') return 'Case Details';
794
+ if (t.includes('/infopage') || t === 'infopage') return 'Info Page';
795
+ if (t.includes('/')) return 'Previous Page';
796
+ } catch {}
797
+ return 'Previous Page';
798
+ }
799
  }
src/app/case-details-summary-page/case-details-summary-page.component.css ADDED
@@ -0,0 +1,481 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --masthead-min-height: 140px;
3
+ }
4
+
5
+ /* Base */
6
+ body {
7
+ background: linear-gradient(135deg, #e0e7ef 0%, #38bdf8 100%);
8
+ min-height: 100vh;
9
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
10
+ }
11
+
12
+ /* Layout */
13
+ .case-details-summary-layout {
14
+ display: flex;
15
+ min-height: 90vh; /* unified (was 88vh in an earlier duplicate) */
16
+ background: linear-gradient(120deg, #e0f2fe 0%, #f4f6fa 100%);
17
+ }
18
+
19
+ .masthead {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 24px;
23
+ padding: 24px 24px 12px;
24
+ min-height: var(--masthead-min-height);
25
+ background: transparent;
26
+ margin-bottom: 48px;
27
+ }
28
+
29
+ /* Header (from infopage) */
30
+ .site-header {
31
+ background: #011329;
32
+ box-shadow: 0 2px 12px #38bdf844;
33
+ margin-bottom: 0;
34
+ position: relative;
35
+ z-index: 10;
36
+ padding-bottom: 0;
37
+ }
38
+
39
+ .header-inner {
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: space-between;
43
+ padding: 18px 32px 0 32px;
44
+ position: relative;
45
+ }
46
+
47
+ .logo-cluster {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 18px;
51
+ }
52
+
53
+ .logo-img-header {
54
+ width: 54px;
55
+ height: 54px;
56
+ border-radius: 50%;
57
+ background: #fff;
58
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
59
+ padding: 4px;
60
+ margin-top: -6px;
61
+ margin-bottom: 1vh;
62
+ }
63
+
64
+ .py-detect-title-header {
65
+ font-size: 2.1rem;
66
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
67
+ font-weight: 900;
68
+ letter-spacing: 6px;
69
+ color: #38bdf8;
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 2px;
73
+ margin-bottom: 1.5vh;
74
+ }
75
+
76
+ .py-detect-title-header .py-letter.p,
77
+ .py-detect-title-header .py-letter.d,
78
+ .py-detect-title-header .py-letter.t,
79
+ .py-detect-title-header .py-letter.c {
80
+ color: #e3f6ff;
81
+ text-shadow: 0 0 6px #38bdf8;
82
+ }
83
+
84
+ .py-detect-title-header .py-letter.y,
85
+ .py-detect-title-header .py-letter.e,
86
+ .py-detect-title-header .py-letter.e2,
87
+ .py-detect-title-header .py-letter.t2 {
88
+ color: #38bdf8;
89
+ text-shadow: 0 0 6px #38bdf8;
90
+ }
91
+
92
+ .py-detect-title-header .py-shape {
93
+ color: #e3f6ff;
94
+ background: #e3f6ff;
95
+ text-shadow: 0 0 6px #38bdf8;
96
+ box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
97
+ border: 2px solid #23272b;
98
+ width: 18px;
99
+ height: 4px;
100
+ display: inline-block;
101
+ margin: 0 8px;
102
+ border-radius: 2px;
103
+ }
104
+
105
+ /* Sidebar */
106
+ .sidebar {
107
+ width: 370px;
108
+ height: 84vh;
109
+ color: #fff;
110
+ box-shadow: 0 8px 32px #2563eb33;
111
+ display: flex;
112
+ flex-direction: column;
113
+ padding-top: 4px;
114
+ border-top-right-radius: 0;
115
+ border-bottom-right-radius: 0;
116
+ margin-right: 0;
117
+ overflow-y: auto;
118
+ background: linear-gradient(to right, #011022, #01030a);
119
+ }
120
+
121
+ .sidebar ul {
122
+ list-style: none;
123
+ padding: 14px;
124
+ margin: 0;
125
+ }
126
+
127
+ .sidebar > ul > li > button {
128
+ border-radius: 12px !important;
129
+ margin-bottom: 18px;
130
+ padding: 18px 32px;
131
+ box-shadow: 0 4px 16px #38bdf855;
132
+ cursor: pointer !important;
133
+ transition: all 0.3s ease !important;
134
+ letter-spacing: 1px;
135
+ text-align: left;
136
+ outline: none;
137
+ position: relative !important;
138
+ display: flex !important;
139
+ align-items: center !important;
140
+ gap: 8px !important;
141
+ min-width: 150px !important;
142
+ justify-content: center !important;
143
+ background: linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.05)) !important;
144
+ border: 1px solid rgba(0,212,255,0.3) !important;
145
+ backdrop-filter: blur(10px) !important;
146
+ color: #e0e6ed !important;
147
+ font-weight: 900 !important;
148
+ font-size: 1.18rem !important;
149
+ }
150
+
151
+ .sidebar > ul > li > button.active,
152
+ .sidebar > ul > li > button:focus {
153
+ background: linear-gradient(90deg, #38bdf8 0%, #bfcfe7 100%);
154
+ box-shadow: 0 8px 32px #2563eb55;
155
+ transform: scale(1.06);
156
+ }
157
+
158
+ .sidebar > ul > li > button:hover {
159
+ background: linear-gradient(90deg, #0ea5e9 0%, #bfcfe7 100%);
160
+ }
161
+
162
+ .sidebar ul ul {
163
+ background: rgba(255, 255, 255, 0.18);
164
+ border-radius: 14px;
165
+ margin-top: 2px;
166
+ box-shadow: 0 2px 8px #38bdf822;
167
+ padding: 8px 0;
168
+ animation: slideDown 0.35s cubic-bezier(.77, 0, .175, 1);
169
+ }
170
+
171
+ .sidebar ul ul li {
172
+ background: linear-gradient(90deg, #f8fafc 0%, #e0e7ef 100%);
173
+ color: #010610;
174
+ font-size: 1.05em;
175
+ font-weight: 700;
176
+ border-radius: 10px;
177
+ margin: 6px 18px;
178
+ padding: 12px 22px;
179
+ box-shadow: 0 2px 8px #2563eb11;
180
+ cursor: pointer;
181
+ transition: background 0.18s, color 0.18s, box-shadow 0.18s, transform 0.18s;
182
+ border: 2px solid transparent;
183
+ }
184
+
185
+ .sidebar ul ul li.active,
186
+ .sidebar ul ul li:focus {
187
+ background: linear-gradient(90deg, #38bdf8 0%, #0c0c0c 100%);
188
+ color: #fff;
189
+ border: 2px solid #2563eb;
190
+ box-shadow: 0 4px 16px #38bdf855;
191
+ transform: scale(1.04);
192
+ }
193
+
194
+ .sidebar ul ul li:hover {
195
+ background: linear-gradient(90deg, #e0f2fe 0%, #38bdf8 100%);
196
+ color: #2563eb;
197
+ border: 2px solid #38bdf8;
198
+ }
199
+
200
+ /* Main */
201
+ .main-content {
202
+ flex: 1;
203
+ padding: 6px 6px;
204
+ background: linear-gradient(120deg, #f4f6fa 0%, #e0e7ef 100%);
205
+ min-height: calc(100vh - var(--masthead-min-height) - 48px);
206
+ border-radius: 0;
207
+ box-shadow: 0 8px 32px #2563eb22;
208
+ animation: fadeIn 0.5s cubic-bezier(.77, 0, .175, 1);
209
+ overflow-y: auto;
210
+ }
211
+
212
+ /* Slide panel */
213
+ .slide-panel {
214
+ background: #fff;
215
+ border-radius: 0;
216
+ box-shadow: 0 4px 24px #23272b18;
217
+ padding: 38px 54px;
218
+ min-height: 340px;
219
+ transition: transform 0.3s cubic-bezier(.77, 0, .175, 1), box-shadow 0.2s;
220
+ transform: translateX(0);
221
+ animation: fadeIn 0.5s cubic-bezier(.77, 0, .175, 1);
222
+ }
223
+
224
+ .slide-panel.open {
225
+ transform: translateX(0);
226
+ box-shadow: 0 12px 48px #2563eb22;
227
+ }
228
+
229
+ /* Tabs */
230
+ .tabs {
231
+ display: flex;
232
+ gap: 18px;
233
+ margin-bottom: 32px;
234
+ }
235
+
236
+ .tabs button {
237
+ background: #f8fafc;
238
+ color: #2563eb;
239
+ border: none;
240
+ border-radius: 8px;
241
+ font-weight: 700;
242
+ font-size: 1.08em;
243
+ padding: 12px 32px;
244
+ cursor: pointer;
245
+ box-shadow: 0 2px 8px #2563eb11;
246
+ transition: background 0.25s, color 0.25s, transform 0.18s;
247
+ }
248
+
249
+ .tabs button.active,
250
+ .tabs button:focus {
251
+ background: #2563eb;
252
+ color: #fff;
253
+ box-shadow: 0 4px 16px #38bdf855;
254
+ transform: scale(1.04);
255
+ }
256
+
257
+ .tabs button:hover {
258
+ background: #0ea5e9;
259
+ color: #fff;
260
+ }
261
+
262
+ /* Table */
263
+ table {
264
+ width: 100%;
265
+ border-collapse: collapse;
266
+ margin: 24px 0;
267
+ }
268
+
269
+ th {
270
+ background: linear-gradient(180deg, #38bdf8 0%, #2563eb 100%);
271
+ color: #fff;
272
+ padding: 16px;
273
+ text-align: left;
274
+ font-weight: 700;
275
+ font-size: 1.1em;
276
+ border-top-left-radius: 8px;
277
+ border-top-right-radius: 8px;
278
+ }
279
+
280
+ td {
281
+ background: #fff;
282
+ padding: 16px;
283
+ border-bottom: 2px solid #e0e7ef;
284
+ color: #50575d;
285
+ font-size: 1em;
286
+ vertical-align: middle;
287
+ }
288
+
289
+ tr:hover td {
290
+ background: #f1f9ff;
291
+ }
292
+
293
+ tr:last-child td {
294
+ border-bottom: none;
295
+ }
296
+
297
+ /* Footer */
298
+ footer {
299
+ background: linear-gradient(to right, #011022, #01030a);
300
+ color: #fff;
301
+ text-align: center;
302
+ padding: 10px 0;
303
+ position: fixed;
304
+ bottom: 0;
305
+ left: 0;
306
+ width: 100%;
307
+ z-index: 100;
308
+ margin-top: 0;
309
+ }
310
+
311
+ /* Cards */
312
+ .details-card {
313
+ background: #fff;
314
+ border-radius: 0;
315
+ box-shadow: 0 4px 32px #2563eb22;
316
+ padding: 5px 5px;
317
+ margin-top: -5px;
318
+ margin-bottom: 32px;
319
+ max-width: 1510px;
320
+ width: 100%;
321
+ animation: fadeIn 0.6s cubic-bezier(.77, 0, .175, 1);
322
+ }
323
+
324
+ .details-header {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 18px;
328
+ margin-bottom: 32px;
329
+ }
330
+
331
+ .details-header h2 {
332
+ font-size: 2.3em;
333
+ font-weight: 900;
334
+ color: #2563eb;
335
+ margin: 0;
336
+ letter-spacing: 1.5px;
337
+ text-shadow: 1px 0 #fff;
338
+ }
339
+
340
+ .subheader-pill {
341
+ background: linear-gradient(90deg, #000000 0%, #0066ff 100%);
342
+ color: #fff;
343
+ font-weight: 700;
344
+ font-size: 1.1em;
345
+ border-radius: 16px;
346
+ padding: 8px 22px;
347
+ margin-left: 18px;
348
+ box-shadow: 0 2px 8px #38bdf855;
349
+ display: inline-block;
350
+ }
351
+
352
+ .details-grid {
353
+ display: grid;
354
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
355
+ gap: 6px 6px;
356
+ }
357
+
358
+ .details-field {
359
+ background: linear-gradient(90deg, #f8fafc 0%, #e0e7ef 100%);
360
+ border-radius: 0;
361
+ box-shadow: 0 2px 8px #2563eb11;
362
+ padding: 3px 3px;
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 3px;
366
+ min-height: 3px;
367
+ transition: box-shadow 0.18s, background 0.18s;
368
+ }
369
+
370
+ .field-label {
371
+ color: #2563eb;
372
+ font-weight: 700;
373
+ font-size: 1.09em;
374
+ margin-bottom: 2px;
375
+ }
376
+
377
+ .field-value {
378
+ color: #23272b;
379
+ font-size: 1.08em;
380
+ font-weight: 500;
381
+ word-break: break-word;
382
+ }
383
+
384
+ .details-field .field-value {
385
+ max-height: 220px;
386
+ overflow-y: auto;
387
+ word-break: break-word;
388
+ white-space: pre-wrap;
389
+ }
390
+
391
+ .details-field.description-field {
392
+ min-height: 32px;
393
+ }
394
+
395
+ .details-field.description-field .field-value {
396
+ white-space: pre-wrap;
397
+ min-height: 32px;
398
+ max-height: 400px;
399
+ overflow-y: auto;
400
+ resize: vertical;
401
+ transition: min-height 0.2s, max-height 0.2s;
402
+ }
403
+
404
+ /* Animated back button */
405
+ .animated-back {
406
+ background: linear-gradient(90deg, #38bdf8 0%, #bfcfe7 100%);
407
+ color: #23272b; /* text color */
408
+ font-size: 1em;
409
+ font-weight: 900;
410
+ border: none;
411
+ border-radius: 5px;
412
+ margin-bottom: 18px;
413
+ padding: 0px 4px;
414
+ box-shadow: 0 4px 16px #38bdf855;
415
+ cursor: pointer;
416
+ transition: background 0.25s, color 0.25s, box-shadow 0.25s, transform 0.18s;
417
+ letter-spacing: 1px;
418
+ text-align: center;
419
+ outline: none;
420
+ position: relative;
421
+ animation: backBtnFadeIn 0.6s cubic-bezier(.77,0,.175,1);
422
+ display: flex;
423
+ align-items: center;
424
+ justify-content: center;
425
+ gap: 0px;
426
+ }
427
+
428
+ .animated-back:hover {
429
+ background: linear-gradient(90deg, #0ea5e9 0%, #bfcfe7 100%);
430
+ transform: scale(1.08);
431
+ box-shadow: 0 8px 32px #2563eb55;
432
+ }
433
+
434
+ .back-icon {
435
+ font-size: 1em;
436
+ margin-right: 4px;
437
+ margin-top: -4px;
438
+ transition: transform 0.2s;
439
+ }
440
+
441
+ .animated-back:hover .back-icon {
442
+ transform: translateX(-4px) scale(1.2);
443
+ }
444
+
445
+
446
+ /* Animations */
447
+ @keyframes fadeIn {
448
+ 0% {
449
+ opacity: 0;
450
+ transform: translateY(10px);
451
+ }
452
+
453
+ 100% {
454
+ opacity: 1;
455
+ transform: translateY(0);
456
+ }
457
+ }
458
+
459
+ @keyframes slideDown {
460
+ 0% {
461
+ opacity: 0;
462
+ transform: translateY(-10px);
463
+ }
464
+
465
+ 100% {
466
+ opacity: 1;
467
+ transform: translateY(0);
468
+ }
469
+ }
470
+
471
+ @keyframes backBtnFadeIn {
472
+ 0% {
473
+ opacity: 0;
474
+ transform: translateX(-20px);
475
+ }
476
+
477
+ 100% {
478
+ opacity: 1;
479
+ transform: translateX(0);
480
+ }
481
+ }
src/app/case-details-summary-page/case-details-summary-page.component.html ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Modern UI header with logo and PyDetect title -->
2
+ <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span style="cursor:default;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
+ </div>
20
+ <div class="header-actions-right">
21
+ <button class="back-btn animated-back" (click)="navigateBack()">
22
+ <span class="back-icon">←</span> Back
23
+ </button>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+
29
+ <div class="case-details-summary-layout">
30
+ <aside class="sidebar">
31
+ <ul>
32
+ <li>
33
+ <button (click)="setMainHeader('crime')" [class.active]="expandedMainHeader==='crime'">Crime Details</button>
34
+ <ul *ngIf="expandedMainHeader==='crime'">
35
+ <ng-container *ngFor="let subgroupName of crimeSubgroupOrder">
36
+ <li *ngIf="sections['crime']?.subgroups[subgroupName]" (click)="setSubheader('crime', subgroupName)" [class.active]="activeSubheader==='crime-' + subgroupName">
37
+ {{ subgroupName }}
38
+ </li>
39
+ </ng-container>
40
+ </ul>
41
+ </li>
42
+ <li>
43
+ <button (click)="setMainHeader('suspect')" [class.active]="expandedMainHeader==='suspect'">Suspect Details</button>
44
+ <ul *ngIf="expandedMainHeader==='suspect'">
45
+ <ng-container *ngFor="let subgroupName of suspectSubgroupOrder">
46
+ <li *ngIf="sections['suspect']?.subgroups[subgroupName]" (click)="setSubheader('suspect', subgroupName)" [class.active]="activeSubheader==='suspect-' + subgroupName">
47
+ {{ subgroupName }}
48
+ </li>
49
+ </ng-container>
50
+ </ul>
51
+ </li>
52
+ <li>
53
+ <button (click)="setMainHeader('notes')" [class.active]="expandedMainHeader==='notes'">Documents/Evidence</button>
54
+ <ul *ngIf="expandedMainHeader==='notes'">
55
+ <ng-container *ngFor="let subgroupName of notesSubgroupOrder">
56
+ <li *ngIf="sections['notes']?.subgroups[subgroupName]" (click)="setSubheader('notes', subgroupName)" [class.active]="activeSubheader==='notes-' + subgroupName">
57
+ {{ subgroupName }}
58
+ </li>
59
+ </ng-container>
60
+ </ul>
61
+ </li>
62
+ </ul>
63
+ </aside>
64
+ <main class="main-content">
65
+ <div *ngIf="activeMainHeader && activeSubheader">
66
+ <div class="details-card">
67
+ <div class="details-header">
68
+ <h2>{{ sections[activeMainHeader]?.title }} <span class="subheader-pill">{{ activeSubheaderName }}</span></h2>
69
+ </div>
70
+ <div *ngIf="sections[activeMainHeader]?.subgroups[activeSubheaderName] as fields">
71
+ <div class="details-grid">
72
+ <div class="details-field" [ngClass]="{'description-field': field.toLowerCase().includes('description') || field.toLowerCase().includes('notes') || field.toLowerCase().includes('findings') }" *ngFor="let field of fields">
73
+ <div class="field-label">{{ field }}</div>
74
+ <div class="field-value">{{ getFilledDetail(activeMainHeader, activeSubheaderName, field) }}</div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </main>
81
+ </div>
82
+
83
+ <!-- Footer from provided design -->
84
+ <footer>
85
+ <p>©2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
86
+ </footer>
87
+ <!-- End of record-card -->
src/app/case-details-summary-page/case-details-summary-page.component.ts ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { InfopageComponent } from '../infopage/infopage.component';
2
+ import { Component, OnInit } from '@angular/core';
3
+ import { ActivatedRoute, Router } from '@angular/router';
4
+ import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
5
+
6
+ @Component({
7
+ selector: 'app-case-details-summary-page',
8
+ templateUrl: './case-details-summary-page.component.html',
9
+ styleUrls: ['./case-details-summary-page.component.css']
10
+ })
11
+ export class CaseDetailsSummaryPageComponent implements OnInit {
12
+ caseId: string | null = null;
13
+ activeSection: string = 'crime';
14
+ activeSubgroup: string = '';
15
+ // Added ordered list for crime subgroups to ensure sidebar shows in desired order
16
+ crimeSubgroupOrder: string[] = [
17
+ 'Identification & Timing',
18
+ 'Location & People',
19
+ 'Offence & Context',
20
+ 'Evidence & Scene',
21
+ 'Operational Notes',
22
+ 'Status & Linkage',
23
+ 'Remark'
24
+ ];
25
+ // Added ordered list for suspect subgroups
26
+ suspectSubgroupOrder: string[] = [
27
+ 'Identity',
28
+ 'Physical Description',
29
+ 'Background',
30
+ 'Known Associates',
31
+ 'Prior Records',
32
+ 'Remark'
33
+ ];
34
+
35
+ // Added ordered list for notes/evidence subgroups
36
+ notesSubgroupOrder: string[] = [
37
+ 'Investigation Notes',
38
+ 'Evidence Files',
39
+ 'Links and Recommendation',
40
+ 'Remark'
41
+ ];
42
+ sections: any = {
43
+ crime: {
44
+ title: 'Crime Details',
45
+ subgroups: {
46
+ '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'],
47
+ 'Location & People': ['Location', 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Reported Contact', 'Witness Count', 'Victim Name', 'Victim Contact', 'Victim Summary', 'Suspected Offender Known?', 'Suspect Link'],
48
+ 'Offence & Context': ['Legal Sections / Charges', 'Offence Category', 'Offence Description', 'Suspected Motive', 'Confirmed Motive', 'Weapon Involved', 'Property Loss / Damage'],
49
+ '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'],
50
+ 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'riskLevel', 'Confidentiality'],
51
+ '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'],
52
+ 'Remark': ['Remark']
53
+ }
54
+ },
55
+ suspect: {
56
+ title: 'Suspect Details',
57
+ subgroups: {
58
+ 'Identity': ['Suspect ID', 'Suspect Name', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'],
59
+ 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Tattoo Details', 'Hair Color', 'Scar Details', 'Distinguishing Marks', 'Build', 'Eye Color', 'Photo Upload'],
60
+ 'Background': ['Employment', 'Education', 'Occupation', 'Company', 'Workplace Address', 'Marital Status', 'Known Habits', 'Known Financial Details'],
61
+ 'Known Associates': ['Associate Names', 'Gang Affiliation', 'Family Connections', 'Social Media Handles'],
62
+ 'Prior Records': ['Criminal History', 'Prior Arrests', 'Probation/Parole Status'],
63
+ 'Remark': ['Remark']
64
+ }
65
+ },
66
+ notes: {
67
+ title: 'Evidence and Documents',
68
+ subgroups: {
69
+ 'Investigation Notes': ['Initial Findings', 'Detailed Notes', 'Status', 'Version History / Updates'],
70
+ 'Evidence Files': ['Evidence Photos', 'Evidence Videos', 'Evidence Documents'],
71
+ 'Links and Recommendation': ['Links to Evidence', 'Final Recommendations'],
72
+ 'Remark': ['Remark']
73
+ }
74
+ }
75
+ };
76
+ mainTitles: string[] = ['Crime Details', 'Suspect Details', 'Evidence and Documents'];
77
+ mainKeys: string[] = ['crime', 'suspect', 'notes'];
78
+ subgroups: string[] = [];
79
+ activeTab: string = 'Crime Details';
80
+ tabs: string[] = ['Crime Details', 'Suspect Details', 'Evidence and Documents']; // Added tabs property
81
+ slideOpen: boolean = true; // Added slideOpen property
82
+ activeMainHeader: string = 'crime';
83
+ activeSubheader: string = '';
84
+ activeSubheaderName: string = '';
85
+ expandedMainHeader: string | null = 'crime'; // Track which main header is expanded
86
+ selectedCase: PoliceCase | null = null;
87
+ fromPage: string = 'record'; // default
88
+ returnId: string | null = null;
89
+
90
+ // date sets for formatting
91
+ private dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']);
92
+ private dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']);
93
+
94
+ constructor(private route: ActivatedRoute, private router: Router, private caseStore: CaseStoreService) {
95
+ // Read route params
96
+ this.route.paramMap.subscribe(params => {
97
+ this.caseId = params.get('id');
98
+ });
99
+
100
+ // Read query params (preferable for reliable behavior)
101
+ this.route.queryParams.subscribe(params => {
102
+ if (params['from']) {
103
+ this.fromPage = params['from'];
104
+ }
105
+ if (params['returnId']) {
106
+ this.returnId = params['returnId'];
107
+ }
108
+ });
109
+
110
+ // Also read navigation state (router state or history.state) as a fallback
111
+ const nav = this.router.getCurrentNavigation();
112
+ if (nav && nav.extras && (nav.extras.state as any)) {
113
+ const st = nav.extras.state as any;
114
+ if (st.from) this.fromPage = st.from;
115
+ if (st.returnId) this.returnId = st.returnId;
116
+ if (st.case) this.selectedCase = st.case as PoliceCase;
117
+ if (st.caseId) this.returnId = this.returnId || st.caseId;
118
+ }
119
+
120
+ // Fallback to history.state for cases where getCurrentNavigation() is unavailable (e.g., after reload)
121
+ const hist = (window && (window as any).history && (window as any).history.state) || {};
122
+ if (hist.from) this.fromPage = hist.from;
123
+ if (hist.returnId) this.returnId = this.returnId || hist.returnId;
124
+ if (hist.caseId) this.returnId = this.returnId || hist.caseId;
125
+ if (hist.case && !this.selectedCase) this.selectedCase = hist.case as PoliceCase;
126
+
127
+ // Debug log to help trace where navigation came from
128
+ console.log('[CaseDetailsSummary] init sources:', {
129
+ urlParamId: this.caseId,
130
+ queryFrom: (this.route.snapshot.queryParamMap.get('from')),
131
+ queryReturnId: (this.route.snapshot.queryParamMap.get('returnId')),
132
+ navState: nav?.extras?.state,
133
+ historyState: hist,
134
+ resolvedFrom: this.fromPage,
135
+ resolvedReturnId: this.returnId
136
+ });
137
+ }
138
+
139
+ ngOnInit() {
140
+ this.setSection('crime');
141
+ // Ensure the main header and first subgroup are selected so main content shows by default
142
+ const crimeSubgroups = Object.keys(this.sections['crime']?.subgroups || {});
143
+ const first = crimeSubgroups[0] || '';
144
+ this.activeMainHeader = 'crime';
145
+ this.expandedMainHeader = 'crime';
146
+ this.activeSubheader = 'crime-' + first;
147
+ this.activeSubheaderName = first;
148
+
149
+ // Load the selected case if not already provided in navigation state
150
+ if (!this.selectedCase) {
151
+ // Prefer caseId from route param or returnId
152
+ const id = this.caseId || this.returnId || null;
153
+ if (id) {
154
+ const found = this.caseStore.getPoliceCases().find(c => c.caseId === id || String(c.caseId).toLowerCase() === id.toLowerCase());
155
+ if (found) this.selectedCase = found;
156
+ }
157
+ }
158
+ }
159
+
160
+ setSection(section: string) {
161
+ this.activeSection = section;
162
+ this.subgroups = Object.keys(this.sections[section].subgroups);
163
+ this.activeSubgroup = this.subgroups[0];
164
+ this.activeTab = this.sections[section].title;
165
+ }
166
+
167
+ setSubgroup(subgroup: string) {
168
+ this.activeSubgroup = subgroup;
169
+ }
170
+
171
+ setTab(tab: string) {
172
+ // Find section key by tab title
173
+ const sectionKey = this.mainKeys[this.mainTitles.indexOf(tab)];
174
+ if (sectionKey) {
175
+ this.activeTab = tab;
176
+ this.setSection(sectionKey);
177
+ }
178
+ }
179
+
180
+ setMainHeader(header: string) {
181
+ if (this.expandedMainHeader === header) {
182
+ // Collapse if already expanded
183
+ this.expandedMainHeader = null;
184
+ } else {
185
+ // Expand and collapse others
186
+ this.expandedMainHeader = header;
187
+ this.activeMainHeader = header;
188
+ // Set first subgroup as default
189
+ const subgroups = Object.keys(this.sections[header]?.subgroups || {});
190
+ this.activeSubheader = header + '-' + (subgroups[0] || '');
191
+ this.activeSubheaderName = subgroups[0] || '';
192
+ }
193
+ }
194
+
195
+ setSubheader(mainHeader: string, subgroup: string) {
196
+ this.activeSubheader = mainHeader + '-' + subgroup;
197
+ this.activeSubheaderName = subgroup;
198
+ }
199
+
200
+ getFilledDetail(mainHeader: string, subgroup: string, field: string): string {
201
+ // If we have a selectedCase, map the field to the case object first
202
+ if (this.selectedCase) {
203
+ const v = this.getFieldValueFromCase(this.selectedCase, field);
204
+ if (v !== null && v !== undefined && v !== '') {
205
+ // format dates if needed
206
+ if (this.dateFields && this.dateFields.has(field)) {
207
+ const d = new Date(v);
208
+ if (!isNaN(d.getTime())) return d.toISOString().slice(0,10);
209
+ }
210
+ if (this.dateTimeFields && this.dateTimeFields.has(field)) {
211
+ const d = new Date(v);
212
+ if (!isNaN(d.getTime())) return d.toLocaleString();
213
+ }
214
+ return String(v);
215
+ }
216
+ }
217
+
218
+ // Load saved form data from localStorage as fallback (legacy behavior)
219
+ const savedData = localStorage.getItem('pydetect-form-data');
220
+ if (savedData) {
221
+ const data = JSON.parse(savedData);
222
+ if (data.formData && data.formData[field] !== undefined && data.formData[field] !== null) {
223
+ return data.formData[field];
224
+ }
225
+ }
226
+ return '';
227
+ }
228
+
229
+ // Map field labels to properties on PoliceCase (copied/adapted from recordpage)
230
+ private getFieldValueFromCase(sc: any, field: string): any {
231
+ const fieldMap: { [key: string]: string | string[] } = {
232
+ 'Case ID': 'caseId',
233
+ 'FIR / Ref #': 'firRef',
234
+ 'Crime Type': 'crime',
235
+ 'Case Category': 'caseCategory',
236
+ 'Date & Time (Entry)': 'dateTime',
237
+ 'Occurred From': 'occurredFrom',
238
+ 'Occurred To': 'occurredTo',
239
+ 'Time Reported': 'timeReported',
240
+ 'Time Discovered': 'timeDiscovered',
241
+ 'Country': 'country',
242
+ 'State': 'state',
243
+ 'District': 'district',
244
+ 'Number of Victims': 'numberOfVictims',
245
+ 'Brief Description': 'briefDescription',
246
+ 'Location': ['police', 'address'],
247
+ 'Jurisdiction / PS': 'jurisdiction',
248
+ 'Scene Type': 'sceneType',
249
+ 'Reported By': 'reportedBy',
250
+ 'Reported Contact': 'reportedContact',
251
+ 'Witness Count': 'witnessCount',
252
+ 'Victim Name': 'victimName',
253
+ 'Victim Contact': 'victimContact',
254
+ 'Victim Summary': 'victimSummary',
255
+ 'Suspected Offender Known?': 'suspectedOffenderKnown',
256
+ 'Suspect Link': 'suspectLink',
257
+ 'Legal Sections / Charges': 'legalSections',
258
+ 'Offence Category': 'offenceCategory',
259
+ 'Offence Description': 'offenceDescription',
260
+ 'Suspected Motive': 'suspectedMotive',
261
+ 'Confirmed Motive': 'confirmedMotive',
262
+ 'Weapon Involved': 'weaponInvolved',
263
+ 'Property Loss / Damage': 'propertyLoss',
264
+ 'Evidence Collected': 'evidenceCollected',
265
+ 'Forensic Tests Required': 'forensicTestsRequired',
266
+ 'Scene Condition': 'sceneCondition',
267
+ 'Photos / Video?': 'photosVideo',
268
+ 'CCTV Present?': 'cctvPresent',
269
+ 'CCTV Sources / IDs': 'cctvSources',
270
+ 'Physical Evidence (list)': 'physicalEvidence',
271
+ 'Chain of Custody?': 'chainOfCustody',
272
+ 'Digital Evidence': 'digitalEvidence',
273
+ 'Evidence Storage Reference': 'evidenceStorageReference',
274
+ 'Investigating Officer': ['police', 'name'],
275
+ 'Duty Person': ['police', 'dutyPerson'],
276
+ 'Supervising Officer': ['police', 'supervisingOfficer'],
277
+ 'Patrol Notes': ['police', 'patrolNotes'],
278
+ 'Arrest Made': 'arrestMade',
279
+ 'Arrest Location': 'arrestLocation',
280
+ 'Initial Actions Taken': 'initialActionsTaken',
281
+ 'riskLevel': 'riskLevel',
282
+ 'Confidentiality': 'confidentiality',
283
+ 'Biometric / Forensic IDs': 'biometricIds',
284
+ 'DNA Ref ID': 'dnaRefId',
285
+ 'Fingerprint ID': 'fingerprintId',
286
+ 'Case Status': 'status',
287
+ 'Linked Cases': 'linkedCases',
288
+ 'arrestCount': 'arrestCount',
289
+ 'Case Priority': 'casePriority',
290
+ 'Follow-up Date': 'followUpDate',
291
+ 'Court Case ID': 'courtCaseId',
292
+ 'Next Hearing Date': 'nextHearingDate',
293
+ 'Final Summary': 'finalSummary',
294
+ 'Remark': 'remark',
295
+ // Suspect Details
296
+ 'Suspect ID': ['accused', 'suspectId'],
297
+ 'Suspect Name': ['accused', 'name'],
298
+ 'Alias / Nickname': ['accused', 'alias'],
299
+ 'Age': ['accused', 'age'],
300
+ 'Gender': ['accused', 'gender'],
301
+ 'Nationality': ['accused', 'nationality'],
302
+ 'Nationality ID / Passport Number': ['accused', 'passportNumber'],
303
+ 'Languages': ['accused', 'languages'],
304
+ 'Address': ['accused', 'address'],
305
+ 'Known Aliases': ['accused', 'knownAliases'],
306
+ 'Government ID': ['accused', 'governmentId'],
307
+ 'Height (cm)': ['accused', 'height'],
308
+ 'Weight (kg)': ['accused', 'weight'],
309
+ 'Build': ['accused', 'build'],
310
+ 'Hair Color': ['accused', 'hairColor'],
311
+ 'Eye Color': ['accused', 'eyeColor'],
312
+ 'Distinguishing Marks': ['accused', 'distinguishingMarks'],
313
+ 'Tattoo Details': ['accused', 'tattooDetails'],
314
+ 'Scar Details': ['accused', 'scarDetails'],
315
+ 'Photo Upload': ['accused', 'photoUpload'],
316
+ 'Employment': ['accused', 'employment'],
317
+ 'Education': ['accused', 'education'],
318
+ 'Occupation': ['accused', 'occupation'],
319
+ 'Company': ['accused', 'company'],
320
+ 'Workplace Address': ['accused', 'workplaceAddress'],
321
+ 'Marital Status': ['accused', 'maritalStatus'],
322
+ 'Known Habits': ['accused', 'knownHabits'],
323
+ 'Known Financial Details': ['accused', 'knownFinancialDetails'],
324
+ 'Associate Names': ['accused', 'associateNames'],
325
+ 'Gang Affiliation': ['accused', 'gangAffiliation'],
326
+ 'Family Connections': ['accused', 'familyConnections'],
327
+ 'Social Media Handles': ['accused', 'socialMediaHandles'],
328
+ 'Criminal History': ['accused', 'criminalHistory'],
329
+ 'Prior Arrests': ['accused', 'priorArrests'],
330
+ 'Probation/Parole Status': ['accused', 'probationStatus'],
331
+ // Notes/Evidence
332
+ 'Initial Findings': ['police', 'information'],
333
+ 'Detailed Notes': ['notes', 'detailedNotes'],
334
+ 'Status': 'status',
335
+ 'Version History / Updates': ['notes', 'versionHistory'],
336
+ 'Evidence Photos': ['legal', 'evidencePhotos'],
337
+ 'Evidence Videos': ['legal', 'evidenceVideos'],
338
+ 'Evidence Documents': ['legal', 'evidenceDocuments'],
339
+ 'Links to Evidence': ['legal', 'linksToEvidence'],
340
+ 'Final Recommendations': ['legal', 'finalRecommendations'],
341
+ 'Witness Statements': ['legal', 'witnessStatements'],
342
+ 'Confessions': ['legal', 'confessions'],
343
+ // Audit Fields
344
+ 'Created By': 'createdBy',
345
+ 'Creation Date': 'creationDate',
346
+ 'Last Updated': 'lastUpdated',
347
+ 'Verified By': 'verifiedBy'
348
+ };
349
+
350
+ const path = fieldMap[field] || field;
351
+ let value: any = undefined;
352
+ if (Array.isArray(path)) {
353
+ let v: any = sc as any;
354
+ for (const p of path) {
355
+ if (v && v[p] !== undefined) v = v[p];
356
+ else { v = undefined; break; }
357
+ }
358
+ value = v;
359
+ } else {
360
+ value = (sc as any) && (sc as any)[path] !== undefined ? (sc as any)[path] : undefined;
361
+ }
362
+
363
+ // If not found on mapped path, try raw formData saved with the case
364
+ if (value === null || value === undefined || value === '') {
365
+ try {
366
+ const fd = this.getFormDataArray(sc as any);
367
+ const norm = (s: any) => {
368
+ if (s === null || s === undefined) return '';
369
+ let t = String(s).toLowerCase();
370
+ t = t.replace(/&/g, '');
371
+ t = t.replace(/and/g, '');
372
+ t = t.replace(/entry/g, '');
373
+ t = t.replace(/\s+/g, '');
374
+ return t.replace(/[^a-z0-9]/g, '');
375
+ };
376
+
377
+ if (fd && fd.length) {
378
+ let kv = fd.find(k => k && String(k.key).toLowerCase() === String(field).toLowerCase());
379
+ if (kv) value = kv.value;
380
+ if (value === null || value === undefined || value === '') {
381
+ const fieldNorm = norm(field);
382
+ const pathName = Array.isArray(path) ? path[path.length -1] : String(path);
383
+ const pathNorm = norm(pathName);
384
+ kv = fd.find(k => k && (norm(k.key) === fieldNorm || norm(k.key) === pathNorm || norm(k.key).includes(fieldNorm) || fieldNorm.includes(norm(k.key))));
385
+ if (kv) value = kv.value;
386
+ }
387
+ }
388
+
389
+ if ((value === null || value === undefined || value === '') && (sc as any) && (sc as any).formData && typeof (sc as any).formData === 'object') {
390
+ if ((sc as any).formData[field] !== undefined) value = (sc as any).formData[field];
391
+ else {
392
+ const fieldNorm = (s: any) => s === null || s === undefined ? '' : String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
393
+ const target = fieldNorm(field);
394
+ for (const k of Object.keys((sc as any).formData)) {
395
+ if (fieldNorm(k) === target || k.toLowerCase() === field.toLowerCase() || k.toLowerCase().includes(field.toLowerCase())) {
396
+ value = (sc as any).formData[k];
397
+ break;
398
+ }
399
+ }
400
+ }
401
+ }
402
+ } catch (e) {
403
+ // ignore
404
+ }
405
+ }
406
+
407
+ return value;
408
+ }
409
+
410
+ // Helpers reused from other components
411
+ private isFormDataArray(fd: any): boolean {
412
+ return Array.isArray(fd) && fd.length >0 && fd.every((item: any) => item && Object.prototype.hasOwnProperty.call(item, 'key'));
413
+ }
414
+
415
+ private getFormDataArray(caseObj: any): Array<{ key: string; value: any }> {
416
+ if (!caseObj || !(caseObj as any).formData) return [];
417
+ const fd = (caseObj as any).formData;
418
+ if (this.isFormDataArray(fd)) return fd as Array<{ key: string; value: any }>;
419
+ if (typeof fd === 'object') return Object.keys(fd).map(k => ({ key: k, value: fd[k] }));
420
+ return [{ key: 'value', value: fd }];
421
+ }
422
+
423
+ // New helper: safely format display value for formData
424
+ formatFormValue(value: any): string {
425
+ if (value === null || value === undefined || value === '') return '—';
426
+ if (typeof value === 'object') {
427
+ try { return JSON.stringify(value, null,2); } catch { return String(value); }
428
+ }
429
+ return String(value);
430
+ }
431
+
432
+ navigateBack() {
433
+ // Prefer explicit fromPage values set via queryParam or navigation state
434
+ if (this.fromPage === 'case-details') {
435
+ // If returnId is available, pass it back in navigation state for the target to use
436
+ if (this.returnId) {
437
+ this.router.navigate(['/case-details'], { state: { caseId: this.returnId } });
438
+ } else {
439
+ this.router.navigate(['/case-details']);
440
+ }
441
+ } else {
442
+ // Default/record
443
+ this.router.navigate(['/record']);
444
+ }
445
+ }
446
+ }
src/app/data/case-data.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const CASE_DATA = [
2
+ {
3
+ caseId: 'CASE-007', officer: 'Ganesh', date: '2025-10-15',
4
+ question: 'Did you visit the location on12th?', answer: 'Yes, I was there for about20 minutes.', duration: '00:18', truthProbability: '78%', dominantEmotion: 'Nervous 😟', emotion: 'Calm',
5
+ stressLevel:68, confidence: 'Moderate', sentiment: 'Negative (-0.45)', responseDelay: '3.1 sec', eyeContact: '78%', blinkRate: '12/min',
6
+ posture: 'Neutral', handMovement: 'Low', legMovement: 'Moderate', microExpressions: '2 detected',
7
+ physicalExpression: 'Neutral, Low hand, Moderate leg,2 detected', physicalScore: '2%', voiceExpression: 'Stress68,64%', voiceScore: '33%', overallScore: '33%'
8
+ },
9
+ {
10
+ caseId: 'CASE-007', officer: 'Ganesh', date: '2025-10-15',
11
+ question: 'Were you alone at the scene?', answer: 'No, my friend was with me.', duration: '00:22', truthProbability: '62%', dominantEmotion: 'Nervous 😟', emotion: 'Nervous',
12
+ stressLevel:72, confidence: 'Low', sentiment: 'Negative (-0.32)', responseDelay: '2.7 sec', eyeContact: '65%', blinkRate: '15/min',
13
+ posture: 'Defensive', handMovement: 'Medium', legMovement: 'Low', microExpressions: '3 detected',
14
+ physicalExpression: 'Defensive, Medium hand, Low leg,3 detected', physicalScore: '3%', voiceExpression: 'Stress72,51%', voiceScore: '27%', overallScore: '27%'
15
+ },
16
+ {
17
+ caseId: 'CASE-007', officer: 'Ganesh', date: '2025-10-15',
18
+ question: 'Did you know the victim?', answer: 'Yes, we worked together.', duration: '00:15', truthProbability: '85%', dominantEmotion: 'Calm 😌', emotion: 'Calm',
19
+ stressLevel:38, confidence: 'High', sentiment: 'Positive (+0.22)', responseDelay: '1.2 sec', eyeContact: '82%', blinkRate: '10/min',
20
+ posture: 'Relaxed', handMovement: 'Low', legMovement: 'Low', microExpressions: '1 detected',
21
+ physicalExpression: 'Relaxed, Low hand, Low leg,1 detected', physicalScore: '1%', voiceExpression: 'Stress38,64%', voiceScore: '33%', overallScore: '33%'
22
+ },
23
+ {
24
+ caseId: 'CASE-007', officer: 'Ganesh', date: '2025-10-15',
25
+ question: 'Did you handle any objects?', answer: 'I picked up a bag to check for ID.', duration: '00:19', truthProbability: '44%', dominantEmotion: 'Defensive 🛡️', emotion: 'Defensive',
26
+ stressLevel:81, confidence: 'Low', sentiment: 'Negative (-0.61)', responseDelay: '4.0 sec', eyeContact: '55%', blinkRate: '18/min',
27
+ posture: 'Tense', handMovement: 'High', legMovement: 'High', microExpressions: '4 detected',
28
+ physicalExpression: 'Tense, High hand, High leg,4 detected', physicalScore: '4%', voiceExpression: 'Stress81,56%', voiceScore: '30%', overallScore: '30%'
29
+ }
30
+ ];
src/app/homepage/auth-card/auth-card.component.css ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* auth-card layout: side-by-side faces that slide horizontally */
2
+ :host { display: block; }
3
+ .auth-popup { display:flex; align-items:center; justify-content:center; }
4
+
5
+ .auth-card {
6
+ width: 100%;
7
+ max-width: 900px;
8
+ margin: 0 auto;
9
+ border-radius: 18px;
10
+ overflow: hidden;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ /* inner holds both faces side-by-side (200% width) */
15
+ .card-inner {
16
+ width: 200%;
17
+ height: 620px;
18
+ display: flex; /* place front and back side-by-side */
19
+ transition: transform 0.6s cubic-bezier(.22,.9,.32,1);
20
+ will-change: transform;
21
+ }
22
+
23
+ /* When flipped, slide to show back face */
24
+ .auth-card.flipped .card-inner {
25
+ transform: translate3d(-50%, 0, 0);
26
+ }
27
+ .auth-card:not(.flipped) .card-inner {
28
+ transform: translate3d(0, 0, 0);
29
+ }
30
+
31
+ /* Each face takes 50% of inner width */
32
+ .card-front, .card-back {
33
+ width: 50%;
34
+ height: 100%;
35
+ flex: 0 0 50%;
36
+ box-sizing: border-box;
37
+ position: relative;
38
+ overflow: hidden;
39
+ }
40
+
41
+ @media (max-width: 900px) {
42
+ .card-inner { width: 200%; height: auto; flex-direction: column; }
43
+ .card-front, .card-back { width: 100%; height: auto; flex: 0 0 auto; }
44
+ }
45
+
46
+ /* Layout inside each face: side-panel + main-panel split */
47
+ .card-content { display: flex; height: 100%;flex-direction:row-reverse; }
48
+ .side-panel { flex: 0 0 auto; position: relative; overflow: hidden; }
49
+ .main-panel { flex: 1 1 auto; padding: 6px 8px; box-sizing: border-box; background:#fff; overflow: auto; }
50
+
51
+ /* --- Per-face explicit sizes (FRONT: 48% / 47%) --- */
52
+ .card-front .side-panel.side-left { flex: 0 0 48%; max-width: 48%; }
53
+ .card-front .main-panel { flex: 0 0 47%; max-width: 47%; }
54
+ .card-front .side-panel.side-right { flex: 0 0 auto; }
55
+
56
+ /* Front face coloring */
57
+ .card-front .side-panel.side-left { background: linear-gradient(135deg,#ff416c 0%,#ff4b2b 100%); }
58
+ .card-front .side-panel.side-right { background: #ffffff; }
59
+
60
+ /* --- Per-face explicit sizes (BACK: 35% / 65%) --- */
61
+ .card-back .side-panel.side-left { flex: 0 0 35%; max-width: 35%; }
62
+ .card-back .main-panel { flex: 0 0 65%; max-width: 65%; }
63
+ .card-back .side-panel.side-right { flex: 0 0 auto; }
64
+
65
+ /* Back face coloring */
66
+ .card-back .side-panel.side-left { background: #ffffff; }
67
+ .card-back .side-panel.side-right { background: linear-gradient(135deg,#ff416c 0%,#ff4b2b 100%); }
68
+
69
+ /* Text color rules to ensure good contrast depending on panel */
70
+ .card-front .side-panel.side-left .side-inner,
71
+ .card-back .side-panel.side-right .side-inner { color: #fff; }
72
+ .card-front .side-panel.side-right .side-inner,
73
+ .card-back .side-panel.side-left .side-inner { color: #222; }
74
+
75
+ /* Ensure main panel text is dark on white backgrounds */
76
+ .card-front .main-panel, .card-back .main-panel { background: #fff; color: #222; }
77
+
78
+ /* Keep image behavior inside side panels */
79
+ .side-panel .side-img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center center; }
80
+
81
+ /* overlay and layering */
82
+ .card-front .side-panel.side-left::before { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.06), rgba(0,0,0,0.12)); pointer-events: none; z-index: 1; }
83
+ .card-front .side-panel.side-left .side-inner { position: relative; z-index: 2; padding: 24px; }
84
+
85
+ /* responsive adjustments */
86
+ @media (max-width: 700px) {
87
+ .card-content { flex-direction: column; }
88
+ .card-front .side-panel.side-left, .card-back .side-panel.side-left { flex: 0 0 auto; height: 220px; max-width: none; }
89
+ .card-front .main-panel, .card-back .main-panel { flex: 0 0 auto; }
90
+ .card-front .side-panel.side-left::before { display: none; }
91
+ }
src/app/homepage/auth-card/auth-card.component.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <section class="auth-popup">
2
+ <div class="auth-card" [class.flipped]="isFlipped">
3
+ <div class="card-inner">
4
+ <div class="card-front">
5
+ <!-- Embed sign-in content directly -->
6
+ <ng-container *ngTemplateOutlet="signInTemplate"></ng-container>
7
+ </div>
8
+ <div class="card-back">
9
+ <!-- Embed sign-up content directly -->
10
+ <ng-container *ngTemplateOutlet="signUpTemplate"></ng-container>
11
+ </div>
12
+ </div>
13
+ </div>
14
+
15
+ <!-- Templates passed in from parent component -->
16
+ <ng-content select="[signInTemplate]"></ng-content>
17
+ <ng-content select="[signUpTemplate]"></ng-content>
18
+ </section>
src/app/homepage/auth-card/auth-card.component.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component, Input, Output, EventEmitter } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ @Component({
5
+ selector: 'app-auth-card',
6
+ standalone: true,
7
+ imports: [CommonModule],
8
+ templateUrl: './auth-card.component.html',
9
+ styleUrls: ['./auth-card.component.css']
10
+ })
11
+ export class AuthCardComponent {
12
+ @Input() isFlipped = false;
13
+ @Output() flip = new EventEmitter<boolean>();
14
+
15
+ toggleFlip() {
16
+ this.isFlipped = !this.isFlipped;
17
+ this.flip.emit(this.isFlipped);
18
+ }
19
+ }
src/app/homepage/auth-wrapper.component.css ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Styles for the card swipe container and animation */
2
+ .card-swipe-container {
3
+ width:100%;
4
+ overflow: hidden;
5
+ position: relative;
6
+ will-change: transform;
7
+ }
8
+
9
+ /* Optional: add transition for smoother effect */
10
+ .card-swipe-container > * {
11
+ width:100%;
12
+ min-height:100%;
13
+ }
src/app/homepage/auth-wrapper.component.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { trigger, state, style, animate, transition } from '@angular/animations';
3
+
4
+ @Component({
5
+ selector: 'app-auth-wrapper',
6
+ template: `
7
+ <div class="card-swipe-container" [@cardSwipe]="cardState">
8
+ <app-sign-in
9
+ *ngIf="cardState === 'signin'"
10
+ [cardState]="cardState"
11
+ (switchToSignUp)="switchToSignUp()"
12
+ ></app-sign-in>
13
+ <app-sign-up
14
+ *ngIf="cardState === 'signup'"
15
+ [cardState]="cardState"
16
+ (switchToSignIn)="switchToSignIn()"
17
+ ></app-sign-up>
18
+ </div>
19
+ `,
20
+ styleUrls: ['./auth-wrapper.component.css'],
21
+ animations: [
22
+ trigger('cardSwipe', [
23
+ state('signup', style({ transform: 'translateX(0%)' })),
24
+ state('signin', style({ transform: 'translateX(-100%)' })),
25
+ transition('signup <=> signin', [
26
+ animate('400ms cubic-bezier(.25,.8,.25,1)')
27
+ ]),
28
+ ])
29
+ ]
30
+ })
31
+ export class AuthWrapperComponent {
32
+ cardState: 'signup' | 'signin' = 'signin';
33
+
34
+ switchToSignUp() { this.cardState = 'signup'; }
35
+ switchToSignIn() { this.cardState = 'signin'; }
36
+ }
src/app/homepage/homepage.component.css CHANGED
@@ -984,3 +984,64 @@ footer .social-icons {
984
  box-shadow: 0 4px 24px #38bdf8cc;
985
  opacity: 1;
986
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
  box-shadow: 0 4px 24px #38bdf8cc;
985
  opacity: 1;
986
  }
987
+
988
+ /* Replace the existing .auth-topright .auth-btn rules only for image variant */
989
+ .auth-topright .auth-btn {
990
+ width: 56px;
991
+ height: 56px;
992
+ padding: 0;
993
+ border-radius: 50%;
994
+ display: inline-flex;
995
+ align-items: center;
996
+ justify-content: center;
997
+ overflow: hidden;
998
+ background: linear-gradient(90deg, #38bdf8 0%, #23272b 100%);
999
+ border: none;
1000
+ box-shadow: 0 2px 12px #38bdf888;
1001
+ }
1002
+
1003
+ .auth-topright .auth-btn:hover {
1004
+ transform: scale(1.03);
1005
+ }
1006
+
1007
+ .auth-btn-img {
1008
+ width: 42px;
1009
+ height: 42px;
1010
+ object-fit: contain;
1011
+ display: block;
1012
+ }
1013
+
1014
+ /* Fallback: if image not available, show text inside small circle */
1015
+ .auth-topright .auth-btn:empty::before {
1016
+ content: 'Sign In';
1017
+ color: #fff;
1018
+ font-weight: 700;
1019
+ font-size: 0.8rem;
1020
+ }
1021
+
1022
+ /* Replace the existing .auth-topright .auth-btn rules only for svg variant */
1023
+ .auth-topright .auth-btn {
1024
+ width: 56px;
1025
+ height: 56px;
1026
+ padding: 0;
1027
+ border-radius: 50%;
1028
+ display: inline-flex;
1029
+ align-items: center;
1030
+ justify-content: center;
1031
+ overflow: hidden;
1032
+ background: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.6));
1033
+ border: 2px solid rgba(14,165,201,0.12);
1034
+ box-shadow: 0 4px 18px rgba(2,6,23,0.6);
1035
+ cursor: pointer;
1036
+ }
1037
+
1038
+ .auth-topright .auth-btn:hover { transform: scale(1.04); }
1039
+
1040
+ .auth-btn-svg { width: 42px; height: 42px; display: block; }
1041
+ .auth-ring { stroke: rgba(168,213,241,0.85); }
1042
+ .auth-head { fill: rgba(168,213,241,0.95); }
1043
+ .auth-body { stroke: rgba(168,213,241,0.95); }
1044
+
1045
+ /* Fallback text hidden when svg present */
1046
+ .auth-topright .auth-btn:empty::before { content: 'Login'; color: #fff; }
1047
+
src/app/homepage/homepage.component.html CHANGED
@@ -22,8 +22,15 @@
22
 
23
  <!-- Top-right auth buttons (unchanged) -->
24
  <div class="auth-topright">
25
- <button class="auth-btn" (click)="openSignIn()">Sign In</button>
26
- <button class="auth-btn" (click)="openSignUp()">Sign Up</button>
 
 
 
 
 
 
 
27
  </div>
28
 
29
  <!-- New Landing Hero + Sections -->
 
22
 
23
  <!-- Top-right auth buttons (unchanged) -->
24
  <div class="auth-topright">
25
+ <!-- Inline SVG-based circular login button -->
26
+ <button class="auth-btn" (click)="openSignIn()" aria-label="Open Sign In">
27
+ <!-- user-circle icon: outer ring with person silhouette -->
28
+ <svg class="auth-btn-svg" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true">
29
+ <circle cx="32" cy="32" r="30" class="auth-ring" fill="none" stroke-width="2" />
30
+ <circle cx="32" cy="20" r="8" class="auth-head" />
31
+ <path class="auth-body" d="M16 48c0-8.8 7.2-16 16-16s16 7.2 16 16" fill="none" stroke-width="6" stroke-linecap="round" />
32
+ </svg>
33
+ </button>
34
  </div>
35
 
36
  <!-- New Landing Hero + Sections -->
src/app/homepage/homepage.component.ts CHANGED
@@ -27,14 +27,19 @@ export class HomepageComponent implements OnInit, OnDestroy {
27
 
28
  welcomeText = `Py-Detect is an intelligent lie detection platform designed to analyze human responses and uncover hidden truths. During each session, we also record and analyze body language using video to provide deeper behavioral insights. Whether you're evaluating a suspect, interviewing a student, or conducting sensitive assessments, Py-Detect uses advanced algorithms to ask targeted questions and deliver a truthfulness score—a percentage-based estimate of how likely the person is being honest.`;
29
 
 
 
30
  constructor(private router: Router, private renderer: Renderer2) { }
31
 
32
  ngOnInit() {
33
  this.renderer.addClass(document.body, 'homepage-bg');
 
 
34
  }
35
 
36
  ngOnDestroy() {
37
  this.renderer.removeClass(document.body, 'homepage-bg');
 
38
  }
39
 
40
  // === Auth modal open/close (same as your last version in the template) ===
 
27
 
28
  welcomeText = `Py-Detect is an intelligent lie detection platform designed to analyze human responses and uncover hidden truths. During each session, we also record and analyze body language using video to provide deeper behavioral insights. Whether you're evaluating a suspect, interviewing a student, or conducting sensitive assessments, Py-Detect uses advanced algorithms to ask targeted questions and deliver a truthfulness score—a percentage-based estimate of how likely the person is being honest.`;
29
 
30
+ private authCloseListener = () => { this.closeModal(); };
31
+
32
  constructor(private router: Router, private renderer: Renderer2) { }
33
 
34
  ngOnInit() {
35
  this.renderer.addClass(document.body, 'homepage-bg');
36
+ // Listen for global auth-close events emitted by sign-in/sign-up components
37
+ window.addEventListener('auth-close', this.authCloseListener as EventListener);
38
  }
39
 
40
  ngOnDestroy() {
41
  this.renderer.removeClass(document.body, 'homepage-bg');
42
+ window.removeEventListener('auth-close', this.authCloseListener as EventListener);
43
  }
44
 
45
  // === Auth modal open/close (same as your last version in the template) ===
src/app/homepage/sign-in/sign-in.component.css CHANGED
@@ -1,3 +1,5 @@
 
 
1
  .signin-popup {
2
  position: fixed;
3
  top: 0;
@@ -10,8 +12,11 @@
10
  justify-content: center;
11
  z-index: 1000;
12
  background: rgb(30 41 59 / 67%);
 
13
  }
14
 
 
 
15
  .signin-header {
16
  width: 420px;
17
  max-width: 95vw;
@@ -52,6 +57,13 @@
52
  cursor: pointer;
53
  }
54
  .signin-box {
 
 
 
 
 
 
 
55
  background: #18314a;
56
  border-radius: 22px;
57
  box-shadow: 0 12px 48px #000a;
@@ -61,7 +73,6 @@
61
  display: flex;
62
  flex-direction: column;
63
  align-items: center;
64
- position: relative;
65
  }
66
  .signin-title {
67
  color: #38bdf8;
@@ -147,9 +158,9 @@ form {
147
  cursor: pointer;
148
  }
149
  .signin-btn {
150
- width: 100%;
151
- background: #38bdf8;
152
- color: #18314a;
153
  border: none;
154
  border-radius: 8px;
155
  padding: 14px 0;
@@ -158,11 +169,10 @@ form {
158
  margin-bottom: 18px;
159
  cursor: pointer;
160
  transition: background 0.2s, color 0.2s;
161
- box-shadow: 0 2px 8px #0003;
162
- }
163
- .signin-btn:hover {
164
- background: #13bfa6;
165
  }
 
 
 
166
  .signin-footer {
167
  color: #b0b8c1;
168
  font-size: 0.95rem;
@@ -224,3 +234,940 @@ form {
224
  gap: 0;
225
  }
226
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* root colors and variables remain unchanged */
2
+
3
  .signin-popup {
4
  position: fixed;
5
  top: 0;
 
12
  justify-content: center;
13
  z-index: 1000;
14
  background: rgb(30 41 59 / 67%);
15
+ backdrop-filter: blur(16px);
16
  }
17
 
18
+ /* Ensure no solid white background is set anywhere */
19
+
20
  .signin-header {
21
  width: 420px;
22
  max-width: 95vw;
 
57
  cursor: pointer;
58
  }
59
  .signin-box {
60
+ margin: 0 auto;
61
+ /* Center the box horizontally and vertically */
62
+ position: relative;
63
+ left: 0;
64
+ right: 0;
65
+ top: 0;
66
+ bottom: 0;
67
  background: #18314a;
68
  border-radius: 22px;
69
  box-shadow: 0 12px 48px #000a;
 
73
  display: flex;
74
  flex-direction: column;
75
  align-items: center;
 
76
  }
77
  .signin-title {
78
  color: #38bdf8;
 
158
  cursor: pointer;
159
  }
160
  .signin-btn {
161
+ width:100%;
162
+ background: #18314a;
163
+ color: #fff;
164
  border: none;
165
  border-radius: 8px;
166
  padding: 14px 0;
 
169
  margin-bottom: 18px;
170
  cursor: pointer;
171
  transition: background 0.2s, color 0.2s;
 
 
 
 
172
  }
173
+ .signin-btn:hover {
174
+ background: #38bdf8;
175
+ }
176
  .signin-footer {
177
  color: #b0b8c1;
178
  font-size: 0.95rem;
 
234
  gap: 0;
235
  }
236
  }
237
+ .ai-particle-bg {
238
+ position: absolute;
239
+ inset: 0;
240
+ pointer-events: none;
241
+ z-index: 0;
242
+ background: url('/assets/particles.svg');
243
+ opacity: 0.18;
244
+ animation: particleDrift 18s linear infinite;
245
+ }
246
+ @keyframes particleDrift {
247
+ 0% { background-position: 0 0; }
248
+ 100% { background-position: 120px 80px; }
249
+ }
250
+ .spinner {
251
+ display: inline-block;
252
+ width: 18px;
253
+ height: 18px;
254
+ border: 3px solid #fff;
255
+ border-top: 3px solid #38bdf8;
256
+ border-radius: 50%;
257
+ animation: spin 0.7s linear infinite;
258
+ vertical-align: middle;
259
+ margin-right: 8px;
260
+ }
261
+
262
+ @keyframes spin {
263
+ 0% { transform: rotate(0deg);}
264
+ 100% { transform: rotate(360deg);}
265
+ }
266
+
267
+ .py-logo-glow {
268
+ width: 54px;
269
+ height: 54px;
270
+ border-radius: 50%;
271
+ box-shadow: 0 0 24px #38bdf8, 0 0 12px #13bfa6;
272
+ animation: logoGlow 3.5s ease-in-out infinite alternate;
273
+ }
274
+ @keyframes logoGlow {
275
+ 0% { box-shadow: 0 0 12px #38bdf8, 0 0 6px #13bfa6; }
276
+ 100% { box-shadow: 0 0 32px #38bdf8, 0 0 18px #13bfa6; }
277
+ }
278
+ .ai-scan-line {
279
+ display: none;
280
+ }
281
+ .ai-slide-in {
282
+ animation: slideInBox 0.8s cubic-bezier(.39,.58,.57,1) both;
283
+ }
284
+ @keyframes slideInBox {
285
+ 0% { opacity: 0; transform: translateY(40px) scale(0.98); }
286
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
287
+ }
288
+ .lock-icon {
289
+ color: #38bdf8;
290
+ font-size: 1.2em;
291
+ margin-right: 8px;
292
+ vertical-align: middle;
293
+ }
294
+ .signin-tagline {
295
+ color: #b0b8c1;
296
+ font-size: 1.08em;
297
+ text-align: center;
298
+ margin-bottom: 8px;
299
+ font-weight: 600;
300
+ }
301
+ .signin-welcome {
302
+ color: #38bdf8;
303
+ font-size: 1.05em;
304
+ text-align: center;
305
+ margin-bottom: 18px;
306
+ font-weight: 600;
307
+ }
308
+ .switch {
309
+ position: relative;
310
+ display: inline-block;
311
+ width: 38px;
312
+ height: 22px;
313
+ }
314
+ .switch input {
315
+ opacity: 0;
316
+ width: 0;
317
+ height: 0;
318
+ }
319
+ .slider {
320
+ position: absolute;
321
+ cursor: pointer;
322
+ top: 0; left: 0; right: 0; bottom: 0;
323
+ background: #b0b8c1;
324
+ border-radius: 22px;
325
+ transition: .4s;
326
+ }
327
+ .switch input:checked + .slider {
328
+ background: #38bdf8;
329
+ }
330
+ .slider:before {
331
+ position: absolute;
332
+ content: "";
333
+ height: 16px;
334
+ width: 16px;
335
+ left: 3px;
336
+ bottom: 3px;
337
+ background: #fff;
338
+ border-radius: 50%;
339
+ transition: .4s;
340
+ }
341
+ .switch input:checked + .slider:before {
342
+ transform: translateX(16px);
343
+ }
344
+ .signin-hint {
345
+ color: #b0b8c1;
346
+ font-size: 0.95em;
347
+ text-align: right;
348
+ margin-bottom: 8px;
349
+ }
350
+ .ai-pulse {
351
+ animation: aiPulseGlow 1.5s infinite alternate;
352
+ }
353
+ @keyframes aiPulseGlow {
354
+ 0% { box-shadow: 0 2px 12px #38bdf844; }
355
+ 100% { box-shadow: 0 2px 24px #38bdf888; }
356
+ }
357
+ .signin-session-tip {
358
+ color: #b0b8c1;
359
+ font-size: 0.95em;
360
+ text-align: center;
361
+ margin-bottom: 8px;
362
+ }
363
+ .signin-security-note {
364
+ color: #ff5252;
365
+ font-size: 0.95em;
366
+ font-weight: 600;
367
+ display: block;
368
+ margin-bottom: 4px;
369
+ }
370
+ .signin-error-toast {
371
+ background: #ff5252;
372
+ color: #fff;
373
+ font-weight: 700;
374
+ border-radius: 8px;
375
+ padding: 8px 18px;
376
+ margin: 12px 0;
377
+ text-align: center;
378
+ animation: shakeError 0.3s cubic-bezier(.39,.58,.57,1);
379
+ }
380
+ @keyframes shakeError {
381
+ 0% { transform: translateX(0); }
382
+ 20% { transform: translateX(-8px); }
383
+ 40% { transform: translateX(8px); }
384
+ 60% { transform: translateX(-8px); }
385
+ 80% { transform: translateX(8px); }
386
+ 100% { transform: translateX(0); }
387
+ }
388
+ .forgot-modal-bg {
389
+ position: fixed;
390
+ inset: 0;
391
+ background: rgba(30,41,59,0.9);
392
+ z-index: 2000;
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ animation: fadeInModalBg 0.4s;
397
+ }
398
+ @keyframes fadeInModalBg {
399
+ from { opacity: 0; }
400
+ to { opacity: 1; }
401
+ }
402
+ .forgot-modal {
403
+ background: #fff;
404
+ border-radius: 18px;
405
+ box-shadow: 0 8px 32px #38bdf844, 0 0 24px #1e293b88;
406
+ padding: 32px 36px 28px 36px;
407
+ min-width: 320px;
408
+ max-width: 90vw;
409
+ text-align: center;
410
+ z-index: 2001;
411
+ display: flex;
412
+ flex-direction: column;
413
+ align-items: center;
414
+ animation: fadeInModal 0.4s;
415
+ }
416
+ @keyframes fadeInModal {
417
+ from { opacity: 0; transform: scale(0.98); }
418
+ to { opacity: 1; transform: scale(1); }
419
+ }
420
+ .forgot-modal h3 {
421
+ color: #38bdf8;
422
+ margin: 12px 0 8px 0;
423
+ font-size: 1.4em;
424
+ font-weight: 700;
425
+ }
426
+ .forgot-modal p {
427
+ color: #23272b;
428
+ font-size: 1.08em;
429
+ margin-bottom: 18px;
430
+ }
431
+ .forgot-modal input[type="email"] {
432
+ background: #f4f6fa;
433
+ color: #18314a;
434
+ border: none;
435
+ border-radius: 8px;
436
+ padding: 12px 14px;
437
+ font-size: 1rem;
438
+ margin-bottom: 12px;
439
+ box-shadow: 0 1px 4px #0002;
440
+ width: 100%;
441
+ }
442
+ .modal-close {
443
+ width: 23%;
444
+ background: #18314a;
445
+ color: #fff;
446
+ border: none;
447
+ border-radius: 8px;
448
+ padding: 14px 0;
449
+ font-size: 1.1rem;
450
+ font-weight: 700;
451
+ margin-bottom: 0;
452
+ cursor: pointer;
453
+ transition: background 0.2s, color 0.2s;
454
+ display: block;
455
+ }
456
+
457
+ .modal-close:hover {
458
+ background: #38bdf8;
459
+ color: #18314a;
460
+ }
461
+ .google-signin-row {
462
+ width: 100%;
463
+ display: flex;
464
+ justify-content: center;
465
+ margin-bottom: 12px;
466
+ }
467
+ #google-btn-div {
468
+ width: 100%;
469
+ display: flex;
470
+ justify-content: center;
471
+ }
472
+ #google-btn-div > div {
473
+ width: 100%;
474
+ min-width: 0;
475
+ border-radius: 8px !important;
476
+ padding: 14px 0 !important;
477
+ font-size: 1.1rem !important;
478
+ font-weight: 700 !important;
479
+ box-shadow: 0 2px 8px #0003 !important;
480
+ background: #fff !important;
481
+ color: #18314a !important;
482
+ border: none !important;
483
+ margin: 0 !important;
484
+ display: flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ }
488
+ .g-signin2 {
489
+ width: 100%;
490
+ min-width: 0;
491
+ border-radius: 8px !important;
492
+ padding: 14px 0 !important;
493
+ font-size: 1.1rem !important;
494
+ font-weight: 700 !important;
495
+ box-shadow: 0 2px 8px #0003 !important;
496
+ background: #fff !important;
497
+ color: #18314a !important;
498
+ border: none !important;
499
+ margin: 0 !important;
500
+ display: flex;
501
+ align-items: center;
502
+ justify-content: center;
503
+ }
504
+
505
+ .eye-toggle {
506
+ position: absolute;
507
+ right: 12px;
508
+ top: 38px;
509
+ background: none;
510
+ border: none;
511
+ font-size: 1.3em;
512
+ color: #888;
513
+ cursor: pointer;
514
+ z-index: 2;
515
+ padding: 0;
516
+ line-height: 1;
517
+ opacity: 0.7;
518
+ transition: color 0.2s, opacity 0.2s;
519
+ }
520
+ .eye-toggle:hover {
521
+ color: #555;
522
+ opacity: 1;
523
+ }
524
+ .eye-toggle:focus {
525
+ outline: none;
526
+ }
527
+ /* --- UI DESIGN UPDATE TO MATCH SCREENSHOT --- */
528
+ .auth-box {
529
+ display: grid;
530
+ grid-template-columns: 1fr 1fr;
531
+ width: 820px;
532
+ max-width: 98vw;
533
+ min-width: 340px;
534
+ border-radius: 18px;
535
+ box-shadow: 0 8px 32px #0002;
536
+ overflow: hidden;
537
+ background: none;
538
+ }
539
+
540
+ .panel-left {
541
+ background: #fff;
542
+ color: #222;
543
+ display: flex;
544
+ flex-direction: column;
545
+ align-items: center;
546
+ justify-content: center;
547
+ padding: 48px 32px;
548
+ }
549
+ .panel-left .signin-title {
550
+ font-size: 2.1rem;
551
+ font-weight: 800;
552
+ margin-bottom: 12px;
553
+ text-align: center;
554
+ color: #222;
555
+ }
556
+ .panel-left .signin-desc {
557
+ font-size: 1.05rem;
558
+ margin-bottom: 24px;
559
+ text-align: center;
560
+ color: #555;
561
+ }
562
+ .panel-left .signin-btn {
563
+ background: var(--primary-cyan-mid);
564
+ color: #fff;
565
+ border: none;
566
+ border-radius: 999px;
567
+ padding: 12px 32px;
568
+ font-size: 1.1rem;
569
+ font-weight: 700;
570
+ cursor: pointer;
571
+ transition: background 0.2s, color 0.2s;
572
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
573
+ }
574
+ .panel-left .signin-btn:hover {
575
+ background: var(--primary-cyan-dark);
576
+ color: #fff;
577
+ }
578
+
579
+ .panel-right {
580
+ background: linear-gradient(135deg, var(--primary-cyan-dark) 0%, var(--primary-cyan-mid) 100%);
581
+ color: #fff;
582
+ display: flex;
583
+ flex-direction: column;
584
+ align-items: center;
585
+ justify-content: center;
586
+ padding: 48px 32px;
587
+ }
588
+ .panel-right .signup-title {
589
+ font-size: 2rem;
590
+ font-weight: 800;
591
+ margin-bottom: 18px;
592
+ text-align: left;
593
+ color: #fff;
594
+ }
595
+ .panel-right .signup-desc {
596
+ font-size: 1.05rem;
597
+ margin-bottom: 24px;
598
+ text-align: center;
599
+ color: #fff;
600
+ }
601
+ .panel-right .signup-btn {
602
+ background: none;
603
+ color: #fff;
604
+ border: 2px solid #fff;
605
+ border-radius: 999px;
606
+ padding: 12px 32px;
607
+ font-size: 1.1rem;
608
+ font-weight: 700;
609
+ cursor: pointer;
610
+ transition: background 0.2s, color 0.2s;
611
+ box-shadow: none;
612
+ }
613
+
614
+
615
+ /* Input fields and buttons for left panel */
616
+ .panel-left .signin-field input,
617
+ .panel-left .signin-field select {
618
+ background: #f5f5f5;
619
+ color: #222;
620
+ border: 1px solid #ddd;
621
+ border-radius: 8px;
622
+ padding: 12px 14px;
623
+ font-size: 1rem;
624
+ margin-bottom: 2px;
625
+ box-shadow: 0 1px 4px #0001;
626
+ transition: border 0.2s, box-shadow 0.2s;
627
+ }
628
+ .panel-left .signin-field input:focus,
629
+ .panel-left .signin-field select:focus {
630
+ outline: 2px solid #ff416c;
631
+ border-color: #ff416c;
632
+ box-shadow: 0 0 0 2px #ff416c44;
633
+ }
634
+
635
+ /* Social buttons row for left panel */
636
+ .panel-left .social-row {
637
+ display: flex;
638
+ gap: 12px;
639
+ margin-bottom: 18px;
640
+ justify-content: center;
641
+ }
642
+ .panel-left .social-btn {
643
+ width: 38px;
644
+ height: 38px;
645
+ border-radius: 50%;
646
+ background: #f5f5f5;
647
+ display: flex;
648
+ align-items: center;
649
+ justify-content: center;
650
+ font-size: 1.3em;
651
+ color: var(--primary-cyan-dark);
652
+ border: none;
653
+ cursor: pointer;
654
+ box-shadow: 0 2px 8px #0001;
655
+ transition: background 0.2s, color 0.2s;
656
+ }
657
+ .panel-left .social-btn:hover {
658
+ background: var(--primary-cyan-dark);
659
+ color: #fff;
660
+ }
661
+
662
+ /* Professional shadow and rounded corners for the whole box */
663
+ .auth-box {
664
+ box-shadow: 0 8px 32px #0002;
665
+ border-radius: 18px;
666
+ }
667
+
668
+ /* Ensure identical size and prevent clipping */
669
+ .auth-box {
670
+ display: grid;
671
+ grid-template-columns: 420px 600px; /* match sign-up columns */
672
+ width: 1020px; /* increased to accommodate both panels */
673
+ height: 620px; /* increased height to avoid overflow */
674
+ max-width: 98vw;
675
+ min-width: 340px;
676
+ border-radius: 18px;
677
+ box-shadow: 0 8px 32px #0002;
678
+ overflow: visible; /* ensure no content is clipped */
679
+ background: none;
680
+ position: relative;
681
+ box-sizing: border-box;
682
+ }
683
+
684
+ /* Reduce right panel width to make it less large */
685
+ .auth-box {
686
+ display: grid;
687
+ grid-template-columns: 360px 840px; /* reduced right panel from 600px to 520px */
688
+ width: 850px; /* adjusted total width to match columns */
689
+ height: 820px;
690
+ max-width: 98vw;
691
+ min-width: 340px;
692
+ border-radius: 18px;
693
+ box-shadow: 0 8px 32px #0002;
694
+ overflow: visible; /* ensure no content is clipped */
695
+ background: none;
696
+ position: relative;
697
+ box-sizing: border-box;
698
+ justify-content:end;
699
+ align-items:baseline;
700
+ justify-items:start;
701
+ }
702
+
703
+ /* Ensure panels occupy full height and use box-sizing */
704
+ .panel-left, .panel-right {
705
+ height: 100%;
706
+ box-sizing: border-box;
707
+ }
708
+
709
+ /* Card viewport and sliding inner container */
710
+ .auth-card {
711
+ width: 1140px; /* viewport width remains */
712
+ height: 700px; /* increased height to accommodate content and remove scroll */
713
+ perspective: none;
714
+ overflow: hidden;
715
+ border-radius: 12px;
716
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12);
717
+ margin: 0 auto;
718
+ }
719
+
720
+ .card-inner {
721
+ width:200%; /* contains both faces side-by-side */
722
+ height:100%;
723
+ display: flex; /* place front and back side-by-side */
724
+ /* much slower transition so flip is clearly visible to users */
725
+ transition: transform 0.7s cubic-bezier(.22, .9, .32,1);
726
+ will-change: transform;
727
+ }
728
+
729
+ .auth-card.flipped .card-inner {
730
+ transform: translate3d(-50%,0,0);
731
+ }
732
+ .auth-card:not(.flipped) .card-inner {
733
+ transform: translate3d(0,0,0);
734
+ }
735
+
736
+ /* panel fade with proper syntax */
737
+ .card-front .main-panel,
738
+ .card-back .main-panel {
739
+ transition: opacity 1s ease 0.15s;
740
+ }
741
+ .card-front[aria-hidden="true"] .main-panel,
742
+ .card-back[aria-hidden="true"] .main-panel {
743
+ opacity:0;
744
+ }
745
+ .card-front[aria-hidden="false"] .main-panel,
746
+ .card-back[aria-hidden="false"] .main-panel {
747
+ opacity:1;
748
+ }
749
+
750
+ /* faces: each takes 50% of the inner width (i.e. the viewport) */
751
+ .card-front, .card-back {
752
+ width: 50%;
753
+ height: 100%;
754
+ flex: 0 0 50%;
755
+ box-sizing: border-box;
756
+ position: relative; /* allow absolute children inside */
757
+ overflow: hidden;
758
+ }
759
+
760
+ /* layout inside each face: side-panel + main-panel split */
761
+ .card-content {
762
+ display: flex;
763
+ height: 100%;
764
+ flex-direction: row-reverse;
765
+ }
766
+ .side-panel { width: 48%; display:flex; align-items:center; justify-content:center; }
767
+ .main-panel { width: 55%; padding: 36px 48px; box-sizing:border-box; background:#fff; overflow: visible; }
768
+
769
+ .side-right {
770
+ background: linear-gradient(135deg,#137ec4 0%,#137ec4 100%);
771
+ }
772
+ .side-left {
773
+ background: linear-gradient(135deg, #137ec4 0%, #38bdf8 100%);
774
+ }
775
+ .side-inner { color:#fff; text-align:center; padding: 32px; }
776
+ .side-inner h3 { font-size: 1.6rem; margin-bottom:12px; }
777
+ .side-text { opacity: 0.95; }
778
+ .panel-cta { background:none; border:2px solid #fff; color:#fff; padding:10px 22px; border-radius:999px; cursor:pointer; margin-top:12px; }
779
+
780
+ /* Ensure sign-in text colors for white main panel */
781
+ .card-front .main-panel .signin-title { color: #222; }
782
+ .card-back .main-panel .signup-title { color: #222; }
783
+
784
+ /* Maintain previous smaller-screen behavior (stack panels) */
785
+ @media (max-width: 900px){
786
+ .auth-card{ width:92vw; height:auto; }
787
+ .card-inner{ width: 200%; /* still double width but we'll stack */ }
788
+ .card-content{ flex-direction: column; }
789
+ .side-panel{ width:100%; height:200px; }
790
+ .main-panel{ width:100%; overflow: visible; }
791
+ }
792
+
793
+ /* keep existing control styles unchanged (inputs/buttons/etc.) */
794
+
795
+ /* minimal overwrite of previous rules that used absolute positioning */
796
+ .m, .signin-close, .card-back .signin-close { position: absolute; top: 5px; right: 5px; z-index: 10; }
797
+
798
+ /* accessibility: hide offscreen face from assistive tech when sliding */
799
+ .card-front[aria-hidden="true"], .card-back[aria-hidden="true"] {
800
+ pointer-events: none;
801
+ }
802
+
803
+ /* Change right panel gradient to blue/cyan */
804
+ .side-panel.side-right {
805
+ background: linear-gradient(135deg, #1d608b 0%, #1d608b 100%);
806
+ }
807
+
808
+ /* Info box styling for right panel */
809
+ .side-info-box {
810
+ position: absolute;
811
+ top: 164px;
812
+ left: 0;
813
+ width: 88%;
814
+ padding: 0 32px;
815
+ z-index: 2;
816
+ text-align: left;
817
+ }
818
+ .side-info-title {
819
+ font-size: 1.35rem;
820
+ font-weight: 800;
821
+ color: #fff;
822
+ margin-bottom: 6px;
823
+ letter-spacing: 0.5px;
824
+ }
825
+ .side-info-desc {
826
+ font-size: 1.08rem;
827
+ color: #e0f7fa;
828
+ margin-bottom: 8px;
829
+ }
830
+ .side-info-link {
831
+ color: #fff;
832
+ font-weight: 700;
833
+ text-decoration: underline;
834
+ cursor: pointer;
835
+ transition: color 0.2s;
836
+ }
837
+ .side-info-link:hover {
838
+ color: #23395d;
839
+ }
840
+
841
+ /* Ensure info box is above image */
842
+ .side-panel {
843
+ position: relative;
844
+ }
845
+
846
+ /* Google button row spacing */
847
+ .google-signin-row {
848
+ width: 100%;
849
+ display: flex;
850
+ justify-content: center;
851
+ margin-bottom: 12px;
852
+ }
853
+ #google-btn-div {
854
+ width: 100%;
855
+ display: flex;
856
+ justify-content: center;
857
+ }
858
+ .g-signin2 {
859
+
860
+ margin: 0 auto;
861
+ }
862
+
863
+ /* Ensure main-panel text is black (readable on white background) */
864
+ .card-front .main-panel,
865
+ .card-front .main-panel .signin-title,
866
+ .card-front .main-panel .signin-tagline,
867
+ .card-front .main-panel .signin-welcome,
868
+ .card-front .main-panel .signin-session-tip,
869
+ .card-front .main-panel .signin-hint,
870
+ .card-front .main-panel .signin-footer,
871
+ .card-front .main-panel label,
872
+ .card-front .main-panel .signin-field small,
873
+ .card-front .main-panel .signin-field label {
874
+ color: #23395d !important;
875
+ }
876
+
877
+ /* Keep links styled but darken slightly for contrast */
878
+ .card-front .main-panel a {
879
+ color: #0b57a4 !important;
880
+ }
881
+ .signin-divider-row {
882
+ display: flex;
883
+ align-items: center;
884
+ justify-content: center;
885
+ width:100%;
886
+ margin:12px 018px 0;
887
+ }
888
+ .divider {
889
+ flex:1;
890
+ height:1px;
891
+ background: #b0b8c1;
892
+ margin:08px;
893
+ }
894
+ .divider-or {
895
+ color: #23395d;
896
+ font-size:1.08em;
897
+ font-weight:600;
898
+ margin:08px;
899
+ }
900
+ .google-btn {
901
+ width: 100%;
902
+ height: 45px;
903
+ background: #18314a;
904
+ color: #fff;
905
+ border: none;
906
+ border-radius: 8px;
907
+ padding: 14px 0;
908
+ font-size: 1.1rem;
909
+ font-weight: 700;
910
+ margin-bottom: 18px;
911
+ cursor: pointer;
912
+ box-shadow: 02px 8px #0003;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ gap: 12px;
917
+ transition: background 0.2s, color 0.2s;
918
+ }
919
+ .google-btn:hover {
920
+ background: #38bdf8;
921
+ }
922
+ .google-logo {
923
+ width:24px;
924
+ height:24px;
925
+ }
926
+ .fact-rotator {
927
+ font-size:1.18rem;
928
+ font-weight:700;
929
+ color: #fff;
930
+ margin-bottom:8px;
931
+ min-height:32px;
932
+ text-align: left;
933
+ transition: opacity 0.6s;
934
+ letter-spacing:0.5px;
935
+ animation: fadeFact0.6s;
936
+ }
937
+ @keyframes fadeFact {
938
+ from { opacity:0; }
939
+ to { opacity:1; }
940
+ }
941
+ .side-panel.side-left {
942
+ position: relative;
943
+ overflow: hidden;
944
+ background: #1d608b; /* Strong blue for contrast */
945
+ min-height: 400px;
946
+ }
947
+ .side-img {
948
+ position: absolute;
949
+ top:0; left:0;
950
+ width:100%; height:100%;
951
+ object-fit: cover;
952
+ z-index:0;
953
+ opacity:0.12;
954
+ }
955
+ .side-bg-shapes {
956
+ position: absolute;
957
+ top:0; left:0; right:0; bottom:0;
958
+ width:100%;
959
+ height:100%;
960
+ z-index:1;
961
+ pointer-events: none;
962
+ }
963
+ .bg-circle {
964
+ position: absolute;
965
+ border-radius:50%;
966
+ opacity:0.85;
967
+ }
968
+ .circle1 {
969
+ width: 120px;
970
+ height: 120px;
971
+ top: 18px;
972
+ left: 18px;
973
+ background: linear-gradient(135deg, #38bdf8 80%, #137ec4 100%);
974
+ }
975
+ .circle2 {
976
+ width: 80px;
977
+ height: 80px;
978
+ top: 71%;
979
+ left: 113px;
980
+ background: linear-gradient(135deg, #38bdf8 80%, #137ec4 100%);
981
+ }
982
+
983
+ .circle3 {
984
+ width: 48px;
985
+ height: 48px;
986
+ top: 80%;
987
+ left: 70%;
988
+ background: linear-gradient(135deg, #38bdf8 80%, #137ec4 100%);
989
+ }
990
+
991
+ .circle4 {
992
+ width: 160px;
993
+ height: 160px;
994
+ top: 70px;
995
+ right: 204px;
996
+ background: linear-gradient(135deg, #38bdf8 80%, #137ec4 100%);
997
+ opacity: 0.7;
998
+ }
999
+
1000
+ .circle5 {
1001
+ width: 36px;
1002
+ height: 36px;
1003
+ top: 30%;
1004
+ left: 80%;
1005
+ background: linear-gradient(135deg, #38bdf8 80%, #137ec4 100%);
1006
+ opacity: 0.6;
1007
+ }
1008
+
1009
+ .circle6 {
1010
+ width: 60px;
1011
+ height: 60px;
1012
+ bottom: 12%;
1013
+ right: 18%;
1014
+ background: linear-gradient(135deg, #38bdf8 80%, #137ec4 100%);
1015
+ opacity: 0.5;
1016
+ }
1017
+
1018
+ .circle7 {
1019
+ width: 54px;
1020
+ height: 54px;
1021
+ top: 10%;
1022
+ right: 10%;
1023
+ background: #18314a;
1024
+ opacity: 0.8;
1025
+ }
1026
+
1027
+ .circle8 {
1028
+ width: 32px;
1029
+ height: 32px;
1030
+ bottom: 20%;
1031
+ left: 60%;
1032
+ background: #14263c;
1033
+ opacity: 0.8;
1034
+ }
1035
+
1036
+ .circle9 {
1037
+ width: 44px;
1038
+ height: 44px;
1039
+ top: 75%;
1040
+ left: 40%;
1041
+ background: #18314a;
1042
+ opacity: 0.7;
1043
+ }
1044
+
1045
+ .circle10 {
1046
+ width: 28px;
1047
+ height: 28px;
1048
+ top: 15%;
1049
+ left: 60%;
1050
+ background: #14263c;
1051
+ opacity: 0.9;
1052
+ }
1053
+
1054
+ .circle11 {
1055
+ width: 38px;
1056
+ height: 38px;
1057
+ bottom: 10%;
1058
+ right: 10%;
1059
+ background: #18314a;
1060
+ opacity: 0.7;
1061
+ }
1062
+
1063
+ .circle12 {
1064
+ width: 22px;
1065
+ height: 22px;
1066
+ top: 85%;
1067
+ right: 20%;
1068
+ background: #14263c;
1069
+ opacity: 0.8;
1070
+ }
1071
+
1072
+ /* New large solid circles for sign-in page background */
1073
+ .circle-large1 {
1074
+ width:220px;
1075
+ height:220px;
1076
+ top:40px;
1077
+ left:60px;
1078
+ background: #137ec4;
1079
+ opacity:0.45;
1080
+ z-index:0;
1081
+ }
1082
+ .circle-large2 {
1083
+ width:180px;
1084
+ height:180px;
1085
+ bottom:40px;
1086
+ right:40px;
1087
+ background: #38bdf8;
1088
+ opacity:0.35;
1089
+ z-index:0;
1090
+ }
1091
+ .circle-large3 {
1092
+ width:140px;
1093
+ height:140px;
1094
+ top:60%;
1095
+ left:60%;
1096
+ background: #18314a;
1097
+ opacity:0.25;
1098
+ z-index:0;
1099
+ }
1100
+ .circle-large4 {
1101
+ width:100px;
1102
+ height:100px;
1103
+ bottom:11%;
1104
+ left:5%;
1105
+ background: #13bfa6;
1106
+ opacity:0.52;
1107
+ z-index:0;
1108
+ }
1109
+
1110
+ /* Add more white rings for contrast */
1111
+ .bg-ring {
1112
+ position: absolute;
1113
+ border-radius:50%;
1114
+ border:6px solid #fff;
1115
+ background: none;
1116
+ opacity:0.8;
1117
+ z-index:1;
1118
+ }
1119
+ .ring1 { width:90px; height:90px; bottom:24px; left:18px; }
1120
+ .ring2 { width:48px; height:48px; top:60px; right:32px; }
1121
+ .ring3 { width:72px; height:72px; top:10%; left:70%; border-color: #fff; opacity:0.7; }
1122
+ .ring4 { width:110px; height:110px; bottom:2%; right:43%; border-color: #fff; opacity:0.5; }
1123
+ .ring5 { width:38px; height:38px; top:75%; left:10%; border-color: #fff; opacity:0.8; }
1124
+ .ring6 { width:64px; height:64px; top:49%; right:20%; border-color: #fff; opacity:0.7; }
1125
+ .ring7 { width:120px; height:120px; top:14%; left:49%; border-color: #fff; opacity:0.6; }
1126
+ .ring8 { width:80px; height:80px; bottom:20%; right:30%; border-color: #fff; opacity:0.7; }
1127
+ .ring9 { width:60px; height:60px; top:65%; left:36%; border-color: #fff; opacity:0.7; }
1128
+
1129
+ .side-welcome-overlay {
1130
+ position: absolute;
1131
+ top:30%;
1132
+ left:50px;
1133
+ width:93%;
1134
+ text-align: start;
1135
+ z-index:2;
1136
+ padding:024px;
1137
+ pointer-events: auto;
1138
+ }
1139
+ .welcome-back-title {
1140
+ font-size:2.1rem;
1141
+ font-weight:800;
1142
+ color: #fff;
1143
+ margin-bottom:8px;
1144
+
1145
+ }
1146
+ .welcome-back-desc {
1147
+ font-size:1rem;
1148
+ color: #e0f7fa;
1149
+ margin-bottom:18px;
1150
+
1151
+ }
1152
+ .action-btn {
1153
+ width:21%;
1154
+ background: #18314a;
1155
+ color: #fff;
1156
+ border: none;
1157
+ border-radius:8px;
1158
+ padding:14px 0;
1159
+ font-size:1.1rem;
1160
+ font-weight:700;
1161
+ margin-top:18px;
1162
+ margin-bottom:0;
1163
+ display: inline-block;
1164
+ letter-spacing:0.5px;
1165
+ box-shadow:02px 8px #0003;
1166
+ cursor: pointer;
1167
+ transition: background 0.2s, color 0.2s;
1168
+ }
1169
+ .action-btn:hover {
1170
+ background: #38bdf8;
1171
+ color: #18314a;
1172
+ }
1173
+
src/app/homepage/sign-in/sign-in.component.html CHANGED
@@ -1,44 +1,192 @@
1
- <section class="signin-popup">
2
- <div class="signin-header">
3
- <div class="signin-logo">
4
- <!-- No logo or CO. text as per sign-up style -->
5
- </div>
6
- </div>
7
- <div class="signin-box">
8
- <button class="signin-close" type="button" (click)="closePopup()" aria-label="Close">&times;</button>
9
- <h2 class="signin-title">Log In</h2>
10
- <form [formGroup]="form" (ngSubmit)="signIn()" novalidate>
11
- <div class="signin-row">
12
- <div class="signin-field">
13
- <label for="email">Email</label>
14
- <input id="email" type="email" placeholder="you@example.com" formControlName="email" [attr.aria-invalid]="form.get('email')?.invalid && form.get('email')?.touched" />
15
- <small class="error" *ngIf="form.get('email')?.touched && form.get('email')?.hasError('required')">Email is required.</small>
16
- <small class="error" *ngIf="form.get('email')?.touched && form.get('email')?.hasError('email')">Please enter a valid email address.</small>
17
- </div>
18
- </div>
19
- <div class="signin-row">
20
- <div class="signin-field">
21
- <label for="password">Password</label>
22
- <input id="password" type="password" placeholder="••••••••" formControlName="password" [attr.aria-invalid]="form.get('password')?.invalid && form.get('password')?.touched" />
23
- <small class="error" *ngIf="form.get('password')?.touched && form.get('password')?.hasError('required')">Password is required.</small>
24
- </div>
25
- </div>
26
- <div class="signin-row signin-options-row">
27
- <div class="remember-me">
28
- <input id="rememberMe" type="checkbox" />
29
- <label for="rememberMe">Remember me</label>
30
- </div>
31
- <div class="forgot-password">
32
- <a href="#" class="forgot-link">Forgot password?</a>
33
- </div>
34
- </div>
35
- <button class="signin-btn" type="submit">Sign In</button>
36
- </form>
37
- <div class="signin-footer">
38
- <span>Don't have an account?</span>
39
- <a href="#" (click)="goToSignUp(); $event.preventDefault()">Sign up here!</a>
40
- <span class="footer-sep"></span>
41
- &copy; Pykara Technologies, 2025. All rights reserved.
42
- </div>
43
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  </section>
 
1
+ <section class="signin-popup ai-bg-animate">
2
+ <div class="ai-particle-bg"></div>
3
+ <div class="signin-header">
4
+ </div>
5
+
6
+ <div class="auth-card" [class.flipped]="isFlipped">
7
+ <div class="card-inner">
8
+ <!-- FRONT: sign-in on main panel, image side on left -->
9
+ <div class="card-front">
10
+ <div class="card-content">
11
+ <div class="side-panel side-left">
12
+ <!-- Decorative background shapes -->
13
+ <div class="side-bg-shapes">
14
+ <div class="bg-circle circle1"></div>
15
+ <div class="bg-circle circle2"></div>
16
+ <div class="bg-circle circle3"></div>
17
+ <div class="bg-circle circle4"></div>
18
+ <div class="bg-circle circle5"></div>
19
+ <div class="bg-circle circle6"></div>
20
+ <div class="bg-circle circle7"></div>
21
+ <div class="bg-circle circle8"></div>
22
+ <div class="bg-circle circle9"></div>
23
+ <div class="bg-circle circle10"></div>
24
+ <div class="bg-circle circle11"></div>
25
+ <div class="bg-circle circle12"></div>
26
+ <div class="bg-circle circle13"></div>
27
+ <div class="bg-circle circle14"></div>
28
+ <div class="bg-circle circle-large1"></div>
29
+ <div class="bg-circle circle-large2"></div>
30
+ <div class="bg-circle circle-large3"></div>
31
+ <div class="bg-circle circle-large4"></div>
32
+ <div class="bg-ring ring1"></div>
33
+ <div class="bg-ring ring2"></div>
34
+ <div class="bg-ring ring3"></div>
35
+ <div class="bg-ring ring4"></div>
36
+ <div class="bg-ring ring5"></div>
37
+ <div class="bg-ring ring6"></div>
38
+ <div class="bg-ring ring7"></div>
39
+ <div class="bg-ring ring8"></div>
40
+ <div class="bg-ring ring9"></div>
41
+ </div>
42
+ <!-- Overlay text and button -->
43
+ <div class="side-welcome-overlay">
44
+ <div class="welcome-back-title">Welcome back!</div>
45
+ <div class="welcome-back-desc">You can sign in to access with your existing account.</div>
46
+ <div class="welcome-back-desc">First time here? Join now.</div>
47
+ <button class="action-btn" type="button" (click)="flipToSignUp()">Sign Up</button>
48
+ </div>
49
+ <!-- Replace /assets/side-placeholder-left.jpg with your chosen image -->
50
+ </div>
51
+
52
+ <div class="main-panel" style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 420px;">
53
+ <!-- existing sign-in form -->
54
+ <button class="signin-close" type="button" (click)="closePopup()" aria-label="Close" style="align-self: flex-end; margin-bottom: 10px;">&times;</button>
55
+ <h2 class="signin-title" style="text-align: center; margin-bottom: 24px;">
56
+ <span class="login-text">Login</span>
57
+ </h2>
58
+ <form [formGroup]="form" (ngSubmit)="signIn()" novalidate style="width: 100%; max-width: 340px;">
59
+ <div class="signin-row">
60
+ <div class="signin-field">
61
+ <label for="email">Email</label>
62
+ <input id="email" type="email" placeholder="you@example.com" formControlName="email" [attr.aria-invalid]="form.get('email')?.invalid && form.get('email')?.touched" />
63
+ </div>
64
+ </div>
65
+ <div class="signin-row">
66
+ <div class="signin-field" style="position:relative;">
67
+ <label for="password">Password</label>
68
+ <input id="password" [type]="showPassword ? 'text' : 'password'" placeholder="••••••••" formControlName="password" [attr.aria-invalid]="form.get('password')?.invalid && form.get('password')?.touched" />
69
+ <button type="button" class="eye-toggle" (click)="togglePasswordVisibility()" tabindex="-1" aria-label="Show/Hide password">
70
+ </button>
71
+ <div *ngIf="errorMessage" class="signin-error-toast">{{ errorMessage }}</div>
72
+ </div>
73
+ </div>
74
+ <div class="signin-row signin-options-row">
75
+ <div class="remember-me">
76
+ <label class="switch">
77
+ <input id="rememberMe" type="checkbox" />
78
+ <span class="slider"></span>
79
+ </label>
80
+ <span>Remember me</span>
81
+ </div>
82
+ <div class="forgot-password">
83
+ <a href="#" class="forgot-link" (click)="openForgotModal($event)">Forgot password?</a>
84
+ </div>
85
+ </div>
86
+ <!-- Hint removed for minimal UI -->
87
+ <button class="signin-btn ai-pulse" type="submit" [disabled]="loading">
88
+ <ng-container *ngIf="!loading; else signingIn">
89
+ Sign In
90
+ </ng-container>
91
+ <ng-template #signingIn>
92
+ <span class="spinner"></span> Signing In...
93
+ </ng-template>
94
+ </button>
95
+
96
+ <!-- Divider with 'or' -->
97
+ <div class="signin-divider-row">
98
+ <div class="divider"></div>
99
+ <span class="divider-or">or</span>
100
+ <div class="divider"></div>
101
+ </div>
102
+
103
+ <!-- Custom Google Sign-In button -->
104
+ <button class="google-btn" type="button" (click)="onGoogleSignIn()">
105
+ <img src="assets/google-logo.svg" alt="Google logo" class="google-logo" />
106
+ <span>Log in with Google</span>
107
+ </button>
108
+
109
+ </form>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- BACK: sign-up on main panel, image side on right -->
115
+ <div class="card-back">
116
+ <div class="card-content card-content-reverse">
117
+ <div class="main-panel">
118
+ <div class="panel-right-embed">
119
+
120
+ <app-sign-up [embedded]="true" (switchToSignIn)="flipToSignIn()"></app-sign-up>
121
+ </div>
122
+ <div class="signin-footer">
123
+ <a href="#" (click)="flipToSignIn(); $event.preventDefault()">Back to Sign In</a>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="side-panel side-right">
128
+ <!-- Decorative background shapes -->
129
+ <div class="side-bg-shapes">
130
+ <div class="bg-circle circle1"></div>
131
+ <div class="bg-circle circle2"></div>
132
+ <div class="bg-circle circle3"></div>
133
+ <div class="bg-circle circle4"></div>
134
+ <div class="bg-circle circle5"></div>
135
+ <div class="bg-circle circle6"></div>
136
+ <div class="bg-circle circle7"></div>
137
+ <div class="bg-circle circle8"></div>
138
+ <div class="bg-circle circle9"></div>
139
+ <div class="bg-circle circle10"></div>
140
+ <div class="bg-circle circle11"></div>
141
+ <div class="bg-circle circle12"></div>
142
+ <div class="bg-circle circle13"></div>
143
+ <div class="bg-circle circle14"></div>
144
+ <!-- New large solid circles -->
145
+ <div class="bg-circle circle-large1"></div>
146
+ <div class="bg-circle circle-large2"></div>
147
+ <div class="bg-circle circle-large3"></div>
148
+ <div class="bg-circle circle-large4"></div>
149
+ <!-- More white rings for contrast -->
150
+ <div class="bg-ring ring1"></div>
151
+ <div class="bg-ring ring2"></div>
152
+ <div class="bg-ring ring3"></div>
153
+ <div class="bg-ring ring4"></div>
154
+ <div class="bg-ring ring5"></div>
155
+ <div class="bg-ring ring6"></div>
156
+ <div class="bg-ring ring7"></div>
157
+ <div class="bg-ring ring8"></div>
158
+ <div class="bg-ring ring9"></div>
159
+ </div>
160
+ <div class="side-info-box">
161
+ <div class="welcome-back-title">Investigate Smarter.</div>
162
+ <div class="welcome-back-title">Analyze Deeper.</div>
163
+ <div class="welcome-back-desc">
164
+ Py-Detect transforms traditional investigations with advanced emotion, tone, and consistency analysis.
165
+ </div>
166
+ <div class="welcome-back-desc">
167
+ Get started today — powered by the intelligence of tomorrow.
168
+ </div>
169
+ <div class="welcome-back-desc">
170
+ Existing user? Log in to your account.
171
+ </div>
172
+ <button class="action-btn" type="button" (click)="goToLogin()">Sign In</button>
173
+ </div>
174
+ <!-- Replace /assets/side-placeholder-right.jpg with your chosen image -->
175
+ <img class="side-img" src="/assets/side-placeholder-right.jpg" alt="" />
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ </div>
181
+ </div>
182
+
183
+ <div *ngIf="showForgotModal" class="forgot-modal-bg">
184
+ <div class="forgot-modal">
185
+ <h3>Forgot Password</h3>
186
+ <p>Enter your email to receive password reset instructions.</p>
187
+ <input type="email" [(ngModel)]="forgotEmail" placeholder="Your email" />
188
+ <button class="signin-btn" (click)="sendForgotEmail()">Send Reset Link</button>
189
+ <button class="modal-close" (click)="closeForgotModal()">Close</button>
190
+ </div>
191
+ </div>
192
  </section>
src/app/homepage/sign-in/sign-in.component.ts CHANGED
@@ -1,14 +1,16 @@
1
  // sign-in.component.ts
2
  import { Component, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core';
3
  import { CommonModule } from '@angular/common';
4
- import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
5
  import { Router, RouterLink } from '@angular/router';
6
- import { SignInService } from './sign-in.service'; // Import SignInService
 
 
7
 
8
  @Component({
9
  selector: 'app-sign-in',
10
  standalone: true,
11
- imports: [CommonModule, ReactiveFormsModule, RouterLink],
12
  templateUrl: './sign-in.component.html',
13
  styleUrls: ['./sign-in.component.css'],
14
  changeDetection: ChangeDetectionStrategy.OnPush
@@ -18,7 +20,52 @@ export class SignInComponent {
18
  @Output() close = new EventEmitter<void>();
19
  form: FormGroup;
20
 
21
- constructor(private fb: FormBuilder, private router: Router, private signInService: SignInService) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  this.form = this.fb.group({
23
  email: ['', [Validators.required, Validators.email]], // Added email validation
24
  password: ['', [Validators.required]]
@@ -35,41 +82,78 @@ export class SignInComponent {
35
 
36
  signIn() {
37
  if (this.form.invalid) {
38
- console.log("Form is invalid:", this.form.errors);
39
  return;
40
  }
41
-
 
42
  const payload = {
43
  email: this.form.get('email')?.value,
44
  password: this.form.get('password')?.value
45
  };
46
-
47
  this.signInService.signIn(payload).subscribe(
48
  (response) => {
49
- console.log('Login successful:', response);
50
- // Save role to localStorage for future logins
51
- if (response && response.role) {
52
- localStorage.setItem('role', response.role);
53
- }
54
- // Redirect based on role
55
- if (response && response.role === 'admin') {
56
- this.router.navigate(['/infopage']);
57
- } else {
58
- this.router.navigate(['/case-details']);
59
- }
60
  },
61
  (error) => {
62
- console.error('Login failed:', error);
63
- alert('Invalid email or password!');
 
 
 
 
 
 
 
 
64
  }
65
  );
66
  }
67
 
68
- ngOnInit() {
69
- // If already logged in and role is admin, redirect to infopage
70
- const role = localStorage.getItem('role');
71
- if (role === 'admin') {
72
- this.router.navigate(['/infopage']);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
  }
 
 
 
 
 
75
  }
 
1
  // sign-in.component.ts
2
  import { Component, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core';
3
  import { CommonModule } from '@angular/common';
4
+ import { ReactiveFormsModule, FormBuilder, Validators, FormGroup, FormsModule } from '@angular/forms';
5
  import { Router, RouterLink } from '@angular/router';
6
+ import { SignInService } from './sign-in.service';
7
+ import { AuthService } from '../../auth.service'; // Import SignInService
8
+ import { SignUpComponent } from '../sign-up/sign-up.component';
9
 
10
  @Component({
11
  selector: 'app-sign-in',
12
  standalone: true,
13
+ imports: [CommonModule, ReactiveFormsModule, RouterLink, FormsModule, SignUpComponent],
14
  templateUrl: './sign-in.component.html',
15
  styleUrls: ['./sign-in.component.css'],
16
  changeDetection: ChangeDetectionStrategy.OnPush
 
20
  @Output() close = new EventEmitter<void>();
21
  form: FormGroup;
22
 
23
+ loading = false;
24
+ errorMessage = '';
25
+
26
+ // UI and template state
27
+ isFlipped = false;
28
+ typingTitle = '';
29
+ showPassword = false;
30
+ showForgotModal = false;
31
+ forgotEmail = '';
32
+
33
+ // Template methods
34
+ flipToSignUp() {
35
+ this.isFlipped = true;
36
+ }
37
+ flipToSignIn() {
38
+ this.isFlipped = false;
39
+ }
40
+ togglePasswordVisibility() {
41
+ this.showPassword = !this.showPassword;
42
+ }
43
+ openForgotModal(event?: Event) {
44
+ if (event) event.preventDefault();
45
+ this.showForgotModal = true;
46
+ }
47
+ closeForgotModal() {
48
+ this.showForgotModal = false;
49
+ this.forgotEmail = '';
50
+ }
51
+ sendForgotEmail() {
52
+ this.closeForgotModal();
53
+ alert('Password reset link sent to your email.');
54
+ }
55
+ onGoogleSignIn() {
56
+ // Implement Google sign-in logic here
57
+ console.log('Google sign-in clicked');
58
+ }
59
+ goToLogin() {
60
+ this.flipToSignIn();
61
+ }
62
+
63
+ constructor(
64
+ private fb: FormBuilder,
65
+ private router: Router,
66
+ private signInService: SignInService,
67
+ private authService: AuthService
68
+ ) {
69
  this.form = this.fb.group({
70
  email: ['', [Validators.required, Validators.email]], // Added email validation
71
  password: ['', [Validators.required]]
 
82
 
83
  signIn() {
84
  if (this.form.invalid) {
85
+ this.errorMessage = '';
86
  return;
87
  }
88
+ this.loading = true;
89
+ this.errorMessage = '';
90
  const payload = {
91
  email: this.form.get('email')?.value,
92
  password: this.form.get('password')?.value
93
  };
 
94
  this.signInService.signIn(payload).subscribe(
95
  (response) => {
96
+ this.loading = false;
97
+ console.log(response);
98
+ // Store user info and navigate based on role
99
+ this.handleSuccessfulLogin(response, payload.email);
 
 
 
 
 
 
 
100
  },
101
  (error) => {
102
+ this.loading = false;
103
+ console.log('Sign-in error:', error);
104
+ if (error && error.status === 401) {
105
+ this.errorMessage = 'Password is incorrect';
106
+ } else {
107
+ this.errorMessage = 'An error occurred. Please try again.';
108
+ }
109
+ setTimeout(() => {
110
+ this.errorMessage = '';
111
+ }, 3000);
112
  }
113
  );
114
  }
115
 
116
+ private handleSuccessfulLogin(response: any, email: string): void {
117
+ // Extract role from backend response
118
+ const userRole = response?.data?.user?.role || response?.user?.role || response?.role || 'user';
119
+ const userData = response?.data?.user || response?.user || { email: email, role: userRole };
120
+
121
+ // Store in localStorage
122
+
123
+ localStorage.setItem('userRole', userRole);
124
+ localStorage.setItem('user', JSON.stringify({
125
+ email: email,
126
+ role: userRole,
127
+ ...userData
128
+ }));
129
+
130
+ this.redirectBasedOnRole(userRole);
131
+ }
132
+
133
+ private handleLocalStorageLogin(): void {
134
+ const userRole = this.authService.getUserRole() || 'user';
135
+ this.redirectBasedOnRole(userRole);
136
+ }
137
+
138
+ private redirectBasedOnRole(userRole: string): void {
139
+ console.log('User role detected:', userRole);
140
+ console.log('Current URL before redirect:', window.location.href);
141
+
142
+ if (userRole === 'admin') {
143
+ console.log('Redirecting admin to infopage');
144
+ this.router.navigate(['/infopage']).then((success) => {
145
+ console.log('Navigation to infopage successful:', success);
146
+ });
147
+ } else {
148
+ console.log('Redirecting user to case-details');
149
+ this.router.navigate(['/case-details']).then((success) => {
150
+ console.log('Navigation to case-details successful:', success);
151
+ });
152
  }
153
  }
154
+
155
+ ngOnInit() {
156
+ console.log('Sign-in component initialized');
157
+ // Do not auto-redirect - user must manually login
158
+ }
159
  }
src/app/homepage/sign-in/sign-in.service.ts CHANGED
@@ -10,6 +10,6 @@ export class SignInService {
10
  constructor(private http: HttpClient) { }
11
 
12
  signIn(payload: any): Observable<any> {
13
- return this.http.post('http://127.0.0.1:5000/sign-in', payload);
14
  }
15
  }
 
10
  constructor(private http: HttpClient) { }
11
 
12
  signIn(payload: any): Observable<any> {
13
+ return this.http.post('http://127.0.0.1:5002/sign-in', payload);
14
  }
15
  }
src/app/homepage/sign-up/sign-up.component.css CHANGED
@@ -1,170 +1,365 @@
1
  :host {
2
  display: block;
 
 
3
  }
4
 
5
- /* Two-column card */
6
- .auth-box {
7
- width: 49vw;
8
- display: grid;
9
- grid-template-columns: 1fr;
10
- background: #2b1b6b; /* purple base under left panel */
11
- border-radius: 14px;
12
- overflow: hidden;
13
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
14
- position: relative;
15
- }
16
- /* Right column (image) */
17
- .panel-right {
18
- position: relative;
19
- background: radial-gradient(120% 120% at 20% 50%, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0) 60%);
20
  }
21
 
22
- .panel-right::before {
23
- /* vertical fold shade between left and right */
24
- content: "";
25
- position: absolute;
26
- inset: 0;
27
- background: linear-gradient(90deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0) 26%);
28
- pointer-events: none;
29
- }
30
-
31
- .right-image {
32
  display: flex;
 
 
 
 
 
 
33
  align-items: center;
34
  justify-content: center;
35
- width: 23.9vw;
36
  }
37
 
38
- .right-image img {
39
- width: 100%;
40
- display: block;
41
- }
 
 
42
 
43
- /* Left column (form) */
44
- .panel-left {
45
- padding: clamp(22px, 3.5vw, 36px);
46
- background: white; /* brighter purple */
47
- color: black;
48
  }
49
 
50
- .brand-mark {
51
- width: 4vw;
52
- margin-bottom: 14px;
53
- border: 2px solid #b1b1b17d;
54
- box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
 
 
 
 
 
 
55
  }
56
 
57
- .title {
58
- margin: 0 0 6px;
59
- font-size: clamp(22px, 3vw, 26px);
60
- font-weight: 700;
 
 
 
61
  }
62
 
63
- .subtitle {
64
- margin: 0 0 18px;
 
 
 
 
 
 
 
 
65
  }
66
 
67
- .form {
68
- display: grid;
69
- gap: 25px; /* Increased gap for better spacing between fields */
 
 
 
 
 
70
  }
71
 
72
- /* Field-specific margin adjustments */
73
- .field {
 
74
  display: grid;
75
- gap: 12px; /* Increased gap between label/input pairs */
 
 
 
76
  }
77
 
78
- label {
 
 
79
  font-weight: 600;
80
- font-size: 13px;
 
 
 
81
  }
82
 
83
- /* Inputs: translucent with subtle border */
84
- input[type="text"], input[type="password"], select.input-field {
85
- color: #000000;
86
- border: 1px solid rgb(0 0 0 / 57%);
87
- border-radius: 10px;
88
- padding: 12px 14px; /* Increased padding */
89
- outline: none;
90
  }
91
 
92
- input::placeholder {
93
- color: #808080;
 
 
 
94
  }
95
 
96
- input:focus, select.input-field:focus {
97
- border-color: #a78bfa;
98
- box-shadow: 0 0 0 3px rgba(167, 139, 250, .25);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
- select.input-field {
102
- font-size: 16px;
 
 
 
 
 
 
103
  width: 100%;
104
- background: #fff;
105
- box-sizing: border-box;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
 
108
- .error {
109
- color: red;
110
- font-size: 12px;
 
111
  }
112
 
113
- /* Buttons */
114
- .btn {
 
 
115
  width: 100%;
116
- border-radius: 999px;
117
- padding: 12px 18px;
118
- cursor: pointer;
119
  }
120
 
121
- .btn-primary {
122
- background: #0b0f1a;
 
123
  color: #fff;
124
- border: none;
125
- font-weight: 700;
126
  }
127
 
128
- .btn-dark {
129
- background: #111827;
130
- color: #fff;
131
- border: none;
132
- margin-top: 8px;
133
  }
134
 
135
- .btn[aria-busy="true"] {
136
- opacity: .75;
137
- cursor: progress;
 
 
 
138
  }
139
 
140
- .footnote {
141
- margin: 14px 0 0;
142
- font-size: 13px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
- .footnote a {
146
- color: red;
147
- text-decoration: underline;
148
  }
149
 
150
- /* Layout: stack on small, split on >= 900px */
151
- @media(min-width: 900px) {
152
- .auth-box {
153
- grid-template-columns: 520px 1fr;
154
  }
155
  }
156
 
157
- .topTitle {
 
 
 
 
 
 
 
 
 
 
 
158
  display: flex;
159
  align-items: center;
160
- gap: 21px;
 
 
 
 
161
  }
162
 
163
- .topHeader {
164
- font-size: 1vw;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  }
166
 
167
- @keyframes fadeInBackdrop {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  from {
169
  opacity: 0;
170
  }
@@ -174,260 +369,303 @@ select.input-field {
174
  }
175
  }
176
 
177
- .signup-popup {
178
- position: fixed;
179
- top: 0;
180
- left: 0;
181
- width: 100vw;
182
- height: 100vh;
183
  display: flex;
184
- flex-direction: column;
185
  align-items: center;
186
  justify-content: center;
187
- z-index: 1000;
188
- background: rgb(30 41 59 / 67%);
189
  }
190
 
191
- .signup-header {
192
- width: 100vw;
 
193
  display: flex;
194
- justify-content: space-between;
195
- align-items: flex-start;
196
- padding: 32px 48px 0 48px;
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  position: absolute;
198
  top: 0;
199
  left: 0;
200
- pointer-events: none;
 
 
201
  }
202
- .signup-logo {
203
- display: flex;
204
- align-items: center;
205
- gap: 12px;
206
- pointer-events: auto;
 
207
  }
208
- .signup-logo img {
209
- width: 48px;
210
- height: 48px;
211
- border-radius: 8px;
212
- background: #fff;
213
- border: 2px solid #b1b1b17d;
 
214
  }
215
- .signup-brand {
216
- color: #1de9b6;
217
- font-size: 2.2rem;
218
- font-weight: 900;
219
- letter-spacing: 2px;
 
 
220
  }
221
- /* Removed header login link positioning; new variant below form */
222
- .signup-login-link {
223
- font-size: 1.05rem;
224
- color: #fff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  text-align: center;
226
- margin: 14px 0 4px;
227
- }
228
- .signup-login-link.below { margin-top: 8px; }
229
- .signup-login-link a {
230
- color: #38bdf8;
231
- text-decoration: underline;
232
- margin-left: 6px;
233
- font-weight: 600;
234
- cursor: pointer;
235
- }
236
- .signup-box {
237
- background: #18314a;
238
- border-radius: 22px;
239
- box-shadow: 0 12px 48px #000a;
240
- padding: 48px 38px 32px 38px;
241
- width: 520px;
242
- max-width: 95vw;
243
  display: flex;
244
  flex-direction: column;
245
  align-items: center;
246
- position: relative;
 
247
  }
248
- .signup-title {
249
- color: #38bdf8;
250
- font-size: 2.1rem;
251
  font-weight: 800;
252
- margin-bottom: 32px;
253
- text-align: center;
254
- letter-spacing: 1px;
255
- text-shadow: 0 2px 8px #0008;
256
- }
257
- form {
258
- width: 100%;
259
- }
260
- .signup-row {
261
- display: flex;
262
- gap: 24px;
263
  margin-bottom: 18px;
 
264
  }
265
- .signup-field {
266
- flex: 1;
267
- display: flex;
268
- flex-direction: column;
 
 
 
269
  }
270
- .signup-field label {
 
 
271
  color: #fff;
272
- font-weight: 600;
273
- margin-bottom: 6px;
274
- font-size: 1rem;
275
- letter-spacing: 0.5px;
276
- }
277
- .signup-field input,
278
- .signup-field select {
279
- background: #fff;
280
- color: #18314a;
281
- border: none;
282
- border-radius: 8px;
283
- padding: 12px 14px;
284
- font-size: 1rem;
285
- margin-bottom: 2px;
286
- box-shadow: 0 1px 4px #0002;
287
- transition: border 0.2s, box-shadow 0.2s;
288
- }
289
- .signup-field input:focus,
290
- .signup-field select:focus {
291
- outline: 2px solid #1de9b6;
292
- border-color: #1de9b6;
293
- box-shadow: 0 0 0 2px #1de9b688;
294
- }
295
- .signup-field input::placeholder {
296
- color: #b0b8c1;
297
- opacity: 1;
298
- }
299
- .signup-field small.error {
300
- color: #ff5252;
301
- font-size: 0.85rem;
302
- margin-top: 2px;
303
- text-shadow: 0 1px 2px #0008;
304
  }
305
- .signup-checkbox {
 
 
 
 
 
 
 
306
  display: flex;
307
  align-items: center;
308
- margin-bottom: 22px;
309
- color: #fff;
310
- font-size: 1rem;
311
- }
312
- .signup-checkbox input[type="checkbox"] {
313
- margin-right: 10px;
314
- accent-color: #1de9b6;
315
  }
316
- .signup-checkbox a {
317
- color: #38bdf8;
318
- text-decoration: underline;
319
- margin-left: 4px;
 
 
 
 
320
  }
321
- .signup-btn {
322
- width: 100%;
323
- background: #38bdf8;
324
- color: #18314a;
325
- border: none;
326
- border-radius: 8px;
327
- padding: 14px 0;
328
- font-size: 1.1rem;
329
- font-weight: 700;
330
- margin-bottom: 8px; /* reduced because login link follows */
331
- cursor: pointer;
332
- transition: background 0.2s, color 0.2s;
333
- box-shadow: 0 2px 8px #0003;
334
  }
335
- .signup-btn:hover {
336
- background: #13bfa6;
 
 
 
 
 
 
 
 
 
337
  }
338
- .signup-social-row {
339
- display: flex;
340
- gap: 18px;
341
- width: 100%;
342
- margin-bottom: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  }
344
- .signup-social {
345
- flex: 1;
346
- background: #fff;
347
- color: #18314a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  border: none;
349
- border-radius: 8px;
350
- padding: 12px 0;
351
- font-size: 1rem;
352
- font-weight: 600;
353
  cursor: pointer;
354
- transition: background 0.2s, color 0.2s;
355
- }
356
- .signup-social.facebook:hover {
357
- background: #3b5998;
358
- color: #fff;
359
- }
360
- .signup-social.twitter:hover {
361
- background: #1da1f2;
362
- color: #fff;
363
- }
364
- .signup-footer {
365
- color: #b0b8c1;
366
- font-size: 0.95rem;
367
- text-align: center;
368
- margin-top: 8px;
369
- }
370
- .signup-close {
371
- position: absolute;
372
- top: 18px;
373
- right: 18px;
374
- width: 38px;
375
- height: 38px;
376
- border: none;
377
- background: #14263c;
378
- color: #fff;
379
- border-radius: 50%;
380
- font-size: 2rem;
381
- font-weight: bold;
382
- display: flex;
383
  align-items: center;
384
  justify-content: center;
385
- cursor: pointer;
386
- z-index: 10;
387
- transition: background 0.2s, color 0.2s;
388
- box-shadow: 0 2px 8px #0005;
389
  }
390
- .signup-close:hover {
391
- background: #38bdf8;
392
- color: #18314a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  }
394
 
395
- /* Animated glow loop for role info button when closed */
396
- @keyframes pulseGlow { 0%{box-shadow:0 0 0 0 rgba(56,189,248,.75);} 70%{box-shadow:0 0 12px 6px rgba(56,189,248,0);} 100%{box-shadow:0 0 0 0 rgba(56,189,248,0);} }
397
- .role-info-btn.glow-loop { animation:pulseGlow 2s ease-in-out infinite; }
398
- .role-info-btn.glow-loop:hover { animation:none; }
 
 
 
399
 
400
- /* Pop-in animation for role-help panel */
401
- @keyframes popIn { 0%{opacity:0; transform:translateY(-8px) scale(.95);} 60%{opacity:1; transform:translateY(2px) scale(1.02);} 100%{opacity:1; transform:translateY(0) scale(1);} }
402
- .role-help { animation:popIn .35s cubic-bezier(.23,1,.32,1); }
 
 
403
 
404
- @media (max-width: 700px) {
405
- .signup-box {
406
- padding: 18px 6vw 18px 6vw;
407
- width: 98vw;
408
- }
409
- .signup-header {
410
- flex-direction: column;
411
- align-items: flex-start;
412
- width: 98vw;
413
- padding: 0 0 12px 0;
414
- }
415
- .signup-title {
416
- font-size: 1.3rem;
417
- }
418
- .signup-row {
419
- flex-direction: column;
420
- gap: 0;
421
- }
422
- .signup-login-link { font-size: .95rem; }
423
- }
424
-
425
- /* Detached overlay modal (centered) */
426
- .role-help-overlay { position:fixed; inset:0; background:rgba(15,33,50,.72); backdrop-filter:blur(4px); display:flex; align-items:center; justify-content:center; z-index:1400; animation:fadeInBackdrop .25s ease-out; }
427
- .role-help-modal { position:relative; width:min(480px,90vw); background:#18314a; color:#e6f4fa; border:1px solid #27526b; border-radius:20px; padding:46px 34px 32px; box-shadow:0 18px 48px rgba(0,0,0,.55),0 0 0 1px #1d4358; animation:popIn .38s cubic-bezier(.23,1,.32,1); }
428
- .role-help-title { margin:0 0 14px; font-size:1.55rem; font-weight:800; letter-spacing:.5px; color:#38bdf8; text-shadow:0 2px 8px #0008; }
429
- .role-help-list { margin:0 0 14px 18px; padding:0; list-style:disc; }
430
- .role-help-list li { margin:0 0 6px; line-height:1.4; }
431
- .role-help-tip { margin:4px 0 0; font-size:.85rem; color:#9bd7ff; font-style:italic; }
432
- .role-help-close { position:absolute; top:12px; right:12px; width:42px; height:42px; border:none; border-radius:50%; background:#102536; color:#38bdf8; font-size:1.5rem; font-weight:700; cursor:pointer; box-shadow:0 0 0 1px #1e3a4d; transition:background .25s,color .25s, transform .25s, box-shadow .25s; }
433
- .role-help-close:hover { background:#38bdf8; color:#102536; box-shadow:0 0 0 2px #38bdf8,0 0 14px #38bdf8aa; transform:rotate(90deg); }
 
1
  :host {
2
  display: block;
3
+ width: 100%;
4
+ min-height: 100vh;
5
  }
6
 
7
+ .signup-bg {
8
+ min-height: 100vh;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ background: #f7fafd;
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
+ .signup-container {
 
 
 
 
 
 
 
 
 
16
  display: flex;
17
+ width: 100vw;
18
+ max-width: 1200px;
19
+ min-height: 600px;
20
+ border-radius: 0;
21
+ box-shadow: none;
22
+ overflow: hidden;
23
  align-items: center;
24
  justify-content: center;
 
25
  }
26
 
27
+ .signup-panel-right {
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ padding: 32px 0;
32
+ }
33
 
34
+ .signup-panel-right {
35
+ flex: 1 1 0;
36
+ background: #f7fafd;
 
 
37
  }
38
 
39
+ .create-card {
40
+ background: #fff;
41
+ width: 90%;
42
+ max-width: 540px;
43
+ display: flex;
44
+ flex-direction: column;
45
+ align-items: center;
46
+ padding: 38px 38px 28px 38px;
47
+ border-radius: 18px;
48
+ box-shadow: 0 12px 48px rgba(2, 6, 23, 0.18), 0 0 0 2px rgba(56, 189, 248, 0.08);
49
+ margin: 0 auto;
50
  }
51
 
52
+ .create-title {
53
+ font-size: 2.1rem;
54
+ font-weight: 900;
55
+ text-align: center;
56
+ margin-bottom: 28px;
57
+ letter-spacing: 0.6px;
58
+ color: #23395d;
59
  }
60
 
61
+ .signup-title.center-title {
62
+ text-align: center;
63
+ margin-bottom: 32px;
64
+ width: 100%;
65
+ font-size: 2.1rem;
66
+ font-weight: 800;
67
+ letter-spacing: 1px;
68
+ color: #23395d;
69
+ text-shadow: 0 2px 8px #0008;
70
+ /* animation: logoGlow 3.5s ease-in-out infinite alternate; */
71
  }
72
 
73
+ @keyframes logoGlow {
74
+ 0% {
75
+ text-shadow: 0 2px 8px #0008, 0 0 12px #38bdf8, 0 0 6px #13bfa6;
76
+ }
77
+
78
+ 100% {
79
+ text-shadow: 0 2px 8px #0008, 0 0 32px #38bdf8, 0 0 18px #13bfa6;
80
+ }
81
  }
82
 
83
+ .create-form {
84
+ width: 100%;
85
+ max-width: 510px;
86
  display: grid;
87
+ grid-template-columns: 1fr 1fr;
88
+ gap: 12px 15px;
89
+ align-items: start;
90
+ margin-bottom: 14px;
91
  }
92
 
93
+ .terms-info {
94
+ color: #137ec4;
95
+ font-size: 1.08rem;
96
  font-weight: 600;
97
+ text-align: left;
98
+ margin: 12px 0 0 0;
99
+ letter-spacing: 0.5px;
100
+ display: block;
101
  }
102
 
103
+ .form-row {
104
+ display: contents;
 
 
 
 
 
105
  }
106
 
107
+ .form-field {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 8px;
111
+ width: 100%;
112
  }
113
 
114
+ .form-field label {
115
+ font-size: 1.05rem;
116
+ font-weight: 700;
117
+ color: #23395d;
118
+ }
119
+
120
+ .form-field input,
121
+ .form-field select {
122
+ background: #fff;
123
+ color: #23395d;
124
+ border: none;
125
+ border-radius: 8px;
126
+ padding: 12px 14px;
127
+ font-size: 1rem;
128
+ margin-bottom: 2px;
129
+ box-shadow: 0 1px 4px #0002;
130
+ transition: border 0.2s, box-shadow 0.2s;
131
+ width: 100%;
132
+ min-width: 0;
133
+ max-width: 100%;
134
+ height: 46px;
135
+ box-sizing: border-box;
136
+ }
137
+
138
+ .form-field input:focus,
139
+ .form-field select:focus {
140
+ outline: 2px solid #1de9b6;
141
+ border-color: #1de9b6;
142
+ box-shadow: 0 0 6px rgba(56, 189, 248, 0.5), 0 0 0 2px #1de9b688;
143
+ }
144
+
145
+ .form-field input::placeholder {
146
+ color: #b0b8c1;
147
+ opacity: 1;
148
+ }
149
+
150
+ .form-checkbox {
151
+ grid-column: 1 / -1;
152
+ display: flex;
153
+ gap: 10px;
154
+ align-items: center;
155
+ color: #2b5160;
156
+ margin-top: 8px;
157
  }
158
 
159
+ .form-checkbox input[type="checkbox"] {
160
+ width: 18px;
161
+ height: 18px;
162
+ accent-color: #38bdf8;
163
+ }
164
+
165
+ .create-btn {
166
+ grid-column: 1 / -1;
167
  width: 100%;
168
+ background: #23395d;
169
+ color: #fff;
170
+ padding: 14px 18px;
171
+ border-radius: 10px;
172
+ font-weight: 800;
173
+ border: none;
174
+ box-shadow: 0 10px 30px rgba(3, 20, 36, 0.32);
175
+ cursor: pointer;
176
+ font-size: 1.15rem;
177
+ margin-top: 10px;
178
+ }
179
+
180
+ .create-btn:hover {
181
+ background: #38bdf8;
182
+ color: #fff;
183
+ }
184
+
185
+ .create-login-link {
186
+ grid-column: 1 / -1;
187
+ text-align: center;
188
+ color: #137ec4;
189
+ margin-top: 0px;
190
+ }
191
+
192
+ .create-login-link a {
193
+ color: #137ec4;
194
+ font-weight: 700;
195
+ }
196
+
197
+ .create-footer {
198
+ grid-column: 1 / -1;
199
+ text-align: center;
200
+ color: #010207;
201
+ font-size: 0.9rem;
202
+ margin-top: 16px;
203
  }
204
 
205
+ .form-field .error {
206
+ color: #ff5252;
207
+ font-size: 0.85rem;
208
+ margin-top: 0px;
209
  }
210
 
211
+ .welcome-info-box {
212
+ position: absolute;
213
+ top: 32px;
214
+ left: 0;
215
  width: 100%;
216
+ padding: 0 32px;
217
+ z-index: 2;
218
+ text-align: left;
219
  }
220
 
221
+ .welcome-info-title {
222
+ font-size: 1.35rem;
223
+ font-weight: 800;
224
  color: #fff;
225
+ margin-bottom: 6px;
226
+ letter-spacing: 0.5px;
227
  }
228
 
229
+ .welcome-info-desc {
230
+ font-size: 1.08rem;
231
+ color: #e0f7fa;
232
+ margin-bottom: 8px;
 
233
  }
234
 
235
+ .welcome-info-link {
236
+ color: #fff;
237
+ font-weight: 700;
238
+ text-decoration: underline;
239
+ cursor: pointer;
240
+ transition: color 0.2s;
241
  }
242
 
243
+ .welcome-info-link:hover {
244
+ color: #23395d;
245
+ }
246
+
247
+ /* Extra whitespace and centering for small screens */
248
+ @media (max-width: 900px) {
249
+ .signup-container {
250
+ flex-direction: column;
251
+ width: 98vw;
252
+ align-items: center;
253
+ justify-content: center;
254
+ }
255
+
256
+ .signup-panel-right {
257
+ padding: 18px 6vw;
258
+ }
259
+
260
+ .create-card {
261
+ padding: 18px 6vw;
262
+ margin: 0 auto;
263
+ }
264
+
265
+ .create-form {
266
+ grid-template-columns: 1fr;
267
+ gap: 18px;
268
+ }
269
+
270
+ .create-btn {
271
+ width: 100%;
272
+ }
273
  }
274
 
275
+ @media (max-width: 600px) {
276
+ .create-card {
277
+ padding: 10px 2vw;
278
  }
279
 
280
+ .signup-panel-right {
281
+ padding: 10px 2vw;
 
 
282
  }
283
  }
284
 
285
+ .signin-close {
286
+ position: absolute;
287
+ top: 5px;
288
+ right: 5px;
289
+ width: 38px;
290
+ height: 38px;
291
+ border: none;
292
+ background: #14263c;
293
+ color: #fff;
294
+ border-radius: 50%;
295
+ font-size: 2rem;
296
+ font-weight: bold;
297
  display: flex;
298
  align-items: center;
299
+ justify-content: center;
300
+ cursor: pointer;
301
+ z-index: 10;
302
+ transition: background 0.2s, color 0.2s;
303
+ box-shadow: 0 2px 8px #0005;
304
  }
305
 
306
+ .signin-close:hover {
307
+ background: #38bdf8;
308
+ color: #18314a;
309
+ }
310
+
311
+ /* Eye toggle inside password fields: match sign-in positioning */
312
+ .form-field .eye-toggle {
313
+ position: absolute;
314
+ right: 12px;
315
+ top: 38px;
316
+ background: none;
317
+ border: none;
318
+ font-size: 1.3em;
319
+ color: #888;
320
+ cursor: pointer;
321
+ z-index: 2;
322
+ padding: 0;
323
+ line-height: 1;
324
+ opacity: 0.9;
325
+ transition: color 0.2s, opacity 0.2s;
326
  }
327
 
328
+ .form-field .eye-toggle:hover {
329
+ color: #555;
330
+ opacity: 1;
331
+ }
332
+
333
+ .form-field .eye-toggle:focus {
334
+ outline: none;
335
+ }
336
+
337
+ /* Ensure button SVG scales nicely */
338
+ .form-field .eye-toggle svg {
339
+ width: 22px;
340
+ height: 22px;
341
+ }
342
+
343
+ /* Small screen tweak: move eye toggle slightly up if spacing differs */
344
+ @media (max-width: 700px) {
345
+ .form-field .eye-toggle {
346
+ top: 28px;
347
+ }
348
+ }
349
+
350
+ .fact-rotator {
351
+ font-size: 1.18rem;
352
+ font-weight: 700;
353
+ color: #fff;
354
+ margin-bottom: 18px;
355
+ min-height: 32px;
356
+ text-align: center;
357
+ transition: opacity 0.6s;
358
+ letter-spacing: 0.5px;
359
+ animation: fadeFact 0.6s;
360
+ }
361
+
362
+ @keyframes fadeFact {
363
  from {
364
  opacity: 0;
365
  }
 
369
  }
370
  }
371
 
372
+ .side-panel.side-right {
373
+ position: relative;
374
+ overflow: hidden;
 
 
 
375
  display: flex;
 
376
  align-items: center;
377
  justify-content: center;
378
+ background: linear-gradient(135deg, #38bdf8 0%, #7b2ff2 100%);
379
+ min-height: 100%;
380
  }
381
 
382
+ .side-panel.side-left {
383
+ position: relative;
384
+ overflow: hidden;
385
  display: flex;
386
+ align-items: center;
387
+ justify-content: center;
388
+
389
+ min-height: 100%;
390
+ }
391
+
392
+ .wave-bg {
393
+ position: absolute;
394
+ inset: 0;
395
+ width: 100%;
396
+ height: 100%;
397
+ z-index: 1;
398
+ overflow: hidden;
399
+ }
400
+
401
+ .wave-svg {
402
  position: absolute;
403
  top: 0;
404
  left: 0;
405
+ width: 100%;
406
+ height: 100%;
407
+ z-index: 1;
408
  }
409
+
410
+ .circle {
411
+ position: absolute;
412
+ border-radius: 50%;
413
+ background: linear-gradient(135deg, #38bdf8 0%, #7b2ff2 100%);
414
+ opacity: 0.85;
415
  }
416
+
417
+ .circle1 {
418
+ width: 90px;
419
+ height: 90px;
420
+ top: 80px;
421
+ left: 60px;
422
+ box-shadow: 0 0 32px #7b2ff2aa;
423
  }
424
+
425
+ .circle2 {
426
+ width: 60px;
427
+ height: 60px;
428
+ top: 220px;
429
+ left: 220px;
430
+ box-shadow: 0 0 24px #38bdf8aa;
431
  }
432
+
433
+ .circle3 {
434
+ width: 120px;
435
+ height: 120px;
436
+ bottom: 40px;
437
+ left: 120px;
438
+ box-shadow: 0 0 48px #7b2ff2aa;
439
+ }
440
+
441
+ .circle4 {
442
+ width: 36px;
443
+ height: 36px;
444
+ bottom: 80px;
445
+ right: 60px;
446
+ box-shadow: 0 0 12px #38bdf8aa;
447
+ }
448
+
449
+ .welcome-content {
450
+ position: relative;
451
+ z-index: 2;
452
+ width: 100%;
453
  text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  display: flex;
455
  flex-direction: column;
456
  align-items: center;
457
+ justify-content: center;
458
+ margin-top: 80px;
459
  }
460
+
461
+ .welcome-title {
462
+ font-size: 2.2rem;
463
  font-weight: 800;
464
+ color: #fff;
 
 
 
 
 
 
 
 
 
 
465
  margin-bottom: 18px;
466
+ text-shadow: 0 2px 16px #0006;
467
  }
468
+
469
+ .welcome-subtitle {
470
+ font-size: 1.08rem;
471
+ color: #e0e7ef;
472
+ margin-bottom: 32px;
473
+ letter-spacing: 1px;
474
+ text-shadow: 0 2px 8px #0004;
475
  }
476
+
477
+ .welcome-footer {
478
+ font-size: 1.05rem;
479
  color: #fff;
480
+ opacity: 0.7;
481
+ margin-top: 120px;
482
+ letter-spacing: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  }
484
+
485
+ /* Glossy info popup for role info */
486
+ .role-info-popup-bg {
487
+ position: fixed;
488
+ inset: 0;
489
+ background: rgba(30, 41, 59, 0.55);
490
+ backdrop-filter: blur(1px);
491
+ z-index: 0;
492
  display: flex;
493
  align-items: center;
494
+ justify-content: flex-end;
495
+ padding: 48px 56px 48px 24px;
496
+ animation: fadeInModalBg 0.3s;
 
 
 
 
497
  }
498
+
499
+ @keyframes fadeInModalBg {
500
+ from {
501
+ opacity: 0;
502
+ }
503
+
504
+ to {
505
+ opacity: 1;
506
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  }
508
+
509
+ @keyframes popupOpen {
510
+ 0% {
511
+ opacity: 0;
512
+ transform: scale(0.92) translateY(24px);
513
+ }
514
+
515
+ 100% {
516
+ opacity: 1;
517
+ transform: scale(1) translateY(0);
518
+ }
519
  }
520
+
521
+ .role-info-popup {
522
+ background: rgba(255, 255, 255, 0.92);
523
+ border-radius: 4px;
524
+ box-shadow: 0 8px 32px #38bdf844 0 24px #1e293b88;
525
+ padding: 22px 28px 18px 28px;
526
+ min-width: 220px;
527
+ max-width: 90vw;
528
+ text-align: left;
529
+ z-index: 3001;
530
+ font-size: 0.98em;
531
+ color: #23395d;
532
+ letter-spacing: 0.2px;
533
+ line-height: 1.5;
534
+ position: relative;
535
+ font-family: inherit;
536
+ opacity: 1;
537
+ animation: popupOpen 1.2s cubic-bezier(.22, .9, .32, 1) both;
538
  }
539
+
540
+ .role-info-popup .close-btn {
541
+ position: absolute;
542
+ top: 8px;
543
+ right: 8px;
544
+ width: 24px;
545
+ height: 24px;
546
+ border: none;
547
+ background: #14263c;
548
+ color: #fff;
549
+ border-radius: 50%;
550
+ font-size: 1.1rem;
551
+ font-weight: bold;
552
+ display: flex;
553
+ align-items: center;
554
+ justify-content: center;
555
+ cursor: pointer;
556
+ z-index: 10;
557
+ transition: background 0.2s, color 0.2s;
558
+ box-shadow: 0 2px 8px #0005;
559
+ }
560
+
561
+ .role-info-popup .close-btn:hover {
562
+ background: #38bdf8;
563
+ color: #18314a;
564
+ }
565
+
566
+ .role-info-btn {
567
+ background: none;
568
  border: none;
569
+ color: #38bdf8;
570
+ font-size: 1.15em;
 
 
571
  cursor: pointer;
572
+ margin-left: 6px;
573
+ vertical-align: middle;
574
+ padding: 0;
575
+ display: inline-flex;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  align-items: center;
577
  justify-content: center;
578
+ transition: color 0.2s;
 
 
 
579
  }
580
+
581
+ .role-info-btn:hover {
582
+ color: #137ec4;
583
+ }
584
+
585
+ /* Spinner for loading state on buttons */
586
+ .spinner {
587
+ display: inline-block;
588
+ width:18px;
589
+ height:18px;
590
+ border:3px solid #fff;
591
+ border-top:3px solid #38bdf8;
592
+ border-radius:50%;
593
+ animation: spin0.7s linear infinite;
594
+ vertical-align: middle;
595
+ margin-right:8px;
596
+ }
597
+ @keyframes spin {
598
+ 0% { transform: rotate(0deg);}
599
+ 100% { transform: rotate(360deg);}
600
+ }
601
+
602
+ /* Info button and floating info popup styles */
603
+ .info-btn {
604
+ background: #38bdf8;
605
+ color: #fff;
606
+ border: none;
607
+ border-radius:50%;
608
+ width:28px;
609
+ height:28px;
610
+ font-size:1.1rem;
611
+ font-weight: bold;
612
+ cursor: pointer;
613
+ margin-left:8px;
614
+ }
615
+
616
+ .info-popup-bg {
617
+ position: fixed;
618
+ inset:0;
619
+ background: rgba(30,41,59,0.45);
620
+ backdrop-filter: blur(2px);
621
+ z-index:;
622
+ display: flex;
623
+ align-items: center;
624
+ justify-content: center;
625
+ }
626
+
627
+ .info-popup {
628
+ background: rgba(255,255,255,0.85);
629
+ border-radius: 16px;
630
+ box-shadow: 08px 32px #38bdf844,0024px #1e293b88;
631
+ padding: 24px 28px 18px 28px;
632
+ min-width: 320px;
633
+ max-width: 90vw;
634
+ text-align: left;
635
+ font-size: 0.98rem;
636
+ color: #23395d;
637
+ position: relative;
638
+ font-family: inherit;
639
+ left: 840px;
640
  }
641
 
642
+ .info-title {
643
+ font-size:1.08rem;
644
+ font-weight:700;
645
+ margin-bottom:8px;
646
+ color: #38bdf8;
647
+ letter-spacing:0.5px;
648
+ }
649
 
650
+ .info-text {
651
+ font-size:0.95rem;
652
+ color: #23395d;
653
+ opacity:0.95;
654
+ }
655
 
656
+ .info-close {
657
+ position: absolute;
658
+ top:8px;
659
+ right:12px;
660
+ background: none;
661
+ border: none;
662
+ font-size:1.5rem;
663
+ color: #38bdf8;
664
+ cursor: pointer;
665
+ font-weight: bold;
666
+ opacity:0.7;
667
+ transition: opacity 0.2s;
668
+ }
669
+ .info-close:hover {
670
+ opacity:1;
671
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/homepage/sign-up/sign-up.component.html CHANGED
@@ -1,73 +1,118 @@
1
- <section class="signup-popup" (click)="hideRoleInfo()">
 
2
  <div class="signup-header">
3
- <div class="signup-logo"></div>
 
 
4
  </div>
5
- <div class="signup-box" (click)="$event.stopPropagation()">
6
- <button class="signup-close" type="button" (click)="closePopup()" aria-label="Close">&times;</button>
7
- <h2 class="signup-title">Create An Account</h2>
8
- <form [formGroup]="form" (ngSubmit)="submit()" novalidate>
9
- <div class="signup-row">
10
- <div class="signup-field">
11
- <label for="firstName">First Name</label>
12
- <input id="firstName" type="text" placeholder="First Name" formControlName="name" [attr.aria-invalid]="controlHasError('name')" />
13
- <small *ngIf="controlHasError('name','required') && form.get('name')?.touched" class="error">First name is required.</small>
14
- <small *ngIf="controlHasError('name','minlength') && form.get('name')?.touched" class="error">Enter at least 2 characters.</small>
15
- </div>
16
- <div class="signup-field">
17
- <label for="lastName">Last Name</label>
18
- <input id="lastName" type="text" placeholder="Last Name" />
19
- </div>
20
- </div>
21
- <div class="signup-row">
22
- <div class="signup-field">
23
- <label for="email">Email</label>
24
- <input id="email" type="text" placeholder="email@gmail.com" formControlName="email" [attr.aria-invalid]="controlHasError('email')" />
25
- <small *ngIf="controlHasError('email','required') && form.get('email')?.touched" class="error">Email is required.</small>
26
- <small *ngIf="controlHasError('email','pattern') && form.get('email')?.touched" class="error">Enter a valid email/phone.</small>
27
- </div>
28
- <div class="signup-field role-field-wrapper">
29
- <label for="role">
30
- Role
31
- <button type="button" class="role-info-btn" [class.glow-loop]="!showRoleInfo" (click)="toggleRoleInfo($event)" aria-label="Role descriptions">i</button>
32
- </label>
33
- <select id="role" formControlName="role" [attr.aria-invalid]="controlHasError('role')">
34
- <option value="">-- Select Role --</option>
35
- <option value="admin">Admin</option>
36
- <option value="teachers">Teachers</option>
37
- <option value="lawyers">Lawyers</option>
38
- <option value="investigators">Investigators</option>
39
- <option value="others">Others</option>
40
- </select>
41
- <small *ngIf="controlHasError('role','required') && form.get('role')?.touched" class="error">Role is required.</small>
42
- </div>
43
- </div>
44
- <div class="signup-row">
45
- <div class="signup-field">
46
- <label for="password">Create Password</label>
47
- <input id="password" type="password" placeholder="••••••••" formControlName="password" [attr.aria-invalid]="controlHasError('password')" />
48
- <small *ngIf="controlHasError('password','required') && form.get('password')?.touched" class="error">Password is required.</small>
49
- <small *ngIf="controlHasError('password','minlength') && form.get('password')?.touched" class="error">Use at least 6 characters.</small>
50
- </div>
51
- <div class="signup-field">
52
- <label for="confirmPassword">Confirm Password</label>
53
- <input id="confirmPassword" type="password" placeholder="••••••••" formControlName="confirmPassword" [attr.aria-invalid]="showPwdMismatch()" />
54
- <small *ngIf="controlHasError('confirmPassword','required') && form.get('confirmPassword')?.touched" class="error">Confirm password is required.</small>
55
- <small *ngIf="showPwdMismatch() && form.get('confirmPassword')?.touched" class="error">Passwords do not match.</small>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  </div>
57
  </div>
58
- <div class="signup-checkbox">
59
- <input type="checkbox" id="terms" required />
60
- <label for="terms">Creating your account and you accepting <a href="#">Terms & Conditions.</a></label>
61
- </div>
62
- <button class="signup-btn" type="submit">Create Account</button>
63
- <div class="signup-login-link below">
64
- <span>Already have an account?</span>
65
- <a href="#" (click)="goToLogin(); $event.preventDefault()">Sign in here!</a>
66
- </div>
67
- </form>
68
- <div class="signup-footer">&copy; Pykara Technologies, 2025. All rights reserved.</div>
69
  </div>
70
-
71
  <!-- Detached overlay/modal so layout doesn't shift -->
72
  <div class="role-help-overlay" *ngIf="showRoleInfo" (click)="hideRoleInfo()">
73
  <div class="role-help-modal" role="dialog" aria-label="Role descriptions" (click)="$event.stopPropagation()">
@@ -83,4 +128,26 @@
83
  <p class="role-help-tip">Not sure? Select Others; an Admin can upgrade your role later.</p>
84
  </div>
85
  </div>
86
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <section class="signup-popup ai-bg-animate">
2
+ <div class="ai-particle-bg"></div>
3
  <div class="signup-header">
4
+ <div class="signup-logo">
5
+
6
+ </div>
7
  </div>
8
+
9
+ <div class="auth-card">
10
+ <div class="card-inner">
11
+ <!-- FRONT: sign-up on main panel, image side on left -->
12
+ <div class="card-front">
13
+ <button class="signin-close" type="button" (click)="closePopup()" aria-label="Close">&times;</button>
14
+
15
+ <div class="card-content">
16
+ <div class="side-panel side-left">
17
+ <div class="signup-panel-left">
18
+
19
+ </div>
20
+ <div class="main-panel">
21
+ <h2 class="signup-title center-title">Create An Account</h2>
22
+
23
+ <form [formGroup]="form" (ngSubmit)="submit()" class="create-form" novalidate>
24
+ <div class="form-row">
25
+ <div class="form-field">
26
+ <label for="firstName">First Name</label>
27
+ <input id="firstName" type="text" placeholder="First Name" formControlName="name" [attr.aria-invalid]="controlHasError('name')" />
28
+ <small *ngIf="controlHasError('name','required') && form.get('name')?.touched" class="error">First name is required.</small>
29
+ <small *ngIf="controlHasError('name','minlength') && form.get('name')?.touched" class="error">Enter at least 2 characters.</small>
30
+ <small *ngIf="controlHasError('name','invalidName') && form.get('name')?.touched" class="error">Only alphabets and spaces allowed.</small>
31
+ </div>
32
+ <div class="form-field">
33
+ <label for="lastName">Last Name</label>
34
+ <input id="lastName" type="text" placeholder="Last Name" formControlName="lastName" [attr.aria-invalid]="controlHasError('lastName')" />
35
+ <small *ngIf="controlHasError('lastName','required') && form.get('lastName')?.touched" class="error">Last name is required.</small>
36
+ <small *ngIf="controlHasError('lastName','minlength') && form.get('lastName')?.touched" class="error">Enter at least 2 characters.</small>
37
+ <small *ngIf="controlHasError('lastName','invalidName') && form.get('lastName')?.touched" class="error">Only alphabets and spaces allowed.</small>
38
+ </div>
39
+ </div>
40
+ <div class="form-row">
41
+ <div class="form-field">
42
+ <label for="email">Email</label>
43
+ <input id="email" type="text" placeholder="email@gmail.com" formControlName="email" [attr.aria-invalid]="controlHasError('email')" />
44
+ <small *ngIf="controlHasError('email','required') && form.get('email')?.touched" class="error">Email is required.</small>
45
+ <small *ngIf="controlHasError('email','pattern') && form.get('email')?.touched" class="error">Enter a valid email/phone.</small>
46
+ </div>
47
+ <div class="form-field role-field-wrapper">
48
+ <label for="role">
49
+ Role
50
+ <button class="info-btn" type="button" (click)="showInfo = true">i</button>
51
+ </label>
52
+ <select id="role" formControlName="role" [attr.aria-invalid]="controlHasError('role')">
53
+ <option value="">-- Select Role --</option>
54
+ <option value="admin">Admin</option>
55
+ <option value="teachers">Teachers</option>
56
+ <option value="lawyers">Lawyers</option>
57
+ <option value="investigators">Investigators</option>
58
+ <option value="others">Others</option>
59
+ </select>
60
+ <small *ngIf="controlHasError('role','required') && form.get('role')?.touched" class="error">Role is required.</small>
61
+ </div>
62
+ </div>
63
+ <div class="form-row">
64
+ <div class="form-field" style="position:relative;">
65
+ <label for="password">Create Password</label>
66
+ <input id="password" [type]="showPassword ? 'text' : 'password'" placeholder="••••••••" formControlName="password" [attr.aria-invalid]="controlHasError('password')" />
67
+ <button type="button" class="eye-toggle" (click)="toggleConfirmPasswordVisibility()" tabindex="-1" aria-label="Show/Hide confirm password">
68
+ </button>
69
+ <small *ngIf="controlHasError('password','required') && form.get('password')?.touched" class="error">Password is required.</small>
70
+ <small *ngIf="controlHasError('password','minlength') && form.get('password')?.touched" class="error">Use at least 8 characters.</small>
71
+ <small *ngIf="form.get('password')?.hasError('passwordPolicy') && form.get('password')?.touched" class="policy-info">
72
+ Create a strong password with at least 8 characters using letters, numbers, and special symbols.
73
+ </small>
74
+ </div>
75
+
76
+ <div class="form-field" style="position:relative;">
77
+ <label for="confirmPassword">Confirm Password</label>
78
+ <input id="confirmPassword" [type]="showConfirmPassword ? 'text' : 'password'" placeholder="••••••••" formControlName="confirmPassword" [attr.aria-invalid]="showPwdMismatch()" />
79
+ <button type="button" class="eye-toggle" (click)="toggleConfirmPasswordVisibility()" tabindex="-1" aria-label="Show/Hide confirm password">
80
+ </button>
81
+ <small *ngIf="controlHasError('confirmPassword','required') && form.get('confirmPassword')?.touched" class="error">Confirm password is required.</small>
82
+ <small *ngIf="showPwdMismatch() && form.get('confirmPassword')?.touched" class="error">Passwords do not match.</small>
83
+ </div>
84
+ </div>
85
+ <div class="form-checkbox">
86
+ <input type="checkbox" id="terms" formControlName="terms" />
87
+ <label for="terms">Creating your account and you accepting <a href="#">Terms & Conditions.</a></label>
88
+ </div>
89
+ <div *ngIf="submitted && !form.get('terms')?.value" class="terms-info">
90
+ Please accept Terms &amp; Conditions.
91
+ </div>
92
+ <button class="create-btn ai-pulse" type="submit" [disabled]="loading">
93
+ <ng-container *ngIf="!loading; else creatingAccount">
94
+ Create Account
95
+ </ng-container>
96
+ <ng-template #creatingAccount>
97
+ <span class="spinner"></span> Creating Account...
98
+ </ng-template>
99
+ </button>
100
+
101
+
102
+ <!-- Google Sign-In button -->
103
+ <div class="google-signup-row">
104
+ <div id="google-signup-btn-div">
105
+ <div class="g-signin2" data-width="240" data-height="50" data-longtitle="true"></div>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="create-footer"><b>© Pykara Technologies, 2025. All rights reserved.</b></div>
110
+ </form>
111
+ </div>
112
+ </div>
113
  </div>
114
  </div>
 
 
 
 
 
 
 
 
 
 
 
115
  </div>
 
116
  <!-- Detached overlay/modal so layout doesn't shift -->
117
  <div class="role-help-overlay" *ngIf="showRoleInfo" (click)="hideRoleInfo()">
118
  <div class="role-help-modal" role="dialog" aria-label="Role descriptions" (click)="$event.stopPropagation()">
 
128
  <p class="role-help-tip">Not sure? Select Others; an Admin can upgrade your role later.</p>
129
  </div>
130
  </div>
131
+
132
+ <!-- Floating Info Popup -->
133
+ <div *ngIf="showInfo" class="info-popup-bg">
134
+ <div class="info-popup">
135
+ <button class="info-close" type="button" (click)="showInfo = false">&times;</button>
136
+ <div class="info-title">Role Information</div>
137
+ <div class="info-text">
138
+ <ul>
139
+ <li><strong>Admin:</strong> Full control: users, roles, system settings.</li>
140
+ <li><strong>Teachers:</strong> Run assessments / training evaluations.</li>
141
+ <li><strong>Lawyers:</strong> Review analytics for case & witness prep.</li>
142
+ <li><strong>Investigators:</strong> Conduct interviews and capture signals.</li>
143
+ <li><strong>Others:</strong> General or limited access usage.</li>
144
+ </ul>
145
+ <p>Not sure? Select Others; an Admin can upgrade your role later.</p>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+
151
+
152
+
153
+
src/app/homepage/sign-up/sign-up.component.ts CHANGED
@@ -1,46 +1,114 @@
1
- import { Component, Output, EventEmitter } from '@angular/core';
 
2
  import { CommonModule } from '@angular/common';
3
- import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl } from '@angular/forms';
4
  import { Router, RouterLink } from '@angular/router';
5
  import { SignUpService } from './sign-up.service'; // Import the SignUpService
6
  import { AuthService } from '../../auth.service';
 
 
 
 
 
 
 
 
 
 
7
 
8
  @Component({
9
  selector: 'app-sign-up',
10
  standalone: true,
11
  imports: [CommonModule, ReactiveFormsModule, RouterLink],
12
  templateUrl: './sign-up.component.html',
13
- styleUrls: ['./sign-up.component.css']
 
 
 
 
 
 
 
 
 
 
 
14
  })
15
- export class SignUpComponent {
 
 
16
  form: FormGroup;
17
  private isSubmitting = false;
18
 
19
- // Added: state & handlers for role info popover used in template
20
  showRoleInfo = false;
21
  toggleRoleInfo(ev?: Event) { ev?.stopPropagation(); this.showRoleInfo = !this.showRoleInfo; }
22
  hideRoleInfo() { this.showRoleInfo = false; }
23
 
24
- @Output() switchToSignIn = new EventEmitter<void>();
25
  @Output() close = new EventEmitter<void>();
26
 
27
- constructor(private fb: FormBuilder, private router: Router, private signUpService: SignUpService) { // Inject SignUpService
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  this.form = this.fb.group({
29
- name: ['', [Validators.required, Validators.minLength(2)]],
 
30
  email: ['', [
31
  Validators.required,
32
  Validators.pattern(/(^[^@]+@[^@]+\.[^@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)
33
  ]],
34
- //gender: ['', [Validators.required]],
35
- password: ['', [Validators.required, Validators.minLength(6)]],
36
  confirmPassword: ['', [Validators.required]],
37
  role: ['', [Validators.required]],
 
38
  }, { validators: [this.passwordsMatchValidator] });
39
 
40
  // Close popover when clicking anywhere in document (capture phase not needed here)
41
  document.addEventListener('click', () => this.hideRoleInfo());
42
  }
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  control(path: string): AbstractControl | null { return this.form.get(path); }
45
 
46
  controlHasError(path: string, error?: string): boolean {
@@ -62,11 +130,20 @@ export class SignUpComponent {
62
  return pw && cpw && pw === cpw ? null : { passwordMismatch: true };
63
  }
64
 
65
- async submit() {
 
 
 
 
 
 
 
66
  // Confirm button click
67
  alert("Sign-Up button clicked!");
68
  console.log("Sign-Up button clicked!");
69
 
 
 
70
  // Mark all form controls as touched to trigger validation
71
  this.form.markAllAsTouched();
72
 
@@ -84,12 +161,20 @@ export class SignUpComponent {
84
  return;
85
  }
86
 
 
 
 
 
 
 
 
 
87
  try {
88
  // Prepare the payload to send to the backend
89
  const payload = {
90
  name: this.control('name')?.value,
 
91
  email: this.control('email')?.value,
92
- //gender: this.control('gender')?.value,
93
  password: this.control('password')?.value,
94
  role: this.control('role')?.value
95
  };
@@ -98,17 +183,33 @@ export class SignUpComponent {
98
  console.log("Payload to send:", payload);
99
 
100
  // Make the HTTP request
101
- await this.signUpService.signUp(payload).toPromise();
102
- console.log("Sign-up request sent successfully!");
103
-
104
- // On success, navigate based on role
105
- if (payload.role === 'admin') {
106
- this.router.navigateByUrl('/infopage');
107
- } else {
108
- this.router.navigateByUrl('/auth/signin');
109
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  } catch (error) {
111
  console.error("Error occurred during sign-up:", error); // Log any errors from the API call
 
112
  }
113
  }
114
 
@@ -119,11 +220,48 @@ export class SignUpComponent {
119
  }
120
 
121
  closePopup() {
 
 
 
 
 
 
 
122
  this.close.emit();
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
 
125
  tr(key: string): string {
126
  const map: Record<string, string> = { title: 'Create your account', subtitle: 'Join to continue' };
127
  return map[key] || '';
128
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
 
1
+
2
+ import { Component, Output, EventEmitter, ChangeDetectorRef, Input, OnInit, OnDestroy } from '@angular/core';
3
  import { CommonModule } from '@angular/common';
4
+ import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
5
  import { Router, RouterLink } from '@angular/router';
6
  import { SignUpService } from './sign-up.service'; // Import the SignUpService
7
  import { AuthService } from '../../auth.service';
8
+ import { trigger, transition, style, animate } from '@angular/animations';
9
+
10
+ export function nameValidator(control: AbstractControl): ValidationErrors | null {
11
+ const value = control.value || '';
12
+ // Only allow alphabets and spaces, min2 chars
13
+ if (!/^[A-Za-z ]{2,}$/.test(value)) {
14
+ return { invalidName: true };
15
+ }
16
+ return null;
17
+ }
18
 
19
  @Component({
20
  selector: 'app-sign-up',
21
  standalone: true,
22
  imports: [CommonModule, ReactiveFormsModule, RouterLink],
23
  templateUrl: './sign-up.component.html',
24
+ styleUrls: ['./sign-up.component.css'],
25
+ animations: [
26
+ trigger('fadeInOut', [
27
+ transition(':enter', [
28
+ style({ opacity:0 }),
29
+ animate('600ms', style({ opacity:1 }))
30
+ ]),
31
+ transition(':leave', [
32
+ animate('600ms', style({ opacity:0 }))
33
+ ])
34
+ ])
35
+ ]
36
  })
37
+ export class SignUpComponent implements OnInit, OnDestroy {
38
+ @Input() embedded = false; // when true, render only inner panel (for embedding in auth-card)
39
+ @Output() switchToSignIn = new EventEmitter<void>();
40
  form: FormGroup;
41
  private isSubmitting = false;
42
 
43
+ // Role info popover logic preserved
44
  showRoleInfo = false;
45
  toggleRoleInfo(ev?: Event) { ev?.stopPropagation(); this.showRoleInfo = !this.showRoleInfo; }
46
  hideRoleInfo() { this.showRoleInfo = false; }
47
 
 
48
  @Output() close = new EventEmitter<void>();
49
 
50
+ showPassword = false;
51
+ showConfirmPassword = false;
52
+ errorMessage = '';
53
+
54
+ isSignUpActive = true; // ← Added state for sign-up panel activation
55
+ public loading = false; // Used to disable the button during sign-up
56
+ submitted = false; // Track form submission status
57
+
58
+ // Added: terms & conditions error handling
59
+ termsError: string = '';
60
+
61
+ facts: string[] = [
62
+ '🧠 Py-Detect AI analyzes tone, emotion, and consistency.',
63
+ '🎥 Supports video and audio interrogation analysis.',
64
+ '📊 Generates instant investigation summary reports.'
65
+ ];
66
+ currentFact: string = this.facts[0];
67
+ private factIndex =0;
68
+ private factInterval: any;
69
+
70
+ showInfo = false;
71
+
72
+ constructor(
73
+ private fb: FormBuilder,
74
+ private router: Router,
75
+ private signUpService: SignUpService,
76
+ private cdr: ChangeDetectorRef
77
+ ) {
78
  this.form = this.fb.group({
79
+ name: ['', [Validators.required, Validators.minLength(2), nameValidator]],
80
+ lastName: ['', [Validators.required, Validators.minLength(2), nameValidator]],
81
  email: ['', [
82
  Validators.required,
83
  Validators.pattern(/(^[^@]+@[^@]+\.[^@]+$)|(^\+?\d[\d\-\s]{8,14}\d$)/)
84
  ]],
85
+ password: ['', [Validators.required, Validators.minLength(8), passwordPolicyValidator]],
 
86
  confirmPassword: ['', [Validators.required]],
87
  role: ['', [Validators.required]],
88
+ terms: [false, Validators.requiredTrue] // Added terms control with requiredTrue validator
89
  }, { validators: [this.passwordsMatchValidator] });
90
 
91
  // Close popover when clicking anywhere in document (capture phase not needed here)
92
  document.addEventListener('click', () => this.hideRoleInfo());
93
  }
94
 
95
+ ngOnInit() {
96
+ this.startFactRotation();
97
+ }
98
+
99
+ ngOnDestroy() {
100
+ if (this.factInterval) {
101
+ clearInterval(this.factInterval);
102
+ }
103
+ }
104
+
105
+ startFactRotation() {
106
+ this.factInterval = setInterval(() => {
107
+ this.factIndex = (this.factIndex +1) % this.facts.length;
108
+ this.currentFact = this.facts[this.factIndex];
109
+ },5000);
110
+ }
111
+
112
  control(path: string): AbstractControl | null { return this.form.get(path); }
113
 
114
  controlHasError(path: string, error?: string): boolean {
 
130
  return pw && cpw && pw === cpw ? null : { passwordMismatch: true };
131
  }
132
 
133
+ togglePasswordVisibility() {
134
+ this.showPassword = !this.showPassword;
135
+ }
136
+ toggleConfirmPasswordVisibility() {
137
+ this.showConfirmPassword = !this.showConfirmPassword;
138
+ }
139
+
140
+ submit() {
141
  // Confirm button click
142
  alert("Sign-Up button clicked!");
143
  console.log("Sign-Up button clicked!");
144
 
145
+ this.submitted = true; // Track the form submission attempt
146
+
147
  // Mark all form controls as touched to trigger validation
148
  this.form.markAllAsTouched();
149
 
 
161
  return;
162
  }
163
 
164
+ // Check terms & conditions acceptance
165
+ if (!this.form.get('terms')?.value) {
166
+ this.termsError = 'Please accept Terms & Conditions.';
167
+ return;
168
+ }
169
+ this.termsError = '';
170
+
171
+ this.loading = true; // Set loading to true when starting submission
172
  try {
173
  // Prepare the payload to send to the backend
174
  const payload = {
175
  name: this.control('name')?.value,
176
+ lastName: this.control('lastName')?.value,
177
  email: this.control('email')?.value,
 
178
  password: this.control('password')?.value,
179
  role: this.control('role')?.value
180
  };
 
183
  console.log("Payload to send:", payload);
184
 
185
  // Make the HTTP request
186
+ this.signUpService.signUp(payload).subscribe(
187
+ (response) => {
188
+ this.errorMessage = '';
189
+ console.log("Sign-up request sent successfully!");
190
+ this.loading = false; // Reset loading on success
191
+ // Wait for loader to finish, then navigate
192
+ setTimeout(() => {
193
+ this.router.navigate(['/auth/signin']);
194
+ }, 500);
195
+ },
196
+ (error) => {
197
+ if (error && error.status === 400) {
198
+ this.errorMessage = 'This email or username is already registered.';
199
+ } else {
200
+ this.errorMessage = 'An error occurred. Please try again.';
201
+ }
202
+ this.loading = false; // Reset loading on error
203
+ this.cdr.markForCheck();
204
+ setTimeout(() => {
205
+ this.errorMessage = '';
206
+ this.cdr.markForCheck();
207
+ }, 3000);
208
+ }
209
+ );
210
  } catch (error) {
211
  console.error("Error occurred during sign-up:", error); // Log any errors from the API call
212
+ this.loading = false; // Reset loading on exception
213
  }
214
  }
215
 
 
220
  }
221
 
222
  closePopup() {
223
+ try {
224
+ // dispatch a global event so parent or other listeners always can close modals
225
+ window.dispatchEvent(new CustomEvent('auth-close'));
226
+ } catch (e) {
227
+ // ignore
228
+ }
229
+
230
  this.close.emit();
231
+ // Defensive: remove modal/backdrop if parent didn't hide them
232
+ try {
233
+ const modal = document.querySelector('.modal');
234
+ if (modal && modal.parentElement) modal.parentElement.removeChild(modal);
235
+ const backdrop = document.querySelector('.modal-backdrop');
236
+ if (backdrop && backdrop.parentElement) backdrop.parentElement.removeChild(backdrop);
237
+ } catch (e) {
238
+ console.warn('Failed to remove modal/backdrop DOM elements', e);
239
+ }
240
+ // Ensure change detection updates
241
+ this.cdr.markForCheck();
242
  }
243
 
244
  tr(key: string): string {
245
  const map: Record<string, string> = { title: 'Create your account', subtitle: 'Join to continue' };
246
  return map[key] || '';
247
  }
248
+
249
+ goToSignIn() {
250
+ // Emit to parent when embedded so the card can slide back
251
+ this.switchToSignIn.emit();
252
+ }
253
+
254
+ goToSignUp() {
255
+ // no-op when embedded
256
+ }
257
+ }
258
+
259
+ function passwordPolicyValidator(control: AbstractControl): ValidationErrors | null {
260
+ const value = control.value || '';
261
+ // Policy: min 8 chars, 1 uppercase, 1 lowercase, 1 number, 1 special char
262
+ const policy = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>\/?]).{8,}$/;
263
+ if (!policy.test(value)) {
264
+ return { passwordPolicy: true };
265
+ }
266
+ return null;
267
  }
src/app/homepage/sign-up/sign-up.service.ts CHANGED
@@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; // Import Observable for async handling
6
  providedIn: 'root'
7
  })
8
  export class SignUpService {
9
- private apiUrl = 'http://127.0.0.1:5000'; // The base URL for your Flask backend
10
  constructor(private http: HttpClient) { }
11
 
12
  // Method to sign up a user
 
6
  providedIn: 'root'
7
  })
8
  export class SignUpService {
9
+ private apiUrl = 'http://127.0.0.1:5002'; // The base URL for your Flask backend
10
  constructor(private http: HttpClient) { }
11
 
12
  // Method to sign up a user
src/app/infopage/infopage.component.css CHANGED
@@ -51,7 +51,7 @@ body::before {
51
  justify-content: space-between;
52
  padding: 18px 32px 0 32px;
53
  position: relative;
54
- gap: 32px;
55
  }
56
 
57
  .logo-cluster {
@@ -100,7 +100,8 @@ body::before {
100
  align-items: center;
101
  gap: 14px;
102
  margin-right: 32px;
103
- margin-left:89vh;
 
104
  }
105
 
106
  .pykara-analysis-label {
@@ -110,26 +111,6 @@ body::before {
110
  letter-spacing: 1px;
111
  }
112
 
113
- .pykara-progress-bar {
114
- width: 220px;
115
- height: 14px;
116
- background: #e3f6ff;
117
- border-radius: 8px;
118
- overflow: hidden;
119
- box-shadow: 0 2px 8px #38bdf844, 0 0 12px #38bdf8aa;
120
- position: relative;
121
- }
122
-
123
- .pykara-progress-bar-inner {
124
- height: 100%;
125
- background: linear-gradient(270deg, #38bdf8, #06ffa5, #ff006e, #38bdf8);
126
- background-size: 400% 100%;
127
- border-radius: 8px 0 0 8px;
128
- transition: width 0.4s cubic-bezier(.4,2,.6,1);
129
- animation: progressBarGradientMove 2.5s linear infinite;
130
- box-shadow: 0 0 16px #38bdf8cc, 0 0 8px #06ffa5aa;
131
- }
132
-
133
  @keyframes progressBarGradientMove {
134
  0% { background-position: 0% 50%; }
135
  100% { background-position: 100% 50%; }
@@ -674,6 +655,7 @@ html {
674
  color: white !important;
675
  font-weight: 500 !important;
676
  backdrop-filter: blur(10px) !important;
 
677
  }
678
 
679
  .autosave-indicator.saving {
@@ -1197,8 +1179,8 @@ html {
1197
  }
1198
 
1199
  .pykara-progress-bar {
1200
- width: 220px;
1201
- height: 14px;
1202
  background: #e3f6ff;
1203
  border-radius: 8px;
1204
  overflow: hidden;
 
51
  justify-content: space-between;
52
  padding: 18px 32px 0 32px;
53
  position: relative;
54
+ gap:0px;
55
  }
56
 
57
  .logo-cluster {
 
100
  align-items: center;
101
  gap: 14px;
102
  margin-right: 32px;
103
+ margin-left: 92vh;
104
+ margin-bottom: 15px;
105
  }
106
 
107
  .pykara-analysis-label {
 
111
  letter-spacing: 1px;
112
  }
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  @keyframes progressBarGradientMove {
115
  0% { background-position: 0% 50%; }
116
  100% { background-position: 100% 50%; }
 
655
  color: white !important;
656
  font-weight: 500 !important;
657
  backdrop-filter: blur(10px) !important;
658
+ margin-bottom: 15px;
659
  }
660
 
661
  .autosave-indicator.saving {
 
1179
  }
1180
 
1181
  .pykara-progress-bar {
1182
+ width: 186px;
1183
+ height: 6px;
1184
  background: #e3f6ff;
1185
  border-radius: 8px;
1186
  overflow: hidden;
src/app/infopage/infopage.component.html CHANGED
@@ -30,12 +30,14 @@
30
  <span>{{ autoSaveStatus }}</span>
31
  </div>
32
  <!-- View Records Button -->
33
- <button class="view-records-btn" style="margin-left:16px;vertical-align:middle;"
34
  type="button"
35
- (click)="goToRecords()">
 
 
36
  <i class="fas fa-folder-open"></i>
37
- <span>View Records</span>
38
- </button>
39
  </div>
40
  </div>
41
  </div>
@@ -112,7 +114,8 @@
112
  </button>
113
  <!-- Field Selector Popup: floating, anchored to button -->
114
  <div class="modern-field-selector-popup"
115
- *ngIf="showFieldSelector === (currentSection + '-' + currentSubgroup)">
 
116
  <div class="popup-header">
117
  <span><i class="fas fa-list-check"></i> Select Fields to Display</span>
118
  <button class="popup-close-btn" (click)="closeFieldSelector()" type="button">
@@ -124,6 +127,7 @@
124
  <label class="popup-field-label">
125
  <input type="checkbox"
126
  [checked]="isFieldSelected(field)"
 
127
  (change)="toggleFieldSelection(field, $event)" />
128
  <span class="popup-field-text">{{ field }}</span>
129
  </label>
@@ -287,12 +291,12 @@
287
  <ng-template #textInput>
288
  <!-- Description fields as textarea -->
289
  <textarea *ngIf="field.toLowerCase().includes('description') || field === 'Remark'; else regularInput"
290
- class="field-input"
291
  [class.compact]="isCompactField(field)"
292
  [(ngModel)]="formData[field]"
293
  (input)="onFieldChange(field)"
294
  [placeholder]="getFieldPlaceholder(field)"
295
- [maxlength]="getMaxLength(field)"
296
  rows="3"></textarea>
297
  <!-- Recording status for Remark -->
298
  <div *ngIf="field === 'Remark' && isRecording" style="margin-top:4px;color:#e74c3c;font-size:0.95em;">
@@ -356,3 +360,4 @@
356
  </footer>
357
 
358
 
 
 
30
  <span>{{ autoSaveStatus }}</span>
31
  </div>
32
  <!-- View Records Button -->
33
+ <div class="view-records-btn" style="margin-left: 16px; vertical-align: middle; color: #fff; font-size: 1.5em; width: 2em; height: 2em; display: flex; align-items: center; justify-content: center; cursor: pointer; position: relative; margin-bottom: 15px;"
34
  type="button"
35
+ (click)="goToRecords()"
36
+ (mouseenter)="showViewRecordsTooltip = true"
37
+ (mouseleave)="showViewRecordsTooltip = false">
38
  <i class="fas fa-folder-open"></i>
39
+ <span *ngIf="showViewRecordsTooltip" style="position:absolute;top:100%;left:0%;transform:translateX(-50%);background:#222;color:#fff;padding:4px 12px;border-radius:6px;font-size:0.6em;white-space:nowrap;z-index:10;box-shadow:0 2px 8px rgba(0,0,0,0.12);">View Records</span>
40
+ </div>
41
  </div>
42
  </div>
43
  </div>
 
114
  </button>
115
  <!-- Field Selector Popup: floating, anchored to button -->
116
  <div class="modern-field-selector-popup"
117
+ *ngIf="showFieldSelector === (currentSection + '-' + currentSubgroup)"
118
+ (click)="$event.stopPropagation()">
119
  <div class="popup-header">
120
  <span><i class="fas fa-list-check"></i> Select Fields to Display</span>
121
  <button class="popup-close-btn" (click)="closeFieldSelector()" type="button">
 
127
  <label class="popup-field-label">
128
  <input type="checkbox"
129
  [checked]="isFieldSelected(field)"
130
+ (click)="$event.stopPropagation()"
131
  (change)="toggleFieldSelection(field, $event)" />
132
  <span class="popup-field-text">{{ field }}</span>
133
  </label>
 
291
  <ng-template #textInput>
292
  <!-- Description fields as textarea -->
293
  <textarea *ngIf="field.toLowerCase().includes('description') || field === 'Remark'; else regularInput"
294
+ class="field-input auto-scroll-textarea"
295
  [class.compact]="isCompactField(field)"
296
  [(ngModel)]="formData[field]"
297
  (input)="onFieldChange(field)"
298
  [placeholder]="getFieldPlaceholder(field)"
299
+ [attr.maxlength]="field === 'Brief Description' ? null : getMaxLength(field)"
300
  rows="3"></textarea>
301
  <!-- Recording status for Remark -->
302
  <div *ngIf="field === 'Remark' && isRecording" style="margin-top:4px;color:#e74c3c;font-size:0.95em;">
 
360
  </footer>
361
 
362
 
363
+
src/app/infopage/infopage.component.ts CHANGED
@@ -5,1193 +5,1224 @@ import { Router } from '@angular/router';
5
  import { CaseStoreService } from '../shared/case-store.service';
6
 
7
  @Component({
8
- selector: 'app-infopage',
9
- templateUrl: './infopage.component.html',
10
- styleUrls: ['./infopage.component.css'],
11
- animations: [
12
- // Simple card animation
13
- trigger('cardSlide', [
14
- transition(':enter', [
15
- style({ transform: 'translateY(20px)', opacity: 0 }),
16
- animate('300ms ease-out',
17
- style({ transform: 'translateY(0)', opacity: 1 }))
18
- ])
19
- ]),
20
- // Field animation
21
- trigger('fieldAnimation', [
22
- transition(':enter', [
23
- style({ opacity: 0, transform: 'translateY(10px)' }),
24
- animate('200ms ease-out',
25
- style({ opacity: 1, transform: 'translateY(0)' }))
26
- ])
27
- ]),
28
- // Simple fade animation
29
- trigger('fadeIn', [
30
- transition(':enter', [
31
- style({ opacity: 0 }),
32
- animate('200ms ease-in', style({ opacity: 1 }))
33
- ]),
34
- transition(':leave', [
35
- animate('150ms ease-out', style({ opacity: 0 }))
36
- ])
37
- ]),
38
- // Help animation
39
- trigger('helpAnimation', [
40
- transition(':enter', [
41
- style({ opacity: 0, transform: 'translateY(-10px)' }),
42
- animate('200ms ease-out',
43
- style({ opacity: 1, transform: 'translateY(0)' }))
44
- ]),
45
- transition(':leave', [
46
- animate('150ms ease-in',
47
- style({ opacity: 0, transform: 'translateY(-10px)' }))
48
- ])
49
- ])
50
- ]
 
 
 
 
 
 
 
51
  })
52
  export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
53
- showRemarkModal: boolean = false;
54
- showSubmitPopup: boolean = false;
55
- showMicPopup: boolean = false;
56
- isRecording: boolean = false;
57
- constructor(private router: Router, private caseStore: CaseStoreService) {}
58
- // Core state
59
- currentSection: 'crime' | 'suspect' | 'notes' = 'crime';
60
- currentSubgroup: string = 'Identification & Timing';
61
- showHelpFor: string | null = null;
62
-
63
- // UI state
64
- isAutoSaving: boolean = false;
65
- autoSaveStatus: string = 'Saved';
66
- isDragOver: boolean = false;
67
-
68
- // Card state
69
- isCardMinimized = {
70
- primary: false,
71
- secondary: false,
72
- tertiary: false
73
- };
74
-
75
- // Form data and validation
76
- formData: Record<string, any> = {};
77
- fieldValidation: Record<string, { hasError: boolean, isValid: boolean, message: string }> = {};
78
- completedFields: Set<string> = new Set();
79
- completedSubgroups: Set<string> = new Set();
80
- completedSections: Set<string> = new Set();
81
-
82
- // Subjects for reactive programming
83
- private destroy$ = new Subject<void>();
84
- private autoSave$ = new Subject<void>();
85
-
86
- // Constants - Reverted to original values except for Identification & Timing
87
- readonly sectionKeys: ('crime' | 'suspect' | 'notes')[] = ['crime', 'suspect', 'notes'];
88
- readonly maxFieldsPerCard = 8; // Reverted to original
89
- readonly maxFieldsPerSecondaryCard = 8; // Reverted to original
90
- readonly maxFieldsPerCardIdentificationTiming = 6; // Special for Identification & Timing
91
- readonly maxFieldsPerSecondaryCardIdentificationTiming = 6; // Special for Identification & Timing
92
-
93
- @ViewChild('formCard1') formCard1!: ElementRef<HTMLDivElement>;
94
- @ViewChild('formCard2') formCard2!: ElementRef<HTMLDivElement>;
95
- @ViewChild('formCard3') formCard3!: ElementRef<HTMLDivElement>;
96
-
97
- // Enhanced field definitions with validation rules
98
- readonly requiredFields = new Set<string>([
99
- 'Case ID', 'Crime Type', 'Date & Time (Entry)', 'Location', 'Suspect Name', 'Age', 'Gender',
100
- 'FIR / Ref #', 'Case Category', 'Occurred From', 'Country', 'State', 'District',
101
- 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Case Status', 'Investigating Officer'
102
- ]);
103
-
104
- readonly compactFields = new Set<string>([
105
- 'Age', 'Gender', 'Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color',
106
- 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count', 'Case Priority',
107
- 'Photos / Video?', 'CCTV Present?', 'Arrest Made', 'risk Level', 'Confidentiality'
108
- ]);
109
-
110
- readonly numericFields = new Set<string>([
111
- 'Age', 'Height (cm)', 'Weight (kg)', 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count'
112
- ]);
113
-
114
- // File type configurations
115
- readonly fileTypeConfig: Record<string, string> = {
116
- 'Photo Upload': 'image/*',
117
- 'Evidence Photos': 'image/*',
118
- 'Evidence Videos': 'video/*',
119
- 'Evidence Documents': '.pdf,.doc,.docx,.txt',
120
- 'Evidence Files': '*',
121
- 'Upload Evidence Files': '*',
122
- 'Digital Evidence': '*'
123
- };
124
-
125
- // Track selected values (cascading dropdown logic)
126
- selectedValues: Record<string, string> = {};
127
-
128
- // Date field groups
129
- dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']);
130
- dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']);
131
-
132
- // Country/State/District data
133
- countries = ['India'];
134
- indiaStates = [
135
- 'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chhattisgarh', 'Goa', 'Gujarat',
136
- 'Haryana', 'Himachal Pradesh', 'Jharkhand', 'Karnataka', 'Kerala', 'Madhya Pradesh',
137
- 'Maharashtra', 'Manipur', 'Meghalaya', 'Mizoram', 'Nagaland', 'Odisha', 'Punjab',
138
- 'Rajasthan', 'Sikkim', 'Tamil Nadu', 'Telangana', 'Tripura', 'Uttar Pradesh',
139
- 'Uttarakhand', 'West Bengal'
140
- ];
141
-
142
- tamilNaduDistricts = [
143
- 'Ariyalur', 'Chengalpattu', 'Chennai', 'Coimbatore', 'Cuddalore', 'Dharmapuri', 'Dindigul',
144
- 'Erode', 'Kallakurichi', 'Kanchipuram', 'Kanyakumari', 'Karur', 'Krishnagiri', 'Madurai',
145
- 'Mayiladuthurai', 'Nagapattinam', 'Namakkal', 'Nilgiris', 'Perambalur', 'Pudukkottai',
146
- 'Ramanathapuram', 'Ranipet', 'Salem', 'Sivaganga', 'Tenkasi', 'Thanjavur', 'Theni',
147
- 'Thoothukudi (Tuticorin)', 'Tiruchirappalli', 'Tirunelveli', 'Tirupathur', 'Tiruppur',
148
- 'Tiruvallur', 'Tiruvannamalai', 'Tiruvarur', 'Vellore', 'Viluppuram', 'Virudhunagar'
149
- ];
150
-
151
- // Enhanced select options - Added missing field options
152
- selectOptions: Record<string, string[]> = {
153
- 'Crime Type': ['Theft', 'Assault', 'Homicide', 'Cybercrime', 'Fraud', 'Narcotics', 'Arson', 'Kidnapping', 'General', 'Other'],
154
- 'Case Category': ['Property', 'Violent', 'Cyber', 'Financial', 'Public Order', 'Narcotics', 'Organized', 'General', 'Other'],
155
- 'Number of Victims': ['0', '1', '2', '3', '4', '5+'],
156
- 'Jurisdiction / PS': ['Central PS', 'East Division', 'West Division', 'Rural Unit', 'Cyber Cell', 'General'],
157
- 'Scene Type': ['Residential', 'Commercial', 'Public Space', 'Vehicle', 'Rural', 'Online', 'General', 'Other'],
158
- 'Witness Count': ['0', '1', '2', '3', '4', '5+'],
159
- 'Victim Summary': ['Stable', 'Injured', 'Critical', 'Deceased', 'Unknown'],
160
- 'Suspected Offender Known?': ['Yes', 'No', 'Unknown'],
161
- 'Offence Category': ['Minor', 'Serious', 'Organized', 'Cyber', 'Financial', 'Violent', 'General', 'Other'],
162
- 'Suspected Motive': ['Financial Gain', 'Revenge', 'Jealousy', 'Ideological', 'Political', 'Personal Dispute', 'Unknown', 'General', 'Other'],
163
- 'Confirmed Motive': ['Financial Gain', 'Revenge', 'Jealousy', 'Ideological', 'Political', 'Personal Dispute', 'Unknown', 'General', 'Other'],
164
- 'Weapon Involved': ['None', 'Knife', 'Firearm', 'Blunt Object', 'Explosive', 'Chemical', 'Other', 'Unknown', 'General'],
165
- 'Property Loss / Damage': ['None', 'Minor', 'Moderate', 'Major', 'Severe', 'Unknown'],
166
- 'Photos / Video?': ['Yes', 'No'],
167
- 'CCTV Present?': ['Yes', 'No'],
168
- 'Scene Condition': ['Intact', 'Disturbed', 'Contaminated', 'Secured', 'Compromised', 'General'],
169
- 'Chain of Custody?': ['Initiated', 'Ongoing', 'Complete', 'Not Started'],
170
- 'Forensic Tests Required': ['None', 'DNA', 'Fingerprints', 'Ballistics', 'Toxicology', 'Digital Forensics', 'Trace', 'General', 'Other'],
171
- 'Arrest Made': ['Yes', 'No'],
172
- 'riskLevel': ['Low', 'Medium', 'High', 'Critical'],
173
- 'Confidentiality': ['Internal', 'Restricted', 'Sensitive', 'Sealed'],
174
- 'Initial Actions Taken': ['Scene Secured', 'Medical Aid', 'Evidence Logged', 'Witness Statements', 'Suspect Detained', 'General', 'Other'],
175
- 'Case Status': ['Open', 'Active', 'Suspended', 'Closed', 'Archived'],
176
- 'Case Priority': ['Low', 'Normal', 'High', 'Urgent', 'Critical'],
177
- 'Gender': ['Male', 'Female', 'Other'],
178
- 'Nationality': ['India'],
179
- 'Languages': ['English', 'Hindi', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Bengali', 'Marathi', 'Gujarati', 'Other'],
180
- 'Build': ['Slim', 'Average', 'Athletic', 'Heavy', 'Obese'],
181
- 'Hair Color': ['Black', 'Brown', 'Blonde', 'Red', 'Grey', 'White', 'Dyed / Other', 'Unknown'],
182
- 'Eye Color': ['Brown', 'Blue', 'Green', 'Hazel', 'Grey', 'Black', 'Unknown'],
183
- 'Employment': ['Employed', 'Unemployed', 'Self-Employed', 'Student', 'Retired', 'Unknown'],
184
- 'Education': ['None', 'Primary', 'Secondary', 'Diploma', 'Bachelor', 'Master', 'Doctorate', 'Other'],
185
- 'Marital Status': ['Single', 'Married', 'Divorced', 'Separated', 'Widowed', 'Unknown'],
186
- 'Known Habits': ['Smoking', 'Alcohol', 'Substance Use', 'Gambling', 'None', 'Unknown'],
187
- 'Occupation': ['Unskilled', 'Skilled Labour', 'Professional', 'Executive', 'Military', 'Law Enforcement', 'IT', 'Healthcare', 'Education', 'Finance', 'Other'],
188
- 'Known Financial Details': ['None', 'Low Income', 'Moderate Income', 'High Income', 'Wealthy', 'Unknown'],
189
- 'Gang Affiliation': ['None', 'Local', 'Regional', 'International', 'Unknown'],
190
- 'Criminal History': ['None', 'Minor', 'Multiple', 'Serious'],
191
- 'Prior Arrests': ['0', '1', '2', '3', '4', '5+'],
192
- 'Probation/Parole Status': ['None', 'On Probation', 'On Parole', 'Completed', 'Unknown'],
193
- 'Status': ['Draft', 'In Progress', 'Completed', 'Archived'],
194
- // Additional field options for complete coverage
195
- 'arrestCount': ['0', '1', '2', '3', '4', '5+'],
196
- 'Linked Cases': [], // Will be populated dynamically with existing case IDs
197
- 'Suspect Link': [], // Will be populated dynamically with existing suspect IDs
198
- 'Government ID': ['Aadhaar Card', 'PAN Card', 'Driving License', 'Passport', 'Voter ID', 'Other'],
199
- 'Family Connections': ['Spouse', 'Parent', 'Child', 'Sibling', 'Relative', 'Friend', 'Other'],
200
- 'Social Media Handles': [], // Text input field for multiple handles
201
- 'Version History / Updates': [] // Text area for version tracking
202
- };
203
-
204
- // File upload fields
205
- fileFields = new Set<string>([
206
- 'Photo Upload', 'Evidence Photos', 'Evidence Videos', 'Evidence Documents',
207
- 'Evidence Files', 'Upload Evidence Files', 'Digital Evidence'
208
- ]);
209
-
210
- uploadedFiles: Record<string, File[]> = {};
211
-
212
- // Section icons
213
- sectionIcons = {
214
- crime: 'fas fa-gavel',
215
- suspect: 'fas fa-user-secret',
216
- notes: 'fas fa-sticky-note'
217
- };
218
-
219
- sections: any = {
220
- crime: {
221
- title: 'Crime Details',
222
- subgroups: {
223
- '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'],
224
- 'Location & People': ['Location', 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Reported Contact', 'Witness Count', 'Victim Name', 'Victim Contact', 'Victim Summary', 'Suspected Offender Known?', 'Suspect Link'],
225
- 'Offence & Context': ['Legal Sections / Charges', 'Offence Category', 'Offence Description', 'Suspected Motive', 'Confirmed Motive', 'Weapon Involved', 'Property Loss / Damage'],
226
- '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'],
227
- 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'riskLevel', 'Confidentiality'],
228
- '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'],
229
- 'Remark': ['Remark']
230
- }
231
- },
232
- suspect: {
233
- title: 'Suspect Details',
234
- subgroups: {
235
- 'Identity': ['Suspect ID', 'Suspect Name', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'],
236
- 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color', 'Distinguishing Marks', 'Tattoo Details', 'Scar Details', 'Photo Upload'],
237
- 'Background': ['Employment', 'Education', 'Occupation', 'Company', 'Workplace Address', 'Marital Status', 'Known Habits', 'Known Financial Details'],
238
- 'Known Associates': ['Associate Names', 'Gang Affiliation', 'Family Connections', 'Social Media Handles'],
239
- 'Prior Records': ['Criminal History', 'Prior Arrests', 'Probation/Parole Status'],
240
- 'Remark': ['Remark']
241
- }
242
- },
243
- notes: {
244
- title: 'Evidence and Documents',
245
- subgroups: {
246
- 'Investigation Notes': ['Initial Findings', 'Detailed Notes', 'Status', 'Version History / Updates'],
247
- 'Evidence Files': ['Evidence Photos', 'Evidence Videos', 'Evidence Documents'],
248
- 'Links and Recommendation': ['Links to Evidence', 'Final Recommendations'],
249
- 'Remark': ['Remark']
250
- }
251
- }
252
- };
253
-
254
- // Complete field descriptions
255
- fieldDescriptions: Record<string, string> = {
256
- // Crime: Identification & Timing
257
- 'Case ID': 'Unique internal tracking identifier for this case.',
258
- 'FIR / Ref #': 'Official First Information Report or reference number.',
259
- 'Crime Type': 'Primary legal / investigative classification of the offence.',
260
- 'Case Category': 'Broader grouping used for analytics and reporting.',
261
- 'Date & Time (Entry)': 'Timestamp when the case was first registered in the system.',
262
- 'Occurred From': 'Start of the known / suspected offence time window.',
263
- 'Occurred To': 'End of the known / suspected offence time window.',
264
- 'Time Reported': 'When it was first reported to authorities.',
265
- 'Time Discovered': 'When the incident was first discovered (may differ from reported).',
266
- 'Country': 'Country where the offence occurred.',
267
- 'State': 'State / province of occurrence.',
268
- 'District': 'Administrative district of occurrence (Tamil Nadu districts supported).',
269
- 'Number of Victims': 'Total count of direct victims involved.',
270
- 'Brief Description': 'Short narrative summary for quick reference.',
271
-
272
- // Crime: Location & People
273
- 'Location': 'Exact address / geo description of the scene.',
274
- 'Jurisdiction / PS': 'Police Station or jurisdiction handling the investigation.',
275
- 'Scene Type': 'Type of environment where the offence occurred.',
276
- 'Reported By': 'Name of the reporting individual / entity.',
277
- 'Reported Contact': 'Contact details for the reporting party.',
278
- 'Witness Count': 'Number of identified witnesses so far.',
279
- 'Victim Name': 'Primary victim name (or placeholder if protected).',
280
- 'Victim Contact': 'Phone / email / other contact channel for victim.',
281
- 'Victim Summary': 'Short summary of victim condition or status.',
282
- 'Suspected Offender Known?': 'Whether victim / witnesses know the offender.',
283
- 'Suspect Link': 'Internal reference to related suspect record.',
284
-
285
- // Crime: Offence & Context
286
- 'Legal Sections / Charges': 'Applicable statutory sections / penal codes.',
287
- 'Offence Category': 'Higher level grouping (e.g., violent, cyber).',
288
- 'Offence Description': 'Detailed narrative of what occurred.',
289
- 'Suspected Motive': 'Preliminary perceived motive (subject to change).',
290
- 'Confirmed Motive': 'Validated motive after evidence review.',
291
- 'Weapon Involved': 'Weapon(s) used or suspected; choose Unknown if unclear.',
292
- 'Property Loss / Damage': 'Summary / valuation of property loss or damage.',
293
-
294
- // Crime: Evidence & Scene
295
- 'Evidence Collected': 'General list of all evidentiary items gathered.',
296
- 'Forensic Tests Required': 'Pending or requested forensic examinations.',
297
- 'Scene Condition': 'Condition of scene upon first secure entry.',
298
- 'Photos / Video?': 'Whether any media was captured.',
299
- 'CCTV Present?': 'If relevant CCTV sources exist.',
300
- 'CCTV Sources / IDs': 'Identifiers / locations for each CCTV source.',
301
- 'Physical Evidence (list)': 'Individual tangible exhibits (bagged / tagged).',
302
- 'Chain of Custody?': 'Status of formal evidence transfer logging.',
303
- 'Digital Evidence': 'Electronic sources: phones, email dumps, logs, socials.',
304
- 'Evidence Storage Reference': 'Locker / repository / digital vault reference ID.',
305
-
306
- // Crime: Operational Notes
307
- 'Investigating Officer': 'Lead officer responsible for case progress.',
308
- 'Duty Person': 'Officer / staff who received the report.',
309
- 'Supervising Officer': 'Oversight / escalation point for the case.',
310
- 'Patrol Notes': 'First responder observations / scene notes.',
311
- 'Arrest Made': 'Indicates whether an arrest has occurred.',
312
- 'Arrest Location': 'Location at which arrest was executed.',
313
- 'Initial Actions Taken': 'Immediate remedial or containment actions.',
314
- 'riskLevel': 'Risk classification influencing priority.',
315
- 'Confidentiality': 'Access / visibility level of case records.',
316
-
317
- // Crime: Status & Linkage
318
- 'Biometric / Forensic IDs': 'External forensic system identifiers (AFIS, DNA DB).',
319
- 'DNA Ref ID': 'Laboratory DNA reference identifier.',
320
- 'Fingerprint ID': 'Fingerprint database reference.',
321
- 'Case Status': 'Lifecycle status (Open / Active / Closed etc.).',
322
- 'Linked Cases': 'Related or associated case identifiers.',
323
- 'arrestCount': 'Total arrests associated with this case.',
324
- 'Case Priority': 'Operational prioritisation level.',
325
- 'Follow-up Date': 'Next scheduled investigative review date.',
326
- 'Court Case ID': 'Judicial / docket identifier once filed.',
327
- 'Next Hearing Date': 'Date of next scheduled court proceeding.',
328
- 'Final Summary': 'Closure narrative entered at completion.',
329
-
330
- // Suspect: Identity
331
- 'Suspect ID': 'Internal unique suspect identifier.',
332
- 'Suspect Name': 'Full legal or recorded name.',
333
- 'Alias / Nickname': 'Commonly used alternative names.',
334
- 'Age': 'Approximate or confirmed age.',
335
- 'Gender': 'Recorded gender descriptor.',
336
- 'Nationality': 'Country of citizenship.',
337
- 'Nationality ID / Passport Number': 'Official national ID / passport number.',
338
- 'Languages': 'Languages spoken or understood by suspect.',
339
- 'Address': 'Primary last known address.',
340
- 'Known Aliases': 'Additional identity variations.',
341
- 'Government ID': 'Government issued identification (license / ID card).',
342
-
343
- // Suspect: Physical Description
344
- 'Height (cm)': 'Height in centimetres measured or estimated.',
345
- 'Weight (kg)': 'Weight in kilograms measured or estimated.',
346
- 'Build': 'General body build classification.',
347
- 'Hair Color': 'Observed or recorded hair colour.',
348
- 'Eye Color': 'Observed or recorded eye colour.',
349
- 'Distinguishing Marks': 'Unique visible physical markers.',
350
- 'Tattoo Details': 'Location and description of tattoos.',
351
- 'Scar Details': 'Location and description of scars.',
352
- 'Photo Upload': 'Most recent or relevant facial photograph.',
353
-
354
- // Suspect: Background
355
- 'Employment': 'Current employment status.',
356
- 'Education': 'Highest completed education level.',
357
- 'Occupation': 'Primary occupation / role.',
358
- 'Company': 'Employer / organisation name.',
359
- 'Workplace Address': 'Physical address of workplace.',
360
- 'Marital Status': 'Current marital / relationship status.',
361
- 'Known Habits': 'Behavioural patterns (substances, gambling, etc.).',
362
- 'Known Financial Details': 'Financial profile relevant to investigation.',
363
-
364
- // Suspect: Known Associates
365
- 'Associate Names': 'Key associate individuals linked to suspect.',
366
- 'Gang Affiliation': 'Known gang or group membership.',
367
- 'Family Connections': 'Notable family relational links.',
368
- 'Social Media Handles': 'Identifiers used on social platforms.',
369
-
370
- // Suspect: Prior Records
371
- 'Criminal History': 'Summary of prior criminal involvement.',
372
- 'Prior Arrests': 'Number / list of previous arrests.',
373
- 'Probation/Parole Status': 'Current supervision / release status.',
374
-
375
- // Notes: Investigation Notes
376
- 'Initial Findings': 'Early observations at investigation start.',
377
- 'Detailed Notes': 'Progressive narrative & analytical details.',
378
- 'Status': 'Progress state category for notes.',
379
- 'Version History / Updates': 'Chronological changes & authorship log.',
380
-
381
- // Notes: Evidence Files
382
- 'Evidence Photos': 'Photographic evidence references.',
383
- 'Evidence Videos': 'Video evidence references.',
384
- 'Evidence Documents': 'Document / PDF evidence references.',
385
-
386
- // Notes: Links and Recommendation
387
- 'Links to Evidence': 'External or internal reference links to sources.',
388
- 'Final Recommendations': 'Closing recommendations / actions summary.'
389
- };
390
-
391
- subgroupIcons: any = {
392
- 'Identification & Timing': 'fas fa-clock',
393
- 'Location & People': 'fas fa-map-marker-alt',
394
- 'Offence & Context': 'fas fa-gavel',
395
- 'Evidence & Scene': 'fas fa-search',
396
- 'Operational Notes': 'fas fa-clipboard',
397
- 'Status & Linkage': 'fas fa-link',
398
- 'Identity': 'fas fa-id-card',
399
- 'Physical Description': 'fas fa-user',
400
- 'Background': 'fas fa-user-graduate',
401
- 'Known Associates': 'fas fa-users',
402
- 'Prior Records': 'fas fa-file-alt',
403
- 'Investigation Notes': 'fas fa-sticky-note',
404
- 'Evidence Files': 'fas fa-folder',
405
- 'Links and Recommendation': 'fas fa-link',
406
- 'Recommendations': 'fas fa-thumbs-up'
407
- };
408
-
409
- ngOnInit(): void {
410
- // Set up autosave
411
- this.autoSave$.pipe(
412
- debounceTime(2000),
413
- takeUntil(this.destroy$)
414
- ).subscribe(() => {
415
- this.performAutoSave();
416
- });
417
-
418
- // Load saved form data
419
- this.loadFormData();
420
-
421
- // Load field selections
422
- this.loadFieldSelections();
423
- }
424
-
425
- ngAfterViewInit(): void {
426
- // No special scroll handling needed anymore
427
- }
428
-
429
- ngOnDestroy(): void {
430
- this.destroy$.next();
431
- this.destroy$.complete();
432
- }
433
-
434
- // Progress Calculation
435
- get progressPercentage(): number {
436
- const totalFields = this.getAllFields().length;
437
- const completedFields = this.completedFields.size;
438
- return totalFields > 0 ? Math.round((completedFields / totalFields) * 100) : 0;
439
- }
440
-
441
- private getAllFields(): string[] {
442
- let allFields: string[] = [];
443
- for (const section of this.sectionKeys) {
444
- for (const subgroup of Object.keys(this.sections[section].subgroups)) {
445
- allFields = allFields.concat(this.sections[section].subgroups[subgroup]);
446
- }
447
- }
448
- return allFields;
449
- }
450
-
451
- // Helper method to check if we're in Identification & Timing
452
- private isIdentificationAndTimingPage(): boolean {
453
- return this.currentSubgroup === 'Identification & Timing';
454
- }
455
-
456
- // Helper method to check if we're in Location & People
457
- private isLocationAndPeoplePage(): boolean {
458
- return this.currentSubgroup === 'Location & People';
459
- }
460
-
461
- // Helper method to check if we need compact layout (applies to all pages now)
462
- private needsCompactLayout(): boolean {
463
- return true; // Apply compact layout to all pages to prevent main page scroll
464
- }
465
-
466
- // Card Management - Updated for single card layout across ALL pages
467
- showSecondaryCard(): boolean {
468
- return false; // No secondary card for any page - single card layout for all
469
- }
470
-
471
- showTertiaryCard(): boolean {
472
- return false; // No tertiary card for any page - single card layout for all
473
- }
474
-
475
- getPrimaryFields(): string[] {
476
- // Return selected fields for display instead of all fields
477
- return this.getSelectedFieldsForDisplay();
478
- }
479
-
480
- getSecondaryFields(): string[] {
481
- // Return empty array for single card layout across all pages
482
- return [];
483
- }
484
-
485
- getTertiaryFields(): string[] {
486
- // Return empty array for single card layout across all pages
487
- return [];
488
- }
489
-
490
- private getCurrentFields(): string[] {
491
- return this.sections[this.currentSection].subgroups[this.currentSubgroup] || [];
492
- }
493
-
494
- toggleCardMinimize(card: 'primary' | 'secondary' | 'tertiary'): void {
495
- this.isCardMinimized[card] = !this.isCardMinimized[card];
496
- }
497
-
498
- // Field Management
499
- isFieldRequired(field: string): boolean {
500
- return this.requiredFields.has(field);
501
- }
502
-
503
- isCompactField(field: string): boolean {
504
- return this.compactFields.has(field);
505
- }
506
-
507
- getInputType(field: string): string {
508
- if (this.numericFields.has(field)) return 'number';
509
- if (this.dateTimeFields.has(field)) return 'datetime-local';
510
- if (this.dateFields.has(field)) return 'date';
511
- if (field.toLowerCase().includes('email')) return 'email';
512
- if (field.toLowerCase().includes('phone') || field.toLowerCase().includes('contact')) return 'tel';
513
- if (field.toLowerCase().includes('url') || field.toLowerCase().includes('link')) return 'url';
514
- if (field.toLowerCase().includes('description')) return 'textarea';
515
- return 'text';
516
- }
517
-
518
- getFieldPlaceholder(field: string): string {
519
- if (field === 'Age') return 'Enter age (18-99)';
520
- if (field === 'Height (cm)') return 'Height in cm';
521
- if (field === 'Weight (kg)') return 'Weight in kg';
522
- if (this.dateTimeFields.has(field)) return 'dd-mm-yyyy --:--';
523
- if (this.dateFields.has(field)) return 'dd-mm-yyyy';
524
- if (field.toLowerCase().includes('email')) return 'Enter email address';
525
- if (field.toLowerCase().includes('phone')) return 'Enter phone number';
526
- if (field.toLowerCase().includes('description')) return 'Enter detailed description...';
527
- return `Enter ${field.toLowerCase()}`;
528
- }
529
-
530
- getMaxLength(field: string): number {
531
- if (field === 'Age') return 2;
532
- if (field === 'Gender') return 10;
533
- if (field === 'Height (cm)') return 3;
534
- if (field === 'Weight (kg)') return 3;
535
- if (this.compactFields.has(field)) return 20;
536
- return 500;
537
- }
538
-
539
- // Validation
540
- validateField(field: string): void {
541
- const value = this.formData[field];
542
- let hasError = false;
543
- let isValid = false;
544
- let message = '';
545
-
546
- if (this.isFieldRequired(field) && (!value || value.toString().trim() === '')) {
547
- hasError = true;
548
- message = `${field} is required`;
549
- } else if (value && value.toString().trim() !== '') {
550
- // Field-specific validation
551
- if (field === 'Age') {
552
- const age = parseInt(value);
553
- if (isNaN(age) || age < 1 || age > 120) {
554
- hasError = true;
555
- message = 'Age must be a valid number between 1 and 120';
556
- } else {
557
- isValid = true;
558
- }
559
- } else if (field === 'Email') {
560
- // Basic email pattern; improve with regex if needed
561
- const emailPattern = /\S+@\S+\.\S+/;
562
- isValid = emailPattern.test(value);
563
- if (!isValid) {
564
- hasError = true;
565
- message = 'Invalid email address format';
566
- }
567
- } else {
568
- // Generic validation for other fields (extend as needed)
569
- isValid = true;
570
- }
571
- }
572
-
573
- // Update field validation state
574
- this.fieldValidation[field] = { hasError, isValid, message };
575
-
576
- // Update overall form completion status
577
- this.updateCompletionStatus();
578
- }
579
-
580
- private updateCompletionStatus(): void {
581
- this.completedFields.clear();
582
-
583
- for (const field of Object.keys(this.formData)) {
584
- if (this.formData[field] !== null && this.formData[field] !== undefined && this.formData[field] !== '') {
585
- this.completedFields.add(field);
586
- }
587
- }
588
-
589
- // Update completed subgroups and sections
590
- this.updateCompletedGroupsAndSections();
591
- }
592
-
593
- private updateCompletedGroupsAndSections(): void {
594
- this.completedSubgroups.clear();
595
- this.completedSections.clear();
596
-
597
- for (const section of this.sectionKeys) {
598
- const subgroups = Object.keys(this.sections[section].subgroups);
599
- for (const subgroup of subgroups) {
600
- const fields = this.sections[section].subgroups[subgroup];
601
- const allFieldsCompleted = fields.every((field: string) => this.completedFields.has(field));
602
-
603
- if (allFieldsCompleted) {
604
- this.completedSubgroups.add(subgroup);
605
- }
606
- }
607
-
608
- if (this.completedSubgroups.size === subgroups.length) {
609
- this.completedSections.add(section);
610
- }
611
- }
612
- }
613
-
614
- // Field selection functionality - Updated to allow all fields by default
615
- selectedFields: Record<string, string[]> = {}; // Store selected fields per subgroup
616
- showFieldSelector: string | null = null; // Track which field selector is open
617
- readonly maxSelectableFields = 50; // Increased limit to allow more fields
618
-
619
- // Get all available fields for current subgroup
620
- getAvailableFields(): string[] {
621
- return this.getCurrentFields();
622
- }
623
-
624
- // Get total available fields count dynamically
625
- getTotalAvailableFieldsCount(): number {
626
- return this.getAvailableFields().length;
627
- }
628
-
629
- // Get currently selected fields for display (default to ALL fields if none selected)
630
- getSelectedFieldsForDisplay(): string[] {
631
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
632
- if (this.selectedFields[subgroupKey] && this.selectedFields[subgroupKey].length >= 0) {
633
- return this.selectedFields[subgroupKey];
634
- }
635
- // Default to ALL fields if no selection made
636
- return this.getCurrentFields();
637
- }
638
-
639
- // Toggle field selection with enhanced debugging
640
- toggleFieldSelection(field: string, event?: Event): void {
641
- if (event) {
642
- event.preventDefault();
643
- event.stopPropagation();
644
- }
645
-
646
- console.log('Toggling field selection for:', field);
647
-
648
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
649
- if (!this.selectedFields[subgroupKey]) {
650
- // Initialize with all fields selected by default
651
- this.selectedFields[subgroupKey] = [...this.getCurrentFields()];
652
- }
653
-
654
- const currentSelection = this.selectedFields[subgroupKey];
655
- const fieldIndex = currentSelection.indexOf(field);
656
-
657
- console.log('Current selection:', currentSelection);
658
- console.log('Field index:', fieldIndex);
659
-
660
- if (fieldIndex > -1) {
661
- // Remove field if already selected
662
- currentSelection.splice(fieldIndex, 1);
663
- console.log('Removed field:', field);
664
- } else {
665
- // Add field if not selected and under limit
666
- if (currentSelection.length < this.maxSelectableFields) {
667
- currentSelection.push(field);
668
- console.log('Added field:', field);
669
- } else {
670
- console.log('Selection limit reached, cannot add:', field);
671
- }
672
- }
673
-
674
- // Trigger change detection
675
- this.selectedFields = { ...this.selectedFields };
676
- this.saveFieldSelections();
677
- // Do NOT close the popup here; just update selection and save
678
- }
679
-
680
- // Check if field is currently selected
681
- isFieldSelected(field: string): boolean {
682
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
683
- const selections = this.selectedFields[subgroupKey];
684
- if (!selections) {
685
- // If no selections made, all fields are selected by default
686
- return true;
687
- }
688
- return selections.includes(field);
689
- }
690
-
691
- // Get count of selected fields
692
- getSelectedFieldCount(): number {
693
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
694
- const selections = this.selectedFields[subgroupKey];
695
- if (!selections) {
696
- return this.getCurrentFields().length; // All fields selected by default
697
- }
698
- return selections.length;
699
- }
700
-
701
- // Check if selection limit is reached
702
- isSelectionLimitReached(): boolean {
703
- return this.getSelectedFieldCount() >= this.maxSelectableFields;
704
- }
705
-
706
- // Reset field selection for current subgroup (clear all)
707
- resetFieldSelection(event?: Event): void {
708
- if (event) {
709
- event.preventDefault();
710
- event.stopPropagation();
711
- }
712
-
713
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
714
- this.selectedFields[subgroupKey] = [];
715
- this.selectedFields = { ...this.selectedFields };
716
- this.saveFieldSelections();
717
- // Do NOT close the popup here; keep it open for further selection
718
- }
719
-
720
- // Select all fields
721
- selectAllFields(event?: Event): void {
722
- if (event) {
723
- event.preventDefault();
724
- event.stopPropagation();
725
- }
726
-
727
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
728
- const allFields = this.getCurrentFields();
729
- this.selectedFields[subgroupKey] = [...allFields];
730
- this.selectedFields = { ...this.selectedFields };
731
- this.saveFieldSelections();
732
- }
733
-
734
- // Select default fields (first 10 or all if less than 10) - Renamed for clarity
735
- selectDefaultFields(event?: Event): void {
736
- if (event) {
737
- event.preventDefault();
738
- event.stopPropagation();
739
- }
740
-
741
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
742
- const allFields = this.getCurrentFields();
743
- this.selectedFields[subgroupKey] = allFields.slice(0, Math.min(10, allFields.length));
744
- this.selectedFields = { ...this.selectedFields };
745
- this.saveFieldSelections();
746
- }
747
-
748
- // Get dynamic max selectable based on available fields
749
- getDynamicMaxSelectable(): number {
750
- const totalFields = this.getTotalAvailableFieldsCount();
751
- return Math.min(this.maxSelectableFields, totalFields);
752
- }
753
-
754
- // Check if all fields are selected
755
- areAllFieldsSelected(): boolean {
756
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
757
- const selections = this.selectedFields[subgroupKey];
758
- const totalFields = this.getCurrentFields().length;
759
-
760
- if (!selections) {
761
- return true; // All fields selected by default
762
- }
763
-
764
- return selections.length === totalFields;
765
- }
766
-
767
- // Check if no fields are selected
768
- areNoFieldsSelected(): boolean {
769
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
770
- const selections = this.selectedFields[subgroupKey];
771
-
772
- if (!selections) {
773
- return false; // All fields selected by default
774
- }
775
-
776
- return selections.length === 0;
777
- }
778
-
779
- // Navigation and section management methods
780
- getSubgroups(): string[] {
781
- return Object.keys(this.sections[this.currentSection].subgroups);
782
- }
783
-
784
- showSection(section: 'crime' | 'suspect' | 'notes'): void {
785
- // Close field selector when changing sections
786
- this.closeFieldSelector();
787
- this.currentSection = section;
788
- this.currentSubgroup = Object.keys(this.sections[this.currentSection].subgroups)[0];
789
- this.showHelpFor = null;
790
- this.triggerAutoSave();
791
- }
792
-
793
- // Enhanced document click handler
794
- @HostListener('document:click', ['$event'])
795
- handleDoc(event: Event): void {
796
- this.showHelpFor = null;
797
- // Only close field selector if click is outside the popup
798
- const target = event.target as HTMLElement;
799
- if (this.showFieldSelector && !target.closest('.field-selector-container')) {
800
- this.closeFieldSelector();
801
- }
802
- }
803
-
804
- // Handle section/subgroup changes to refresh field selector
805
- setSubgroup(key: string): void {
806
- // Close field selector when changing subgroups
807
- this.closeFieldSelector();
808
- this.currentSubgroup = key;
809
- this.showHelpFor = null;
810
- this.triggerAutoSave();
811
- }
812
-
813
- // Section and subgroup completion status
814
- isSectionCompleted(section: string): boolean {
815
- return this.completedSections.has(section);
816
- }
817
-
818
- isSubgroupCompleted(subgroup: string): boolean {
819
- return this.completedSubgroups.has(subgroup);
820
- }
821
-
822
- // Section descriptions
823
- getSectionDescription(section: string): string {
824
- const descriptions = {
825
- crime: 'Capture complete crime intelligence: timing, location, evidence, and operational context. Use dropdown values for consistency and upload supporting materials.',
826
- suspect: 'Document comprehensive suspect profile: identity, physical characteristics, background, associations, and criminal history. Include recent photographs where available.',
827
- notes: 'Maintain detailed investigative records: findings, evidence files, reference materials, and final recommendations with proper version control.'
828
- };
829
- return descriptions[section as keyof typeof descriptions] || '';
830
- }
831
-
832
- // Field handling and validation
833
- onFieldChange(field: string): void {
834
- this.validateField(field);
835
- this.triggerAutoSave();
836
- }
837
-
838
- // Auto-save functionality
839
- private triggerAutoSave(): void {
840
- this.autoSave$.next();
841
- }
842
-
843
- private performAutoSave(): void {
844
- this.isAutoSaving = true;
845
- this.autoSaveStatus = 'Saving...';
846
-
847
- // Save to localStorage
848
- this.saveFormData();
849
-
850
- // Simulate save delay
851
- setTimeout(() => {
852
- this.isAutoSaving = false;
853
- this.autoSaveStatus = 'Saved';
854
-
855
- // Reset status after a moment
856
- setTimeout(() => {
857
- this.autoSaveStatus = 'Auto-save enabled';
858
- }, 2000);
859
- }, 500);
860
- }
861
-
862
- private saveFormData(): void {
863
- const saveData = {
864
- formData: this.formData,
865
- completedFields: Array.from(this.completedFields),
866
- completedSubgroups: Array.from(this.completedSubgroups),
867
- completedSections: Array.from(this.completedSections),
868
- currentSection: this.currentSection,
869
- currentSubgroup: this.currentSubgroup
870
- };
871
- localStorage.setItem('pydetect-form-data', JSON.stringify(saveData));
872
- }
873
-
874
- private loadFormData(): void {
875
- const savedData = localStorage.getItem('pydetect-form-data');
876
- if (savedData) {
877
- const data = JSON.parse(savedData);
878
- this.formData = data.formData || {};
879
- this.completedFields = new Set(data.completedFields || []);
880
- this.completedSubgroups = new Set(data.completedSubgroups || []);
881
- this.completedSections = new Set(data.completedSections || []);
882
- this.currentSection = data.currentSection || 'crime';
883
- this.currentSubgroup = data.currentSubgroup || 'Identification & Timing';
884
- }
885
- }
886
-
887
- // Dropdown options and cascading logic
888
- getOptions(field: string): string[] | undefined {
889
- if (field === 'Country') return this.countries;
890
- if (field === 'State') return (this.selectedValues['Country'] === 'India' || !this.selectedValues['Country']) ? this.indiaStates : [];
891
- if (field === 'District') {
892
- if (this.selectedValues['State'] === 'Tamil Nadu') {
893
- return this.tamilNaduDistricts;
894
- } else if (this.selectedValues['State']) {
895
- return [];
896
- } else {
897
- return [];
898
- }
899
- }
900
- return this.selectOptions[field];
901
- }
902
-
903
- onSelectChange(field: string, event: Event): void {
904
- const value = (event.target as HTMLSelectElement).value;
905
- this.selectedValues[field] = value;
906
- this.formData[field] = value;
907
-
908
- // Clear dependent fields
909
- if (field === 'Country') {
910
- delete this.selectedValues['State'];
911
- delete this.selectedValues['District'];
912
- delete this.formData['State'];
913
- delete this.formData['District'];
914
- }
915
- if (field === 'State') {
916
- delete this.selectedValues['District'];
917
- delete this.formData['District'];
918
- }
919
-
920
- this.validateField(field);
921
- this.triggerAutoSave();
922
- }
923
-
924
- // Field help functionality
925
- toggleFieldInfo(field: string, ev: MouseEvent): void {
926
- ev.stopPropagation();
927
- this.showHelpFor = this.showHelpFor === field ? null : field;
928
- }
929
-
930
- closeFieldInfo(): void {
931
- this.showHelpFor = null;
932
- }
933
-
934
- // File upload functionality
935
- onFileChange(field: string, event: Event): void {
936
- const input = event.target as HTMLInputElement;
937
- const files = input.files ? Array.from(input.files) : [];
938
- if (files.length) {
939
- this.uploadedFiles[field] = (this.uploadedFiles[field] || []).concat(files);
940
- this.validateField(field);
941
- this.triggerAutoSave();
942
- }
943
- }
944
-
945
- onDragOver(event: DragEvent): void {
946
- event.preventDefault();
947
- this.isDragOver = true;
948
- }
949
-
950
- onDragLeave(event: DragEvent): void {
951
- event.preventDefault();
952
- this.isDragOver = false;
953
- }
954
-
955
- onFileDrop(field: string, event: DragEvent): void {
956
- event.preventDefault();
957
- this.isDragOver = false;
958
-
959
- const files = event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : [];
960
- if (files.length) {
961
- this.uploadedFiles[field] = (this.uploadedFiles[field] || []).concat(files);
962
- this.validateField(field);
963
- this.triggerAutoSave();
964
- }
965
- }
966
-
967
- removeFile(field: string, file: File): void {
968
- if (this.uploadedFiles[field]) {
969
- this.uploadedFiles[field] = this.uploadedFiles[field].filter(f => f !== file);
970
- this.triggerAutoSave();
971
- }
972
- }
973
-
974
- getAcceptedFileTypes(field: string): string {
975
- return this.fileTypeConfig[field] || '*';
976
- }
977
-
978
- getFileIcon(filename: string): string {
979
- const ext = filename.split('.').pop()?.toLowerCase();
980
- switch (ext) {
981
- case 'pdf': return 'fas fa-file-pdf';
982
- case 'doc':
983
- case 'docx': return 'fas fa-file-word';
984
- case 'jpg':
985
- case 'jpeg':
986
- case 'png':
987
- case 'gif': return 'fas fa-file-image';
988
- case 'mp4':
989
- case 'avi':
990
- case 'mov': return 'fas fa-file-video';
991
- default: return 'fas fa-file';
992
- }
993
- }
994
-
995
- // Navigation helper methods for floating buttons
996
- getPreviousSubgroup(): string {
997
- const list = this.getSubgroups();
998
- const currentIndex = list.indexOf(this.currentSubgroup);
999
- return currentIndex > 0 ? list[currentIndex - 1] : '';
1000
- }
1001
-
1002
- getNextSubgroup(): string {
1003
- const subgroups = this.getSubgroups();
1004
- const currentIndex = subgroups.indexOf(this.currentSubgroup);
1005
- if (currentIndex < subgroups.length - 1) {
1006
- return subgroups[currentIndex + 1];
1007
- }
1008
- // If last subgroup, return first subgroup of next section if exists
1009
- const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1010
- if (sectionIndex < this.sectionKeys.length - 1) {
1011
- const nextSection = this.sectionKeys[sectionIndex + 1];
1012
- return Object.keys(this.sections[nextSection].subgroups)[0];
1013
- }
1014
- return '';
1015
- }
1016
-
1017
- isLastSubgroup(): boolean {
1018
- const list = this.getSubgroups();
1019
- return list.indexOf(this.currentSubgroup) === list.length - 1;
1020
- }
1021
-
1022
- canNextSubgroup(): boolean {
1023
- // Enable Next on last subgroup if not notes section
1024
- if (this.isLastSubgroup()) {
1025
- return !(this.currentSection === 'notes' && this.currentSubgroup === 'Remark');
1026
- }
1027
- return true;
1028
- }
1029
-
1030
- nextSubgroup(): void {
1031
- const subgroups = this.getSubgroups();
1032
- const currentIndex = subgroups.indexOf(this.currentSubgroup);
1033
- // If not last subgroup, go to next subgroup
1034
- if (currentIndex < subgroups.length - 1) {
1035
- this.setSubgroup(subgroups[currentIndex + 1]);
1036
- return;
1037
- }
1038
- // If last subgroup, go to first subgroup of next section (if exists)
1039
- const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1040
- if (sectionIndex < this.sectionKeys.length - 1) {
1041
- const nextSection = this.sectionKeys[sectionIndex + 1];
1042
- this.currentSection = nextSection;
1043
- this.currentSubgroup = Object.keys(this.sections[nextSection].subgroups)[0];
1044
- this.showHelpFor = null;
1045
- this.triggerAutoSave();
1046
- }
1047
- }
1048
-
1049
- prevSubgroup(): void {
1050
- const subgroups = this.getSubgroups();
1051
- const currentIndex = subgroups.indexOf(this.currentSubgroup);
1052
- if (currentIndex > 0) {
1053
- this.setSubgroup(subgroups[currentIndex - 1]);
1054
- return;
1055
- }
1056
- // If first subgroup, go to last subgroup of previous section (if exists)
1057
- const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1058
- if (sectionIndex > 0) {
1059
- const prevSection = this.sectionKeys[sectionIndex - 1];
1060
- const prevSubgroups = Object.keys(this.sections[prevSection].subgroups);
1061
- this.currentSection = prevSection;
1062
- this.currentSubgroup = prevSubgroups[prevSubgroups.length - 1];
1063
- this.showHelpFor = null;
1064
- this.triggerAutoSave();
1065
- }
1066
- }
1067
-
1068
- submitCurrentSection(): void {
1069
- // Perform final validation
1070
- const currentFields = this.getCurrentFields();
1071
- const requiredFields = currentFields.filter(f => this.isFieldRequired(f));
1072
- const missingFields = requiredFields.filter(f => !this.completedFields.has(f));
1073
-
1074
- if (missingFields.length > 0) {
1075
- alert(`Please complete the following required fields: ${missingFields.join(', ')}`);
1076
- return;
1077
- }
1078
-
1079
- this.performAutoSave();
1080
-
1081
- // Map flat formData to nested structure expected by addOrUpdateFromInfoForm
1082
- const crime = {
1083
- caseId: this.formData['Case ID'] || '',
1084
- dateTime: this.formData['Date & Time (Entry)'] || '',
1085
- crimeType: this.formData['Crime Type'] || '',
1086
- location: this.formData['Location'] || '',
1087
- victimName: this.formData['Victim Name'] || '',
1088
- caseCategory: this.formData['Case Category'] || '',
1089
- reportedBy: this.formData['Reported By'] || '',
1090
- briefDescription: this.formData['Brief Description'] || '',
1091
- 'FIR / Ref #': this.formData['FIR / Ref #'] || '',
1092
- 'Occurred From': this.formData['Occurred From'] || '',
1093
- 'Occurred To': this.formData['Occurred To'] || '',
1094
- 'Jurisdiction / PS': this.formData['Jurisdiction / PS'] || '',
1095
- 'Scene Type': this.formData['Scene Type'] || ''
1096
- };
1097
- const suspect = {
1098
- fullName: this.formData['Suspect Name'] || '',
1099
- age: this.formData['Age'] || '',
1100
- gender: this.formData['Gender'] || '',
1101
- address: this.formData['Address'] || '',
1102
- alias: this.formData['Alias / Nickname'] || ''
1103
- };
1104
- const notes = {
1105
- status: this.formData['Case Status'] || this.formData['Status'] || 'Open',
1106
- officerInCharge: this.formData['Investigating Officer'] || '',
1107
- initialFindings: this.formData['Initial Findings'] || '',
1108
- verifiedBy: this.formData['Verified By'] || ''
1109
- };
1110
- const legal = {
1111
- witnessStatements: this.formData['Witness Statements'] || '',
1112
- confessions: this.formData['Confessions'] || '',
1113
- evidence: this.uploadedFiles['Evidence Files'] || []
1114
- };
1115
-
1116
- this.caseStore.addOrUpdateFromInfoForm({ crime, suspect, notes, legal });
1117
- // Show popup first
1118
- this.showSubmitPopup = true;
1119
- }
1120
-
1121
- onSubmitPopupClose(): void {
1122
- this.showSubmitPopup = false;
1123
- this.router.navigate(['/record'], { state: { formData: this.formData } });
1124
- }
1125
-
1126
- // Keyboard navigation
1127
- @HostListener('document:keydown', ['$event'])
1128
- handleKeydown(event: KeyboardEvent): void {
1129
- // Keyboard navigation
1130
- if (event.ctrlKey && event.key === 'ArrowRight') {
1131
- event.preventDefault();
1132
- this.nextSubgroup();
1133
- } else if (event.ctrlKey && event.key === 'ArrowLeft') {
1134
- event.preventDefault();
1135
- this.prevSubgroup();
1136
- } else if (event.ctrlKey && event.key === 's') {
1137
- event.preventDefault();
1138
- this.performAutoSave();
1139
- } else if (event.key === 'Escape') {
1140
- this.closeFieldInfo();
1141
- }
1142
- }
1143
-
1144
- // Toggle field selector visibility
1145
- toggleFieldSelector(event?: Event): void {
1146
- if (event) {
1147
- event.preventDefault();
1148
- event.stopPropagation();
1149
- }
1150
-
1151
- const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
1152
- this.showFieldSelector = this.showFieldSelector === subgroupKey ? null : subgroupKey;
1153
- }
1154
-
1155
- // Close field selector
1156
- closeFieldSelector(): void {
1157
- this.showFieldSelector = null;
1158
- }
1159
-
1160
- // Save field selections to localStorage
1161
- private saveFieldSelections(): void {
1162
- try {
1163
- localStorage.setItem('pydetect-field-selections', JSON.stringify(this.selectedFields));
1164
- } catch (error) {
1165
- console.warn('Could not save field selections to localStorage:', error);
1166
- }
1167
- }
1168
-
1169
- // Load field selections from localStorage
1170
- private loadFieldSelections(): void {
1171
- try {
1172
- const savedSelections = localStorage.getItem('pydetect-field-selections');
1173
- if (savedSelections) {
1174
- this.selectedFields = JSON.parse(savedSelections);
1175
- }
1176
- } catch (error) {
1177
- console.warn('Could not load field selections from localStorage:', error);
1178
- this.selectedFields = {};
1179
- }
1180
- }
1181
-
1182
- // Track by function for ngFor optimization
1183
- trackByField(index: number, field: string): string {
1184
- return field;
1185
- }
1186
-
1187
- toggleRecording() {
1188
- this.isRecording = !this.isRecording;
1189
- // Add actual recording logic here if needed
1190
- // For now, just toggles the state
1191
- }
1192
-
1193
- goToRecords(): void {
1194
- this.router.navigate(['/record']);
1195
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1196
 
1197
  }
 
5
  import { CaseStoreService } from '../shared/case-store.service';
6
 
7
  @Component({
8
+ selector: 'app-infopage',
9
+ templateUrl: './infopage.component.html',
10
+ styleUrls: ['./infopage.component.css'],
11
+ animations: [
12
+ // Simple card animation
13
+ trigger('cardSlide', [
14
+ transition(':enter', [
15
+ style({ transform: 'translateY(20px)', opacity:0 }),
16
+ animate('300ms ease-out',
17
+ style({ transform: 'translateY(0)', opacity:1 }))
18
+ ]),
19
+ transition(':leave', [
20
+ animate('300ms ease-in',
21
+ style({ transform: 'translateY(20px)', opacity:0 }))
22
+ ])
23
+ ]),
24
+ // Field animation
25
+ trigger('fieldAnimation', [
26
+ transition(':enter', [
27
+ style({ opacity:0, transform: 'translateY(10px)' }),
28
+ animate('200ms ease-out',
29
+ style({ opacity:1, transform: 'translateY(0)' }))
30
+ ]),
31
+ transition(':leave', [
32
+ animate('150ms ease-out', style({ opacity:0, transform: 'translateY(10px)' }))
33
+ ])
34
+ ]),
35
+ // Simple fade animation
36
+ trigger('fadeIn', [
37
+ transition(':enter', [
38
+ style({ opacity:0 }),
39
+ animate('200ms ease-in', style({ opacity:1 }))
40
+ ]),
41
+ transition(':leave', [
42
+ animate('150ms ease-out', style({ opacity:0 }))
43
+ ])
44
+ ]),
45
+ // Help animation
46
+ trigger('helpAnimation', [
47
+ transition(':enter', [
48
+ style({ opacity:0, transform: 'translateY(-10px)' }),
49
+ animate('200ms ease-out',
50
+ style({ opacity:1, transform: 'translateY(0)' }))
51
+ ]),
52
+ transition(':leave', [
53
+ animate('150ms ease-in',
54
+ style({ opacity:0, transform: 'translateY(-10px)' }))
55
+ ])
56
+ ])
57
+ ]
58
  })
59
  export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
60
+ showRemarkModal: boolean = false;
61
+ showSubmitPopup: boolean = false;
62
+ showMicPopup: boolean = false;
63
+ isRecording: boolean = false;
64
+ constructor(private router: Router, private caseStore: CaseStoreService) {}
65
+ // Core state
66
+ currentSection: 'crime' | 'suspect' | 'notes' = 'crime';
67
+ currentSubgroup: string = 'Identification & Timing';
68
+ showHelpFor: string | null = null;
69
+
70
+ // UI state
71
+ isAutoSaving: boolean = false;
72
+ autoSaveStatus: string = 'Saved';
73
+ isDragOver: boolean = false;
74
+ showViewRecordsTooltip: boolean = false;
75
+
76
+ // Card state
77
+ isCardMinimized = {
78
+ primary: false,
79
+ secondary: false,
80
+ tertiary: false
81
+ };
82
+
83
+ // Form data and validation
84
+ formData: Record<string, any> = {};
85
+ fieldValidation: Record<string, { hasError: boolean, isValid: boolean, message: string }> = {};
86
+ completedFields: Set<string> = new Set();
87
+ completedSubgroups: Set<string> = new Set();
88
+ completedSections: Set<string> = new Set();
89
+
90
+ // Subjects for reactive programming
91
+ private destroy$ = new Subject<void>();
92
+ private autoSave$ = new Subject<void>();
93
+
94
+ // Constants - Reverted to original values except for Identification & Timing
95
+ readonly sectionKeys: ('crime' | 'suspect' | 'notes')[] = ['crime', 'suspect', 'notes'];
96
+ readonly maxFieldsPerCard =8; // Reverted to original
97
+ readonly maxFieldsPerSecondaryCard =8; // Reverted to original
98
+ readonly maxFieldsPerCardIdentificationTiming =6; // Special for Identification & Timing
99
+ readonly maxFieldsPerSecondaryCardIdentificationTiming =6; // Special for Identification & Timing
100
+
101
+ @ViewChild('formCard1') formCard1!: ElementRef<HTMLDivElement>;
102
+ @ViewChild('formCard2') formCard2!: ElementRef<HTMLDivElement>;
103
+ @ViewChild('formCard3') formCard3!: ElementRef<HTMLDivElement>;
104
+
105
+ // Enhanced field definitions with validation rules
106
+ readonly requiredFields = new Set<string>([
107
+ 'Case ID', 'Crime Type', 'Date & Time (Entry)', 'Location', 'Suspect Name', 'Age', 'Gender',
108
+ 'FIR / Ref #', 'Case Category', 'Occurred From', 'Country', 'State', 'District',
109
+ 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Case Status', 'Investigating Officer'
110
+ ]);
111
+
112
+ readonly compactFields = new Set<string>([
113
+ 'Age', 'Gender', 'Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color',
114
+ 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count', 'Case Priority',
115
+ 'Photos / Video?', 'CCTV Present?', 'Arrest Made', 'risk Level', 'Confidentiality'
116
+ ]);
117
+
118
+ readonly numericFields = new Set<string>([
119
+ 'Age', 'Height (cm)', 'Weight (kg)', 'Number of Victims', 'Witness Count', 'Prior Arrests', 'arrest Count'
120
+ ]);
121
+
122
+ // File type configurations
123
+ readonly fileTypeConfig: Record<string, string> = {
124
+ 'Photo Upload': 'image/*',
125
+ 'Evidence Photos': 'image/*',
126
+ 'Evidence Videos': 'video/*',
127
+ 'Evidence Documents': '.pdf,.doc,.docx,.txt',
128
+ 'Evidence Files': '*',
129
+ 'Upload Evidence Files': '*',
130
+ 'Digital Evidence': '*'
131
+ };
132
+
133
+ // Track selected values (cascading dropdown logic)
134
+ selectedValues: Record<string, string> = {};
135
+
136
+ // Date field groups
137
+ dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']);
138
+ dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']);
139
+
140
+ // Country/State/District data
141
+ countries = ['India'];
142
+ indiaStates = [
143
+ 'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chhattisgarh', 'Goa', 'Gujarat',
144
+ 'Haryana', 'Himachal Pradesh', 'Jharkhand', 'Karnataka', 'Kerala', 'Madhya Pradesh',
145
+ 'Maharashtra', 'Manipur', 'Meghalaya', 'Mizoram', 'Nagaland', 'Odisha', 'Punjab',
146
+ 'Rajasthan', 'Sikkim', 'Tamil Nadu', 'Telangana', 'Tripura', 'Uttar Pradesh',
147
+ 'Uttarakhand', 'West Bengal'
148
+ ];
149
+
150
+ tamilNaduDistricts = [
151
+ 'Ariyalur', 'Chengalpattu', 'Chennai', 'Coimbatore', 'Cuddalore', 'Dharmapuri', 'Dindigul',
152
+ 'Erode', 'Kallakurichi', 'Kanchipuram', 'Kanyakumari', 'Karur', 'Krishnagiri', 'Madurai',
153
+ 'Mayiladuthurai', 'Nagapattinam', 'Namakkal', 'Nilgiris', 'Perambalur', 'Pudukkottai',
154
+ 'Ramanathapuram', 'Ranipet', 'Salem', 'Sivaganga', 'Tenkasi', 'Thanjavur', 'Theni',
155
+ 'Thoothukudi (Tuticorin)', 'Tiruchirappalli', 'Tirunelveli', 'Tirupathur', 'Tiruppur',
156
+ 'Tiruvallur', 'Tiruvannamalai', 'Tiruvarur', 'Vellore', 'Viluppuram', 'Virudhunagar'
157
+ ];
158
+
159
+ // Enhanced select options - Added missing field options
160
+ selectOptions: Record<string, string[]> = {
161
+ 'Crime Type': ['Theft', 'Assault', 'Homicide', 'Cybercrime', 'Fraud', 'Narcotics', 'Arson', 'Kidnapping', 'General', 'Other'],
162
+ 'Case Category': ['Property', 'Violent', 'Cyber', 'Financial', 'Public Order', 'Narcotics', 'Organized', 'General', 'Other'],
163
+ 'Number of Victims': ['0', '1', '2', '3', '4', '5+'],
164
+ 'Jurisdiction / PS': ['Central PS', 'East Division', 'West Division', 'Rural Unit', 'Cyber Cell', 'General'],
165
+ 'Scene Type': ['Residential', 'Commercial', 'Public Space', 'Vehicle', 'Rural', 'Online', 'General', 'Other'],
166
+ 'Witness Count': ['0', '1', '2', '3', '4', '5+'],
167
+ 'Victim Summary': ['Stable', 'Injured', 'Critical', 'Deceased', 'Unknown'],
168
+ 'Suspected Offender Known?': ['Yes', 'No', 'Unknown'],
169
+ 'Offence Category': ['Minor', 'Serious', 'Organized', 'Cyber', 'Financial', 'Violent', 'General', 'Other'],
170
+ 'Suspected Motive': ['Financial Gain', 'Revenge', 'Jealousy', 'Ideological', 'Political', 'Personal Dispute', 'Unknown', 'General', 'Other'],
171
+ 'Confirmed Motive': ['Financial Gain', 'Revenge', 'Jealousy', 'Ideological', 'Political', 'Personal Dispute', 'Unknown', 'General', 'Other'],
172
+ 'Weapon Involved': ['None', 'Knife', 'Firearm', 'Blunt Object', 'Explosive', 'Chemical', 'Other', 'Unknown', 'General'],
173
+ 'Property Loss / Damage': ['None', 'Minor', 'Moderate', 'Major', 'Severe', 'Unknown'],
174
+ 'Photos / Video?': ['Yes', 'No'],
175
+ 'CCTV Present?': ['Yes', 'No'],
176
+ 'Scene Condition': ['Intact', 'Disturbed', 'Contaminated', 'Secured', 'Compromised', 'General'],
177
+ 'Chain of Custody?': ['Initiated', 'Ongoing', 'Complete', 'Not Started'],
178
+ 'Forensic Tests Required': ['None', 'DNA', 'Fingerprints', 'Ballistics', 'Toxicology', 'Digital Forensics', 'Trace', 'General', 'Other'],
179
+ 'Arrest Made': ['Yes', 'No'],
180
+ 'riskLevel': ['Low', 'Medium', 'High', 'Critical'],
181
+ 'Confidentiality': ['Internal', 'Restricted', 'Sensitive', 'Sealed'],
182
+ 'Initial Actions Taken': ['Scene Secured', 'Medical Aid', 'Evidence Logged', 'Witness Statements', 'Suspect Detained', 'General', 'Other'],
183
+ 'Case Status': ['Open', 'Active', 'Suspended', 'Closed', 'Archived'],
184
+ 'Case Priority': ['Low', 'Normal', 'High', 'Urgent', 'Critical'],
185
+ 'Gender': ['Male', 'Female', 'Other'],
186
+ 'Nationality': ['India'],
187
+ 'Languages': ['English', 'Hindi', 'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Bengali', 'Marathi', 'Gujarati', 'Other'],
188
+ 'Build': ['Slim', 'Average', 'Athletic', 'Heavy', 'Obese'],
189
+ 'Hair Color': ['Black', 'Brown', 'Blonde', 'Red', 'Grey', 'White', 'Dyed / Other', 'Unknown'],
190
+ 'Eye Color': ['Brown', 'Blue', 'Green', 'Hazel', 'Grey', 'Black', 'Unknown'],
191
+ 'Employment': ['Employed', 'Unemployed', 'Self-Employed', 'Student', 'Retired', 'Unknown'],
192
+ 'Education': ['None', 'Primary', 'Secondary', 'Diploma', 'Bachelor', 'Master', 'Doctorate', 'Other'],
193
+ 'Marital Status': ['Single', 'Married', 'Divorced', 'Separated', 'Widowed', 'Unknown'],
194
+ 'Known Habits': ['Smoking', 'Alcohol', 'Substance Use', 'Gambling', 'None', 'Unknown'],
195
+ 'Occupation': ['Unskilled', 'Skilled Labour', 'Professional', 'Executive', 'Military', 'Law Enforcement', 'IT', 'Healthcare', 'Education', 'Finance', 'Other'],
196
+ 'Known Financial Details': ['None', 'Low Income', 'Moderate Income', 'High Income', 'Wealthy', 'Unknown'],
197
+ 'Gang Affiliation': ['None', 'Local', 'Regional', 'International', 'Unknown'],
198
+ 'Criminal History': ['None', 'Minor', 'Multiple', 'Serious'],
199
+ 'Prior Arrests': ['0', '1', '2', '3', '4', '5+'],
200
+ 'Probation/Parole Status': ['None', 'On Probation', 'On Parole', 'Completed', 'Unknown'],
201
+ 'Status': ['Draft', 'In Progress', 'Completed', 'Archived'],
202
+ // Additional field options for complete coverage
203
+ 'arrestCount': ['0', '1', '2', '3', '4', '5+'],
204
+ 'Linked Cases': [], // Will be populated dynamically with existing case IDs
205
+ 'Suspect Link': [], // Will be populated dynamically with existing suspect IDs
206
+ 'Government ID': ['Aadhaar Card', 'PAN Card', 'Driving License', 'Passport', 'Voter ID', 'Other'],
207
+ 'Family Connections': ['Spouse', 'Parent', 'Child', 'Sibling', 'Relative', 'Friend', 'Other'],
208
+ 'Social Media Handles': [], // Text input field for multiple handles
209
+ 'Version History / Updates': [] // Text area for version tracking
210
+ };
211
+
212
+ // File upload fields
213
+ fileFields = new Set<string>([
214
+ 'Photo Upload', 'Evidence Photos', 'Evidence Videos', 'Evidence Documents',
215
+ 'Evidence Files', 'Upload Evidence Files', 'Digital Evidence'
216
+ ]);
217
+
218
+ uploadedFiles: Record<string, File[]> = {};
219
+
220
+ // Section icons
221
+ sectionIcons = {
222
+ crime: 'fas fa-gavel',
223
+ suspect: 'fas fa-user-secret',
224
+ notes: 'fas fa-sticky-note'
225
+ };
226
+
227
+ sections: any = {
228
+ crime: {
229
+ title: 'Crime Details',
230
+ subgroups: {
231
+ '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'],
232
+ 'Location & People': ['Location', 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Reported Contact', 'Witness Count', 'Victim Name', 'Victim Contact', 'Victim Summary', 'Suspected Offender Known?', 'Suspect Link'],
233
+ 'Offence & Context': ['Legal Sections / Charges', 'Offence Category', 'Offence Description', 'Suspected Motive', 'Confirmed Motive', 'Weapon Involved', 'Property Loss / Damage'],
234
+ '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'],
235
+ 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'riskLevel', 'Confidentiality'],
236
+ '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'],
237
+ 'Remark': ['Remark']
238
+ }
239
+ },
240
+ suspect: {
241
+ title: 'Suspect Details',
242
+ subgroups: {
243
+ 'Identity': ['Suspect ID', 'Suspect Name', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'],
244
+ 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Tattoo Details', 'Hair Color', 'Scar Details', 'Distinguishing Marks', 'Build', 'Eye Color', 'Photo Upload'],
245
+ 'Background': ['Employment', 'Education', 'Occupation', 'Company', 'Workplace Address', 'Marital Status', 'Known Habits', 'Known Financial Details'],
246
+ 'Known Associates': ['Associate Names', 'Gang Affiliation', 'Family Connections', 'Social Media Handles'],
247
+ 'Prior Records': ['Criminal History', 'Prior Arrests', 'Probation/Parole Status'],
248
+ 'Remark': ['Remark']
249
+ }
250
+ },
251
+ notes: {
252
+ title: 'Evidence and Documents',
253
+ subgroups: {
254
+ 'Investigation Notes': ['Initial Findings', 'Detailed Notes', 'Status', 'Version History / Updates'],
255
+ 'Evidence Files': ['Evidence Photos', 'Evidence Videos', 'Evidence Documents'],
256
+ 'Links and Recommendation': ['Links to Evidence', 'Final Recommendations'],
257
+ 'Remark': ['Remark']
258
+ }
259
+ }
260
+ };
261
+
262
+ // Complete field descriptions
263
+ fieldDescriptions: Record<string, string> = {
264
+ // Crime: Identification & Timing
265
+ 'Case ID': 'Unique internal tracking identifier for this case.',
266
+ 'FIR / Ref #': 'Official First Information Report or reference number.',
267
+ 'Crime Type': 'Primary legal / investigative classification of the offence.',
268
+ 'Case Category': 'Broader grouping used for analytics and reporting.',
269
+ 'Date & Time (Entry)': 'Timestamp when the case was first registered in the system.',
270
+ 'Occurred From': 'Start of the known / suspected offence time window.',
271
+ 'Occurred To': 'End of the known / suspected offence time window.',
272
+ 'Time Reported': 'When it was first reported to authorities.',
273
+ 'Time Discovered': 'When the incident was first discovered (may differ from reported).',
274
+ 'Country': 'Country where the offence occurred.',
275
+ 'State': 'State / province of occurrence.',
276
+ 'District': 'Administrative district of occurrence (Tamil Nadu districts supported).',
277
+ 'Number of Victims': 'Total count of direct victims involved.',
278
+ 'Brief Description': 'Short narrative summary for quick reference.',
279
+
280
+ // Crime: Location & People
281
+ 'Location': 'Exact address / geo description of the scene.',
282
+ 'Jurisdiction / PS': 'Police Station or jurisdiction handling the investigation.',
283
+ 'Scene Type': 'Type of environment where the offence occurred.',
284
+ 'Reported By': 'Name of the reporting individual / entity.',
285
+ 'Reported Contact': 'Contact details for the reporting party.',
286
+ 'Witness Count': 'Number of identified witnesses so far.',
287
+ 'Victim Name': 'Primary victim name (or placeholder if protected).',
288
+ 'Victim Contact': 'Phone / email / other contact channel for victim.',
289
+ 'Victim Summary': 'Short summary of victim condition or status.',
290
+ 'Suspected Offender Known?': 'Whether victim / witnesses know the offender.',
291
+ 'Suspect Link': 'Internal reference to related suspect record.',
292
+
293
+ // Crime: Offence & Context
294
+ 'Legal Sections / Charges': 'Applicable statutory sections / penal codes.',
295
+ 'Offence Category': 'Higher level grouping (e.g., violent, cyber).',
296
+ 'Offence Description': 'Detailed narrative of what occurred.',
297
+ 'Suspected Motive': 'Preliminary perceived motive (subject to change).',
298
+ 'Confirmed Motive': 'Validated motive after evidence review.',
299
+ 'Weapon Involved': 'Weapon(s) used or suspected; choose Unknown if unclear.',
300
+ 'Property Loss / Damage': 'Summary / valuation of property loss or damage.',
301
+
302
+ // Crime: Evidence & Scene
303
+ 'Evidence Collected': 'General list of all evidentiary items gathered.',
304
+ 'Forensic Tests Required': 'Pending or requested forensic examinations.',
305
+ 'Scene Condition': 'Condition of scene upon first secure entry.',
306
+ 'Photos / Video?': 'Whether any media was captured.',
307
+ 'CCTV Present?': 'If relevant CCTV sources exist.',
308
+ 'CCTV Sources / IDs': 'Identifiers / locations for each CCTV source.',
309
+ 'Physical Evidence (list)': 'Individual tangible exhibits (bagged / tagged).',
310
+ 'Chain of Custody?': 'Status of formal evidence transfer logging.',
311
+ 'Digital Evidence': 'Electronic sources: phones, email dumps, logs, socials.',
312
+ 'Evidence Storage Reference': 'Locker / repository / digital vault reference ID.',
313
+
314
+ // Crime: Operational Notes
315
+ 'Investigating Officer': 'Lead officer responsible for case progress.',
316
+ 'Duty Person': 'Officer / staff who received the report.',
317
+ 'Supervising Officer': 'Oversight / escalation point for the case.',
318
+ 'Patrol Notes': 'First responder observations / scene notes.',
319
+ 'Arrest Made': 'Indicates whether an arrest has occurred.',
320
+ 'Arrest Location': 'Location at which arrest was executed.',
321
+ 'Initial Actions Taken': 'Immediate remedial or containment actions.',
322
+ 'riskLevel': 'Risk classification influencing priority.',
323
+ 'Confidentiality': 'Access / visibility level of case records.',
324
+
325
+ // Crime: Status & Linkage
326
+ 'Biometric / Forensic IDs': 'External forensic system identifiers (AFIS, DNA DB).',
327
+ 'DNA Ref ID': 'Laboratory DNA reference identifier.',
328
+ 'Fingerprint ID': 'Fingerprint database reference.',
329
+ 'Case Status': 'Lifecycle status (Open / Active / Closed etc.).',
330
+ 'Linked Cases': 'Related or associated case identifiers.',
331
+ 'arrestCount': 'Total arrests associated with this case.',
332
+ 'Case Priority': 'Operational prioritisation level.',
333
+ 'Follow-up Date': 'Next scheduled investigative review date.',
334
+ 'Court Case ID': 'Judicial / docket identifier once filed.',
335
+ 'Next Hearing Date': 'Date of next scheduled court proceeding.',
336
+ 'Final Summary': 'Closure narrative entered at completion.',
337
+
338
+ // Suspect: Identity
339
+ 'Suspect ID': 'Internal unique suspect identifier.',
340
+ 'Suspect Name': 'Full legal or recorded name.',
341
+ 'Alias / Nickname': 'Commonly used alternative names.',
342
+ 'Age': 'Approximate or confirmed age.',
343
+ 'Gender': 'Recorded gender descriptor.',
344
+ 'Nationality': 'Country of citizenship.',
345
+ 'Nationality ID / Passport Number': 'Official national ID / passport number.',
346
+ 'Languages': 'Languages spoken or understood by suspect.',
347
+ 'Address': 'Primary last known address.',
348
+ 'Known Aliases': 'Additional identity variations.',
349
+ 'Government ID': 'Government issued identification (license / ID card).',
350
+
351
+ // Suspect: Physical Description
352
+ 'Height (cm)': 'Height in centimetres measured or estimated.',
353
+ 'Weight (kg)': 'Weight in kilograms measured or estimated.',
354
+ 'Build': 'General body build classification.',
355
+ 'Hair Color': 'Observed or recorded hair colour.',
356
+ 'Eye Color': 'Observed or recorded eye colour.',
357
+ 'Distinguishing Marks': 'Unique visible physical markers.',
358
+ 'Tattoo Details': 'Location and description of tattoos.',
359
+ 'Scar Details': 'Location and description of scars.',
360
+ 'Photo Upload': 'Most recent or relevant facial photograph.',
361
+
362
+ // Suspect: Background
363
+ 'Employment': 'Current employment status.',
364
+ 'Education': 'Highest completed education level.',
365
+ 'Occupation': 'Primary occupation / role.',
366
+ 'Company': 'Employer / organisation name.',
367
+ 'Workplace Address': 'Physical address of workplace.',
368
+ 'Marital Status': 'Current marital / relationship status.',
369
+ 'Known Habits': 'Behavioural patterns (substances, gambling, etc.).',
370
+ 'Known Financial Details': 'Financial profile relevant to investigation.',
371
+
372
+ // Suspect: Known Associates
373
+ 'Associate Names': 'Key associate individuals linked to suspect.',
374
+ 'Gang Affiliation': 'Known gang or group membership.',
375
+ 'Family Connections': 'Notable family relational links.',
376
+ 'Social Media Handles': 'Identifiers used on social platforms.',
377
+
378
+ // Suspect: Prior Records
379
+ 'Criminal History': 'Summary of prior criminal involvement.',
380
+ 'Prior Arrests': 'Number / list of previous arrests.',
381
+ 'Probation/Parole Status': 'Current supervision / release status.',
382
+
383
+ // Notes: Investigation Notes
384
+ 'Initial Findings': 'Early observations at investigation start.',
385
+ 'Detailed Notes': 'Progressive narrative & analytical details.',
386
+ 'Status': 'Progress state category for notes.',
387
+ 'Version History / Updates': 'Chronological changes & authorship log.',
388
+
389
+ // Notes: Evidence Files
390
+ 'Evidence Photos': 'Photographic evidence references.',
391
+ 'Evidence Videos': 'Video evidence references.',
392
+ 'Evidence Documents': 'Document / PDF evidence references.',
393
+
394
+ // Notes: Links and Recommendation
395
+ 'Links to Evidence': 'External or internal reference links to sources.',
396
+ 'Final Recommendations': 'Closing recommendations / actions summary.'
397
+ };
398
+
399
+ subgroupIcons: any = {
400
+ 'Identification & Timing': 'fas fa-clock',
401
+ 'Location & People': 'fas fa-map-marker-alt',
402
+ 'Offence & Context': 'fas fa-gavel',
403
+ 'Evidence & Scene': 'fas fa-search',
404
+ 'Operational Notes': 'fas fa-clipboard',
405
+ 'Status & Linkage': 'fas fa-link',
406
+ 'Identity': 'fas fa-id-card',
407
+ 'Physical Description': 'fas fa-user',
408
+ 'Background': 'fas fa-user-graduate',
409
+ 'Known Associates': 'fas fa-users',
410
+ 'Prior Records': 'fas fa-file-alt',
411
+ 'Investigation Notes': 'fas fa-sticky-note',
412
+ 'Evidence Files': 'fas fa-folder',
413
+ 'Links and Recommendation': 'fas fa-link',
414
+ 'Recommendations': 'fas fa-thumbs-up'
415
+ };
416
+
417
+ ngOnInit(): void {
418
+ // Set up autosave
419
+ this.autoSave$.pipe(
420
+ debounceTime(2000),
421
+ takeUntil(this.destroy$)
422
+ ).subscribe(() => {
423
+ this.performAutoSave();
424
+ });
425
+
426
+ // Do NOT auto-load saved form data on page load/refresh to keep the form empty by default.
427
+ // Load only field selections (UI prefs)
428
+ this.loadFieldSelections();
429
+
430
+ // If navigation state contains prefillFormData (when editing a case), merge it into formData.
431
+ try {
432
+ const navState = (history && (history as any).state) || null;
433
+ const statePrefill = navState && navState.prefillFormData ? navState.prefillFormData : null;
434
+ if (statePrefill && typeof statePrefill === 'object') {
435
+ // Merge saved values into current formData
436
+ this.formData = { ...(this.formData || {}), ...statePrefill };
437
+ // Restore cascading dropdown selections if present
438
+ if (this.formData['Country']) this.selectedValues['Country'] = this.formData['Country'];
439
+ if (this.formData['State']) this.selectedValues['State'] = this.formData['State'];
440
+ if (this.formData['District']) this.selectedValues['District'] = this.formData['District'];
441
+ // Recompute completion status
442
+ this.updateCompletionStatus();
443
+ } else {
444
+ // Ensure form is empty when arriving without prefill (fresh/new case or refresh)
445
+ this.formData = {};
446
+ this.completedFields.clear();
447
+ this.completedSubgroups.clear();
448
+ this.completedSections.clear();
449
+ }
450
+ } catch (e) {
451
+ // ignore
452
+ }
453
+ }
454
+
455
+ ngAfterViewInit(): void {
456
+ // No special scroll handling needed anymore
457
+ }
458
+
459
+ ngOnDestroy(): void {
460
+ this.destroy$.next();
461
+ this.destroy$.complete();
462
+ }
463
+
464
+ // Progress Calculation
465
+ get progressPercentage(): number {
466
+ const totalFields = this.getAllFields().length;
467
+ const completedFields = this.completedFields.size;
468
+ return totalFields >0 ? Math.round((completedFields / totalFields) *100) :0;
469
+ }
470
+
471
+ private getAllFields(): string[] {
472
+ let allFields: string[] = [];
473
+ for (const section of this.sectionKeys) {
474
+ for (const subgroup of Object.keys(this.sections[section].subgroups)) {
475
+ allFields = allFields.concat(this.sections[section].subgroups[subgroup]);
476
+ }
477
+ }
478
+ return allFields;
479
+ }
480
+
481
+ // Helper method to check if we're in Identification & Timing
482
+ private isIdentificationAndTimingPage(): boolean {
483
+ return this.currentSubgroup === 'Identification & Timing';
484
+ }
485
+
486
+ // Helper method to check if we're in Location & People
487
+ private isLocationAndPeoplePage(): boolean {
488
+ return this.currentSubgroup === 'Location & People';
489
+ }
490
+
491
+ // Helper method to check if we need compact layout (applies to all pages now)
492
+ private needsCompactLayout(): boolean {
493
+ return true; // Apply compact layout to all pages to prevent main page scroll
494
+ }
495
+
496
+ // Card Management - Updated for single card layout across ALL pages
497
+ showSecondaryCard(): boolean {
498
+ return false; // No secondary card for any page - single card layout for all
499
+ }
500
+
501
+ showTertiaryCard(): boolean {
502
+ return false; // No tertiary card for any page - single card layout for all
503
+ }
504
+
505
+ getPrimaryFields(): string[] {
506
+ // Return selected fields for display instead of all fields
507
+ return this.getSelectedFieldsForDisplay();
508
+ }
509
+
510
+ getSecondaryFields(): string[] {
511
+ // Return empty array for single card layout across all pages
512
+ return [];
513
+ }
514
+
515
+ getTertiaryFields(): string[] {
516
+ // Return empty array for single card layout across all pages
517
+ return [];
518
+ }
519
+
520
+ private getCurrentFields(): string[] {
521
+ return this.sections[this.currentSection].subgroups[this.currentSubgroup] || [];
522
+ }
523
+
524
+ toggleCardMinimize(card: 'primary' | 'secondary' | 'tertiary'): void {
525
+ this.isCardMinimized[card] = !this.isCardMinimized[card];
526
+ }
527
+
528
+ // Field Management
529
+ isFieldRequired(field: string): boolean {
530
+ return this.requiredFields.has(field);
531
+ }
532
+
533
+ isCompactField(field: string): boolean {
534
+ return this.compactFields.has(field);
535
+ }
536
+
537
+ getInputType(field: string): string {
538
+ if (this.numericFields.has(field)) return 'number';
539
+ if (this.dateTimeFields.has(field)) return 'datetime-local';
540
+ if (this.dateFields.has(field)) return 'date';
541
+ if (field.toLowerCase().includes('email')) return 'email';
542
+ if (field.toLowerCase().includes('phone') || field.toLowerCase().includes('contact')) return 'tel';
543
+ if (field.toLowerCase().includes('url') || field.toLowerCase().includes('link')) return 'url';
544
+ if (field.toLowerCase().includes('description')) return 'textarea';
545
+ return 'text';
546
+ }
547
+
548
+ getFieldPlaceholder(field: string): string {
549
+ if (field === 'Age') return 'Enter age (18-99)';
550
+ if (field === 'Height (cm)') return 'Height in cm';
551
+ if (field === 'Weight (kg)') return 'Weight in kg';
552
+ if (this.dateTimeFields.has(field)) return 'dd-mm-yyyy --:--';
553
+ if (this.dateFields.has(field)) return 'dd-mm-yyyy';
554
+ if (field.toLowerCase().includes('email')) return 'Enter email address';
555
+ if (field.toLowerCase().includes('phone')) return 'Enter phone number';
556
+ if (field.toLowerCase().includes('description')) return 'Enter detailed description...';
557
+ return `Enter ${field.toLowerCase()}`;
558
+ }
559
+
560
+ getMaxLength(field: string): number {
561
+ if (field === 'Age') return 2;
562
+ if (field === 'Gender') return 10;
563
+ if (field === 'Height (cm)') return 3;
564
+ if (field === 'Weight (kg)') return 3;
565
+ if (this.compactFields.has(field)) return 20;
566
+ return 500;
567
+ }
568
+
569
+ // Validation
570
+ validateField(field: string): void {
571
+ const value = this.formData[field];
572
+ let hasError = false;
573
+ let isValid = false;
574
+ let message = '';
575
+
576
+ if (this.isFieldRequired(field) && (!value || value.toString().trim() === '')) {
577
+ hasError = true;
578
+ message = `${field} is required`;
579
+ } else if (value && value.toString().trim() !== '') {
580
+ // Field-specific validation
581
+ if (field === 'Age') {
582
+ const age = parseInt(value);
583
+ if (isNaN(age) || age <1 || age >120) {
584
+ hasError = true;
585
+ message = 'Age must be a valid number between1 and120';
586
+ } else {
587
+ isValid = true;
588
+ }
589
+ } else if (field === 'Email') {
590
+ // Basic email pattern; improve with regex if needed
591
+ const emailPattern = /\S+@\S+\.\S+/;
592
+ isValid = emailPattern.test(value);
593
+ if (!isValid) {
594
+ hasError = true;
595
+ message = 'Invalid email address format';
596
+ }
597
+ } else {
598
+ // Generic validation for other fields (extend as needed)
599
+ isValid = true;
600
+ }
601
+ }
602
+
603
+ // Update field validation state
604
+ this.fieldValidation[field] = { hasError, isValid, message };
605
+
606
+ // Update overall form completion status
607
+ this.updateCompletionStatus();
608
+ }
609
+
610
+ private updateCompletionStatus(): void {
611
+ this.completedFields.clear();
612
+
613
+ for (const field of Object.keys(this.formData)) {
614
+ if (this.formData[field] !== null && this.formData[field] !== undefined && this.formData[field] !== '') {
615
+ this.completedFields.add(field);
616
+ }
617
+ }
618
+
619
+ // Update completed subgroups and sections
620
+ this.updateCompletedGroupsAndSections();
621
+ }
622
+
623
+ private updateCompletedGroupsAndSections(): void {
624
+ this.completedSubgroups.clear();
625
+ this.completedSections.clear();
626
+
627
+ for (const section of this.sectionKeys) {
628
+ const subgroups = Object.keys(this.sections[section].subgroups);
629
+ for (const subgroup of subgroups) {
630
+ const fields = this.sections[section].subgroups[subgroup];
631
+ const allFieldsCompleted = fields.every((field: string) => this.completedFields.has(field));
632
+
633
+ if (allFieldsCompleted) {
634
+ this.completedSubgroups.add(subgroup);
635
+ }
636
+ }
637
+
638
+ if (this.completedSubgroups.size === subgroups.length) {
639
+ this.completedSections.add(section);
640
+ }
641
+ }
642
+ }
643
+
644
+ // Field selection functionality - Updated to allow all fields by default
645
+ selectedFields: Record<string, string[]> = {}; // Store selected fields per subgroup
646
+ showFieldSelector: string | null = null; // Track which field selector is open
647
+ readonly maxSelectableFields =50; // Increased limit to allow more fields
648
+
649
+ // Get all available fields for current subgroup
650
+ getAvailableFields(): string[] {
651
+ return this.getCurrentFields();
652
+ }
653
+
654
+ // Get total available fields count dynamically
655
+ getTotalAvailableFieldsCount(): number {
656
+ return this.getAvailableFields().length;
657
+ }
658
+
659
+ // Get currently selected fields for display (default to ALL fields if none selected)
660
+ getSelectedFieldsForDisplay(): string[] {
661
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
662
+ if (this.selectedFields[subgroupKey] && this.selectedFields[subgroupKey].length >=0) {
663
+ return this.selectedFields[subgroupKey];
664
+ }
665
+ // Default to ALL fields if no selection made
666
+ return this.getCurrentFields();
667
+ }
668
+
669
+ // Toggle field selection with enhanced debugging
670
+ toggleFieldSelection(field: string, event?: Event): void {
671
+ if (event) {
672
+ event.preventDefault();
673
+ event.stopPropagation();
674
+ }
675
+
676
+ console.log('Toggling field selection for:', field);
677
+
678
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
679
+ if (!this.selectedFields[subgroupKey]) {
680
+ // Initialize with all fields selected by default
681
+ this.selectedFields[subgroupKey] = [...this.getCurrentFields()];
682
+ }
683
+
684
+ const currentSelection = this.selectedFields[subgroupKey];
685
+ const fieldIndex = currentSelection.indexOf(field);
686
+
687
+ console.log('Current selection:', currentSelection);
688
+ console.log('Field index:', fieldIndex);
689
+
690
+ if (fieldIndex > -1) {
691
+ // Remove field if already selected
692
+ currentSelection.splice(fieldIndex,1);
693
+ console.log('Removed field:', field);
694
+ } else {
695
+ // Add field if not selected and under limit
696
+ if (currentSelection.length < this.maxSelectableFields) {
697
+ currentSelection.push(field);
698
+ console.log('Added field:', field);
699
+ } else {
700
+ console.log('Selection limit reached, cannot add:', field);
701
+ }
702
+ }
703
+
704
+ // Trigger change detection
705
+ this.selectedFields = { ...this.selectedFields };
706
+ this.saveFieldSelections();
707
+ // Do NOT close the popup here; just update selection and save
708
+ }
709
+
710
+ // Check if field is currently selected
711
+ isFieldSelected(field: string): boolean {
712
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
713
+ const selections = this.selectedFields[subgroupKey];
714
+ if (!selections) {
715
+ // If no selections made, all fields are selected by default
716
+ return true;
717
+ }
718
+ return selections.includes(field);
719
+ }
720
+
721
+ // Get count of selected fields
722
+ getSelectedFieldCount(): number {
723
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
724
+ const selections = this.selectedFields[subgroupKey];
725
+ if (!selections) {
726
+ return this.getCurrentFields().length; // All fields selected by default
727
+ }
728
+ return selections.length;
729
+ }
730
+
731
+ // Check if selection limit is reached
732
+ isSelectionLimitReached(): boolean {
733
+ return this.getSelectedFieldCount() >= this.maxSelectableFields;
734
+ }
735
+
736
+ // Reset field selection for current subgroup (clear all)
737
+ resetFieldSelection(event?: Event): void {
738
+ if (event) {
739
+ event.preventDefault();
740
+ event.stopPropagation();
741
+ }
742
+
743
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
744
+ this.selectedFields[subgroupKey] = [];
745
+ this.selectedFields = { ...this.selectedFields };
746
+ this.saveFieldSelections();
747
+ // Do NOT close the popup here; keep it open for further selection
748
+ }
749
+
750
+ // Select all fields
751
+ selectAllFields(event?: Event): void {
752
+ if (event) {
753
+ event.preventDefault();
754
+ event.stopPropagation();
755
+ }
756
+
757
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
758
+ const allFields = this.getCurrentFields();
759
+ this.selectedFields[subgroupKey] = [...allFields];
760
+ this.selectedFields = { ...this.selectedFields };
761
+ this.saveFieldSelections();
762
+ }
763
+
764
+ // Select default fields (first10 or all if less than10) - Renamed for clarity
765
+ selectDefaultFields(event?: Event): void {
766
+ if (event) {
767
+ event.preventDefault();
768
+ event.stopPropagation();
769
+ }
770
+
771
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
772
+ const allFields = this.getCurrentFields();
773
+ this.selectedFields[subgroupKey] = allFields.slice(0, Math.min(10, allFields.length));
774
+ this.selectedFields = { ...this.selectedFields };
775
+ this.saveFieldSelections();
776
+ }
777
+
778
+ // Get dynamic max selectable based on available fields
779
+ getDynamicMaxSelectable(): number {
780
+ const totalFields = this.getTotalAvailableFieldsCount();
781
+ return Math.min(this.maxSelectableFields, totalFields);
782
+ }
783
+
784
+ // Check if all fields are selected
785
+ areAllFieldsSelected(): boolean {
786
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
787
+ const selections = this.selectedFields[subgroupKey];
788
+ const totalFields = this.getCurrentFields().length;
789
+
790
+ if (!selections) {
791
+ return true; // All fields selected by default
792
+ }
793
+
794
+ return selections.length === totalFields;
795
+ }
796
+
797
+ // Check if no fields are selected
798
+ areNoFieldsSelected(): boolean {
799
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
800
+ const selections = this.selectedFields[subgroupKey];
801
+
802
+ if (!selections) {
803
+ return false; // All fields selected by default
804
+ }
805
+
806
+ return selections.length ===0;
807
+ }
808
+
809
+ // Navigation and section management methods
810
+ getSubgroups(): string[] {
811
+ return Object.keys(this.sections[this.currentSection].subgroups);
812
+ }
813
+
814
+ showSection(section: 'crime' | 'suspect' | 'notes'): void {
815
+ // Close field selector when changing sections
816
+ this.closeFieldSelector();
817
+ this.currentSection = section;
818
+ this.currentSubgroup = Object.keys(this.sections[this.currentSection].subgroups)[0];
819
+ this.showHelpFor = null;
820
+ this.triggerAutoSave();
821
+ }
822
+
823
+ // Enhanced document click handler
824
+ @HostListener('document:click', ['$event'])
825
+ handleDoc(event: Event): void {
826
+ this.showHelpFor = null;
827
+ // Only close field selector if click is outside the popup
828
+ const target = event.target as HTMLElement;
829
+ if (this.showFieldSelector && !target.closest('.field-selector-container')) {
830
+ this.closeFieldSelector();
831
+ }
832
+ }
833
+
834
+ // Handle section/subgroup changes to refresh field selector
835
+ setSubgroup(key: string): void {
836
+ // Close field selector when changing subgroups
837
+ this.closeFieldSelector();
838
+ this.currentSubgroup = key;
839
+ this.showHelpFor = null;
840
+ this.triggerAutoSave();
841
+ }
842
+
843
+ // Section and subgroup completion status
844
+ isSectionCompleted(section: string): boolean {
845
+ return this.completedSections.has(section);
846
+ }
847
+
848
+ isSubgroupCompleted(subgroup: string): boolean {
849
+ return this.completedSubgroups.has(subgroup);
850
+ }
851
+
852
+ // Section descriptions
853
+ getSectionDescription(section: string): string {
854
+ const descriptions = {
855
+ crime: 'Capture complete crime intelligence: timing, location, evidence, and operational context. Use dropdown values for consistency and upload supporting materials.',
856
+ suspect: 'Document comprehensive suspect profile: identity, physical characteristics, background, associations, and criminal history. Include recent photographs where available.',
857
+ notes: 'Maintain detailed investigative records: findings, evidence files, reference materials, and final recommendations with proper version control.'
858
+ };
859
+ return descriptions[section as keyof typeof descriptions] || '';
860
+ }
861
+
862
+ // Field handling and validation
863
+ onFieldChange(field: string): void {
864
+ this.validateField(field);
865
+ this.triggerAutoSave();
866
+ }
867
+
868
+ // Auto-save functionality
869
+ private triggerAutoSave(): void {
870
+ this.autoSave$.next();
871
+ }
872
+
873
+ private performAutoSave(): void {
874
+ this.isAutoSaving = true;
875
+ this.autoSaveStatus = 'Saving...';
876
+
877
+ // Save to localStorage
878
+ this.saveFormData();
879
+
880
+ // Simulate save delay
881
+ setTimeout(() => {
882
+ this.isAutoSaving = false;
883
+ this.autoSaveStatus = 'Saved';
884
+
885
+ // Reset status after a moment
886
+ setTimeout(() => {
887
+ this.autoSaveStatus = 'Auto-save';
888
+ },2000);
889
+ },500);
890
+ }
891
+
892
+ private saveFormData(): void {
893
+ const saveData = {
894
+ formData: this.formData,
895
+ completedFields: Array.from(this.completedFields),
896
+ completedSubgroups: Array.from(this.completedSubgroups),
897
+ completedSections: Array.from(this.completedSections),
898
+ currentSection: this.currentSection,
899
+ currentSubgroup: this.currentSubgroup
900
+ };
901
+ localStorage.setItem('pydetect-form-data', JSON.stringify(saveData));
902
+ }
903
+
904
+ private loadFormData(): void {
905
+ const savedData = localStorage.getItem('pydetect-form-data');
906
+ if (savedData) {
907
+ const data = JSON.parse(savedData);
908
+ this.formData = data.formData || {};
909
+ this.completedFields = new Set(data.completedFields || []);
910
+ this.completedSubgroups = new Set(data.completedSubgroups || []);
911
+ this.completedSections = new Set(data.completedSections || []);
912
+ this.currentSection = data.currentSection || 'crime';
913
+ this.currentSubgroup = data.currentSubgroup || 'Identification & Timing';
914
+ }
915
+ }
916
+
917
+ // Dropdown options and cascading logic
918
+ getOptions(field: string): string[] | undefined {
919
+ if (field === 'Country') return this.countries;
920
+ if (field === 'State') return (this.selectedValues['Country'] === 'India' || !this.selectedValues['Country']) ? this.indiaStates : [];
921
+ if (field === 'District') {
922
+ if (this.selectedValues['State'] === 'Tamil Nadu') {
923
+ return this.tamilNaduDistricts;
924
+ } else if (this.selectedValues['State']) {
925
+ return [];
926
+ } else {
927
+ return [];
928
+ }
929
+ }
930
+ return this.selectOptions[field];
931
+ }
932
+
933
+ onSelectChange(field: string, event: Event): void {
934
+ const value = (event.target as HTMLSelectElement).value;
935
+ this.selectedValues[field] = value;
936
+ this.formData[field] = value;
937
+
938
+ // Clear dependent fields
939
+ if (field === 'Country') {
940
+ delete this.selectedValues['State'];
941
+ delete this.selectedValues['District'];
942
+ delete this.formData['State'];
943
+ delete this.formData['District'];
944
+ }
945
+ if (field === 'State') {
946
+ delete this.selectedValues['District'];
947
+ delete this.formData['District'];
948
+ }
949
+
950
+ this.validateField(field);
951
+ this.triggerAutoSave();
952
+ }
953
+
954
+ // Field help functionality
955
+ toggleFieldInfo(field: string, ev: MouseEvent): void {
956
+ ev.stopPropagation();
957
+ this.showHelpFor = this.showHelpFor === field ? null : field;
958
+ }
959
+
960
+ closeFieldInfo(): void {
961
+ this.showHelpFor = null;
962
+ }
963
+
964
+ // File upload functionality
965
+ onFileChange(field: string, event: Event): void {
966
+ const input = event.target as HTMLInputElement;
967
+ const files = input.files ? Array.from(input.files) : [];
968
+ if (files.length) {
969
+ this.uploadedFiles[field] = (this.uploadedFiles[field] || []).concat(files);
970
+ this.validateField(field);
971
+ this.triggerAutoSave();
972
+ }
973
+ }
974
+
975
+ onDragOver(event: DragEvent): void {
976
+ event.preventDefault();
977
+ this.isDragOver = true;
978
+ }
979
+
980
+ onDragLeave(event: DragEvent): void {
981
+ event.preventDefault();
982
+ this.isDragOver = false;
983
+ }
984
+
985
+ onFileDrop(field: string, event: DragEvent): void {
986
+ event.preventDefault();
987
+ this.isDragOver = false;
988
+
989
+ const files = event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : [];
990
+ if (files.length) {
991
+ this.uploadedFiles[field] = (this.uploadedFiles[field] || []).concat(files);
992
+ this.validateField(field);
993
+ this.triggerAutoSave();
994
+ }
995
+ }
996
+
997
+ removeFile(field: string, file: File): void {
998
+ if (this.uploadedFiles[field]) {
999
+ this.uploadedFiles[field] = this.uploadedFiles[field].filter(f => f !== file);
1000
+ this.triggerAutoSave();
1001
+ }
1002
+ }
1003
+
1004
+ getAcceptedFileTypes(field: string): string {
1005
+ return this.fileTypeConfig[field] || '*';
1006
+ }
1007
+
1008
+ getFileIcon(filename: string): string {
1009
+ const ext = filename.split('.').pop()?.toLowerCase();
1010
+ switch (ext) {
1011
+ case 'pdf': return 'fas fa-file-pdf';
1012
+ case 'doc':
1013
+ case 'docx': return 'fas fa-file-word';
1014
+ case 'jpg':
1015
+ case 'jpeg':
1016
+ case 'png':
1017
+ case 'gif': return 'fas fa-file-image';
1018
+ case 'mp4':
1019
+ case 'avi':
1020
+ case 'mov': return 'fas fa-file-video';
1021
+ default: return 'fas fa-file';
1022
+ }
1023
+ }
1024
+
1025
+ // Navigation helper methods for floating buttons
1026
+ getPreviousSubgroup(): string {
1027
+ const list = this.getSubgroups();
1028
+ const currentIndex = list.indexOf(this.currentSubgroup);
1029
+ return currentIndex >0 ? list[currentIndex -1] : '';
1030
+ }
1031
+
1032
+ getNextSubgroup(): string {
1033
+ const subgroups = this.getSubgroups();
1034
+ const currentIndex = subgroups.indexOf(this.currentSubgroup);
1035
+ if (currentIndex < subgroups.length -1) {
1036
+ return subgroups[currentIndex +1];
1037
+ }
1038
+ // If last subgroup, return first subgroup of next section if exists
1039
+ const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1040
+ if (sectionIndex < this.sectionKeys.length -1) {
1041
+ const nextSection = this.sectionKeys[sectionIndex +1];
1042
+ return Object.keys(this.sections[nextSection].subgroups)[0];
1043
+ }
1044
+ return '';
1045
+ }
1046
+
1047
+ isLastSubgroup(): boolean {
1048
+ const list = this.getSubgroups();
1049
+ return list.indexOf(this.currentSubgroup) === list.length -1;
1050
+ }
1051
+
1052
+ canNextSubgroup(): boolean {
1053
+ // Enable Next on last subgroup if not notes section
1054
+ if (this.isLastSubgroup()) {
1055
+ return !(this.currentSection === 'notes' && this.currentSubgroup === 'Remark');
1056
+ }
1057
+ return true;
1058
+ }
1059
+
1060
+ nextSubgroup(): void {
1061
+ const subgroups = this.getSubgroups();
1062
+ const currentIndex = subgroups.indexOf(this.currentSubgroup);
1063
+ // If not last subgroup, go to next subgroup
1064
+ if (currentIndex < subgroups.length -1) {
1065
+ this.setSubgroup(subgroups[currentIndex +1]);
1066
+ return;
1067
+ }
1068
+ // If last subgroup, go to first subgroup of next section (if exists)
1069
+ const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1070
+ if (sectionIndex < this.sectionKeys.length -1) {
1071
+ const nextSection = this.sectionKeys[sectionIndex +1];
1072
+ this.currentSection = nextSection;
1073
+ this.currentSubgroup = Object.keys(this.sections[nextSection].subgroups)[0];
1074
+ this.showHelpFor = null;
1075
+ this.triggerAutoSave();
1076
+ }
1077
+ }
1078
+
1079
+ prevSubgroup(): void {
1080
+ const subgroups = this.getSubgroups();
1081
+ const currentIndex = subgroups.indexOf(this.currentSubgroup);
1082
+ if (currentIndex >0) {
1083
+ this.setSubgroup(subgroups[currentIndex -1]);
1084
+ return;
1085
+ }
1086
+ // If first subgroup, go to last subgroup of previous section (if exists)
1087
+ const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1088
+ if (sectionIndex >0) {
1089
+ const prevSection = this.sectionKeys[sectionIndex -1];
1090
+ const prevSubgroups = Object.keys(this.sections[prevSection].subgroups);
1091
+ this.currentSection = prevSection;
1092
+ this.currentSubgroup = prevSubgroups[prevSubgroups.length -1];
1093
+ this.showHelpFor = null;
1094
+ this.triggerAutoSave();
1095
+ }
1096
+ }
1097
+
1098
+ submitCurrentSection(): void {
1099
+ // Perform final validation
1100
+ const currentFields = this.getCurrentFields();
1101
+ const requiredFields = currentFields.filter(f => this.isFieldRequired(f));
1102
+ const missingFields = requiredFields.filter(f => !this.completedFields.has(f));
1103
+
1104
+ if (missingFields.length >0) {
1105
+ alert(`Please complete the following required fields: ${missingFields.join(', ')}`);
1106
+ return;
1107
+ }
1108
+
1109
+ this.performAutoSave();
1110
+
1111
+ // Map flat formData to nested structure expected by addOrUpdateFromInfoForm
1112
+ const crime = {
1113
+ caseId: this.formData['Case ID'] || '',
1114
+ dateTime: this.formData['Date & Time (Entry)'] || '',
1115
+ crimeType: this.formData['Crime Type'] || '',
1116
+ location: this.formData['Location'] || '',
1117
+ victimName: this.formData['Victim Name'] || '',
1118
+ caseCategory: this.formData['Case Category'] || '',
1119
+ reportedBy: this.formData['Reported By'] || '',
1120
+ briefDescription: this.formData['Brief Description'] || '',
1121
+ 'FIR / Ref #': this.formData['FIR / Ref #'] || '',
1122
+ 'Occurred From': this.formData['Occurred From'] || '',
1123
+ 'Occurred To': this.formData['Occurred To'] || '',
1124
+ 'Jurisdiction / PS': this.formData['Jurisdiction / PS'] || '',
1125
+ 'Scene Type': this.formData['Scene Type'] || ''
1126
+ };
1127
+ const suspect = {
1128
+ fullName: this.formData['Suspect Name'] || '',
1129
+ age: this.formData['Age'] || '',
1130
+ gender: this.formData['Gender'] || '',
1131
+ address: this.formData['Address'] || '',
1132
+ alias: this.formData['Alias / Nickname'] || ''
1133
+ };
1134
+ const notes = {
1135
+ status: this.formData['Case Status'] || this.formData['Status'] || 'Open',
1136
+ officerInCharge: this.formData['Investigating Officer'] || '',
1137
+ initialFindings: this.formData['Initial Findings'] || '',
1138
+ verifiedBy: this.formData['Verified By'] || ''
1139
+ };
1140
+ const legal = {
1141
+ witnessStatements: this.formData['Witness Statements'] || '',
1142
+ confessions: this.formData['Confessions'] || '',
1143
+ evidence: this.uploadedFiles['Evidence Files'] || []
1144
+ };
1145
+
1146
+ // Pass the full flat formData object so CaseStoreService can save raw inputs
1147
+ this.caseStore.addOrUpdateFromInfoForm({ crime, suspect, notes, legal, formData: this.formData });
1148
+ // Show popup first
1149
+ this.showSubmitPopup = true;
1150
+ }
1151
+
1152
+ onSubmitPopupClose(): void {
1153
+ this.showSubmitPopup = false;
1154
+ this.router.navigate(['/record'], { state: { formData: this.formData } });
1155
+ }
1156
+
1157
+ // Keyboard navigation
1158
+ @HostListener('document:keydown', ['$event'])
1159
+ handleKeydown(event: KeyboardEvent): void {
1160
+ // Keyboard navigation
1161
+ if (event.ctrlKey && event.key === 'ArrowRight') {
1162
+ event.preventDefault();
1163
+ this.nextSubgroup();
1164
+ } else if (event.ctrlKey && event.key === 'ArrowLeft') {
1165
+ event.preventDefault();
1166
+ this.prevSubgroup();
1167
+ } else if (event.ctrlKey && event.key === 's') {
1168
+ event.preventDefault();
1169
+ this.performAutoSave();
1170
+ } else if (event.key === 'Escape') {
1171
+ this.closeFieldInfo();
1172
+ }
1173
+ }
1174
+
1175
+ // Toggle field selector visibility
1176
+ toggleFieldSelector(event?: Event): void {
1177
+ if (event) {
1178
+ event.preventDefault();
1179
+ event.stopPropagation();
1180
+ }
1181
+
1182
+ const subgroupKey = `${this.currentSection}-${this.currentSubgroup}`;
1183
+ this.showFieldSelector = this.showFieldSelector === subgroupKey ? null : subgroupKey;
1184
+ }
1185
+
1186
+ // Close field selector
1187
+ closeFieldSelector(): void {
1188
+ this.showFieldSelector = null;
1189
+ }
1190
+
1191
+ // Save field selections to localStorage
1192
+ private saveFieldSelections(): void {
1193
+ try {
1194
+ localStorage.setItem('pydetect-field-selections', JSON.stringify(this.selectedFields));
1195
+ } catch (error) {
1196
+ console.warn('Could not save field selections to localStorage:', error);
1197
+ }
1198
+ }
1199
+
1200
+ // Load field selections from localStorage
1201
+ private loadFieldSelections(): void {
1202
+ try {
1203
+ const savedSelections = localStorage.getItem('pydetect-field-selections');
1204
+ if (savedSelections) {
1205
+ this.selectedFields = JSON.parse(savedSelections);
1206
+ }
1207
+ } catch (error) {
1208
+ console.warn('Could not load field selections from localStorage:', error);
1209
+ this.selectedFields = {};
1210
+ }
1211
+ }
1212
+
1213
+ // Track by function for ngFor optimization
1214
+ trackByField(index: number, field: string): string {
1215
+ return field;
1216
+ }
1217
+
1218
+ toggleRecording() {
1219
+ this.isRecording = !this.isRecording;
1220
+ // Add actual recording logic here if needed
1221
+ // For now, just toggles the state
1222
+ }
1223
+
1224
+ goToRecords(): void {
1225
+ this.router.navigate(['/record']);
1226
+ }
1227
 
1228
  }
src/app/py-detect/py-detect.component.css CHANGED
@@ -236,7 +236,7 @@ body, html {
236
  grid-template-columns: 1fr 1.5fr;
237
  gap: 32px;
238
  margin: 32px auto 0 auto;
239
- max-width: 1400px;
240
  min-height: 80vh;
241
  background: linear-gradient(120deg, #f6f8fa 60%, #e0f2fe 100%);
242
  border-radius: 24px;
@@ -250,8 +250,8 @@ body, html {
250
  gap: 18px;
251
  justify-content: flex-start;
252
  align-items: flex-start;
253
- min-width: 555px;
254
- max-width: 480px;
255
  }
256
 
257
  .right-panel {
@@ -259,7 +259,8 @@ body, html {
259
  flex-direction: column;
260
  gap: 24px;
261
  align-items: stretch;
262
- min-width: 400px;
 
263
  }
264
 
265
  /* Card style for summary/case info */
@@ -848,7 +849,7 @@ body, html {
848
  position: relative;
849
  animation: fadeInUp 0.7s cubic-bezier(.39,.58,.57,1);
850
  min-height: 180px;
851
- max-width: 480px;
852
  width: 100%;
853
  z-index: 1;
854
  }
@@ -1222,7 +1223,7 @@ body, html {
1222
  font-weight: 700;
1223
  padding: 8px 22px;
1224
  border-radius: 16px;
1225
- box-shadow: 0 2px 12px #ef444488;
1226
  z-index: 10;
1227
  letter-spacing: 1px;
1228
  display: flex;
 
236
  grid-template-columns: 1fr 1.5fr;
237
  gap: 32px;
238
  margin: 32px auto 0 auto;
239
+ max-width: 1700px;
240
  min-height: 80vh;
241
  background: linear-gradient(120deg, #f6f8fa 60%, #e0f2fe 100%);
242
  border-radius: 24px;
 
250
  gap: 18px;
251
  justify-content: flex-start;
252
  align-items: flex-start;
253
+ min-width: 750px;
254
+ max-width: 800px;
255
  }
256
 
257
  .right-panel {
 
259
  flex-direction: column;
260
  gap: 24px;
261
  align-items: stretch;
262
+ min-width: 600px;
263
+ max-width: 900px;
264
  }
265
 
266
  /* Card style for summary/case info */
 
849
  position: relative;
850
  animation: fadeInUp 0.7s cubic-bezier(.39,.58,.57,1);
851
  min-height: 180px;
852
+ max-width: 700px;
853
  width: 100%;
854
  z-index: 1;
855
  }
 
1223
  font-weight: 700;
1224
  padding: 8px 22px;
1225
  border-radius: 16px;
1226
+ max-width: 1000px;
1227
  z-index: 10;
1228
  letter-spacing: 1px;
1229
  display: flex;
src/app/py-detect/py-detect.component.html CHANGED
@@ -35,7 +35,7 @@
35
  <!-- Header action bar with buttons and divider line -->
36
  <div class="header-action-bar">
37
  <div class="header-action-left" style="position:relative;">
38
- <button class="small-btn" (click)="onStartRecording()">
39
  <i class="fas fa-search"></i> Start Investigation
40
  </button>
41
  <div *ngIf="currentQuestionIndex < 0" class="guidance-tooltip">
@@ -51,26 +51,81 @@
51
  <section class="left-panel">
52
  <div class="animated-divider"></div>
53
  <!-- Remove old question/status block, keep only the new decorated card -->
54
- <div class="tts-question-card">
55
- <div class="tts-question-title">Question {{ currentQuestionIndex + 1 }} of {{ totalQuestions }}</div>
56
- <div class="tts-question-text">{{ currentQuestionText }}</div>
57
- <div *ngIf="infoText" class="tts-status-row">{{ infoText }}</div>
58
- <div *ngIf="status() === 'idle-wait'" class="tts-status-row">
59
- Waiting for suspect reply...
60
- </div>
61
- <div *ngIf="isRecording" class="tts-status-row">
62
- Voice is being recognized...
63
- </div>
64
- <!-- Recording Status Bar inside question card -->
65
- <div class="recording-status-bar">
66
- <span class="status-icon">
67
- <ng-container *ngIf="isRecording; else pausedIcon">🔴</ng-container>
68
- <ng-template #pausedIcon>⏸️</ng-template>
69
- </span>
70
- <span>{{ isRecording ? 'Recording' : 'Paused' }}</span>
71
- <span class="status-timer">— {{ isRecording ? elapsedTime : remainingTime }}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  </div>
73
- </div>
74
  </section>
75
  <!-- Right Panel: Video + Transcript -->
76
  <section class="right-panel">
@@ -83,7 +138,7 @@
83
  </div>
84
  </ng-container>
85
  <ng-container *ngIf="videoStream">
86
- <video #videoElement autoplay muted playsinline class="camera-video"></video>
87
  <div *ngIf="isRecording && currentQuestionIndex >= 0 && currentQuestionIndex < totalQuestions" class="recording-indicator">
88
  <span style="font-size:1.3em;">🔴</span> Recording...
89
  </div>
@@ -103,6 +158,7 @@
103
  <button class="submit-evaluate-btn" (click)="navigateToValidationPage()">
104
  Submit & Evaluate
105
  </button>
 
106
  </section>
107
  </main>
108
  <!-- Evidence Panel Sidebar (directly below button bar) -->
@@ -139,7 +195,7 @@
139
  <hr class="evidence-divider" />
140
  <div class="evidence-summary-row">
141
  <label for="evidenceSummary" class="evidence-summary-label">Remark</label>
142
- <textarea id="evidenceSummary" [(ngModel)]="evidenceSummary" rows="3" placeholder="Enter Remark..." class="evidence-summary-textarea"></textarea>
143
  </div>
144
  <button type="submit" class="evidence-submit-btn">Submit</button>
145
  </form>
 
35
  <!-- Header action bar with buttons and divider line -->
36
  <div class="header-action-bar">
37
  <div class="header-action-left" style="position:relative;">
38
+ <button class="small-btn" (click)="onStartInvestigation()">
39
  <i class="fas fa-search"></i> Start Investigation
40
  </button>
41
  <div *ngIf="currentQuestionIndex < 0" class="guidance-tooltip">
 
51
  <section class="left-panel">
52
  <div class="animated-divider"></div>
53
  <!-- Remove old question/status block, keep only the new decorated card -->
54
+ <!-- In tts-question-card, show AI question if available -->
55
+ <div class="tts-question-card">
56
+ <div class="tts-question-title">Question {{ currentQuestionIndex + 1 }}</div>
57
+ <div class="tts-question-text">
58
+ <ng-container *ngIf="questions.length > 0; else noQuestion">
59
+ {{ questions[currentQuestionIndex] }}
60
+ </ng-container>
61
+ <ng-template #noQuestion>
62
+ <span *ngIf="currentQuestionText">{{ currentQuestionText }}</span>
63
+ <span *ngIf="!currentQuestionText">No question available.</span>
64
+ </ng-template>
65
+ </div>
66
+ <div *ngIf="infoText" class="tts-status-row">{{ infoText }}</div>
67
+
68
+ <!-- Manual answer input for testing -->
69
+ <div class="answer-section">
70
+ <div class="answer-card" style="background: #f8fbff; border-radius: 12px; box-shadow: 0 2px 8px #0001; padding: 18px 20px; margin-top: 10px; display: flex; flex-direction: column; gap: 18px; max-width: 650px; width: 100%;">
71
+ <label for="answerInput" style="font-weight: 500; margin-bottom: 4px; color: #2a3b5c;">Your Answer:</label>
72
+ <textarea id="answerInput" [(ngModel)]="textAnswer" (focus)="captureTextStart()" class="answer-input" placeholder="Type or speak your answer here..." rows="4" maxlength="3000" style="width: 100%; font-size: 1.08em; border-radius: 8px; border: 1px solid #bcd0ee; padding: 10px; resize: vertical; background: #fff; box-shadow: 0 1px 4px #0001; min-height: 60px;"></textarea>
73
+ <div style="display: flex; gap: 10px; margin-top: 6px;">
74
+ <button (click)="submitCombinedAnswer()" class="small-btn" style="background: linear-gradient(90deg,#3a8bfd,#6ad1ff); color: #fff; border-radius: 6px; font-weight: 500; padding: 7px 18px; border: none; box-shadow: 0 1px 4px #0001; cursor: pointer;">Submit Answer</button>
75
+ <button (click)="toggleVoiceRecording()" class="mic-btn" style="background: #fff; color: #3a8bfd; border-radius: 6px; font-weight: 500; padding: 7px 18px; border: 1px solid #3a8bfd; box-shadow: 0 1px 4px #0001; cursor: pointer;">
76
+ <span *ngIf="!isVoiceRecording">🎤 Start Recording</span>
77
+ <span *ngIf="isVoiceRecording">⏹️ Stop Recording</span>
78
+ </button>
79
+ </div>
80
+ <div *ngIf="isVoiceRecording" class="voice-hint" style="color: #e74c3c; font-weight: 500; margin-top: 4px;">🎤 Recording... Speak now.</div>
81
+ <!-- Results Section: Truth Score, Face Detection, Involvement -->
82
+ <div class="results-section" style="display: flex; flex-direction: column; gap: 16px; margin-top: 10px;">
83
+ <div *ngIf="truthScore !== null" class="result-card" style="background: #e0f7fa; border-radius: 10px; box-shadow: 0 1px 6px #38bdf822; padding: 12px 16px; margin-bottom: 0;">
84
+ <div style="display: flex; align-items: center; gap: 10px;">
85
+ <i class="fas fa-check-circle" style="color: #38bdf8; font-size: 1.3em;"></i>
86
+ <span style="font-weight: 600; color: #2563eb; font-size: 1.08em;">Truth Score</span>
87
+ <span style="font-weight: 700; color: #222; font-size: 1.15em; margin-left: auto;">{{ truthScore }}</span>
88
+ </div>
89
+ </div>
90
+ <div *ngIf="faceDetectionScore !== null" class="result-card" style="background: #fffde7; border-radius: 10px; box-shadow: 0 1px 6px #ffe08244; padding: 12px 16px; margin-bottom: 0;">
91
+ <div style="display: flex; align-items: center; gap: 10px;">
92
+ <i class="fas fa-user-check" style="color: #ff9800; font-size: 1.3em;"></i>
93
+ <span style="font-weight: 600; color: #ff9800; font-size: 1.08em;">Face Detection Score</span>
94
+ <span style="font-weight: 700; color: #222; font-size: 1.15em; margin-left: auto;">{{ faceDetectionScore }}</span>
95
+ </div>
96
+ </div>
97
+ <div *ngIf="involvementScore !== null" class="result-card" style="background: #f3e8ff; border-radius: 10px; box-shadow: 0 1px 6px #a78bfa44; padding: 12px 16px; margin-bottom: 0;">
98
+ <div style="font-weight:600;color:#6d28d9;display:flex;align-items:center;gap:8px;">
99
+ <i class="fas fa-user-tag" style="color: #6d28d9; font-size: 1.3em;"></i>
100
+ <span>Involvement Score</span>
101
+ <span style="font-weight: 700; color: #222; font-size: 1.15em; margin-left: auto;">{{ involvementScore | number:'1.1-1' }}</span>
102
+ </div>
103
+ <div style="flex:1;background:#e0ecf8;height:14px;border-radius:6px;overflow:hidden;margin-top:8px;">
104
+ <div [style.width]="involvementScore + '%'" [style.background]="'linear-gradient(90deg,#4caf50,#ff9800,#f44336)'" style="height:100%;"></div>
105
+ </div>
106
+ <div *ngIf="involvementCues.length" style="margin-top:6px;display:flex;flex-wrap:wrap;gap:6px;">
107
+ <span *ngFor="let cue of involvementCues" style="background:#3a8bfd11;color:#1d3e63;padding:4px 10px;border:1px solid #3a8bfd33;border-radius:16px;font-size:0.75rem;font-weight:500;">{{ cue.replace('_cue','').replace('_',' ') }}</span>
108
+ </div>
109
+ <div *ngIf="dominantInvestigativeExpression" style="margin-top:4px;font-size:0.75rem;color:#445;">Dominant Investigative Expression: <strong>{{ dominantInvestigativeExpression }}</strong></div>
110
+ <div class="body-language-explanation" *ngIf="bodyLanguageMeaning || bodyLanguageExplanation">
111
+ <span *ngIf="bodyLanguageMeaning" class="explanation-label">Body Language Meaning:</span>
112
+ <span *ngIf="bodyLanguageMeaning" class="explanation-text">{{ bodyLanguageMeaning }}</span><br *ngIf="bodyLanguageMeaning && bodyLanguageExplanation">
113
+ <span *ngIf="bodyLanguageExplanation" class="explanation-label">Body Language Explanation:</span>
114
+ <span *ngIf="bodyLanguageExplanation" class="explanation-text">{{ bodyLanguageExplanation }}</span>
115
+ </div>
116
+ </div>
117
+ <div *ngIf="ferEmotion" class="result-card" style="background: #e3f6ff; border-radius: 10px; box-shadow: 0 1px 6px #38bdf822; padding: 12px 16px; margin-bottom: 0;">
118
+ <div style="display: flex; align-items: center; gap: 10px;">
119
+ <i class="fas fa-smile" style="color: #38bdf8; font-size: 1.3em;"></i>
120
+ <span style="font-weight: 600; color: #2563eb; font-size: 1.08em;">Emotion (FER)</span>
121
+ <span style="font-weight: 700; color: #222; font-size: 1.15em; margin-left: auto;">{{ ferEmotion }}</span>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ <div *ngIf="guidanceCommand" style="margin-top:6px;font-size:0.72rem;color:#555;background:#ffeecd;padding:6px 8px;border-radius:6px;box-shadow:0 1px 3px #0001;">Guidance: {{ guidanceCommand }}</div>
126
+ </div>
127
  </div>
128
+ </div>
129
  </section>
130
  <!-- Right Panel: Video + Transcript -->
131
  <section class="right-panel">
 
138
  </div>
139
  </ng-container>
140
  <ng-container *ngIf="videoStream">
141
+ <video #videoElement [srcObject]="videoStream" autoplay muted playsinline class="camera-video"></video>
142
  <div *ngIf="isRecording && currentQuestionIndex >= 0 && currentQuestionIndex < totalQuestions" class="recording-indicator">
143
  <span style="font-size:1.3em;">🔴</span> Recording...
144
  </div>
 
158
  <button class="submit-evaluate-btn" (click)="navigateToValidationPage()">
159
  Submit & Evaluate
160
  </button>
161
+ <!-- <a *ngIf="recordedVideoUrl" [href]="recordedVideoUrl" download="investigation-video.webm" class="download-btn" style="margin-left: 12px; background: #3a8bfd; color: #fff; padding: 10px 22px; border-radius: 8px; font-weight: 500; text-decoration: none; box-shadow: 0 1px 4px #0001; vertical-align: middle;">Download Video</a> -->
162
  </section>
163
  </main>
164
  <!-- Evidence Panel Sidebar (directly below button bar) -->
 
195
  <hr class="evidence-divider" />
196
  <div class="evidence-summary-row">
197
  <label for="evidenceSummary" class="evidence-summary-label">Remark</label>
198
+ <textarea id="evidenceSummary" [(ngModel)]="evidenceSummary" [ngModelOptions]="{standalone: true}" rows="3" placeholder="Enter Remark..." class="evidence-summary-textarea"></textarea>
199
  </div>
200
  <button type="submit" class="evidence-submit-btn">Submit</button>
201
  </form>
src/app/py-detect/py-detect.component.ts CHANGED
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
3
  import { Router, NavigationStart } from '@angular/router';
4
  import { Subscription } from 'rxjs';
5
  import { FormsModule } from '@angular/forms';
 
6
 
7
  declare global {
8
  interface Window {
@@ -31,29 +32,269 @@ type QAResult = {
31
  styleUrls: ['./py-detect.component.css']
32
  })
33
  export class PyDetectComponent implements OnInit, OnDestroy {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  public showDetailsPanel: boolean = false;
35
  public metadata: any = null;
36
 
37
- // ---- UI state ----
38
- status = signal<'idle' | 'asking' | 'idle-wait' | 'recording' | 'processing'>('idle');
39
- autoMode = signal(true); // Auto Next is always enabled
40
- micOn = signal(false);
41
- ttsEnabled = signal(true); // Speak Questions is always enabled
42
- recognizerReady = signal(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  // ---- TTS active flag ----
45
  private isActive = false;
46
 
47
  // ---- Q/A data ----
48
- currentQuestion = signal<string>('');
49
- questionIndex = signal<number>(0);
50
  log: QAResult[] = [];
51
 
52
 
53
  // ---- Constructor with Router Injection ----
54
  private routerSubscription?: Subscription;
55
 
56
- constructor(private router: Router, private cdr: ChangeDetectorRef) {
 
 
 
 
57
  // Cancel TTS on any navigation away
58
  this.routerSubscription = this.router.events.subscribe(event => {
59
  if (event instanceof NavigationStart) {
@@ -75,7 +316,7 @@ export class PyDetectComponent implements OnInit, OnDestroy {
75
 
76
  private pitchSamples: number[] = [];
77
  private volumeSamples: number[] = [];
78
- private analyserBuffer?: Float32Array;
79
  private analyserTimer?: any;
80
 
81
  // ---- Speech Recognition ----
@@ -90,27 +331,20 @@ export class PyDetectComponent implements OnInit, OnDestroy {
90
  private analyserWindowMs = 100; // Declare analyserWindowMs property
91
 
92
  // Example question source (replace with API call when ready)
93
- public seedQuestions = [
94
- 'Please introduce yourself in two sentences.',
95
- 'What motivates you to take on challenging tasks?',
96
- 'Describe a situation where you solved a tough problem.',
97
- 'How do you handle disagreements in a team?',
98
- 'What is a recent technology you learned and why?'
99
- ];
100
 
101
  // Button state signals
102
- startDisabled = signal(false); // Always enabled
103
- stopDisabled = signal(true);
104
- resumeDisabled = signal(true);
105
- submitDisabled = signal(true);
106
 
107
  // Add missing public methods and properties for template binding
108
- public videoStatus: string = '';
109
  public videoStream?: MediaStream;
110
- public videoRecorder?: MediaRecorder;
111
  @ViewChild('videoElement', { static: false }) videoElement?: ElementRef<HTMLVideoElement>;
112
  public videoChunks: Blob[] = [];
113
  public videoAnswers: Blob[] = [];
 
 
114
 
115
  // UI properties for template
116
  caseId: string = '';
@@ -224,6 +458,7 @@ export class PyDetectComponent implements OnInit, OnDestroy {
224
  this.progress = metadata.progress || 0;
225
  this.progressStage = metadata.progressStage || '';
226
  this.sessionTime = metadata.sessionTime || '';
 
227
  }
228
  }
229
 
@@ -234,8 +469,6 @@ export class PyDetectComponent implements OnInit, OnDestroy {
234
  }
235
  this.cleanupAll();
236
  this.stopVideoRecording();
237
- this.micOn.set(false); // Stop mic indicator
238
- this.recognizerReady.set(false); // Stop recognizer indicator
239
  this.videoStatus = '';
240
  if (this.videoStream) {
241
  this.videoStream.getTracks().forEach(t => t.stop());
@@ -248,139 +481,18 @@ export class PyDetectComponent implements OnInit, OnDestroy {
248
  }
249
 
250
  // ======== Main flow ========
251
- async start(): Promise<void> {
252
- if (this.status() !== 'idle') return;
253
- this.status.set('asking');
254
- this.setupRecognition();
255
- this.recognizerReady.set(!!this.recognition);
256
- await this.startCamera();
257
- this.startDisabled.set(false); // Always enabled
258
- this.stopDisabled.set(false);
259
- this.resumeDisabled.set(true);
260
- this.submitDisabled.set(true);
261
- this.autoMode.set(true); // Enable auto mode for continuous questions
262
- this.nextQuestionLoopRunning = true;
263
- this.nextQuestionLoop();
264
- }
265
-
266
- stopAll(): void {
267
- this.autoMode.set(false); // Stop auto mode to halt question loop
268
- this.nextQuestionLoopRunning = false;
269
- if (this.videoRecorder && this.videoRecorder.state === 'recording') {
270
- this.videoRecorder.stop();
271
- }
272
- if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
273
- this.cleanupRecording();
274
- }
275
- this.stopRecognition();
276
- if (window.speechSynthesis) {
277
- window.speechSynthesis.cancel(); // Stop any TTS audio
278
- }
279
- // Stop and clear video stream
280
- if (this.videoStream) {
281
- this.videoStream.getTracks().forEach(t => t.stop());
282
- this.videoStream = undefined;
283
- if (this.videoElement?.nativeElement) {
284
- this.videoElement.nativeElement.srcObject = null;
285
- }
286
- }
287
- this.videoRecorder = undefined;
288
- this.videoStatus = '';
289
- this.micOn.set(false);
290
- this.recognizerReady.set(false);
291
- this.status.set('idle');
292
- this.startDisabled.set(false); // Always enabled
293
- this.stopDisabled.set(false); // Ensure Stop button is enabled after stopping
294
- this.resumeDisabled.set(false);
295
- this.submitDisabled.set(false);
296
- }
297
-
298
- public resume() {
299
- // Start camera if not already started
300
- this.startCamera();
301
- // Resume video recording if paused
302
- if (this.videoRecorder && this.videoRecorder.state === 'paused') {
303
- this.videoRecorder.resume();
304
- this.videoStatus = 'Recording...';
305
- this.stopDisabled.set(false);
306
- this.resumeDisabled.set(true);
307
- this.submitDisabled.set(true);
308
- }
309
- // Resume question loop if stopped
310
- if (this.status() === 'idle') {
311
- this.autoMode.set(true);
312
- this.nextQuestionLoopRunning = true;
313
- this.startDisabled.set(false);
314
- this.stopDisabled.set(false);
315
- this.resumeDisabled.set(true);
316
- this.submitDisabled.set(true);
317
- this.nextQuestionLoop();
318
- }
319
- }
320
 
321
- private nextQuestionLoopRunning = false;
322
 
323
- private async nextQuestionLoop(): Promise<void> {
324
- while (this.autoMode() && this.nextQuestionLoopRunning) {
325
- // 1) Get next question
326
- const q = await this.fetchNextQuestion();
327
- this.currentQuestion.set(q);
328
- this.status.set('asking'); // Show listening HUD
329
- if (this.ttsEnabled()) {
330
- await this.speak(q);
331
- }
332
- // Start camera and audio recording
333
- await this.startCamera();
334
- this.status.set('recording'); // Show recording HUD
335
- await this.startVideoRecording();
336
- this.transcriptSoFar = '';
337
- this.startRecognition('en-IN');
338
- let silenceTimer: any;
339
- let lastTranscript = '';
340
- let silenceStart: number | null = null;
341
- let answerDone = false;
342
- // Wait for user to start speaking, then monitor for silence
343
- await new Promise<void>((resolve) => {
344
- const poll = () => {
345
- const currentTranscript = this.transcriptSoFar.trim();
346
- if (currentTranscript.length > 0) {
347
- if (lastTranscript !== currentTranscript) {
348
- lastTranscript = currentTranscript;
349
- silenceStart = null;
350
- } else {
351
- if (!silenceStart) silenceStart = Date.now();
352
- if (Date.now() - silenceStart > 10000) {
353
- answerDone = true;
354
- resolve();
355
- return;
356
- }
357
- }
358
- }
359
- setTimeout(poll, 500);
360
- };
361
- poll();
362
- });
363
- // Stop recognition and video recording
364
- this.stopRecognition();
365
- this.stopVideoRecording();
366
- this.status.set('processing');
367
- await this.sleep(700);
368
- this.questionIndex.set(this.questionIndex() + 1);
369
- }
370
- this.status.set('idle');
371
- this.stopVideoRecording();
372
- this.startDisabled.set(false);
373
- this.stopDisabled.set(true);
374
- this.resumeDisabled.set(false);
375
- this.submitDisabled.set(false);
376
- }
377
 
378
  // ======== Question source ========
379
  private async fetchNextQuestion(): Promise<string> {
380
  // Replace this with HTTP call to your backend if needed.
381
  // Example: const { question } = await this.http.get<{question:string}>('/api/next-question').toPromise();
382
- const i = this.questionIndex() % this.seedQuestions.length;
383
- return this.seedQuestions[i];
384
  }
385
 
386
  // ======== TTS (question playback) ========
@@ -423,7 +535,7 @@ export class PyDetectComponent implements OnInit, OnDestroy {
423
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
424
  video: false
425
  });
426
- this.micOn.set(true);
427
 
428
  // 2) prepare MediaRecorder
429
  const mime = this.chooseMimeType();
@@ -455,7 +567,7 @@ export class PyDetectComponent implements OnInit, OnDestroy {
455
  this.stopAnalyser();
456
  this.stopRecognition();
457
  this.cleanupMediaStream();
458
- this.micOn.set(false);
459
 
460
  // 7) build audio URL
461
  const blob = new Blob(this.audioChunks, { type: mime });
@@ -474,8 +586,8 @@ export class PyDetectComponent implements OnInit, OnDestroy {
474
  private waitForSilenceOrContinue() {
475
  if (this.silenceTimeout) clearTimeout(this.silenceTimeout);
476
  this.silenceTimeout = setTimeout(() => {
477
- this.stopAll();
478
- }, 5000); // Timeout after 5 seconds of silence
479
  }
480
 
481
 
@@ -500,7 +612,8 @@ export class PyDetectComponent implements OnInit, OnDestroy {
500
  this.analyser.fftSize = 2048;
501
  this.sourceNode.connect(this.analyser);
502
 
503
- this.analyserBuffer = new Float32Array(this.analyser.fftSize);
 
504
 
505
  const tick = () => {
506
  if (!this.analyser || !this.analyserBuffer) return;
@@ -662,267 +775,310 @@ export class PyDetectComponent implements OnInit, OnDestroy {
662
  }
663
  }
664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  public async startVideoRecording() {
666
  if (!this.videoStream) return;
 
 
 
 
 
667
  this.videoChunks = [];
668
  this.videoRecorder = new MediaRecorder(this.videoStream, { mimeType: 'video/webm' });
669
- this.videoRecorder.ondataavailable = (e) => {
670
  if (e.data && e.data.size > 0) this.videoChunks.push(e.data);
671
  };
672
- this.videoRecorder.onstart = () => {
673
- this.videoStatus = 'Recording...';
674
- };
675
  this.videoRecorder.onstop = () => {
676
- this.videoStatus = 'Stopped';
677
  const videoBlob = new Blob(this.videoChunks, { type: 'video/webm' });
678
- this.videoAnswers.push(videoBlob);
 
 
 
 
 
 
679
  };
680
  this.videoRecorder.start();
 
681
  }
682
 
683
  public stopVideoRecording() {
684
- if (this.videoRecorder && this.videoRecorder.state !== 'inactive') {
 
685
  this.videoRecorder.stop();
 
 
 
686
  }
687
  }
688
 
689
- public stopCamera() {
690
- if (this.videoStream) {
691
- this.videoStream.getTracks().forEach((track: any) => track.stop());
692
- this.videoStream = undefined;
693
- }
694
- this.videoStatus = 'Camera stopped';
695
- }
696
-
697
- // UI methods for template
698
- onStartInterview() {
699
- this.currentQuestionIndex = 0;
700
- this.progress = 0;
701
- this.isRecording = false;
702
- this.isProcessing = false;
703
- }
704
- public submitAll() {
705
- // Do NOT stop audio or video playback here
706
- this.videoStatus = 'Submitted!';
707
- this.startDisabled.set(true);
708
- this.stopDisabled.set(true);
709
- this.resumeDisabled.set(true);
710
- this.submitDisabled.set(true);
711
- // TODO: Upload all videoAnswers to backend
712
-
713
- // Example: Calculate dummy percentages (replace with real logic)
714
- const truePercentage = 70;
715
- const falsePercentage = 30;
716
- this.router.navigate(['/validationpage'], { state: { truePercentage, falsePercentage } });
717
- }
718
-
719
- // Start Investigation workflow
720
- public async onStartRecording() {
721
- this.floatingInfoText = 'Starting camera and microphone...';
722
- this.infoText = null;
723
- await this.sleep(1200); // Wait for UI effect
724
- await this.startCamera();
725
- await this.startVideoRecording();
726
- this.floatingInfoText = 'Camera and microphone ready.';
727
- await this.sleep(800);
728
- this.floatingInfoText = null;
729
- this.currentQuestionIndex = 0; // Ensure first question is selected
730
- this.askCurrentQuestion(); // Automatically ask the first question
731
- }
732
-
733
- // Refactor askCurrentQuestion to allow time for user to answer after silence
734
- public async askCurrentQuestion(): Promise<void> {
735
- if (this.currentQuestionIndex >= this.seedQuestions.length) {
736
- // All questions done
737
- this.floatingInfoText = 'Investigation complete. Camera stopped.';
738
- this.stopVideoRecording();
739
- this.stopCamera();
740
- setTimeout(() => {
741
- this.floatingInfoText = null;
742
- this.showSummary = true;
743
- }, 2000);
744
- return;
745
- }
746
-
747
- // Start camera and video recording for each question
748
  await this.startCamera();
749
  await this.startVideoRecording();
750
-
751
- // Show "Recording..." only during active questions
752
- this.isRecording = (this.currentQuestionIndex >= 0 && this.currentQuestionIndex < this.totalQuestions);
753
- this.infoText = 'Recording in progress – Asking question.';
754
-
755
- // Start audio recording in parallel
756
- this.startRecognitionWithRecording(this.currentQuestionIndex);
757
-
758
- await this.speakQuestion(this.currentQuestionText);
759
- await this.sleep(1000);
760
-
761
- this.infoText = 'Recording in progress – Listening to answer.';
762
-
763
- await this.sleep(10000); // Give user time to answer
764
-
765
- // Hide recording status bar and indicator before processing
766
- this.isRecording = false;
767
- // Stop audio and video recording after answer
768
- this.stopRecognition();
769
- this.stopVideoRecording();
770
- this.infoText = 'Processing your question and answer...';
771
- await this.sleep(5000); // Simulate backend save
772
-
773
- // Hide recording status bar and indicator before showing saved text
774
- this.isRecording = false;
775
- this.infoText = 'Saved successfully.';
776
- await this.sleep(1000);
777
- this.infoText = null;
778
-
779
- this.currentQuestionIndex++;
780
- await this.askCurrentQuestion();
781
- }
782
-
783
- // Move to next question
784
- public nextQuestion() {
785
- this.currentQuestionIndex++;
786
- if (this.currentQuestionIndex < this.seedQuestions.length) {
787
- this.askCurrentQuestion();
788
- } else {
789
- this.infoText = null;
790
- this.floatingInfoText = 'Session complete.';
791
- setTimeout(() => this.floatingInfoText = null, 2000);
792
  }
793
- }
794
-
795
- // Add evidence summary and file handling
796
- public evidenceSummary: string = '';
797
- public evidenceFiles: { document?: File, photo?: File, recording?: File } = {};
798
-
799
- public onEvidenceFileSelect(event: any, type: 'document' | 'photo' | 'recording') {
800
- const file = event.target.files && event.target.files[0];
801
- if (file) {
802
- this.evidenceFiles[type] = file;
 
 
 
 
 
 
 
 
 
 
 
803
  }
 
 
804
  }
805
 
806
- public uploadDocument() {
807
- if (this.evidenceFiles.document) {
808
- this.uploadedDocuments.push(this.evidenceFiles.document.name + (this.evidenceSummary ? ' - ' + this.evidenceSummary : ''));
809
- this.evidenceFiles.document = undefined;
810
- this.evidenceSummary = '';
811
- }
812
- if (this.evidenceFiles.photo) {
813
- this.capturedPhotos.push(this.evidenceFiles.photo.name);
814
- this.evidenceFiles.photo = undefined;
815
- }
816
- if (this.evidenceFiles.recording) {
817
- this.previousRecordings.push(this.evidenceFiles.recording.name);
818
- this.evidenceFiles.recording = undefined;
819
  }
820
- this.showEvidencePanel = false; // Automatically close the panel after submit
821
- }
822
-
823
- // Add missing properties and methods for workflow
824
- public currentQuestionIndex: number = -1;
825
- public get totalQuestions(): number { return this.seedQuestions.length; }
826
- public liveTranscription: string = '';
827
- public get currentQuestionText(): string {
828
- return this.currentQuestionIndex >= 0 ? this.seedQuestions[this.currentQuestionIndex] : '';
829
- }
830
-
831
- // TTS method
832
- public async speakQuestion(text: string): Promise<void> {
833
- return new Promise<void>((resolve) => {
834
- const utterance = new SpeechSynthesisUtterance(text);
835
- utterance.lang = 'en-IN';
836
- utterance.pitch = 1;
837
- utterance.rate = 1;
838
- utterance.volume = 1;
839
- utterance.onend = () => resolve();
840
- window.speechSynthesis.cancel();
841
- window.speechSynthesis.speak(utterance);
842
- });
843
- }
844
-
845
- // Speech recognition stub
846
- public startRecognitionWithRecording(idx: number) {
847
- const Ctor = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition;
848
- if (!Ctor) {
849
- alert('Speech Recognition not supported on this browser.');
850
- return;
851
  }
852
- this.recognition = new Ctor();
853
- this.recognition.lang = 'en-IN';
854
- this.recognition.continuous = true;
855
- this.recognition.interimResults = true;
856
- let hasStartedSpeaking = false;
857
- let silenceTimer: any = null;
858
- this.recognition.onresult = (event: any) => {
859
- const transcript = Array.from(event.results)
860
- .map((res: any) => res[0].transcript)
861
- .join(' ')
862
- .trim();
863
- this.liveTranscription = transcript; // <-- This is critical!
864
- if (transcript.length > 0 && !hasStartedSpeaking) {
865
- hasStartedSpeaking = true;
866
- this.isRecording = true;
867
- }
868
- // Reset silence detection timer
869
- clearTimeout(silenceTimer);
870
- silenceTimer = setTimeout(() => {
871
- if (hasStartedSpeaking) {
872
- // Only stop recognition, do not set isRecording = false here
873
- this.recognition.stop();
874
- }
875
- }, 2000); // Stop after 2 seconds of silence
876
- };
877
- this.recognition.onend = () => {
878
- clearTimeout(silenceTimer);
879
- // Do NOT set isRecording = false here
880
- this.status.set('processing');
881
- // Do NOT auto-advance to next question here. Wait for user to click Next Question.
882
  };
883
- this.recognition.onerror = (err: any) => {
884
- // Do NOT set isRecording = false here
885
- this.status.set('idle');
886
- };
887
- this.recognition.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
  }
889
-
890
- public closeSummary() {
891
- this.showSummary = false;
892
  }
893
-
894
- // Navigate to the validation page
895
- navigateToValidationPage() {
896
- this.router.navigate(['/validationpage']);
897
- }
898
-
899
- // Start timer when recording starts
900
- public startRecordingTimer(durationSeconds: number) {
901
- let elapsed = 0;
902
- let remaining = durationSeconds;
903
- this.elapsedTime = '00:00';
904
- this.remainingTime = this.formatTime(durationSeconds);
905
- clearInterval(this.recordingTimerInterval);
906
- this.recordingTimerInterval = setInterval(() => {
907
- if (this.isRecording) {
908
- elapsed++;
909
- remaining--;
910
- this.elapsedTime = this.formatTime(elapsed);
911
- this.remainingTime = this.formatTime(remaining);
912
- if (remaining <= 0) {
913
- clearInterval(this.recordingTimerInterval);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
  }
 
 
 
915
  }
916
- }, 1000);
917
- }
918
-
919
- public stopRecordingTimer() {
920
- clearInterval(this.recordingTimerInterval);
 
 
 
 
 
921
  }
 
922
 
923
- private formatTime(seconds: number): string {
924
- const m = Math.floor(seconds / 60).toString().padStart(2, '0');
925
- const s = (seconds % 60).toString().padStart(2, '0');
926
- return `${m}:${s}`;
927
- }
928
  }
 
3
  import { Router, NavigationStart } from '@angular/router';
4
  import { Subscription } from 'rxjs';
5
  import { FormsModule } from '@angular/forms';
6
+ import { PyDetectService } from '../services/pydetect.service';
7
 
8
  declare global {
9
  interface Window {
 
32
  styleUrls: ['./py-detect.component.css']
33
  })
34
  export class PyDetectComponent implements OnInit, OnDestroy {
35
+ // Store body language explanation for UI
36
+ public bodyLanguageExplanation: string | null = null;
37
+ public bodyLanguageMeaning: string | null = null;
38
+
39
+ // Fetch explanation for a body language cue from backend
40
+ public fetchBodyLanguageExplanation(cue: string) {
41
+ this.bodyLanguageExplanation = null;
42
+ this.bodyLanguageMeaning = null;
43
+ this.pyDetectService.bodyLanguageExplain(cue).subscribe({
44
+ next: (resp) => {
45
+ if (resp?.explanation) {
46
+ this.bodyLanguageExplanation = resp.explanation;
47
+ }
48
+ if (resp?.meaning) {
49
+ this.bodyLanguageMeaning = resp.meaning;
50
+ }
51
+ console.log('[PyDetect] Body Language:', {
52
+ meaning: resp?.meaning,
53
+ explanation: resp?.explanation
54
+ });
55
+ },
56
+ error: () => {
57
+ this.bodyLanguageExplanation = 'No explanation available.';
58
+ this.bodyLanguageMeaning = null;
59
+ console.warn('[PyDetect] No body language explanation available.');
60
+ }
61
+ });
62
+ }
63
+ // FER emotion result for UI display
64
+ public ferEmotion: string | null = null;
65
+ // Face detection score for UI display
66
+ public faceDetectionScore: number | null = null;
67
+ // --- Patch: Add missing properties for template and logic ---
68
+ public currentQuestionIndex: number = -1;
69
+ public totalQuestions: number = 0;
70
+ public currentQuestionText: string = '';
71
+ public evidenceSummary: string = '';
72
+ // Store the truth score for the last submitted answer
73
+ public truthScore: number | null = null;
74
+ // Timing & frame streaming additions
75
+ public questionWindowStartAt: number | null = null;
76
+ public answerStartAt: number | null = null;
77
+ public answerEndAt: number | null = null;
78
+ public answerMode: 'voice' | 'text' | 'mixed' = 'text';
79
+ private frameIntervalId: any;
80
+ private frameStreamingActive: boolean = false;
81
+ public involvementScore: number | null = null;
82
+ public involvementCues: string[] = [];
83
+ public dominantInvestigativeExpression: string | null = null;
84
+ public behaviorTagDistribution: Record<string, number> | null = null;
85
+ public guidanceCommand: string | null = null;
86
+
87
+ // --- Patch: Add missing stub methods for template bindings ---
88
+ public async speakQuestion(question: string) {
89
+ // Use TTS to speak the question (stub)
90
+ await this.speak(question);
91
+ }
92
+
93
+ public startRecognitionWithRecording(index: number) {
94
+ // Stub for starting recognition with recording
95
+ // You may want to start voice recording and speech recognition here
96
+ }
97
+
98
+ public async navigateToValidationPage() {
99
+ // Stop video recording and release camera
100
+ this.stopVideoRecording();
101
+ if (this.videoStream) {
102
+ this.videoStream.getTracks().forEach(t => t.stop());
103
+ this.videoStream = undefined;
104
+ }
105
+ this.isRecording = false;
106
+ // Wait for the video to finish processing if needed
107
+ await this.sleep(500); // Give time for onstop to fire and recordedVideoUrl to be set
108
+ // Automatically download the recorded video if available
109
+ if (this.recordedVideoUrl) {
110
+ const anchor = document.createElement('a');
111
+ anchor.href = this.recordedVideoUrl;
112
+ anchor.download = 'investigation-video.webm';
113
+ anchor.style.display = 'none';
114
+ document.body.appendChild(anchor);
115
+ anchor.click();
116
+ setTimeout(() => {
117
+ document.body.removeChild(anchor);
118
+ // Optionally revoke the object URL after download
119
+ // URL.revokeObjectURL(this.recordedVideoUrl);
120
+ }, 100);
121
+ }
122
+ // Then navigate to validation page
123
+ this.router.navigate(['/validationpage']);
124
+ }
125
+
126
+ public uploadDocument() {
127
+ // Stub for document upload logic
128
+ // You may want to handle file upload here
129
+ }
130
+
131
+ public onEvidenceFileSelect(event: any, type: string) {
132
+ // Stub for evidence file selection logic
133
+ // You may want to process selected files here
134
+ }
135
+ // Manual answer submission for testing
136
+ public submitTextAnswer() {
137
+ if (!this.textAnswer || !this.sessionId || this.currentQuestionIndex < 0 || !this.questions[this.currentQuestionIndex]) {
138
+ this.infoText = 'Please enter an answer and ensure a question is active.';
139
+ return;
140
+ }
141
+ // Call backend to submit response
142
+ this.pyDetectService.submitResponse(
143
+ this.sessionId,
144
+ this.textAnswer,
145
+ this.questions[this.currentQuestionIndex]
146
+ ).subscribe({
147
+ next: async (res) => {
148
+ // Extract truth score if present
149
+ this.truthScore = (res && (res.truth_score || res.score)) ? Number(res.truth_score || res.score) : null;
150
+ this.infoText = 'Answer submitted.' + (this.truthScore !== null ? ` Truth Score: ${this.truthScore}` : '');
151
+ this.textAnswer = '';
152
+ // Fetch body language explanation for the first involvement cue
153
+ if (this.involvementCues.length) {
154
+ this.fetchBodyLanguageExplanation(this.involvementCues[0]);
155
+ }
156
+ const response = await this.pyDetectService.askQuestion(
157
+ this.sessionId,
158
+ this.crimeType,
159
+ this.briefDescription
160
+ ).toPromise();
161
+ if (response && response.question) {
162
+ this.questions.push(response.question);
163
+ this.currentQuestionIndex++;
164
+ this.cdr.detectChanges();
165
+ await this.speakQuestion(response.question);
166
+ } else {
167
+ this.infoText = 'No more questions.';
168
+ }
169
+ },
170
+ error: (err) => {
171
+ this.infoText = 'Error submitting answer.';
172
+ }
173
+ });
174
+ }
175
  public showDetailsPanel: boolean = false;
176
  public metadata: any = null;
177
 
178
+ // Backend-driven session and investigation state
179
+ sessionId: string = '';
180
+ caseData: any = null;
181
+ briefDescription: string = '';
182
+ isSessionStarted: boolean = false;
183
+ isLoading: boolean = false;
184
+ currentQuestion: string = '';
185
+ textAnswer: string = '';
186
+ lastAnalysisResult: any = null;
187
+ questionCount: number = 0;
188
+ currentInvestigationStage: string = 'Initial Investigation';
189
+ questionNumber: number = 1;
190
+ cameraActive: boolean = false;
191
+ voiceRecordingActive: boolean = false;
192
+ investigationActive: boolean = false;
193
+ investigationStarted: boolean = false;
194
+ caseSummary: any = null;
195
+ processingResponse: boolean = false;
196
+ videoStatus: string = 'Camera Ready';
197
+ ttsEnabled: boolean = false;
198
+ isListening: boolean = false;
199
+ speechRecognition: any = null;
200
+ // Combined answer submission: prefer text box, fallback to transcript
201
+ public submitCombinedAnswer() {
202
+ // Accept answer from text box or voice transcript
203
+ let answerText = (this.textAnswer && this.textAnswer.trim()) ? this.textAnswer.trim() : (this.transcriptSoFar && this.transcriptSoFar.trim()) ? this.transcriptSoFar.trim() : '';
204
+ if (!answerText || !this.sessionId || !this.questions[this.currentQuestionIndex]) {
205
+ this.infoText = 'Please provide your answer before submitting.';
206
+ return;
207
+ }
208
+ this.stopAudioRecording();
209
+ this.infoText = 'Submitting answer...';
210
+ this.textAnswer = '';
211
+ this.transcriptSoFar = '';
212
+ const endTs = Date.now();
213
+ this.answerEndAt = endTs;
214
+ if (!this.answerStartAt) this.answerStartAt = this.questionWindowStartAt || endTs;
215
+ const durationMs = this.answerEndAt - this.answerStartAt;
216
+ this.stopFrameStreaming();
217
+ this.pyDetectService.submitResponse(
218
+ this.sessionId,
219
+ answerText,
220
+ this.questions[this.currentQuestionIndex],
221
+ {
222
+ answer_start_at: this.answerStartAt,
223
+ answer_end_at: this.answerEndAt,
224
+ duration_ms: durationMs,
225
+ mode: this.answerMode
226
+ }
227
+ ).subscribe({
228
+ next: async (res) => {
229
+ // Extract truth score if present
230
+ this.truthScore = (res && (res.truth_score || res.score)) ? Number(res.truth_score || res.score) : null;
231
+ this.infoText = 'Answer submitted.' + (this.truthScore !== null ? ` Truth Score: ${this.truthScore}` : '');
232
+ // Pull involvement metrics
233
+ this.fetchLatestInvolvement();
234
+ // Fetch next question from backend
235
+ const response = await this.pyDetectService.askQuestion(
236
+ this.sessionId,
237
+ this.crimeType,
238
+ this.briefDescription
239
+ ).toPromise();
240
+ if (response && response.question) {
241
+ this.questions.push(response.question);
242
+ this.currentQuestionIndex++;
243
+ this.questionNumber = this.currentQuestionIndex + 1;
244
+ this.cdr.detectChanges();
245
+ await this.startCamera();
246
+ await this.startVideoRecording();
247
+ await this.speakQuestion(response.question);
248
+ // Restart window for next question
249
+ this.startQuestionWindow();
250
+ // Reset answer timing
251
+ this.answerStartAt = null;
252
+ this.answerEndAt = null;
253
+ this.answerMode = 'text';
254
+ } else {
255
+ this.infoText = 'No more questions.';
256
+ this.showSummary = true;
257
+ }
258
+ },
259
+ error: () => {
260
+ this.infoText = 'Error submitting answer.';
261
+ }
262
+ });
263
+ }
264
+ public stopAudioRecording() {
265
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
266
+ this.mediaRecorder.stop();
267
+ // Show the transcribed answer in the text box
268
+ this.textAnswer = this.transcriptSoFar;
269
+ this.infoText = 'Voice recording stopped.';
270
+ // Stop speech recognition when recording stops
271
+ if (this.recognition) {
272
+ try { this.recognition.stop(); } catch {}
273
+ }
274
+ }
275
+ }
276
+ speechSynthesis: any = null;
277
+ voiceSupported: boolean = false;
278
+ microphoneSupported: boolean = false;
279
+ microphonePermissionDenied: boolean = false;
280
+ permissionStatus: string = 'unknown';
281
 
282
  // ---- TTS active flag ----
283
  private isActive = false;
284
 
285
  // ---- Q/A data ----
286
+ // log: QAResult[] = [];
 
287
  log: QAResult[] = [];
288
 
289
 
290
  // ---- Constructor with Router Injection ----
291
  private routerSubscription?: Subscription;
292
 
293
+ constructor(
294
+ private router: Router,
295
+ private cdr: ChangeDetectorRef,
296
+ private pyDetectService: PyDetectService
297
+ ) {
298
  // Cancel TTS on any navigation away
299
  this.routerSubscription = this.router.events.subscribe(event => {
300
  if (event instanceof NavigationStart) {
 
316
 
317
  private pitchSamples: number[] = [];
318
  private volumeSamples: number[] = [];
319
+ private analyserBuffer: Float32Array = new Float32Array(2048);
320
  private analyserTimer?: any;
321
 
322
  // ---- Speech Recognition ----
 
331
  private analyserWindowMs = 100; // Declare analyserWindowMs property
332
 
333
  // Example question source (replace with API call when ready)
334
+ // Remove legacy seedQuestions
335
+ public questions: string[] = [];
 
 
 
 
 
336
 
337
  // Button state signals
338
+ // Remove legacy button state signals
 
 
 
339
 
340
  // Add missing public methods and properties for template binding
341
+ // videoStatus already declared above for backend workflow
342
  public videoStream?: MediaStream;
 
343
  @ViewChild('videoElement', { static: false }) videoElement?: ElementRef<HTMLVideoElement>;
344
  public videoChunks: Blob[] = [];
345
  public videoAnswers: Blob[] = [];
346
+ public videoRecorder?: MediaRecorder;
347
+ public recordedVideoUrl: string = '';
348
 
349
  // UI properties for template
350
  caseId: string = '';
 
458
  this.progress = metadata.progress || 0;
459
  this.progressStage = metadata.progressStage || '';
460
  this.sessionTime = metadata.sessionTime || '';
461
+ this.briefDescription = metadata.briefDescription || '';
462
  }
463
  }
464
 
 
469
  }
470
  this.cleanupAll();
471
  this.stopVideoRecording();
 
 
472
  this.videoStatus = '';
473
  if (this.videoStream) {
474
  this.videoStream.getTracks().forEach(t => t.stop());
 
481
  }
482
 
483
  // ======== Main flow ========
484
+ // Legacy start method removed. Use backend-driven workflow only.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
+ // Legacy stopAll method removed. Use backend-driven workflow only.
487
 
488
+ // Legacy resume method removed. Use backend-driven workflow only.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
  // ======== Question source ========
491
  private async fetchNextQuestion(): Promise<string> {
492
  // Replace this with HTTP call to your backend if needed.
493
  // Example: const { question } = await this.http.get<{question:string}>('/api/next-question').toPromise();
494
+ // Legacy fetchNextQuestion logic removed. Use backend-driven workflow only.
495
+ return '';
496
  }
497
 
498
  // ======== TTS (question playback) ========
 
535
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
536
  video: false
537
  });
538
+ // ...existing code...
539
 
540
  // 2) prepare MediaRecorder
541
  const mime = this.chooseMimeType();
 
567
  this.stopAnalyser();
568
  this.stopRecognition();
569
  this.cleanupMediaStream();
570
+ // ...existing code...
571
 
572
  // 7) build audio URL
573
  const blob = new Blob(this.audioChunks, { type: mime });
 
586
  private waitForSilenceOrContinue() {
587
  if (this.silenceTimeout) clearTimeout(this.silenceTimeout);
588
  this.silenceTimeout = setTimeout(() => {
589
+ // ...existing code...
590
+ }, 5002); // Timeout after 5 seconds of silence
591
  }
592
 
593
 
 
612
  this.analyser.fftSize = 2048;
613
  this.sourceNode.connect(this.analyser);
614
 
615
+ // Use correct constructor for Float32Array
616
+ this.analyserBuffer = new Float32Array(this.analyser.fftSize);
617
 
618
  const tick = () => {
619
  if (!this.analyser || !this.analyserBuffer) return;
 
775
  }
776
  }
777
 
778
+ // ===== Frame streaming for nonverbal analysis =====
779
+ private startQuestionWindow() {
780
+ this.questionWindowStartAt = Date.now();
781
+ this.answerStartAt = null;
782
+ this.answerEndAt = null;
783
+ this.answerMode = 'text'; // default until voice starts
784
+ this.startFrameStreaming();
785
+ }
786
+
787
+ private startFrameStreaming() {
788
+ if (this.frameStreamingActive) return;
789
+ if (!this.videoElement?.nativeElement) return;
790
+ const videoEl = this.videoElement.nativeElement;
791
+ const canvas = document.createElement('canvas');
792
+ canvas.width = 320;
793
+ canvas.height = 240;
794
+ const ctx = canvas.getContext('2d');
795
+ if (!ctx) return;
796
+ this.frameStreamingActive = true;
797
+ this.frameIntervalId = setInterval(() => {
798
+ if (!this.frameStreamingActive) return;
799
+ try {
800
+ ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
801
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.6);
802
+ if (this.sessionId) {
803
+ this.pyDetectService.faceFrame(this.sessionId, dataUrl).subscribe({
804
+ next: (resp) => {
805
+ if (resp?.metrics?.emotion) this.ferEmotion = resp.metrics.emotion;
806
+ if (resp?.command) this.guidanceCommand = resp.command;
807
+ },
808
+ error: () => { /* ignore */ }
809
+ });
810
+ }
811
+ } catch { /* ignore */ }
812
+ }, 150);
813
+ }
814
+
815
+ private stopFrameStreaming() {
816
+ if (this.frameIntervalId) clearInterval(this.frameIntervalId);
817
+ this.frameIntervalId = null;
818
+ this.frameStreamingActive = false;
819
+ }
820
+
821
+ // Capture text start when user focuses the answer textarea
822
+ public captureTextStart() {
823
+ if (!this.answerStartAt) {
824
+ this.answerStartAt = Date.now();
825
+ if (this.answerMode === 'voice') {
826
+ this.answerMode = 'mixed';
827
+ } else {
828
+ this.answerMode = 'text';
829
+ }
830
+ }
831
+ }
832
+
833
+ // Fetch latest involvement metrics (defined here to resolve reference)
834
+ private fetchLatestInvolvement() {
835
+ if (!this.sessionId) return;
836
+ this.pyDetectService.getReport(this.sessionId).subscribe({
837
+ next: (report) => {
838
+ const responses = report?.responses || [];
839
+ if (!responses.length) return;
840
+ const last = responses[responses.length - 1];
841
+ const assess = last?.investigative_assessment;
842
+ const fb = last?.face_body?.metrics;
843
+ if (assess) {
844
+ this.involvementScore = typeof assess.involvement_score === 'number' ? assess.involvement_score : null;
845
+ this.involvementCues = Array.isArray(assess.cues) ? assess.cues : [];
846
+ }
847
+ if (fb) {
848
+ this.dominantInvestigativeExpression = fb.dominant_investigative_expression || null;
849
+ this.behaviorTagDistribution = fb.behavior_tag_distribution || null;
850
+ }
851
+ this.cdr.detectChanges();
852
+ },
853
+ error: () => { /* silent fail */ }
854
+ });
855
+ }
856
+
857
  public async startVideoRecording() {
858
  if (!this.videoStream) return;
859
+ // Prevent double-start
860
+ if (this.videoRecorder && this.videoRecorder.state === 'recording') {
861
+ console.warn('[PyDetect] Video recording already in progress.');
862
+ return;
863
+ }
864
  this.videoChunks = [];
865
  this.videoRecorder = new MediaRecorder(this.videoStream, { mimeType: 'video/webm' });
866
+ this.videoRecorder.ondataavailable = (e: BlobEvent) => {
867
  if (e.data && e.data.size > 0) this.videoChunks.push(e.data);
868
  };
 
 
 
869
  this.videoRecorder.onstop = () => {
 
870
  const videoBlob = new Blob(this.videoChunks, { type: 'video/webm' });
871
+ this.recordedVideoUrl = URL.createObjectURL(videoBlob);
872
+ console.log('[PyDetect] Video recording complete. Blob URL:', this.recordedVideoUrl);
873
+ if (this.videoStream) {
874
+ this.videoStream.getTracks().forEach(t => t.stop());
875
+ this.videoStream = undefined;
876
+ }
877
+ this.cdr.detectChanges();
878
  };
879
  this.videoRecorder.start();
880
+ console.log('[PyDetect] Video recording started.');
881
  }
882
 
883
  public stopVideoRecording() {
884
+ // Prevent double-stop
885
+ if (this.videoRecorder && this.videoRecorder.state === 'recording') {
886
  this.videoRecorder.stop();
887
+ // The onstop handler will release the camera and update the UI
888
+ } else {
889
+ console.warn('[PyDetect] Video recording already stopped or not started.');
890
  }
891
  }
892
 
893
+ // Call startVideoRecording in onStartInvestigation
894
+ public async onStartInvestigation() {
895
+ this.isLoading = true;
896
+ this.infoText = 'Starting investigation...';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
  await this.startCamera();
898
  await this.startVideoRecording();
899
+ await this.startSession();
900
+ // Ensure questions are loaded and index is set
901
+ // Use fallback logic for brief description
902
+ let briefDescriptionToSend = this.briefDescription?.trim() || '';
903
+ if (!briefDescriptionToSend) {
904
+ briefDescriptionToSend =
905
+ sessionStorage.getItem('briefDescription')?.trim() ||
906
+ this.caseData?.briefDescription?.trim() ||
907
+ this.caseData?.police?.information?.trim() ||
908
+ this.caseData?.crime?.trim() ||
909
+ '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
910
  }
911
+ const response = await this.pyDetectService.askQuestion(
912
+ this.sessionId,
913
+ this.crimeType,
914
+ briefDescriptionToSend
915
+ ).toPromise();
916
+ if (response && response.question) {
917
+ this.questions = [response.question]; // Wrap single question in array
918
+ this.currentQuestionIndex = 0;
919
+ this.cdr.detectChanges(); // Force UI update after async
920
+ this.isRecording = true;
921
+ this.infoText = 'Recording in progress – Asking question.';
922
+ this.startRecognitionWithRecording(this.currentQuestionIndex);
923
+ // Start the question window BEFORE speaking so question time included
924
+ this.startQuestionWindow();
925
+ // Speak the first question using TTS (inside window)
926
+ await this.speakQuestion(this.questions[0]);
927
+ this.infoText = 'Recording in progress – Listening to answer.';
928
+ } else {
929
+ this.questions = [];
930
+ this.currentQuestionIndex = -1;
931
+ this.cdr.detectChanges();
932
  }
933
+ this.infoText = 'Investigation started. Please answer the question.';
934
+ this.isLoading = false;
935
  }
936
 
937
+ // Backend-driven session start and first question fetch
938
+ public async startSession(): Promise<void> {
939
+ try {
940
+ this.isLoading = true;
941
+ if (this.voiceSupported) {
942
+ this.ttsEnabled = true;
943
+ setTimeout(() => {
944
+ this.speakQuestion('Investigation starting. I will ask you questions and you can respond using voice or text.');
945
+ }, 1000);
 
 
 
 
946
  }
947
+ let briefDescriptionToSend = this.briefDescription?.trim() || '';
948
+ if (!briefDescriptionToSend) {
949
+ briefDescriptionToSend =
950
+ sessionStorage.getItem('briefDescription')?.trim() ||
951
+ this.caseData?.briefDescription?.trim() ||
952
+ this.caseData?.police?.information?.trim() ||
953
+ this.caseData?.crime?.trim() ||
954
+ '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
955
  }
956
+ const sessionResponse = await this.pyDetectService.startSession(briefDescriptionToSend).toPromise();
957
+ this.sessionId = sessionResponse.session_id;
958
+ sessionStorage.setItem('sessionId', this.sessionId);
959
+ localStorage.setItem('sessionId', this.sessionId);
960
+ const caseData = this.caseData || {};
961
+ const caseDataToSend = {
962
+ ...caseData,
963
+ brief_description: briefDescriptionToSend
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
  };
965
+ await this.pyDetectService.submitCaseDetails(
966
+ this.sessionId,
967
+ caseDataToSend,
968
+ briefDescriptionToSend
969
+ ).toPromise();
970
+ if (briefDescriptionToSend && briefDescriptionToSend.length > 0) {
971
+ const questionsResponse = await this.pyDetectService.askQuestion(
972
+ this.sessionId,
973
+ this.crimeType,
974
+ briefDescriptionToSend
975
+ ).toPromise();
976
+ if (questionsResponse && questionsResponse.questions && questionsResponse.questions.length > 0) {
977
+ this.questions = questionsResponse.questions;
978
+ this.currentQuestion = this.questions[0];
979
+ this.currentQuestionIndex = 0;
980
+ this.questionCount = this.questions.length;
981
+ this.questionNumber = 1;
982
+ // Optionally, update UI to show the first question
983
+ } else {
984
+ this.questions = [];
985
+ this.currentQuestionIndex = -1;
986
+ }
987
+ }
988
+ this.isSessionStarted = true;
989
+ this.investigationStarted = true;
990
+ this.investigationActive = true;
991
+ this.isLoading = false;
992
+ } catch (error) {
993
+ alert('Failed to connect to backend. Please check if the Flask server is running on port 5002.');
994
+ this.isLoading = false;
995
  }
 
 
 
996
  }
997
+ public isVoiceRecording: boolean = false;
998
+
999
+ // Start audio recording and speech recognition
1000
+ public async startAudioRecording() {
1001
+ // Debounce: Prevent rapid start
1002
+ if (this.isVoiceRecording) return;
1003
+ // Clear previous answer before starting new recording
1004
+ this.textAnswer = '';
1005
+ console.log('[PyDetect] Voice recording started.');
1006
+ try {
1007
+ // Request microphone access
1008
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
1009
+ // Setup MediaRecorder
1010
+ const mimeType = this.chooseMimeType();
1011
+ this.mediaRecorder = new MediaRecorder(this.mediaStream, { mimeType });
1012
+ this.audioChunks = [];
1013
+ this.mediaRecorder.ondataavailable = (e) => {
1014
+ if (e.data && e.data.size > 0) this.audioChunks.push(e.data);
1015
+ };
1016
+ this.mediaRecorder.onstop = () => {
1017
+ console.log('[PyDetect] Voice recording stopped.');
1018
+ // Stop speech recognition when recording stops
1019
+ if (this.recognition) {
1020
+ try { this.recognition.stop(); } catch {}
1021
+ }
1022
+ // UI feedback if no transcript
1023
+ if (!this.transcriptSoFar) {
1024
+ this.infoText = 'No voice detected. Please try again or type your answer.';
1025
+ } else {
1026
+ this.infoText = 'Voice recording stopped.';
1027
+ }
1028
+ };
1029
+ this.mediaRecorder.start();
1030
+ // Setup speech recognition
1031
+ const Ctor = window.webkitSpeechRecognition || window.SpeechRecognition;
1032
+ if (Ctor) {
1033
+ this.recognition = new Ctor();
1034
+ this.recognition.lang = 'en-IN';
1035
+ this.recognition.continuous = true;
1036
+ this.recognition.interimResults = false;
1037
+ this.transcriptSoFar = '';
1038
+ this.recognition.onstart = () => {
1039
+ this.infoText = 'Listening...';
1040
+ };
1041
+ this.recognition.onresult = (event: any) => {
1042
+ let finalText = '';
1043
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1044
+ const result = event.results[i];
1045
+ if (result.isFinal) {
1046
+ finalText += result[0].transcript.trim();
1047
+ }
1048
+ }
1049
+ this.transcriptSoFar = finalText.trim();
1050
+ this.textAnswer = this.transcriptSoFar;
1051
+ };
1052
+ this.recognition.onerror = (error: any) => {
1053
+ this.infoText = 'Speech recognition error: ' + error.error;
1054
+ };
1055
+ this.recognition.onend = () => {
1056
+ if (!this.transcriptSoFar) {
1057
+ this.infoText = 'No voice detected. Please try again or type your answer.';
1058
+ } else {
1059
+ this.infoText = 'Voice recording stopped.';
1060
+ }
1061
+ };
1062
+ this.recognition.start();
1063
+ } else {
1064
+ this.infoText = 'Speech Recognition not supported.';
1065
  }
1066
+ this.isVoiceRecording = true;
1067
+ } catch (err) {
1068
+ this.infoText = 'Could not start audio recording.';
1069
  }
1070
+ }
1071
+ public async toggleVoiceRecording() {
1072
+ if (this.isVoiceRecording) {
1073
+ this.stopAudioRecording();
1074
+ this.isVoiceRecording = false;
1075
+ this.infoText = 'Voice recording stopped.';
1076
+ } else {
1077
+ await this.startAudioRecording();
1078
+ this.isVoiceRecording = true;
1079
+ this.infoText = 'Voice recording started. Speak your answer.';
1080
  }
1081
+ }
1082
 
1083
+ // Ensure the class is properly closed
 
 
 
 
1084
  }
src/app/py-detect/test-video.component.html ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <video #videoElement width="400" height="300" autoplay playsinline></video>
2
+ <div *ngIf="!videoStream">Camera is not active.</div>
src/app/question-data.service.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@angular/core';
2
+
3
+ @Injectable({ providedIn: 'root' })
4
+ export class QuestionDataService {
5
+ private questions: any[] = [];
6
+ private caseDetails: any = {};
7
+
8
+ setQuestions(questions: any[]) {
9
+ this.questions = questions;
10
+ }
11
+ getQuestions() {
12
+ return this.questions;
13
+ }
14
+
15
+ setCaseDetails(details: any) {
16
+ this.caseDetails = details;
17
+ }
18
+ getCaseDetails() {
19
+ return this.caseDetails;
20
+ }
21
+ }
src/app/question-summary-page/question-summary-page.component.css ADDED
@@ -0,0 +1,821 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
2
+
3
+
4
+
5
+ /* Modern UI header styles from infopage */
6
+ .site-header {
7
+ background: #011329;
8
+ box-shadow: 0 2px 12px #38bdf844;
9
+ margin-bottom: 0;
10
+ position: relative;
11
+ z-index: 10;
12
+ padding-bottom: 0;
13
+ }
14
+ .header-inner {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: space-between;
18
+ padding: 18px 32px 0 32px;
19
+ position: relative;
20
+ }
21
+
22
+ .logo-cluster {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 18px;
26
+ }
27
+
28
+ .logo-img-header {
29
+ width: 54px;
30
+ height: 54px;
31
+ border-radius: 50%;
32
+ background: #fff;
33
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
34
+ padding: 4px;
35
+ margin-top: -6px;
36
+ margin-bottom: 1vh;
37
+ }
38
+
39
+ .py-detect-title-header {
40
+ font-size: 2.1rem;
41
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
42
+ font-weight: 900;
43
+ letter-spacing: 6px;
44
+ color: #38bdf8;
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 2px;
48
+ margin-bottom: 1.5vh;
49
+ }
50
+
51
+ .py-detect-title-header .py-letter.p {
52
+ color: #e3f6ff;
53
+ text-shadow: 0 0 6px #38bdf8;
54
+ }
55
+
56
+ .py-detect-title-header .py-letter.y {
57
+ color: #38bdf8;
58
+ text-shadow: 0 0 6px #38bdf8;
59
+ }
60
+
61
+ .py-detect-title-header .py-shape {
62
+ color: #e3f6ff;
63
+ background: #e3f6ff;
64
+ text-shadow: 0 0 6px #38bdf8;
65
+ box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
66
+ border: 2px solid #23272b;
67
+ width: 18px;
68
+ height: 4px;
69
+ display: inline-block;
70
+ margin: 0 8px;
71
+ border-radius: 2px;
72
+ }
73
+
74
+ .py-detect-title-header .py-letter.d {
75
+ color: #e3f6ff;
76
+ text-shadow: 0 0 6px #38bdf8;
77
+ }
78
+
79
+ .py-detect-title-header .py-letter.e {
80
+ color: #38bdf8;
81
+ text-shadow: 0 0 6px #38bdf8;
82
+ }
83
+
84
+ .py-detect-title-header .py-letter.t {
85
+ color: #e3f6ff;
86
+ text-shadow: 0 0 6px #38bdf8;
87
+ }
88
+
89
+ .py-detect-title-header .py-letter.e2 {
90
+ color: #38bdf8;
91
+ text-shadow: 0 0 6px #38bdf8;
92
+ }
93
+
94
+ .py-detect-title-header .py-letter.c {
95
+ color: #e3f6ff;
96
+ text-shadow: 0 0 6px #38bdf8;
97
+ }
98
+
99
+ .py-detect-title-header .py-letter.t2 {
100
+ color: #38bdf8;
101
+ text-shadow: 0 0 6px #38bdf8;
102
+ }
103
+
104
+
105
+ .question-summary-fullpage {
106
+ font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
107
+ background: linear-gradient(120deg, #e0f7fa0%, #f8fafc100%);
108
+ padding: 0 0 32px 0;
109
+ padding-bottom: 32px;
110
+
111
+ }
112
+ .qs-header {
113
+ background: linear-gradient(90deg, #f0f9ff 0%, #dbeafe 100%);
114
+ color: #2563eb;
115
+ padding: 10px 18px 0;
116
+ border-radius: 0 0 32px 32px;
117
+ box-shadow: 0 8px 32px #2563eb22, 0 2px 16px #38bdf822;
118
+ margin-bottom: 32px;
119
+ text-align: center;
120
+ }
121
+ .qs-header-title {
122
+ font-size:2.3rem;
123
+ font-weight:900;
124
+ letter-spacing:2px;
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: center;
128
+ gap:18px;
129
+ }
130
+ .qs-logo {
131
+ font-size:2.5rem;
132
+ filter: drop-shadow(02px8px #38bdf8cc);
133
+ }
134
+ .qs-title-main {
135
+ font-size:2.1rem;
136
+ font-weight:900;
137
+ letter-spacing:2px;
138
+ }
139
+ .qs-header-meta {
140
+ margin-top:12px;
141
+ font-size:1.08rem;
142
+ display: flex;
143
+ flex-wrap: wrap;
144
+ gap:18px;
145
+ justify-content: center;
146
+ align-items: center;
147
+ }
148
+ .qs-meta-label {
149
+ color: #0e0f10b5;
150
+ font-weight: 700;
151
+ margin-right: 2px;
152
+ }
153
+ .qs-verdict {
154
+ color: #22c55e;
155
+ font-weight:900;
156
+ font-size:1.13em;
157
+ }
158
+
159
+ .qs-summary-section {
160
+ display: flex;
161
+ flex-wrap: wrap;
162
+ gap:32px;
163
+ justify-content: center;
164
+ margin-bottom:24px;
165
+ }
166
+ .qs-summary-block, .qs-observations-block {
167
+ background: #fff;
168
+ border-radius:1.2rem;
169
+ box-shadow:0 2px 12px #2563eb22;
170
+ padding:18px 24px 12px 24px;
171
+ min-width:320px;
172
+ max-width:480px;
173
+ flex:11320px;
174
+ color: #23272b;
175
+ }
176
+ .qs-summary-title, .qs-observations-title {
177
+ color: #2563eb;
178
+ font-weight:800;
179
+ font-size:1.18em;
180
+ margin-bottom:8px;
181
+ }
182
+ .qs-summary-text, .qs-observations-text {
183
+ color: #23272b;
184
+ font-size:1.07em;
185
+ }
186
+
187
+ .qs-extra-section {
188
+ margin:32px auto 0 auto;
189
+ max-width:700px;
190
+ background: #fff;
191
+ border-radius:1.2rem;
192
+ box-shadow:0 2px 12px #2563eb22;
193
+ padding:18px 24px 12px 24px;
194
+ color: #23272b;
195
+ }
196
+ .qs-extra-title {
197
+ color: #2563eb;
198
+ font-weight:800;
199
+ font-size:1.13em;
200
+ margin-bottom:8px;
201
+ }
202
+ .qs-extra-list {
203
+ list-style: none;
204
+ padding:0;
205
+ margin:0;
206
+ color: #23272b;
207
+ font-size:1.05em;
208
+ }
209
+ .qs-extra-list li {
210
+ margin-bottom:4px;
211
+ padding-left:0.5em;
212
+ }
213
+
214
+ .qs-footer {
215
+ margin-top:32px;
216
+ text-align: center;
217
+ color: #64748b;
218
+ font-size:1.01em;
219
+ display: flex;
220
+ flex-direction: column;
221
+ align-items: center;
222
+ gap:8px;
223
+ }
224
+ .qs-btn-back {
225
+ background: linear-gradient(90deg,#64748b,#2563eb);
226
+ color: #fff;
227
+ border: none;
228
+ border-radius:1.2rem;
229
+ padding:0.5rem 1.2rem;
230
+ font-size:1.01rem;
231
+ font-weight:700;
232
+ letter-spacing:1px;
233
+ box-shadow:0 1px 8px #38bdf888;
234
+ cursor: pointer;
235
+ transition: background 0.3s, box-shadow 0.3s, color 0.2s, transform 0.2s, border 0.2s;
236
+ outline: none;
237
+ margin-top:8px;
238
+ }
239
+ .qs-btn-back:hover {
240
+ box-shadow:0 4px 12px #38bdf888,0 1px 8px #2563eb44;
241
+ transform: scale(1.04);
242
+ border:2px solid #38bdf8;
243
+ }
244
+ .qs-footer-brand {
245
+ margin-top:8px;
246
+ font-size:0.98em;
247
+ color: #b0b0b0;
248
+ }
249
+ .download-btn {
250
+ background: linear-gradient(90deg,#38bdf8,#2563eb);
251
+ color: #fff;
252
+ border: none;
253
+ border-radius:1.2rem;
254
+ padding:0.5rem 1.2rem;
255
+ font-size:1.01rem;
256
+ font-weight:700;
257
+ letter-spacing:1px;
258
+ box-shadow:0 1px 8px #38bdf888;
259
+ cursor: pointer;
260
+ margin-top:18px;
261
+ margin-bottom:0;
262
+ transition: background 0.3s, box-shadow 0.3s, color 0.2s, transform 0.2s, border 0.2s;
263
+ }
264
+ .download-btn:hover {
265
+ box-shadow:0 4px 12px #38bdf888,0 1px 8px #2563eb44;
266
+ transform: scale(1.04);
267
+ border:2px solid #2563eb;
268
+ }
269
+ .download-btn-container {
270
+ margin-bottom:0 !important;
271
+ }
272
+
273
+ /* Footer */
274
+ footer {
275
+ background: linear-gradient(to right, #011022, #01030a);
276
+ color: #fff;
277
+ text-align: center;
278
+ padding: 10px 0px;
279
+ position: fixed;
280
+ left: 0;
281
+ bottom: 0;
282
+ width: 100%;
283
+ z-index: 100;
284
+ margin-top: 0;
285
+ }
286
+
287
+ .video-card {
288
+ display: flex;
289
+ flex-direction: column;
290
+ }
291
+
292
+ .video-metrics {
293
+ margin-left: auto;
294
+ max-width:340px;
295
+ width:100%;
296
+ text-align: right;
297
+ }
298
+
299
+ .qs-video-metrics-card {
300
+ background: #fff;
301
+ border-radius:14px;
302
+ box-shadow:0 4px 24px rgba(0,0,0,0.08);
303
+ padding:20px 18px;
304
+ min-width:420px;
305
+ max-width:600px;
306
+ display: flex;
307
+ flex-direction: column;
308
+ align-items: stretch;
309
+ }
310
+
311
+ .metrics-sections-row {
312
+ display: flex;
313
+ flex-direction: row;
314
+ gap:18px;
315
+ justify-content: space-between;
316
+ min-width:380px;
317
+ width:100%;
318
+ max-width:560px;
319
+ }
320
+
321
+ .metrics-section {
322
+ flex:110;
323
+ min-width:0;
324
+ }
325
+
326
+ .audio-analysis-section {
327
+ border-right:1px solid #e0e7ef;
328
+ padding-right:12px;
329
+ margin-right:12px;
330
+ }
331
+
332
+ .video-analysis-section {
333
+ padding-left:12px;
334
+ }
335
+
336
+ .metrics-section-title {
337
+ font-weight:600;
338
+ color: #2563eb;
339
+ margin-bottom:6px;
340
+ font-size:1.05rem;
341
+ }
342
+
343
+ .audio-badges {
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap:8px;
347
+ margin-bottom:4px;
348
+ }
349
+ .audio-badge {
350
+ background: #e0f7fa;
351
+ color: #2563eb;
352
+ border-radius:8px;
353
+ padding:4px 10px;
354
+ font-size:0.95rem;
355
+ font-weight:500;
356
+ display: inline-block;
357
+ margin-bottom:2px;
358
+ }
359
+ .audio-badge.truth { background: #d1fae5; color: #059669; }
360
+ .audio-badge.emotion { background: #fef3c7; color: #b45309; }
361
+ .audio-badge.duration { background: #e0e7ff; color: #3730a3; }
362
+
363
+ .metrics-grid {
364
+ width:100%;
365
+ display: flex;
366
+ flex-direction: column;
367
+ gap:8px;
368
+ }
369
+ .metric-row {
370
+ display: flex;
371
+ justify-content: space-between;
372
+ align-items: center;
373
+ }
374
+ .metric-label {
375
+ color: #2563eb;
376
+ font-weight:500;
377
+ }
378
+ .metric-value {
379
+ font-weight:600;
380
+ color: #0a192f;
381
+ }
382
+
383
+ .excel-table {
384
+ border-collapse: collapse;
385
+ width:100%;
386
+ outline:10px solid rgba(100,116,139,0.13); /* subtle gray outline */
387
+ box-shadow: none;
388
+ }
389
+ .excel-table th, .excel-table td {
390
+ border:1px solid rgba(100,116,139,0.10); /* faint gray cell borders */
391
+ }
392
+ .excel-table.summary {
393
+ margin-top:32px;
394
+ outline:2px solid rgba(100,116,139,0.10);
395
+ width:100%;
396
+ border-collapse: collapse;
397
+ background: #f8fafc;
398
+ border-radius:12px;
399
+ box-shadow:0 2px 12px #38bdf822;
400
+ }
401
+ .excel-table.summary th {
402
+ background: #e0f7fa;
403
+ color: #2563eb;
404
+ font-weight:700;
405
+ font-size:1.08em;
406
+ padding:10px 14px;
407
+ border-bottom:2px solid #38bdf822;
408
+ }
409
+ .excel-table.summary td {
410
+ background: #fff;
411
+ color: #23272b;
412
+ font-size:1.04em;
413
+ padding:10px 14px;
414
+ border:1px solid rgba(100,116,139,0.08);
415
+ }
416
+ .excel-table.summary tr {
417
+ transition: background 0.2s;
418
+ }
419
+ .excel-table.summary tr:hover {
420
+ background: #e0f7fa55;
421
+ }
422
+ .tables-wrapper {
423
+ max-width:1100px;
424
+ margin:0 auto;
425
+ width:100%;
426
+ display: flex;
427
+ flex-direction: column;
428
+ align-items: center;
429
+ }
430
+ .excel-table-container,
431
+ .summary-table-container {
432
+ width:100%;
433
+ margin:0;
434
+ max-width:100%;
435
+ }
436
+
437
+
438
+ .excel-table.compact,
439
+ .excel-table.summary {
440
+ width:100%;
441
+ min-width:0;
442
+ box-sizing: border-box;
443
+ }
444
+ .summary-table-container {
445
+ margin-bottom:18px;
446
+ max-width:100%;
447
+ }
448
+
449
+ .excel-table-container th {
450
+ background: #e0f7fa;
451
+ color: #2563eb;
452
+ font-weight: 700;
453
+ font-size: 1.08em;
454
+ padding: 10px 14px;
455
+ border-bottom: 2px solid #38bdf822;
456
+ }
457
+
458
+ @media (max-width:700px) {
459
+ .qs-video-metrics-card {
460
+ min-width:0;
461
+ max-width:100%;
462
+ }
463
+ .metrics-sections-row {
464
+ flex-direction: column;
465
+ gap:12px;
466
+ }
467
+ .audio-analysis-section {
468
+ border-right: none;
469
+ border-bottom:1px solid #e0e7ef;
470
+ padding-right:0;
471
+ margin-right:0;
472
+ padding-bottom:10px;
473
+ margin-bottom:10px;
474
+ }
475
+ }
476
+
477
+ /* Added styles */
478
+ .view-details-icon {
479
+ display: inline-block;
480
+ cursor: pointer;
481
+ color: #2563eb;
482
+ background: #e0f7fa;
483
+ border-radius:50%;
484
+ padding:6px 10px;
485
+ transition: background 0.2s, color 0.2s, box-shadow 0.2s;
486
+ font-size:1.2em;
487
+ box-shadow:0 2px 8px #2563eb22;
488
+ margin-left:6px;
489
+ }
490
+ .view-details-icon:hover, .view-details-icon:focus {
491
+ background: #2563eb;
492
+ color: #fff;
493
+ box-shadow:0 4px 12px #38bdf888;
494
+ outline: none;
495
+ }
496
+
497
+ .excel-table.compact th, .excel-table.compact td {
498
+ text-align: left;
499
+ padding:10px 14px;
500
+ font-size:1.05em;
501
+ }
502
+
503
+ .excel-table.compact th {
504
+ background: #e0f7fa;
505
+ color: #2563eb;
506
+ font-weight:700;
507
+ }
508
+
509
+ .excel-table.compact tr:hover {
510
+ background: #e0f7fa55;
511
+ }
512
+
513
+ /* Pagination controls */
514
+ .pagination-controls {
515
+ display:flex;
516
+ gap:8px;
517
+ justify-content:center;
518
+ align-items:center;
519
+ margin-top:18px;
520
+ }
521
+ .page-btn, .page-number {
522
+ background: linear-gradient(90deg,#38bdf8,#2563eb);
523
+ color:#fff;
524
+ border:none;
525
+ padding:6px 10px;
526
+ border-radius:8px;
527
+ font-weight:700;
528
+ cursor:pointer;
529
+ transition: transform 0.16s ease, box-shadow 0.16s ease;
530
+ }
531
+ .page-btn[disabled], .page-number[disabled] {
532
+ opacity:0.5;
533
+ cursor:not-allowed;
534
+ }
535
+ .page-number { background: transparent; color: #0b3b72; border:1px solid rgba(3,102,214,0.08); padding:6px 8px; }
536
+ .page-number.active { background: linear-gradient(90deg,#38bdf8,#2563eb); color:#fff; box-shadow:0 6px 16px rgba(3,102,214,0.12); }
537
+ /* Results toolbar centered pagination */
538
+ .results-toolbar {
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: center; /* center the pagination bar */
542
+ background: #f3f4f6; /* light gray bar */
543
+ padding:12px 16px;
544
+ border-radius:6px;
545
+ gap:16px;
546
+ }
547
+
548
+ .pagination-bar {
549
+ display: flex;
550
+ align-items: center;
551
+ gap:14px;
552
+ }
553
+
554
+ .page-info {
555
+ color: #6b7280;
556
+ font-size:0.95rem;
557
+ margin-right:8px;
558
+ }
559
+
560
+ .pagination-controls {
561
+ display: flex;
562
+ align-items: center;
563
+ gap:8px;
564
+ }
565
+
566
+ .page-prev, .page-next {
567
+ background: transparent;
568
+ border: none;
569
+ color: #6b7280;
570
+ font-weight:600;
571
+ cursor: pointer;
572
+ }
573
+
574
+ .page-number {
575
+ width:32px;
576
+ height:32px;
577
+ min-width:32px;
578
+ border-radius:50%;
579
+ border:1px solid transparent;
580
+ display: inline-flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ background: transparent;
584
+ color: #6b7280;
585
+ font-weight:600;
586
+ cursor: pointer;
587
+ }
588
+
589
+ .page-number.active {
590
+ background: #6366f1; /* primary blue/purple */
591
+ color: #fff;
592
+ box-shadow:0 6px 16px rgba(99,102,241,0.18);
593
+ }
594
+
595
+ .ellipsis {
596
+ color: #9ca3af;
597
+ padding:4px 6px;
598
+ }
599
+
600
+ .page-size-select {
601
+ margin-left: auto; /* keep selector to the right if parent allows */
602
+ }
603
+
604
+ /* Make the toolbar span full width but keep content centered */
605
+ .results-toolbar {
606
+ width:100%;
607
+ max-width:960px;
608
+ margin:12px auto;
609
+ }
610
+
611
+ /* New professional centered pagination toolbar */
612
+ .results-toolbar-new {
613
+ background: #f8fafc; /* very light gray */
614
+ padding:18px 12px;
615
+ border-top:1px solid #e6e9ef;
616
+ border-bottom:1px solid #e6e9ef;
617
+ margin-top:18px;
618
+ }
619
+ .results-toolbar-new .toolbar-inner {
620
+ max-width:920px;
621
+ margin:0 auto;
622
+ display: flex;
623
+ align-items: center;
624
+ justify-content: center;
625
+ gap:18px;
626
+ }
627
+
628
+ /* Position the left-controls (results mini + entries select) in the toolbar */
629
+ .results-toolbar-new { position: relative; }
630
+
631
+ /* place the grouped left-controls at the left of the toolbar */
632
+ .results-toolbar-new .toolbar-inner .left-controls {
633
+ position: absolute;
634
+ left:18px;
635
+ top:50%;
636
+ transform: translateY(-50%);
637
+ display: flex;
638
+ gap:12px;
639
+ align-items: center;
640
+ }
641
+
642
+ /* results mini stays left within the group */
643
+ .results-toolbar-new .toolbar-inner .results-summary-mini {
644
+ margin:0;
645
+ margin-right:8px;
646
+ justify-content: flex-start;
647
+ }
648
+
649
+ /* entries select appears immediately to the right of results mini */
650
+ .results-toolbar-new .toolbar-inner .entries-select {
651
+ display: flex;
652
+ align-items: center;
653
+ gap:8px;
654
+ margin-left:190px;
655
+ }
656
+
657
+ /* Keep existing hide behavior on small screens */
658
+ @media (max-width:640px) {
659
+ .results-toolbar-new .toolbar-inner .left-controls { display: none; }
660
+ }
661
+
662
+ .page-info {
663
+ color: #6b7280;
664
+ font-size:0.95rem;
665
+ margin-right:8px;
666
+ }
667
+
668
+ .pagination-list {
669
+ list-style: none;
670
+ display: flex;
671
+ align-items: center;
672
+ gap:10px;
673
+ padding:0;
674
+ margin:0;
675
+ }
676
+
677
+ .pagination-list li { display: inline-flex; }
678
+
679
+ .page-number {
680
+ width:36px;
681
+ height:36px;
682
+ min-width:36px;
683
+ border-radius:50%;
684
+ border: none;
685
+ background: transparent;
686
+ color: #111827;
687
+ font-weight:600;
688
+ display: inline-flex;
689
+ align-items: center;
690
+ justify-content: center;
691
+ cursor: pointer;
692
+ transition: background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
693
+ }
694
+
695
+ .page-number:hover { transform: translateY(-4px); }
696
+
697
+ .page-number.active {
698
+ background: #6366f1; /* indigo/purple */
699
+ color: #fff;
700
+ box-shadow:0 8px 24px rgba(99,102,241,0.18);
701
+ }
702
+
703
+ .dots {
704
+ color: #9ca3af;
705
+ padding:06px;
706
+ }
707
+
708
+ .nav-btn {
709
+ background: transparent;
710
+ border: none;
711
+ color: #6b7280;
712
+ font-weight:600;
713
+ padding:6px 8px;
714
+ cursor: pointer;
715
+ }
716
+
717
+ .nav-btn[disabled] { opacity:0.45; cursor: default; }
718
+
719
+ @media (max-width:640px) {
720
+ .results-toolbar-new .toolbar-inner { gap:10px; }
721
+ .page-info { display: none; }
722
+ .page-number { width:30px; height:30px; min-width:30px; }
723
+ }
724
+
725
+ /* Back button styling — gradient and subtle animation to match page design */
726
+ .back-btn {
727
+ background: linear-gradient(90deg,#38bdf8,#2563eb);
728
+ color: #fff;
729
+ border: none;
730
+ border-radius: 12px;
731
+ padding: 8px 14px;
732
+ font-size: 0.98rem;
733
+ font-weight: 800;
734
+ letter-spacing: 0.6px;
735
+ cursor: pointer;
736
+ box-shadow: 0 6px 18px rgba(56,189,248,0.12);
737
+ transition: transform 220ms cubic-bezier(.2,.9,.2,1), box-shadow 220ms ease, filter 220ms ease;
738
+ display: inline-flex;
739
+ gap: 8px;
740
+ align-items: center;
741
+ justify-content: center;
742
+ position: relative;
743
+ overflow: hidden;
744
+ margin-bottom: 16px;
745
+ }
746
+
747
+ .back-btn .back-icon {
748
+ font-weight: 900;
749
+ margin-right: 6px;
750
+ }
751
+
752
+ .back-btn:hover {
753
+ transform: translateY(-4px);
754
+ box-shadow: 0 18px 40px rgba(56,189,248,0.18);
755
+ filter: saturate(1.05);
756
+ }
757
+
758
+ .back-btn:active {
759
+ transform: translateY(-1px) scale(0.995);
760
+ }
761
+
762
+ /* subtle sheen on hover */
763
+ .back-btn::after {
764
+ content: "";
765
+ position: absolute;
766
+ left: -40%;
767
+ top: -40%;
768
+ width: 80%;
769
+ height: 180%;
770
+ background: linear-gradient(120deg, rgba(255,255,255,0.18)0%, rgba(255,255,255,0.02)60%, rgba(255,255,255,0)100%);
771
+ transform: rotate(-25deg) translateX(-30%);
772
+ transition: transform 550ms cubic-bezier(.2,.9,.2,1), opacity 350ms ease;
773
+ opacity: 0;
774
+ pointer-events: none;
775
+ }
776
+
777
+ .back-btn:hover::after {
778
+ transform: rotate(-25deg) translateX(120%);
779
+ opacity: 1;
780
+ }
781
+
782
+ /* accessible focus ring */
783
+ .back-btn:focus {
784
+ outline: 3px solid rgba(56,189,248,0.18);
785
+ outline-offset: 2px;
786
+ }
787
+
788
+ @media (max-width:520px) {
789
+ .back-btn {
790
+ padding: 6px 10px;
791
+ font-size: 0.92rem;
792
+ border-radius: 10px;
793
+ }
794
+ }
795
+
796
+ /* small results summary on question summary page */
797
+ .results-summary-mini { display:flex; align-items:center; gap:8px; color:#0b3b72; font-weight:700; justify-self:start; }
798
+ .results-summary-mini .fa { color:#6b7280; }
799
+ .results-mini-text { font-size:0.98rem; }
800
+
801
+ @media (max-width:640px) {
802
+ .results-summary-mini { display:none; }
803
+ }
804
+
805
+ /* Position the small results summary to the left corner of the centered toolbar */
806
+ .results-toolbar-new {
807
+ position: relative; /* allow absolute positioning of children */
808
+ }
809
+
810
+ .results-toolbar-new .results-summary-mini {
811
+ position: absolute;
812
+ left:18px; /* align to left edge of toolbar area */
813
+ top:50%;
814
+ transform: translateY(-50%);
815
+ justify-content: flex-start;
816
+ }
817
+
818
+ /* Ensure it does not overlap on very small screens (keep existing hide behavior) */
819
+ @media (max-width:640px) {
820
+ .results-toolbar-new .results-summary-mini { display: none; }
821
+ }
src/app/question-summary-page/question-summary-page.component.html ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Modern UI header with logo and PyDetect title -->
2
+ <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
+ </div>
20
+ <div class="header-actions-right">
21
+ <button class="back-btn" (click)="goBack()">
22
+ <span class="back-icon">←</span> Back to Validation Summary
23
+ </button>
24
+ </div>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="question-summary-fullpage">
29
+ <div class="qs-header-row">
30
+ <header class="qs-header">
31
+ <div class="qs-header-title">
32
+ <span class="qs-logo">🕵️‍♂️</span>
33
+ <span class="qs-title-main">Investigation Question Summary</span>
34
+ </div>
35
+ <div class="qs-header-meta">
36
+ <span class="qs-meta-label">Case ID:</span> <b>{{ caseDetails.caseId || '—' }}</b>
37
+ <span class="qs-meta-label">Officer:</span> <b>{{ caseDetails.officer || '—' }}</b>
38
+ <span class="qs-meta-label">Suspect:</span> <b>{{ caseDetails.suspect || '—' }}</b>
39
+ <span class="qs-meta-label">Date:</span> <b>{{ caseDetails.date || '—' }}</b>
40
+ <span class="qs-meta-label">Verdict:</span> <b class="qs-verdict">{{ caseDetails.verdict || '—' }}</b>
41
+ </div>
42
+ <div style="margin-top:18px; text-align:center;">
43
+ <button class="download-btn" (click)="downloadExcel()">
44
+ 📊 Download Questions & Answers
45
+ </button>
46
+ </div>
47
+ </header>
48
+ </div>
49
+
50
+ <div class="excel-table-container">
51
+ <table class="excel-table compact">
52
+ <thead>
53
+ <tr>
54
+ <th>S. No</th>
55
+ <th>Question</th>
56
+ <th>Answer</th>
57
+ <th>Truth Probability</th>
58
+ <th>Dominant Emotion</th>
59
+ <th>View Details</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody>
63
+ <tr *ngFor="let q of pagedQuestions; let i = index">
64
+ <td>{{ (currentPage -1) * pageSize + i +1 }}</td>
65
+ <td>{{q.text}}</td>
66
+ <td>{{q.answer}}</td>
67
+ <td>{{q.truthProbability}}%</td>
68
+ <td>{{q.dominantEmotion}}</td>
69
+ <td>
70
+ <span class="view-details-icon"
71
+ matTooltip="View Details"
72
+ (click)="viewDetails(i)"
73
+ tabindex="0">
74
+ <i class="fa fa-eye"></i>
75
+ </span>
76
+ </td>
77
+ </tr>
78
+ </tbody>
79
+ </table>
80
+ </div>
81
+
82
+ <!-- Results toolbar (professional centered pagination) -->
83
+ <div class="results-toolbar-new" *ngIf="totalPages >1">
84
+ <div class="toolbar-inner">
85
+ <div class="left-controls">
86
+ <div class="results-summary-mini">
87
+ <i class="fa fa-list" aria-hidden="true"></i>
88
+ <span class="results-mini-text">Results: {{ resultsStart }} - {{ resultsEnd }} of {{ totalResults }}</span>
89
+ </div>
90
+ <div class="entries-select">
91
+ <label for="pageSizeSelect">Show</label>
92
+ <select id="pageSizeSelect" (change)="changePageSize($event)">
93
+ <option [value]="5" [selected]="pageSize===5">5</option>
94
+ <option [value]="10" [selected]="pageSize===10">10</option>
95
+ <option [value]="20" [selected]="pageSize===20">20</option>
96
+ </select>
97
+ <span class="entries-label">entries</span>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="page-info">Page {{ currentPage }} of {{ totalPages }}</div>
102
+
103
+ <ul class="pagination-list" role="navigation" aria-label="Pagination">
104
+ <li>
105
+ <button class="nav-btn" (click)="prevPage()" [disabled]="currentPage ===1" aria-label="Previous page">«</button>
106
+ </li>
107
+
108
+ <ng-container *ngFor="let p of visiblePages()">
109
+ <li *ngIf="p === '...'" class="dots">…</li>
110
+ <li *ngIf="p !== '...'">
111
+ <button
112
+ class="page-number"
113
+ [class.active]="currentPage === +p"
114
+ (click)="goToPage(+p)"
115
+ [attr.aria-current]="currentPage === +p ? 'page' : null">
116
+ {{ p }}
117
+ </button>
118
+ </li>
119
+ </ng-container>
120
+
121
+ <li>
122
+ <button class="nav-btn" (click)="nextPage()" [disabled]="currentPage === totalPages" aria-label="Next page">»</button>
123
+ </li>
124
+ </ul>
125
+ </div>
126
+ </div>
127
+
128
+ <footer>
129
+ <p>©2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
130
+ </footer>
131
+ </div>
src/app/question-summary-page/question-summary-page.component.ts ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { Router } from '@angular/router';
3
+ import * as XLSX from 'xlsx';
4
+ import { saveAs } from 'file-saver';
5
+ // @ts-ignore
6
+ import jsPDF from 'jspdf';
7
+ // @ts-ignore
8
+ import autoTable from 'jspdf-autotable';
9
+ import { QuestionDataService } from '../question-data.service';
10
+
11
+ @Component({
12
+ selector: 'app-question-summary-page',
13
+ templateUrl: './question-summary-page.component.html',
14
+ styleUrls: ['./question-summary-page.component.css']
15
+ })
16
+ export class QuestionSummaryPageComponent {
17
+ // pagination
18
+ pageSize = 5;
19
+ currentPage = 1;
20
+
21
+ // Example investigation/case details (replace/fetch as needed)
22
+ caseDetails = {
23
+ caseId: 'CASE-007',
24
+ officer: 'Ganesh',
25
+ suspect: 'Jeeva',
26
+ date: '2025-10-15',
27
+ verdict: 'Consistent',
28
+ summary: 'The suspect displayed calm emotions overall but showed minor inconsistency in hand gestures. Recommendation: Conduct a short follow-up session.',
29
+ observations: 'During questioning, the suspect showed hesitation when discussing the time of the incident. Eye movement frequency decreased by25% during key questions. Speech tone remained steady, indicating partial honesty. Recommendation: Further questioning advised for financial motive discussion.',
30
+ location: 'Chennai',
31
+ sessionTime: '00:42:18',
32
+ progress: 92,
33
+ status: 'Closed'
34
+ };
35
+
36
+ questions = [
37
+ { text: 'Did you visit the location on 12th?', answer: 'Yes, I was there for about 20 minutes.', duration: '00:18', truthProbability: 78, dominantEmotion: 'Nervous 😟', body: 'ajslkdfjsa ldfjlska', bodyScore: 33, voice: 'aksjdfkls jadflk', voiceScore: 23, overallScore: '66%', emotion: 'Calm', videoUrl: '', audioUrl: '', eyeContact: '78%', blinkRate: '12/min', posture: 'Neutral', handMovement: 'Low', legMovement: 'Moderate', microExpressions: '2 detected', stressLevel: 68, confidence: 'Moderate', sentiment: 'Negative (-0.45)', responseDelay: '3.1 sec' },
38
+ { text: 'Were you alone at the scene?', answer: 'No, my friend was with me.', duration: '00:22', truthProbability: 62, dominantEmotion: 'Nervous 😟', body: '', bodyScore: '', voice: '', voiceScore: '', overallScore: '', emotion: 'Nervous', videoUrl: '', audioUrl: '', eyeContact: '65%', blinkRate: '15/min', posture: 'Defensive', handMovement: 'Medium', legMovement: 'Low', microExpressions: '3 detected', stressLevel: 72, confidence: 'Low', sentiment: 'Negative (-0.32)', responseDelay: '2.7 sec' },
39
+ { text: 'Did you know the victim?', answer: 'Yes, we worked together.', duration: '00:15', truthProbability: 85, dominantEmotion: 'Calm 😌', body: '', bodyScore: '', voice: '', voiceScore: '', overallScore: '', emotion: 'Calm', videoUrl: '', audioUrl: '', eyeContact: '82%', blinkRate: '10/min', posture: 'Relaxed', handMovement: 'Low', legMovement: 'Low', microExpressions: '1 detected', stressLevel: 38, confidence: 'High', sentiment: 'Positive (+0.22)', responseDelay: '1.2 sec' },
40
+ { text: 'Did you handle any objects?', answer: 'I picked up a bag to check for ID.', duration: '00:19', truthProbability: 44, dominantEmotion: 'Defensive 🛡️', body: '', bodyScore: '', voice: '', voiceScore: '', overallScore: '', emotion: 'Defensive', videoUrl: '', audioUrl: '', eyeContact: '55%', blinkRate: '18/min', posture: 'Tense', handMovement: 'High', legMovement: 'High', microExpressions: '4 detected', stressLevel: 81, confidence: 'Low', sentiment: 'Negative (-0.61)', responseDelay: '4.0 sec' },
41
+ // Demo questions added so paginator is visible
42
+ { text: 'What time did the incident occur?', answer: 'Around 9 PM.', duration: '00:12', truthProbability: 71, dominantEmotion: 'Calm 😌', body: '', bodyScore: '', voice: '', voiceScore: '', overallScore: '', emotion: 'Calm', videoUrl: '', audioUrl: '', eyeContact: '70%', blinkRate: '11/min', posture: 'Relaxed', handMovement: 'Low', legMovement: 'Low', microExpressions: '1 detected', stressLevel: 40, confidence: 'High', sentiment: 'Neutral (0.00)', responseDelay: '1.0 sec' },
43
+ { text: 'Did anyone else accompany you?', answer: 'No one else was present.', duration: '00:10', truthProbability: 79, dominantEmotion: 'Calm 😌', body: '', bodyScore: '', voice: '', voiceScore: '', overallScore: '', emotion: 'Calm', videoUrl: '', audioUrl: '', eyeContact: '80%', blinkRate: '9/min', posture: 'Neutral', handMovement: 'Low', legMovement: 'Low', microExpressions: '0 detected', stressLevel: 34, confidence: 'High', sentiment: 'Positive (+0.15)', responseDelay: '0.8 sec' }
44
+ ];
45
+
46
+ constructor(private router: Router, public questionDataService: QuestionDataService) { }
47
+
48
+ // pagination helpers
49
+ get totalPages(): number {
50
+ return Math.max(1, Math.ceil(this.questions.length / this.pageSize));
51
+ }
52
+
53
+ get pagedQuestions() {
54
+ const start = (this.currentPage - 1) * this.pageSize;
55
+ return this.questions.slice(start, start + this.pageSize);
56
+ }
57
+
58
+ goToPage(page: number) {
59
+ if (page < 1) page = 1;
60
+ if (page > this.totalPages) page = this.totalPages;
61
+ this.currentPage = page;
62
+ window.scrollTo({ top: 0, behavior: 'smooth' });
63
+ }
64
+
65
+ prevPage() { this.goToPage(this.currentPage - 1); }
66
+ nextPage() { this.goToPage(this.currentPage + 1); }
67
+
68
+ // Helpers required by template
69
+ get totalResults(): number {
70
+ return this.questions.length;
71
+ }
72
+
73
+ get resultsStart(): number {
74
+ if (this.totalResults === 0) return 0;
75
+ return (this.currentPage - 1) * this.pageSize + 1;
76
+ }
77
+
78
+ get resultsEnd(): number {
79
+ return Math.min(this.currentPage * this.pageSize, this.totalResults);
80
+ }
81
+
82
+ changePageSize(eventOrSize: any) {
83
+ let newSize: any = eventOrSize;
84
+ // If event passed from template, extract value safely
85
+ if (eventOrSize && typeof eventOrSize === 'object' && 'target' in eventOrSize) {
86
+ const target = eventOrSize.target as HTMLSelectElement | null;
87
+ newSize = target?.value;
88
+ }
89
+ const size = Number(newSize) || 5;
90
+ this.pageSize = size;
91
+ this.currentPage = 1;
92
+ }
93
+
94
+ visiblePages(): (number | string)[] {
95
+ const total = this.totalPages;
96
+ const current = this.currentPage;
97
+ const pages: (number | string)[] = [];
98
+ if (total <= 7) {
99
+ for (let i = 1; i <= total; i++) pages.push(i);
100
+ return pages;
101
+ }
102
+ pages.push(1);
103
+ if (current > 4) pages.push('...');
104
+ const start = Math.max(2, Math.min(current - 1, total - 4));
105
+ const end = Math.min(total - 1, start + 2);
106
+ for (let i = start; i <= end; i++) pages.push(i);
107
+ if (end < total - 1) pages.push('...');
108
+ pages.push(total);
109
+ return pages;
110
+ }
111
+
112
+ goBack() {
113
+ this.router.navigate(['/validationpage']);
114
+ }
115
+
116
+ navigateHome() {
117
+ // TODO: Implement navigation to home page
118
+ }
119
+
120
+ navigateBackToPyDetect() {
121
+ // TODO: Implement navigation to PyDetect investigation page
122
+ }
123
+
124
+ downloadExcel() {
125
+ // First header row (group headers)
126
+ const header1 = [
127
+ 'S. No', 'Case ID', 'Officer', 'Date',
128
+ 'Question', 'Answer', 'Duration', 'Truth Probability', 'Dominant Emotion', 'Emotion',
129
+ 'Audio Analysis', '', '', '', '',
130
+ 'Video Analysis', '', '', '', '', '',
131
+ 'Physical Expression', 'Physical Score', 'Voice Expression', 'Voice Score', 'Overall Score'
132
+ ];
133
+ // Second header row (sub-headers)
134
+ const header2 = [
135
+ '', '', '', '',
136
+ '', '', '', '', '', '',
137
+ 'Stress Level', 'Confidence', 'Sentiment', 'Response Delay',
138
+ 'Eye Contact', 'Blink Rate', 'Posture', 'Hand Movement', 'Leg Movement', 'Micro Expressions',
139
+ '', '', '', '', '', ''
140
+ ];
141
+ // Data rows for main table, now with S. No and summary columns
142
+ const dataRows = this.questions.map((q, i) => [
143
+ i + 1,
144
+ this.caseDetails.caseId || '',
145
+ this.caseDetails.officer || '',
146
+ this.caseDetails.date || '',
147
+ q.text,
148
+ q.answer,
149
+ q.duration,
150
+ (q.truthProbability !== undefined ? q.truthProbability + '%' : ''),
151
+ q.dominantEmotion || '',
152
+ q.emotion || '',
153
+ q.stressLevel || '',
154
+ q.confidence || '',
155
+ q.sentiment || '',
156
+ q.responseDelay || '',
157
+ q.eyeContact || '',
158
+ q.blinkRate || '',
159
+ q.posture || '',
160
+ q.handMovement || '',
161
+ q.legMovement || '',
162
+ q.microExpressions || '',
163
+ this.getPhysicalExpressionSummary(q),
164
+ this.getPhysicalScore(q),
165
+ this.getVoiceExpressionSummary(q),
166
+ this.getVoiceScore(q),
167
+ this.getOverallScore(q)
168
+ ]);
169
+
170
+ // Combine all data for export
171
+ const wsData = [header1, header2, ...dataRows];
172
+ const ws = XLSX.utils.aoa_to_sheet(wsData);
173
+
174
+ // Merge cells for group headers (main table only)
175
+ ws['!merges'] = [
176
+ { s: { r: 0, c: 0 }, e: { r: 1, c: 0 } }, // S. No
177
+ { s: { r: 0, c: 1 }, e: { r: 1, c: 1 } }, // Case ID
178
+ { s: { r: 0, c: 2 }, e: { r: 1, c: 2 } }, // Officer
179
+ { s: { r: 0, c: 3 }, e: { r: 1, c: 3 } }, // Date
180
+ { s: { r: 0, c: 4 }, e: { r: 1, c: 4 } }, // Question
181
+ { s: { r: 0, c: 5 }, e: { r: 1, c: 5 } }, // Answer
182
+ { s: { r: 0, c: 6 }, e: { r: 1, c: 6 } }, // Duration
183
+ { s: { r: 0, c: 7 }, e: { r: 1, c: 7 } }, // Truth Probability
184
+ { s: { r: 0, c: 8 }, e: { r: 1, c: 8 } }, // Dominant Emotion
185
+ { s: { r: 0, c: 9 }, e: { r: 1, c: 9 } }, // Emotion
186
+ { s: { r: 0, c: 10 }, e: { r: 0, c: 14 } }, // Audio Analysis group
187
+ { s: { r: 0, c: 15 }, e: { r: 0, c: 20 } }, // Video Analysis group
188
+ { s: { r: 0, c: 21 }, e: { r: 1, c: 21 } }, // Physical Expression
189
+ { s: { r: 0, c: 22 }, e: { r: 1, c: 22 } }, // Physical Score
190
+ { s: { r: 0, c: 23 }, e: { r: 1, c: 23 } }, // Voice Expression
191
+ { s: { r: 0, c: 24 }, e: { r: 1, c: 24 } }, // Voice Score
192
+ { s: { r: 0, c: 25 }, e: { r: 1, c: 25 } } // Overall Score
193
+ ];
194
+
195
+ const wb = XLSX.utils.book_new();
196
+ XLSX.utils.book_append_sheet(wb, ws, 'Questions');
197
+ const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
198
+ saveAs(new Blob([wbout], { type: 'application/octet-stream' }), 'questions-and-answers.xlsx');
199
+ }
200
+
201
+ getPhysicalExpressionSummary(q: any): string {
202
+ let parts = [];
203
+ if (q.posture) parts.push(q.posture);
204
+ if (q.handMovement) parts.push(q.handMovement + ' hand');
205
+ if (q.legMovement) parts.push(q.legMovement + ' leg');
206
+ if (q.microExpressions) parts.push(q.microExpressions);
207
+ return parts.length ? parts.join(', ') : '—';
208
+ }
209
+ getPhysicalScore(q: any): string {
210
+ let scores = [];
211
+ if (typeof q.handMovement === 'number') scores.push(q.handMovement);
212
+ if (typeof q.legMovement === 'number') scores.push(q.legMovement);
213
+ const match = (q.microExpressions || '').match(/(\d+)/);
214
+ if (match) scores.push(Number(match[1]));
215
+ if (scores.length === 0) return '—';
216
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + '%';
217
+ }
218
+ getVoiceExpressionSummary(q: any): string {
219
+ let parts = [];
220
+ if (q.stressLevel !== undefined) parts.push('Stress ' + q.stressLevel);
221
+ if (q.confidence) parts.push('Conf ' + q.confidence);
222
+ if (q.sentiment) parts.push('Sent ' + this.getSentimentPercent(q.sentiment));
223
+ if (q.responseDelay) parts.push('Delay ' + q.responseDelay);
224
+ return parts.length ? parts.join(', ') : '—';
225
+ }
226
+ getVoiceScore(q: any): string {
227
+ let scores = [];
228
+ if (typeof q.stressLevel === 'number') scores.push(q.stressLevel);
229
+ if (typeof q.confidence === 'number') scores.push(q.confidence);
230
+ else if (q.confidence === 'High') scores.push(90);
231
+ else if (q.confidence === 'Moderate') scores.push(60);
232
+ else if (q.confidence === 'Low') scores.push(30);
233
+ if (scores.length === 0) return '—';
234
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + '%';
235
+ }
236
+ getOverallScore(q: any): string {
237
+ // Example: average of physical and voice scores
238
+ const phys = this.getPhysicalScore(q);
239
+ const voice = this.getVoiceScore(q);
240
+ const physNum = parseInt(phys);
241
+ const voiceNum = parseInt(voice);
242
+ if (isNaN(physNum) && isNaN(voiceNum)) return '—';
243
+ if (isNaN(physNum)) return voice;
244
+ if (isNaN(voiceNum)) return phys;
245
+ return Math.round((physNum + voiceNum) / 2) + '%';
246
+ }
247
+ getSentimentPercent(sentiment: string): string {
248
+ if (!sentiment) return '';
249
+ const match = sentiment.match(/([+-]?\d*\.?\d+)/);
250
+ if (match) {
251
+ const value = parseFloat(match[1]);
252
+ const percent = Math.round(value * 100);
253
+ return (percent > 0 ? '+' : '') + percent + '%';
254
+ }
255
+ return sentiment;
256
+ }
257
+
258
+ viewDetails(index: number) {
259
+ // Set live data in service before navigation
260
+ const normalized = this.questions.map((q: any) => ({ ...q, question: q.question ?? q.text ?? '' }));
261
+ this.questionDataService.setQuestions(normalized);
262
+ this.questionDataService.setCaseDetails(this.caseDetails);
263
+ const absoluteIndex = (this.currentPage - 1) * this.pageSize + index;
264
+ this.router.navigate(['/view-details', absoluteIndex]);
265
+ }
266
+ }
src/app/recordpage/recordpage.component.css CHANGED
@@ -1,6 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /* ===== Header ===== */
2
  :root {
3
  --masthead-min-height: 140px;
 
 
 
 
 
 
 
 
4
  }
5
 
6
  .masthead {
@@ -49,7 +174,7 @@
49
  height: auto;
50
  border-radius: 50%;
51
  padding: 4px;
52
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
53
  transition: transform .25s ease;
54
  }
55
 
@@ -162,7 +287,6 @@
162
  font-weight: 800;
163
  margin-bottom: 12px;
164
  }*/
165
-
166
  /* header tools */
167
  .toolbar {
168
  display: flex;
@@ -208,7 +332,144 @@
208
  overflow: auto;
209
  }
210
 
211
- /* table */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  .records {
213
  width: 100%;
214
  min-width: 1500px;
@@ -483,7 +744,7 @@
483
  color: #fff;
484
  }
485
 
486
- /* Icon buttons */
487
  .icon-btn {
488
  background: none;
489
  border: none;
@@ -713,15 +974,15 @@
713
  border-top: 6px solid #4a5568;
714
  }
715
 
716
- /* Remove global blur and pointer-events when modal is open */
717
- /*
718
- .show-details body > *:not(.cdk-overlay-container):not(app-root),
719
- .show-details app-root > *:not(.modal):not(.modal-backdrop) {
720
- filter: blur(8px) !important;
721
- pointer-events: none !important;
722
- user-select: none !important;
723
- }
724
- */
725
 
726
  .show-details .modal,
727
  .show-details .modal-backdrop {
@@ -920,7 +1181,7 @@
920
  .header-inner {
921
  display: flex;
922
  align-items: center;
923
- justify-content: flex-start;
924
  padding: 18px 32px 0 32px;
925
  position: relative;
926
  }
@@ -1007,6 +1268,49 @@
1007
  text-shadow: 0 0 6px #38bdf8;
1008
  }
1009
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1010
  body, main.content {
1011
  background: #f4f6fa;
1012
  min-height: 100vh;
@@ -1020,7 +1324,7 @@ body, main.content {
1020
  border-radius: 10px;
1021
  box-shadow: 0 2px 8px #0001, 0 1.5px 0 #e5e7eb;
1022
  border: 1.5px solid #e5e7eb;
1023
- margin: 40px auto 0 auto;
1024
  max-width: 98vw;
1025
  width: 98vw;
1026
  min-width: 320px;
@@ -1035,7 +1339,7 @@ body, main.content {
1035
  justify-content: space-between;
1036
  padding: 18px 24px 8px 24px;
1037
  border-bottom: 1.5px solid #e5e7eb;
1038
- background: #f8fafc;
1039
  border-radius: 10px 10px 0 0;
1040
  }
1041
 
@@ -1048,7 +1352,7 @@ body, main.content {
1048
  .record-title {
1049
  font-size: 1.25rem;
1050
  font-weight: 700;
1051
- color: #222b45;
1052
  }
1053
 
1054
  .record-dropdown {
@@ -1098,7 +1402,7 @@ body, main.content {
1098
  width: 100%;
1099
  border-collapse: separate;
1100
  border-spacing: 0;
1101
- background: #fff;
1102
  }
1103
 
1104
  .record-table th, .record-table td {
@@ -1122,9 +1426,7 @@ body, main.content {
1122
  transition: background 0.15s;
1123
  }
1124
 
1125
- .record-table tr:hover {
1126
- background: #f1f5f9;
1127
- }
1128
 
1129
  .record-table a {
1130
  color: #2563eb;
@@ -1282,7 +1584,6 @@ body, main.content {
1282
  color: #0ea5e9;
1283
  }
1284
 
1285
- /* Stylish pills for subgroups */
1286
  .subgroup-pills {
1287
  display: flex;
1288
  flex-wrap: wrap;
@@ -1366,7 +1667,7 @@ body, main.content {
1366
  align-items: center;
1367
  margin: 24px 0 12px 0;
1368
  padding: 12px 18px;
1369
- background: #f8fafc;
1370
  border-radius: 12px;
1371
  box-shadow: 0 2px 8px #2563eb11;
1372
  }
@@ -1413,51 +1714,68 @@ body, main.content {
1413
  background: #ef4444;
1414
  }
1415
 
1416
- .analytics-summary {
1417
- display: flex;
1418
- gap: 32px;
1419
- margin: 18px 0 8px 0;
 
1420
  }
1421
 
1422
- .summary-card {
1423
- background: #f8fafc;
1424
- border-radius: 12px;
1425
- box-shadow: 0 2px 8px #2563eb11;
1426
- padding: 18px 32px;
1427
- min-width: 120px;
1428
- text-align: center;
1429
  }
1430
 
1431
- .summary-card .summary-label {
1432
- font-size: 1em;
1433
- color: #64748b;
1434
- font-weight: 600;
1435
- margin-bottom: 6px;
1436
- }
1437
-
1438
- .summary-card .summary-value {
1439
- font-size: 2em;
1440
- font-weight: 900;
1441
- color: #2563eb;
1442
- }
1443
 
1444
- .summary-card.open .summary-value {
1445
- color: #059669;
1446
- }
 
 
 
 
 
 
1447
 
1448
- .summary-card.closed .summary-value {
1449
- color: #dc2626;
1450
- }
 
 
 
 
1451
 
1452
- .record-meta {
1453
- color: #64748b;
1454
- font-size: 0.98em;
1455
- margin: 0 0 10px 0;
1456
- padding-left: 2px;
1457
- font-weight: 500;
1458
- letter-spacing: 0.1px;
 
 
 
 
 
 
 
 
 
 
1459
  }
1460
 
 
 
 
 
1461
 
1462
  /* Footer */
1463
  footer {
@@ -1465,10 +1783,243 @@ footer {
1465
  color: #fff;
1466
  text-align: center;
1467
  padding: 10px 0px;
1468
- position: relative;
1469
- bottom: 0;
1470
  left: 0;
 
1471
  width: 100%;
1472
- margin-top: 40px;
 
1473
  }
1474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== Header ===== */
2
+ :root {
3
+ --masthead-min-height:140px;
4
+ --analytics-blue-height:110px;
5
+ --primary-accent: #2563eb;
6
+ --primary-accent-light: #38bdf8;
7
+ --primary-accent-dark: #1e40af;
8
+ --secondary-accent: #7c3aed;
9
+ --card-radius:14px;
10
+ --card-shadow:06px24px rgba(30,41,59,0.13);
11
+ --section-gap:32px;
12
+ }
13
+
14
+ body, main.content {
15
+ background: #f4f6fa;
16
+ min-height:100vh;
17
+ margin:0;
18
+ overflow-y: auto;
19
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
20
+ animation: fadeInPage0.7s cubic-bezier(0.4,0.2,0.2,1) both;
21
+ }
22
+
23
+ @keyframes fadeInPage {
24
+ from { opacity:0; transform: translateY(32px); }
25
+ to { opacity:1; transform: none; }
26
+ }
27
+
28
+ /* Card hover animation */
29
+ .record-card, .summary-card, .section-block, .subgroup-block {
30
+ transition: box-shadow 0.25s, transform 0.18s;
31
+ }
32
+ .record-card:hover, .summary-card:hover, .section-block:hover, .subgroup-block:hover {
33
+ box-shadow:08px 32px #2563eb33,02px 8px #38bdf822;
34
+ /*transform: translateY(-2px) scale(1.012);*/
35
+ }
36
+
37
+ /* Table row hover animation */
38
+ .record-table tr {
39
+ transition: background 0.18s, box-shadow 0.18s, transform 0.18s;
40
+ }
41
+ .record-table tr:hover td {
42
+ background: #e0f2fe;
43
+ box-shadow:02px 12px #38bdf822;
44
+ transform: scale(1.01);
45
+ }
46
+
47
+ /* Button and icon-btn animation */
48
+ .btn, .icon-btn {
49
+ transition: background 0.18s, color 0.18s, box-shadow 0.18s, transform 0.18s;
50
+ }
51
+ .btn:hover, .icon-btn:hover {
52
+ transform: scale(1.08) rotate(-2deg);
53
+ box-shadow:04px 16px #2563eb33;
54
+ }
55
+
56
+ /* Modal fade/slide in */
57
+ .modal-backdrop {
58
+ animation: fadeInModalBg0.4s cubic-bezier(0.4,0.2,0.2,1) both;
59
+ }
60
+ @keyframes fadeInModalBg {
61
+ from { opacity:0; }
62
+ to { opacity:1; }
63
+ }
64
+ .modal {
65
+ animation: slideInModal0.5s cubic-bezier(0.4,0.2,0.2,1) both;
66
+ }
67
+ @keyframes slideInModal {
68
+ from { opacity:0; transform: translateY(-40px) scale(0.98); }
69
+ to { opacity:1; transform: none; }
70
+ }
71
+
72
+ /* Status badge pulse for Open */
73
+ .status-label.status-open, .status-open {
74
+ animation: pulseStatusOpen1.6s infinite alternate;
75
+ }
76
+ @keyframes pulseStatusOpen {
77
+ from { box-shadow:0000px #22c55e44; }
78
+ to { box-shadow:0008px #22c55e11; }
79
+ }
80
+
81
+ /* Animated icons */
82
+ .icon-btn .fa-spin, .summary-icon .fa-spin {
83
+ animation: fa-spin1.2s infinite linear;
84
+ }
85
+ @keyframes fa-spin {
86
+ 0% { transform: rotate(0deg); }
87
+ 100% { transform: rotate(359deg); }
88
+ }
89
+ .icon-btn .fa-bounce, .summary-icon .fa-bounce {
90
+ animation: fa-bounce1.2s infinite alternate;
91
+ }
92
+ @keyframes fa-bounce {
93
+ 0% { transform: translateY(0); }
94
+ 100% { transform: translateY(-8px); }
95
+ }
96
+ .icon-btn .fa-beat, .summary-icon .fa-beat {
97
+ animation: fa-beat1.1s infinite alternate;
98
+ }
99
+ @keyframes fa-beat {
100
+ 0% { transform: scale(1); }
101
+ 100% { transform: scale(1.18); }
102
+ }
103
+
104
+ /* Subtle fade for overlays */
105
+ .modal-blur-overlay {
106
+ animation: fadeInModalBg0.4s cubic-bezier(0.4,0.2,0.2,1) both;
107
+ }
108
+
109
+ /* Modern Searchbar animation */
110
+ .modern-searchbar-form {
111
+ transition: box-shadow 0.18s, transform 0.18s;
112
+ }
113
+ .modern-searchbar-form:focus-within {
114
+ box-shadow:06px 24px #38bdf855;
115
+ transform: scale(1.03);
116
+ }
117
+
118
  /* ===== Header ===== */
119
  :root {
120
  --masthead-min-height: 140px;
121
+ --analytics-blue-height:110px; /* Default, can be overridden inline or in other CSS */
122
+ --primary-accent: #2563eb;
123
+ --primary-accent-light: #38bdf8;
124
+ --primary-accent-dark: #1e40af;
125
+ --secondary-accent: #7c3aed;
126
+ --card-radius:14px;
127
+ --card-shadow:06px24px rgba(30,41,59,0.13);
128
+ --section-gap:32px;
129
  }
130
 
131
  .masthead {
 
174
  height: auto;
175
  border-radius: 50%;
176
  padding: 4px;
177
+ box-shadow: 04px 10px rgba(0, 0, 0, 0.25);
178
  transition: transform .25s ease;
179
  }
180
 
 
287
  font-weight: 800;
288
  margin-bottom: 12px;
289
  }*/
 
290
  /* header tools */
291
  .toolbar {
292
  display: flex;
 
332
  overflow: auto;
333
  }
334
 
335
+ /* --- Modern Table Refactor Inspired by Example --- */
336
+
337
+ .record-table {
338
+ width:100%;
339
+ border-collapse: separate;
340
+ border-spacing:0;
341
+ background: #fff;
342
+ border-radius:12px;
343
+ box-shadow:0 2px 12px #2563eb11;
344
+ overflow: hidden;
345
+ }
346
+
347
+ .record-table th, .record-table td {
348
+ padding:14px 14px;
349
+ font-size:1.08rem;
350
+ border-bottom:1.5px solid #e5e7eb;
351
+ text-align: left;
352
+ white-space: nowrap;
353
+ }
354
+
355
+ .record-table th {
356
+ background: #f8fafc;
357
+ color: #222b45;
358
+ font-weight:700;
359
+ font-size:1.08rem;
360
+ letter-spacing:0.5px;
361
+ border-bottom:2.5px solid #e5e7eb;
362
+ text-shadow: none;
363
+ position: sticky;
364
+ top:0;
365
+ z-index:2;
366
+ }
367
+
368
+ .record-table th i {
369
+ color: #e50808;
370
+ margin-right: 7px;
371
+ font-size: 1.1em;
372
+ }
373
+
374
+ .record-table td {
375
+ background: #fff;
376
+ font-size:1.05rem;
377
+ color: #23272b;
378
+ vertical-align: middle;
379
+ }
380
+
381
+ /* Remove alternate row color: force all rows to white */
382
+ .record-table tr:nth-child(odd) td {
383
+ background: #fff !important;
384
+ }
385
+
386
+ .record-table tr:hover td {
387
+ background: #e0f2fe;
388
+ transition: background 0.18s;
389
+ }
390
+
391
+ /* Checkbox column */
392
+ .record-table td.select-col, .record-table th.select-col {
393
+ width:36px;
394
+ text-align: center;
395
+ padding-left:10px;
396
+ padding-right:10px;
397
+ }
398
+
399
+ /* Status badge */
400
+ .status-label {
401
+ display: inline-flex;
402
+ align-items: center;
403
+ gap:6px;
404
+ padding:3px 14px;
405
+ border-radius:12px;
406
+ font-weight:700;
407
+ font-size:1em;
408
+ letter-spacing:0.5px;
409
+ min-width:70px;
410
+ text-align: center;
411
+ background: #e0e7ff;
412
+ color: var(--primary-accent);
413
+ box-shadow:0 2px 8px #2563eb11;
414
+ }
415
+ .status-dot {
416
+ display: inline-block;
417
+ width:9px;
418
+ height:9px;
419
+ border-radius:50%;
420
+ margin-right:4px;
421
+ }
422
+ .status-open { background: #d1fae5; color: #059669; }
423
+ .status-open .status-dot { background: #059669; }
424
+ .status-under { background: #dbeafe; color: #2563eb; }
425
+ .status-under .status-dot { background: #2563eb; }
426
+ .status-closed { background: #fee2e2; color: #dc2626; }
427
+ .status-closed .status-dot { background: #dc2626; }
428
+
429
+ .record-table td.actions-col, .record-table th.actions-col {
430
+ min-width:80px;
431
+ text-align: left;
432
+ padding-right:18px;
433
+ }
434
+
435
+ .icon-btn {
436
+ margin:02px;
437
+ font-size:1.18em;
438
+ border-radius:6px;
439
+ padding:6px 10px;
440
+ transition: background 0.15s, color 0.15s, box-shadow 0.15s;
441
+ background: none;
442
+ border: none;
443
+ cursor: pointer;
444
+ }
445
+ .icon-btn.verify { color: #22c55e; }
446
+ .icon-btn.view { color: #2563eb; }
447
+ .icon-btn.edit { color: #7c3aed; }
448
+ .icon-btn.delete { color: #ef4444; }
449
+ .icon-btn:hover { background: #f0f7ff; }
450
+ .icon-btn.delete:hover { background: #fff0f0; color: #b91c1c; }
451
+ .icon-btn.edit:hover { background: #f3e8ff; color: #5b21b6; }
452
+ .icon-btn.view:hover { background: #e0f2fe; color: #0ea5e9; }
453
+ .icon-btn.verify:hover { background: #e0ffe6; color: #15803d; }
454
+
455
+ /* Responsive: stack columns on small screens */
456
+ @media (max-width:900px) {
457
+ .record-table th, .record-table td {
458
+ padding:10px6px;
459
+ font-size:0.98rem;
460
+ }
461
+ .record-table {
462
+ font-size:0.98rem;
463
+ }
464
+ }
465
+
466
+ .empty {
467
+ text-align: center;
468
+ color: #718096;
469
+ padding: 24px;
470
+ font-size: 1.1em;
471
+ }
472
+
473
  .records {
474
  width: 100%;
475
  min-width: 1500px;
 
744
  color: #fff;
745
  }
746
 
747
+ /* Icon buttons */
748
  .icon-btn {
749
  background: none;
750
  border: none;
 
974
  border-top: 6px solid #4a5568;
975
  }
976
 
977
+ /* Remove global blur and pointer-events when modal is open */
978
+ /*
979
+ .show-details body > *:not(.cdk-overlay-container):not(app-root),
980
+ .show-details app-root > *:not(.modal):not(.modal-backdrop) {
981
+ filter: blur(8px) !important;
982
+ pointer-events: none !important;
983
+ user-select: none !important;
984
+ }
985
+ */
986
 
987
  .show-details .modal,
988
  .show-details .modal-backdrop {
 
1181
  .header-inner {
1182
  display: flex;
1183
  align-items: center;
1184
+ justify-content: space-between;
1185
  padding: 18px 32px 0 32px;
1186
  position: relative;
1187
  }
 
1268
  text-shadow: 0 0 6px #38bdf8;
1269
  }
1270
 
1271
+ header {
1272
+ position: relative;
1273
+ z-index: 10;
1274
+ }
1275
+
1276
+ .header-actions-right {
1277
+ position: absolute;
1278
+ right: 32px;
1279
+ top: 27px;
1280
+ display: flex;
1281
+ align-items: center;
1282
+ z-index: 100;
1283
+ }
1284
+
1285
+ .logout-btn {
1286
+ font-family: 'Montserrat', 'Poppins', 'Arial Black', Arial, sans-serif;
1287
+ font-size: 1.05rem;
1288
+ font-weight: 700;
1289
+ letter-spacing: 2px;
1290
+ background: linear-gradient(90deg, #ef4444 0%, #23272b 100%);
1291
+ color: #fff;
1292
+ box-shadow: 0 2px 16px #ef444488;
1293
+ border: none;
1294
+ border-radius: 12px;
1295
+ padding: 0.55rem 1.3rem;
1296
+ margin: 0 0.3rem;
1297
+ cursor: pointer;
1298
+ transition: background 0.4s, box-shadow 0.4s, color 0.3s, transform 0.2s;
1299
+ overflow: hidden;
1300
+ }
1301
+
1302
+ .logout-btn:hover {
1303
+ background: linear-gradient(90deg, #23272b 0%, #ef4444 100%);
1304
+ color: #fff;
1305
+ box-shadow: 0 2px 24px #ef444488;
1306
+ transform: scale(1.04);
1307
+ }
1308
+
1309
+ .logout-icon {
1310
+ font-size: 1.2em;
1311
+ margin-right: 6px;
1312
+ }
1313
+
1314
  body, main.content {
1315
  background: #f4f6fa;
1316
  min-height: 100vh;
 
1324
  border-radius: 10px;
1325
  box-shadow: 0 2px 8px #0001, 0 1.5px 0 #e5e7eb;
1326
  border: 1.5px solid #e5e7eb;
1327
+ margin: 24px auto 0 auto;
1328
  max-width: 98vw;
1329
  width: 98vw;
1330
  min-width: 320px;
 
1339
  justify-content: space-between;
1340
  padding: 18px 24px 8px 24px;
1341
  border-bottom: 1.5px solid #e5e7eb;
1342
+ background: linear-gradient(90deg, #38bdf8 0%, #6366f1 100%);
1343
  border-radius: 10px 10px 0 0;
1344
  }
1345
 
 
1352
  .record-title {
1353
  font-size: 1.25rem;
1354
  font-weight: 700;
1355
+ color: #fff;
1356
  }
1357
 
1358
  .record-dropdown {
 
1402
  width: 100%;
1403
  border-collapse: separate;
1404
  border-spacing: 0;
1405
+ background: #4654ff;
1406
  }
1407
 
1408
  .record-table th, .record-table td {
 
1426
  transition: background 0.15s;
1427
  }
1428
 
1429
+
 
 
1430
 
1431
  .record-table a {
1432
  color: #2563eb;
 
1584
  color: #0ea5e9;
1585
  }
1586
 
 
1587
  .subgroup-pills {
1588
  display: flex;
1589
  flex-wrap: wrap;
 
1667
  align-items: center;
1668
  margin: 24px 0 12px 0;
1669
  padding: 12px 18px;
1670
+ background: linear-gradient(90deg, #38bdf8 0%, #6366f1 100%);
1671
  border-radius: 12px;
1672
  box-shadow: 0 2px 8px #2563eb11;
1673
  }
 
1714
  background: #ef4444;
1715
  }
1716
 
1717
+ .analytics-panel {
1718
+ margin: 18px 16px 8px 16px;
1719
+ border-radius: 8px;
1720
+ overflow: hidden;
1721
+ box-shadow: 0 4px 24px rgba(15,23,42,0.06);
1722
  }
1723
 
1724
+ .analytics-header {
1725
+ background: linear-gradient(90deg,#6b46ff,#7c3aed);
1726
+ color: #fff;
1727
+ padding: 16px 20px;
1728
+ display: flex;
1729
+ align-items: center;
1730
+ justify-content: space-between;
1731
  }
1732
 
1733
+ .analytics-title {
1734
+ font-weight: 800;
1735
+ font-size: 1.05rem;
1736
+ }
 
 
 
 
 
 
 
 
1737
 
1738
+ .create-project-btn {
1739
+ background: #fff;
1740
+ color: #111827;
1741
+ border: none;
1742
+ padding: 8px 12px;
1743
+ border-radius: 6px;
1744
+ font-weight: 700;
1745
+ cursor: pointer;
1746
+ }
1747
 
1748
+ .analytics-cards {
1749
+ display: flex;
1750
+ gap: 5px;
1751
+ width: 100%;
1752
+ justify-content: stretch;
1753
+ align-items: stretch;
1754
+ }
1755
 
1756
+ .summary-card {
1757
+ flex: 110;
1758
+ min-width: 0;
1759
+ max-width: none;
1760
+ /* keep all previous styles and animations */
1761
+ border: none;
1762
+ border-radius: var(--card-radius);
1763
+ box-shadow: var(--card-shadow);
1764
+ border-left: 5px solid var(--primary-accent-light);
1765
+ transition: box-shadow 0.18s, border 0.18s, transform 0.18s;
1766
+ background: #3f51b526;
1767
+ padding: 10px 12px;
1768
+ display: flex;
1769
+ flex-direction: row;
1770
+ justify-content: space-between;
1771
+ align-items: center;
1772
+ cursor: pointer;
1773
  }
1774
 
1775
+ @media (max-width: 900px) {
1776
+ .analytics-cards { gap: 6px; padding: 8px; }
1777
+ .summary-card { flex: 11100%; min-width: 80px; max-width: 100%; padding: 6px 6px; flex-direction: row; }
1778
+ }
1779
 
1780
  /* Footer */
1781
  footer {
 
1783
  color: #fff;
1784
  text-align: center;
1785
  padding: 10px 0px;
1786
+ position: fixed;
 
1787
  left: 0;
1788
+ bottom: 0;
1789
  width: 100%;
1790
+ z-index: 100;
1791
+ margin-top: 0;
1792
  }
1793
 
1794
+ .back-btn {
1795
+ font-family: 'Montserrat', 'Poppins', 'Arial Black', Arial, sans-serif;
1796
+ font-size: 0.95rem;
1797
+ font-weight: 700;
1798
+ letter-spacing: 1px;
1799
+ background: #fff;
1800
+ color: #23272b;
1801
+ border: 1px solid #d1d5db;
1802
+ border-radius: 6px;
1803
+ padding: 0.32rem 0.8rem;
1804
+ margin: 0 0.3rem;
1805
+ cursor: pointer;
1806
+ transition: background 0.3s, color 0.2s, box-shadow 0.2s, transform 0.2s;
1807
+ box-shadow: 0 2px 8px #d1d5db44;
1808
+ display: inline-flex;
1809
+ align-items: center;
1810
+ gap: 6px;
1811
+ }
1812
+
1813
+ .back-btn:hover {
1814
+ background: #f3f4f6;
1815
+ color: #1976d2;
1816
+ box-shadow: 0 2px 16px #bae6fd88;
1817
+ transform: scale(1.04);
1818
+ }
1819
+
1820
+ .back-icon {
1821
+ font-size: 1.1em;
1822
+ margin-right: 4px;
1823
+ }
1824
+
1825
+ .back-btn,
1826
+ .logout-btn {
1827
+ font-size: 0.85rem;
1828
+ padding: 0.18rem 0.7rem;
1829
+ border-radius: 5px;
1830
+ min-width: unset;
1831
+ min-height: unset;
1832
+ box-shadow: 0 1px 4px #d1d5db22;
1833
+ display: inline-flex;
1834
+ align-items: center;
1835
+ gap: 6px;
1836
+ }
1837
+
1838
+ .analytics-blue {
1839
+ height: var(--analytics-blue-height);
1840
+ animation: analyticsBlueFadeIn0.7s cubic-bezier(0.4,0.2,0.2,1) both;
1841
+ }
1842
+
1843
+ @keyframes analyticsBlueFadeIn {
1844
+ from {
1845
+ opacity:0;
1846
+ transform: translateY(-32px);
1847
+ }
1848
+ to {
1849
+ opacity:1;
1850
+ transform: translateY(0);
1851
+ }
1852
+ }
1853
+
1854
+
1855
+ /* === Modern Additions for Record Page === */
1856
+ :root {
1857
+ --primary-accent: #2563eb;
1858
+ --primary-accent-light: #38bdf8;
1859
+ --primary-accent-dark: #1e40af;
1860
+ --secondary-accent: #7c3aed;
1861
+ --card-radius:14px;
1862
+ --card-shadow:06px24px rgba(30,41,59,0.13);
1863
+ --section-gap:32px;
1864
+ }
1865
+
1866
+ /* Modern gradient for analytics-blue */
1867
+ .analytics-blue {
1868
+ background: linear-gradient(90deg, var(--primary-accent)0%, var(--primary-accent-light)100%);
1869
+ border-radius: var(--card-radius) var(--card-radius)00;
1870
+ box-shadow: var(--card-shadow);
1871
+ }
1872
+
1873
+ /* Modern shadow and accent border for summary-card */
1874
+ .summary-card {
1875
+ border-radius: var(--card-radius);
1876
+ box-shadow: var(--card-shadow);
1877
+ border-left:5px solid var(--primary-accent-light);
1878
+ transition: box-shadow 0.18s, border 0.18s;
1879
+ }
1880
+ .summary-card:hover {
1881
+ box-shadow:0 10px 32px rgba(30,41,59,0.18);
1882
+ border-left:5px solid var(--primary-accent);
1883
+ }
1884
+
1885
+ /* Modern color for summary-label, value, sub */
1886
+ .summary-label {
1887
+ color: var(--primary-accent);
1888
+ font-weight:900;
1889
+ font-size:1.25rem;
1890
+ letter-spacing:1.5px;
1891
+ text-transform: uppercase;
1892
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
1893
+ margin-bottom:-11px;
1894
+ opacity:0.95;
1895
+ }
1896
+ .summary-value {
1897
+ font-size:3.0rem;
1898
+ font-weight:500;
1899
+ color: var(--primary-accent-light);
1900
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
1901
+ letter-spacing:2px;
1902
+ text-shadow:02px 8px #2563eb11;
1903
+ margin-bottom:4px;
1904
+ }
1905
+ .summary-sub {
1906
+ color: var(--secondary-accent);
1907
+ font-size:1.18rem;
1908
+ font-weight:700;
1909
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
1910
+ opacity:0.88;
1911
+ }
1912
+ .summary-icon {
1913
+ width:64px;
1914
+ height:64px;
1915
+ border-radius:12px;
1916
+ display: flex;
1917
+ align-items: center;
1918
+ justify-content: center;
1919
+ font-size:2.6rem;
1920
+ margin-bottom:0;
1921
+ margin-left:18px;
1922
+ }
1923
+
1924
+ /* Slide-in animation for table rows on load */
1925
+ .record-table tbody tr {
1926
+ animation: rowSlideIn0.7s cubic-bezier(0.4,0.2,0.2,1) both;
1927
+ }
1928
+ @keyframes rowSlideIn {
1929
+ from { opacity:0; transform: translateX(-32px); }
1930
+ to { opacity:1; transform: none; }
1931
+ }
1932
+
1933
+ /* Animated highlight (glow, no shadow) on row hover/focus */
1934
+ .record-table tr:hover td,
1935
+ .record-table tr:focus-within td {
1936
+ background: #e0f2fe !important;
1937
+ animation: rowGlow1.2s linear infinite alternate;
1938
+ border-left:4px solid #38bdf8;
1939
+ }
1940
+ @keyframes rowGlow {
1941
+ from { box-shadow:0000px #38bdf800; }
1942
+ to { box-shadow:0004px #38bdf866; }
1943
+ }
1944
+
1945
+ /* Remove box-shadow on hover for table rows (override previous) */
1946
+ .record-table tr:hover td {
1947
+ box-shadow: none !important;
1948
+ }
1949
+
1950
+ /* Animated highlight for selected row (if you use .selected-row) */
1951
+ .record-table tr.selected-row td {
1952
+ background: #bae6fd !important;
1953
+ animation: rowGlowSelected1.2s linear infinite alternate;
1954
+ border-left:4px solid #38bdf8;
1955
+ }
1956
+ @keyframes rowGlowSelected {
1957
+ from { box-shadow:0000px #38bdf800; }
1958
+ to { box-shadow:0004px #38bdf866; }
1959
+ }
1960
+
1961
+ /* Remove all box-shadow from table rows and cells */
1962
+ .record-table td, .record-table th {
1963
+ box-shadow: none !important;
1964
+ }
1965
+
1966
+ /* === Summary Card Animations and Dynamic Styles === */
1967
+ .summary-card {
1968
+ transition: box-shadow 0.25s, border 0.18s, transform 0.18s;
1969
+ cursor: pointer;
1970
+ height: 100px;
1971
+ }
1972
+ .summary-card:hover {
1973
+ transform: scale(1.045) rotate(0deg);
1974
+ /* Default blue glow, overridden below for each type */
1975
+ box-shadow:0 0 24px 0 #38bdf8cc,0 10px 32px rgba(30,41,59,0.18);
1976
+ border-left:5px solid var(--primary-accent);
1977
+ }
1978
+
1979
+ /* Card type specific icon and glow */
1980
+ .summary-card.total .summary-icon {
1981
+ /* background: linear-gradient(135deg, #38bdf8 0%, #7c3aed 100%); */
1982
+ color: #23272b;
1983
+ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); /* effectively no shadow */
1984
+ }
1985
+
1986
+ .summary-card.total:hover {
1987
+ box-shadow: 0 0 32px 0 #38bdf8cc, 0 10px 32px #38bdf822;
1988
+ border-left: 5px solid #38bdf8;
1989
+ }
1990
+
1991
+ .summary-card.closed .summary-icon {
1992
+ /* background: linear-gradient(135deg, #ef4444 0%, #991b1b 100%); */
1993
+ color: #ef4444;
1994
+ }
1995
+
1996
+ .summary-card.closed:hover {
1997
+ box-shadow: 0 0 32px 0 #ef4444cc, 0 10px 32px #ef444422;
1998
+ border-left: 5px solid #ef4444;
1999
+ }
2000
+
2001
+ .summary-card.open .summary-icon {
2002
+ /* background: linear-gradient(135deg, #22c55e 0%, #2563eb 100%); */
2003
+ color: #00ae0e; /* 6 digit hex, avoids linter warnings */
2004
+ }
2005
+
2006
+ .summary-card.open:hover {
2007
+ box-shadow: 0 0 32px 0 #22c55ecc, 0 10px 32px #22c55e22;
2008
+ border-left: 5px solid #22c55e;
2009
+ }
2010
+
2011
+ /* Subtle bounce animation for icon on hover */
2012
+ .summary-card:hover .summary-icon {
2013
+ animation: summary-bounce0.7s cubic-bezier(0.4,0.2,0.2,1) both;
2014
+ }
2015
+ @keyframes summary-bounce {
2016
+ 0% { transform: scale(1) translateY(0); }
2017
+ 30% { transform: scale(1.18) translateY(-6px); }
2018
+ 60% { transform: scale(0.96) translateY(2px); }
2019
+ 100% { transform: scale(1) translateY(0); }
2020
+ }
2021
+
2022
+ .summary-value.blue { color: #2563eb; }
2023
+ .summary-value.green { color: #22c55e; }
2024
+ .summary-value.red { color: #ef4444; }
2025
+
src/app/recordpage/recordpage.component.html CHANGED
@@ -1,260 +1,301 @@
1
  <!-- Modern UI header with logo and PyDetect title -->
2
  <div class="site-header">
3
- <div class="header-inner">
4
- <div class="logo-cluster">
5
- <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
- <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
- </span>
8
- <div class="py-detect-title-header">
9
- <span class="py-letter p">P</span>
10
- <span class="py-letter y">Y</span>
11
- <span class="py-shape"></span>
12
- <span class="py-letter d">D</span>
13
- <span class="py-letter e">E</span>
14
- <span class="py-letter t">T</span>
15
- <span class="py-letter e2">E</span>
16
- <span class="py-letter c">C</span>
17
- <span class="py-letter t2">T</span>
18
- </div>
19
- </div>
20
- </div>
 
 
 
 
 
 
 
 
21
  </div>
22
 
23
  <!-- Salesforce-style card/table content below the header -->
24
  <div class="record-card">
25
- <div class="record-header">
26
- <div class="record-title-group">
27
- <span class="record-title">Police Investigation Records</span>
28
- <select class="record-dropdown">
29
- <option>Recently Viewed</option>
30
- <option>All Records</option>
31
- </select>
32
- </div>
33
- <div class="record-header-actions">
34
- <input class="record-search" type="text" [(ngModel)]="q" placeholder="Search this list..." />
35
- </div>
36
- </div>
37
-
38
  <!-- Analytics summary panel -->
39
- <div class="analytics-summary">
40
- <div class="summary-card total">
41
- <div class="summary-label">Total Cases</div>
42
- <div class="summary-value">{{ totalCases }}</div>
43
- </div>
44
- <div class="summary-card open">
45
- <div class="summary-label">Open</div>
46
- <div class="summary-value">{{ openCases }}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </div>
48
- <div class="summary-card closed">
49
- <div class="summary-label">Closed</div>
50
- <div class="summary-value">{{ closedCases }}</div>
51
  </div>
52
- </div>
53
 
54
- <div class="record-meta" style="padding: 8px 24px 0 24px; color: #6b7280; font-size: 0.98em;">
55
- {{ rows.length }} items • Updated a few seconds ago
56
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- <!-- Filter bar above the table -->
59
- <div class="filter-bar">
60
- <select [(ngModel)]="filterCrimeType">
61
- <option value="">Crime Type</option>
62
- <option *ngFor="let type of crimeTypes">{{ type }}</option>
63
- </select>
64
- <select [(ngModel)]="filterStatus">
65
- <option value="">Status</option>
66
- <option *ngFor="let status of statusTypes">{{ status }}</option>
67
- </select>
68
- <select [(ngModel)]="filterLocation">
69
- <option value="">Location</option>
70
- <option *ngFor="let loc of locations">{{ loc }}</option>
71
- </select>
72
- <select [(ngModel)]="filterOfficer">
73
- <option value="">Officer</option>
74
- <option *ngFor="let officer of officers">{{ officer }}</option>
75
- </select>
76
- <button (click)="applyFilters()">Apply</button>
77
- <button (click)="resetFilters()">Reset</button>
78
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- <table class="record-table">
81
- <thead>
82
- <tr>
83
- <th>#</th>
84
- <th>Case ID</th>
85
- <th>Status</th>
86
- <th>Crime Type</th>
87
- <th>Date &amp; Time</th>
88
- <th>Location</th>
89
- <th>Investigation Officer</th>
90
- <th>Suspect Name</th>
91
- <th>Reported By</th>
92
- <th>Last Updated</th>
93
- <th>Verified By</th>
94
- <th>Actions</th>
95
- </tr>
96
- </thead>
97
- <tbody>
98
- <tr *ngFor="let c of rows; let i = index">
99
- <td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
100
- <td><a (click)="openDetails(c, i)">{{ c.caseId || '—' }}</a></td>
101
- <td>
102
- <span class="status-label"
103
- [ngClass]="{
104
- 'status-open': c.status === 'Open',
105
- 'status-under': c.status === 'Under Investigation',
106
- 'status-closed': c.status === 'Closed'
107
- }">
108
- {{ c.status || '—' }}
109
- </span>
110
- </td>
111
- <td>{{ c.crime || '—' }}</td>
112
- <td>{{ c.dateTime ? (c.dateTime | date:'M/d/yyyy HH:mm') : '—' }}</td>
113
- <td>{{ c.police.address || '—' }}</td>
114
- <td>{{ c.police.name || '—' }}</td>
115
- <td>{{ c.accused.name || '—' }}</td>
116
- <td>{{ c.reportedBy || '—' }}</td>
117
- <td>{{ c.lastUpdated ? (c.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</td>
118
- <td>{{ c.verifiedBy || '—' }}</td>
119
- <td>
120
- <button class="icon-btn verify" (click)="verifyCase((currentPage - 1) * pageSize + i)" title="Verify">
121
- <i class="fas fa-user-check"></i>
122
- </button>
123
- <button class="icon-btn view" (click)="openDetails(c, (currentPage - 1) * pageSize + i)" title="View">
124
- <i class="fas fa-eye"></i>
125
- </button>
126
- <button class="icon-btn edit" (click)="editCase(c, (currentPage - 1) * pageSize + i)" title="Edit">
127
- <i class="fas fa-edit"></i>
128
- </button>
129
- <button class="icon-btn delete" (click)="deleteCase((currentPage - 1) * pageSize + i)" title="Delete">
130
- <i class="fas fa-trash"></i>
131
- </button>
132
- </td>
133
- </tr>
134
- <tr *ngIf="rows.length === 0">
135
- <td colspan="12" class="empty">No records found.</td>
136
- </tr>
137
- </tbody>
138
- </table>
139
 
140
- <!-- Pagination Controls -->
141
- <div class="pagination-controls" style="display:flex;justify-content:center;align-items:center;margin:20px 0;gap:10px;">
142
- <style>
143
- .pagination-controls button {
144
- border: none;
145
- background: #f3f4f6;
146
- color: #333;
147
- border-radius: 8px;
148
- padding: 0 16px;
149
- min-width: 40px;
150
- min-height: 40px;
151
- font-size: 1.1em;
152
- font-weight: 500;
153
- box-shadow: 0 2px 8px rgba(0,0,0,0.04);
154
- transition: background 0.2s, color 0.2s, transform 0.2s;
155
- cursor: pointer;
156
- outline: none;
157
- }
158
 
159
- .pagination-controls button:hover:not(:disabled),
160
- .pagination-controls button:focus:not(:disabled) {
161
- background: #e3eafe;
162
- color: #1976d2;
163
- transform: scale(1.08);
164
- }
 
165
 
166
- .pagination-controls button.active {
167
- background: #1976d2;
168
- color: #fff;
169
- font-weight: bold;
170
- box-shadow: 0 0 0 2px #90caf9;
171
- animation: pulseActive 1s infinite;
172
- }
173
 
174
- @keyframes pulseActive {
175
- 0% {
176
- box-shadow: 0 0 0 2px #90caf9;
177
- }
178
 
179
- 50% {
180
- box-shadow: 0 0 0 6px #90caf9;
 
181
  }
182
 
183
- 100% {
184
- box-shadow: 0 0 0 2px #90caf9;
 
 
185
  }
186
- }
187
-
188
- .pagination-controls span {
189
- font-size: 1.2em;
190
- color: #888;
191
- padding: 0 8px;
192
- }
193
- </style>
194
- <button (click)="prevPage()" [disabled]="currentPage === 1">«</button>
195
- <ng-container *ngFor="let page of getPagination()">
196
- <button *ngIf="page !== '...'" (click)="goToPage(page)" [class.active]="currentPage === page">{{ page }}</button>
197
- <span *ngIf="page === '...'">...</span>
198
- </ng-container>
199
- <button (click)="nextPage()" [disabled]="currentPage === totalPages">»</button>
200
  </div>
201
- </div>
202
 
203
- <!-- Results summary and page size selector -->
204
- <div style="display:flex;align-items:center;justify-content:flex-start;gap:24px;margin-bottom:16px;">
205
- <span style="font-size:1.1em;">Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span>
206
- <select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)" style="padding:4px 12px;border-radius:8px;font-size:1em;">
207
- <option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option>
208
- </select>
209
- </div>
210
 
211
- <!-- Modal -->
212
- <!-- Modal white blur overlay for background -->
213
- <div class="modal-blur-overlay" *ngIf="showDetails"></div>
214
 
215
- <!-- Modal backdrop and modal as before -->
216
- <div class="modal-backdrop" *ngIf="showDetails" (click)="closeDetails()"></div>
217
- <div class="modal" *ngIf="showDetails" role="dialog" aria-modal="true" aria-labelledby="detailsTitle">
218
- <div class="modal-header">
219
- <h2 id="detailsTitle">Case Details</h2>
220
- </div>
221
- <!-- View Modal: Show all subgroup pills and fields for each section -->
222
- <div class="modal-body" *ngIf="selectedCase as sc">
223
- <div class="modal-sections-grid">
224
- <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  <div class="section-block">
226
- <div class="section-title">{{ sectionKey === 'crime' ? 'Crime Details' : sectionKey === 'suspect' ? 'Suspect Details' : 'Evidence and Documents' }}</div>
227
- <ng-container *ngFor="let subgroup of getSubgroups(sectionKey)">
228
- <div class="subgroup-title" style="margin:10px 0 4px 0;font-weight:600;color:#1976d2;">{{ subgroup }}</div>
229
- <div class="fields-grid">
230
- <ng-container *ngFor="let field of getFieldsForSubgroup(sectionKey, subgroup)">
231
- <div class="field-card" style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;">
232
- <span style="font-weight:500;color:#333;">{{ field }}</span>
233
- <span style="color:#444;">{{ getFieldValue(sc, sectionKey, field) }}</span>
234
- </div>
235
- </ng-container>
236
  </div>
237
- </ng-container>
238
- </div>
239
- </ng-container>
240
- <!-- Show all other fields from formData not in the above structure -->
241
- <div class="section-block">
242
- <div class="section-title">All Entered Information</div>
243
- <div class="fields-grid">
244
- <div class="field-card" *ngFor="let key of objectKeys(sc)" style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;">
245
- <span style="font-weight:500;color:#333;">{{ key }}</span>
246
- <span style="color:#444;">{{ getValue(sc, key) }}</span>
247
  </div>
248
  </div>
249
  </div>
250
  </div>
 
 
 
251
  </div>
252
- <div class="modal-footer">
253
- <button type="button" class="btn" (click)="closeDetails()">Close</button>
254
- </div>
255
- </div>
256
 
257
- <!-- Footer from provided design -->
258
- <footer>
259
- <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
260
- </footer>
 
 
1
  <!-- Modern UI header with logo and PyDetect title -->
2
  <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
+ </div>
20
+ <div class="header-actions-right">
21
+ <button class="back-btn" (click)="navigateBackToInfoPage()">
22
+ <span class="back-icon">←</span> Back to Info Page
23
+ </button>
24
+ <button class="logout-btn" (click)="logout()">
25
+ <span class="logout-icon">⎋</span> Logout
26
+ </button>
27
+ </div>
28
+ </div>
29
  </div>
30
 
31
  <!-- Salesforce-style card/table content below the header -->
32
  <div class="record-card">
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  <!-- Analytics summary panel -->
34
+ <div class="analytics-panel">
35
+ <div class="analytics-blue">
36
+
37
+ <div class="record-header">
38
+ <div class="record-title-group">
39
+ <span class="record-title"><i class="fas fa-database"></i> Police Investigation Records</span>
40
+ <select class="record-dropdown">
41
+ <option>Recently Viewed</option>
42
+ <option>All Records</option>
43
+ </select>
44
+ </div>
45
+ <div class="record-header-actions">
46
+ <span style="position:relative;">
47
+ <i class="fas fa-search" style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#b0b0b0;"></i>
48
+ <input class="record-search" type="text" [(ngModel)]="q" (ngModelChange)="applyFilters()" placeholder="Search this list..." style="padding-left:32px;" />
49
+ </span>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="analytics-cards">
54
+ <!-- Total Cases -->
55
+ <div class="summary-card total">
56
+ <div class="summary-left">
57
+ <div class="summary-label">Total Cases</div>
58
+ <div class="summary-value blue">{{ totalCases }}</div>
59
+ <div class="summary-sub">&nbsp;</div>
60
+ </div>
61
+ <div class="summary-icon icon-indigo"><i class="fas fa-folder-open fa-bounce"></i></div>
62
+ </div>
63
+ <!-- Open Cases -->
64
+ <div class="summary-card open">
65
+ <div class="summary-left">
66
+ <div class="summary-label">Open</div>
67
+ <div class="summary-value green">{{ openCases }}</div>
68
+ <div class="summary-sub">&nbsp;</div>
69
+ </div>
70
+ <div class="summary-icon icon-blue"><i class="fas fa-exclamation-circle fa-beat"></i></div>
71
+ </div>
72
+ <!-- Closed Cases -->
73
+ <div class="summary-card closed">
74
+ <div class="summary-left">
75
+ <div class="summary-label">Closed</div>
76
+ <div class="summary-value red">{{ closedCases }}</div>
77
+ <div class="summary-sub">&nbsp;</div>
78
+ </div>
79
+ <div class="summary-icon icon-green"><i class="fas fa-check-circle fa-spin"></i></div>
80
+ </div>
81
+ </div>
82
  </div>
83
+
84
+ <div class="record-meta" style="padding:8px 24px 0 24px; color: #6b7280; font-size:0.98em;">
85
+ {{ rows.length }} items • Updated a few seconds ago
86
  </div>
 
87
 
88
+ <!-- Filter bar above the table -->
89
+ <div class="filter-bar">
90
+ <span style="margin-right:8px;"><i class="fas fa-filter"></i></span>
91
+ <select [(ngModel)]="filterCrimeType">
92
+ <option value="">Crime Type</option>
93
+ <option *ngFor="let type of crimeTypes">{{ type }}</option>
94
+ </select>
95
+ <select [(ngModel)]="filterStatus">
96
+ <option value="">Select Status</option>
97
+ <option *ngFor="let status of statusTypes">{{ status }}</option>
98
+ </select>
99
+ <select [(ngModel)]="filterLocation">
100
+ <option value="">Location</option>
101
+ <option *ngFor="let loc of locations">{{ loc }}</option>
102
+ </select>
103
+ <select [(ngModel)]="filterOfficer">
104
+ <option value="">Officer</option>
105
+ <option *ngFor="let officer of officers">{{ officer }}</option>
106
+ </select>
107
+ <button (click)="applyFilters()"><i class="fas fa-check"></i> Apply</button>
108
+ <button (click)="resetFilters()"><i class="fas fa-undo"></i> Reset</button>
109
+ </div>
110
 
111
+ <table class="record-table">
112
+ <thead>
113
+ <tr>
114
+ <th class="select-col"><i class="fas fa-list-ol"></i> Sl. No</th>
115
+ <th (click)="setSort('caseId')" [attr.aria-sort]="ariaSort('caseId')" style="cursor:pointer;">
116
+ <i class="fas fa-id-badge"></i> Case ID
117
+ <span class="sort" [ngClass]="{'asc': isAsc('caseId'), 'desc': isDesc('caseId'), 'neutral': !isAsc('caseId') && !isDesc('caseId')}"></span>
118
+ </th>
119
+ <th (click)="setSort('status')" [attr.aria-sort]="ariaSort('status')" style="cursor:pointer;">
120
+ <i class="fas fa-info-circle"></i> Status
121
+ <span class="sort" [ngClass]="{'asc': isAsc('status'), 'desc': isDesc('status'), 'neutral': !isAsc('status') && !isDesc('status')}"></span>
122
+ </th>
123
+ <th (click)="setSort('crime')" [attr.aria-sort]="ariaSort('crime')" style="cursor:pointer;">
124
+ <i class="fas fa-gavel"></i> Crime Type
125
+ <span class="sort" [ngClass]="{'asc': isAsc('crime'), 'desc': isDesc('crime'), 'neutral': !isAsc('crime') && !isDesc('crime')}"></span>
126
+ </th>
127
+ <th (click)="setSort('Investigation Officer')" [attr.aria-sort]="ariaSort('Investigation Officer')" style="cursor:pointer;">
128
+ <i class="fas fa-user-tie"></i> Investigator
129
+ <span class="sort" [ngClass]="{'asc': isAsc('Investigation Officer'), 'desc': isDesc('Investigation Officer'), 'neutral': !isAsc('Investigation Officer') && !isDesc('Investigation Officer')}"></span>
130
+ </th>
131
+ <th (click)="setSort('dateTime')" [attr.aria-sort]="ariaSort('dateTime')" style="cursor:pointer;">
132
+ <i class="fas fa-calendar-alt"></i> Date &amp; Time
133
+ <span class="sort" [ngClass]="{'asc': isAsc('dateTime'), 'desc': isDesc('dateTime'), 'neutral': !isAsc('dateTime') && !isDesc('dateTime')}"></span>
134
+ </th>
135
+ <th class="actions-col" style="text-align:left;"><i class="fas fa-cogs"></i> Actions</th>
136
+ </tr>
137
+ </thead>
138
+ <tbody>
139
+ <tr *ngFor="let c of rows, let i = index">
140
+ <td class="select-col">{{ (currentPage -1) * pageSize + i +1 }}</td>
141
+ <td><a (click)="navigateToCaseDetails(c)" style="cursor:pointer">{{ c.caseId || '—' }}</a></td>
142
+ <td>
143
+ <span class="status-label"
144
+ [ngClass]="{
145
+ 'status-open': c.status === 'Open',
146
+ 'status-under': c.status === 'Under Investigation',
147
+ 'status-closed': c.status === 'Closed'
148
+ }">
149
+ <span class="status-dot"></span>{{ c.status || '—' }}
150
+ </span>
151
+ </td>
152
+ <td>{{ c.crime || '—' }}</td>
153
+ <td>{{ c.police?.name || '—' }}</td>
154
+ <td>{{ c.dateTime ? (c.dateTime | date:'M/d/yyyy HH:mm') : '—' }}</td>
155
+ <td class="actions-col" style="text-align:left;">
156
+ <button class="icon-btn view" (click)="navigateToCaseDetails(c)" title="View">
157
+ <i class="fas fa-eye"></i>
158
+ </button>
159
+ <button class="icon-btn edit" (click)="editCase(c, i)" title="Edit">
160
+ <i class="fas fa-edit"></i>
161
+ </button>
162
+ <button class="icon-btn delete" (click)="deleteCase(i)" title="Delete">
163
+ <i class="fas fa-trash"></i>
164
+ </button>
165
+ </td>
166
+ </tr>
167
+ <tr *ngIf="rows.length ===0">
168
+ <td colspan="7" class="empty">No records found.</td>
169
+ </tr>
170
+ </tbody>
171
+ </table>
172
 
173
+ <!-- Pagination Controls -->
174
+ <div class="pagination-controls" style="display:flex;justify-content:center;align-items:center;margin:5px 0;gap:10px;">
175
+ <style>
176
+ .pagination-controls button {
177
+ border: none;
178
+ background: #f3f4f6;
179
+ color: #333;
180
+ border-radius: 8px;
181
+ padding: 016px;
182
+ min-width: 40px;
183
+ min-height: 40px;
184
+ font-size: 1.1em;
185
+ font-weight: 500;
186
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
187
+ transition: background 0.2s, color 0.2s, transform 0.2s;
188
+ cursor: pointer;
189
+ outline: none;
190
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
+ .pagination-controls button:hover:not(:disabled),
193
+ .pagination-controls button:focus:not(:disabled) {
194
+ background: #e3eafe;
195
+ color: #1976d2;
196
+ transform: scale(1.08);
197
+ }
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ .pagination-controls button.active {
200
+ background: #1976d2;
201
+ color: #fff;
202
+ font-weight: bold;
203
+ box-shadow: 0002px #90caf9;
204
+ animation: pulseActive1s infinite;
205
+ }
206
 
207
+ @keyframes pulseActive {
208
+ 0% {
209
+ box-shadow: 0002px #90caf9;
210
+ }
 
 
 
211
 
212
+ 50% {
213
+ box-shadow: 0006px #90caf9;
214
+ }
 
215
 
216
+ 100% {
217
+ box-shadow: 0002px #90caf9;
218
+ }
219
  }
220
 
221
+ .pagination-controls span {
222
+ font-size: 1.2em;
223
+ color: #888;
224
+ padding: 08px;
225
  }
226
+ </style>
227
+ <button (click)="prevPage()" [disabled]="currentPage ===1">«</button>
228
+ <ng-container *ngFor="let page of getPagination()">
229
+ <button *ngIf="page !== '...'" (click)="goToPage(page)" [class.active]="currentPage === page">{{ page }}</button>
230
+ <span *ngIf="page === '...'">...</span>
231
+ </ng-container>
232
+ <button (click)="nextPage()" [disabled]="currentPage === totalPages">»</button>
233
+ </div>
 
 
 
 
 
 
234
  </div>
 
235
 
236
+ <!-- Results summary and page size selector -->
237
+ <div style="display:flex;align-items:center;justify-content:flex-start;gap:24px;margin-bottom:12px;">
238
+ <span style="font-size:1.1em;"><i class="fas fa-list-ol"></i> Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span>
239
+ <select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)" style="padding:4px 12px;border-radius:8px;font-size:1em;">
240
+ <option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option>
241
+ </select>
242
+ </div>
243
 
244
+ <!-- Modal -->
245
+ <!-- Modal white blur overlay for background -->
246
+ <div class="modal-blur-overlay" *ngIf="showDetails"></div>
247
 
248
+ <!-- Modal backdrop and modal as before -->
249
+ <div class="modal-backdrop" *ngIf="showDetails" (click)="closeDetails()"></div>
250
+ <div class="modal" *ngIf="showDetails" role="dialog" aria-modal="true" aria-labelledby="detailsTitle">
251
+ <div class="modal-header">
252
+ <h2 id="detailsTitle"><i class="fas fa-info-circle"></i> Case Details</h2>
253
+ </div>
254
+ <!-- View Modal: Show all subgroup pills and fields for each section -->
255
+ <div class="modal-body" *ngIf="selectedCase as sc">
256
+ <div class="modal-sections-grid">
257
+ <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
258
+ <div class="section-block">
259
+ <div class="section-title">
260
+ <i *ngIf="sectionKey === 'crime'" class="fas fa-gavel"></i>
261
+ <i *ngIf="sectionKey === 'suspect'" class="fas fa-user-secret"></i>
262
+ <i *ngIf="sectionKey === 'notes'" class="fas fa-file-alt"></i>
263
+ {{ sectionKey === 'crime' ? 'Crime Details' : sectionKey === 'suspect' ? 'Suspect Details' : 'Evidence and Documents' }}
264
+ </div>
265
+ <ng-container *ngFor="let subgroup of getSubgroups(sectionKey)">
266
+ <div class="subgroup-title" style="margin:10px 0 4px 0;font-weight:600;color:#1976d2;">
267
+ <i class="fas fa-folder"></i> {{ subgroup }}
268
+ </div>
269
+ <div class="fields-grid">
270
+ <ng-container *ngFor="let field of getFieldsForSubgroup(sectionKey, subgroup)">
271
+ <div class="field-card" style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;">
272
+ <span style="font-weight:500;color:#333;"><i class="fas fa-tag"></i> {{ field }}</span>
273
+ <span style="color:#444;">{{ getFieldValue(sc, sectionKey, field) }}</span>
274
+ </div>
275
+ </ng-container>
276
+ </div>
277
+ </ng-container>
278
+ </div>
279
+ </ng-container>
280
+ <!-- Show all other fields from formData not in the above structure -->
281
  <div class="section-block">
282
+ <div class="section-title"><i class="fas fa-list"></i> All Entered Information</div>
283
+ <div class="fields-grid">
284
+ <div class="field-card" *ngFor="let key of objectKeys(sc)" style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;">
285
+ <span style="font-weight:500;color:#333;"><i class="fas fa-tag"></i> {{ key }}</span>
286
+ <span style="color:#444;">{{ getValue(sc, key) }}</span>
 
 
 
 
 
287
  </div>
 
 
 
 
 
 
 
 
 
 
288
  </div>
289
  </div>
290
  </div>
291
  </div>
292
+ <div class="modal-footer">
293
+ <button type="button" class="btn" (click)="closeDetails()"><i class="fas fa-times"></i> Close</button>
294
+ </div>
295
  </div>
 
 
 
 
296
 
297
+ <footer>
298
+ <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
299
+ </footer>
300
+
301
+
src/app/recordpage/recordpage.component.ts CHANGED
@@ -1,15 +1,21 @@
1
- import { Component, OnInit } from '@angular/core';
2
  import { Router } from '@angular/router';
3
- import { CaseStoreService, PoliceCase } from '../case-store.service';
4
  import { InfopageComponent } from '../infopage/infopage.component';
 
5
 
6
  @Component({
7
  selector: 'app-recordpage',
8
  templateUrl: './recordpage.component.html',
9
  styleUrls: ['./recordpage.component.css']
10
  })
11
- export class RecordpageComponent implements OnInit {
12
  cases: PoliceCase[] = [];
 
 
 
 
 
13
 
14
  // Pagination
15
  currentPage: number = 1;
@@ -187,20 +193,111 @@ export class RecordpageComponent implements OnInit {
187
  };
188
 
189
  const path = fieldMap[field] || field;
 
190
  if (Array.isArray(path)) {
191
- let value = sc;
192
  for (const p of path) {
193
- if (value && value[p] !== undefined) value = value[p];
194
- else return '—';
195
  }
196
- return value !== undefined && value !== null && value !== '' ? value : '—';
197
  } else {
198
- return sc[path] !== undefined && sc[path] !== null && sc[path] !== '' ? sc[path] : '—';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  }
 
 
 
200
  }
201
 
202
  getValue(obj: any, key: string): any {
203
- return obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== '' ? obj[key] : '—';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  }
205
 
206
  constructor(private caseStore: CaseStoreService, private router: Router) { }
@@ -218,18 +315,45 @@ export class RecordpageComponent implements OnInit {
218
 
219
  filteredCases: PoliceCase[] = [];
220
 
 
 
 
221
  ngOnInit(): void {
222
- this.load();
223
- this.populateFilterOptions();
224
- this.applyFilters();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  }
226
 
227
  load(): void {
228
  this.cases = this.caseStore.getPoliceCases();
 
229
  this.populateFilterOptions();
230
  this.applyFilters();
231
  }
232
 
 
 
 
 
 
 
233
  populateFilterOptions() {
234
  this.crimeTypes = [...new Set(this.cases.map(c => c.crime).filter(Boolean))] as string[];
235
  this.statusTypes = [...new Set(this.cases.map(c => c.status).filter(Boolean))] as string[];
@@ -238,13 +362,27 @@ export class RecordpageComponent implements OnInit {
238
  }
239
 
240
  applyFilters() {
241
- this.filteredCases = this.cases.filter(c =>
 
242
  (!this.filterCrimeType || c.crime === this.filterCrimeType) &&
243
  (!this.filterStatus || c.status === this.filterStatus) &&
244
  (!this.filterLocation || c.police?.address === this.filterLocation) &&
245
  (!this.filterOfficer || c.police?.name === this.filterOfficer)
246
  );
247
- this.currentPage = 1; // Reset to first page on filter
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  }
249
 
250
  resetFilters() {
@@ -276,22 +414,56 @@ export class RecordpageComponent implements OnInit {
276
  this.sortKey = key;
277
  this.sortDir = key === 'dateTime' ? 'desc' : 'asc';
278
  }
 
 
 
 
 
 
 
279
  }
280
- isAsc(key: typeof this.sortKey) { return this.sortKey === key && this.sortDir === 'asc'; }
281
- isDesc(key: typeof this.sortKey) { return this.sortKey === key && this.sortDir === 'desc'; }
282
  ariaSort(key: typeof this.sortKey) {
283
  return this.sortKey === key ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
284
  }
285
 
286
- private cell(c: any, key: typeof this.sortKey): string | number {
287
- switch (key) {
288
- case 'caseId': return (c.caseId ?? '').toString();
289
- case 'crime': return (c.crime ?? '').toString();
290
- case 'dateTime': return c.dateTime ? new Date(c.dateTime).getTime() : 0;
291
- case 'location': return (c.police?.address ?? '').toString();
292
- case 'status': return (c.status ?? '').toString();
293
- case 'Investigation Officer': return (c.police?.name ?? '').toString();
294
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  }
296
 
297
  // Update your table to use filteredCases instead of rows
@@ -314,8 +486,51 @@ export class RecordpageComponent implements OnInit {
314
  }
315
 
316
  editCase(c: PoliceCase, i: number): void {
317
- // Navigate to /infopage/:id for editing
318
- this.router.navigate(['/infopage', c.caseId]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  }
320
 
321
  onModernSearch() {
@@ -406,4 +621,14 @@ export class RecordpageComponent implements OnInit {
406
  goToDetect(caseId: string): void {
407
  this.router.navigate(['/py-detect'], { state: { caseId } });
408
  }
 
 
 
 
 
 
 
 
 
 
409
  }
 
1
+ import { Component, OnInit, OnDestroy } from '@angular/core';
2
  import { Router } from '@angular/router';
3
+ import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
4
  import { InfopageComponent } from '../infopage/infopage.component';
5
+ import { Subscription } from 'rxjs';
6
 
7
  @Component({
8
  selector: 'app-recordpage',
9
  templateUrl: './recordpage.component.html',
10
  styleUrls: ['./recordpage.component.css']
11
  })
12
+ export class RecordpageComponent implements OnInit, OnDestroy {
13
  cases: PoliceCase[] = [];
14
+ private casesSub?: Subscription;
15
+
16
+ // Date field groups for formatting
17
+ dateTimeFields = new Set<string>(['Date & Time (Entry)', 'Occurred From', 'Occurred To', 'Time Reported', 'Time Discovered']);
18
+ dateFields = new Set<string>(['Follow-up Date', 'Next Hearing Date']);
19
 
20
  // Pagination
21
  currentPage: number = 1;
 
193
  };
194
 
195
  const path = fieldMap[field] || field;
196
+ let value: any = undefined;
197
  if (Array.isArray(path)) {
198
+ let v = sc;
199
  for (const p of path) {
200
+ if (v && v[p] !== undefined) v = v[p];
201
+ else { v = undefined; break; }
202
  }
203
+ value = v;
204
  } else {
205
+ value = sc && sc[path] !== undefined ? sc[path] : undefined;
206
+ }
207
+
208
+ // Fallback: try raw formData (array or object) saved with the case
209
+ if (value === null || value === undefined || value === '') {
210
+ try {
211
+ const fd = this.getFormDataArray(sc);
212
+ const norm = (s: any) => {
213
+ if (s === null || s === undefined) return '';
214
+ let t = String(s).toLowerCase();
215
+ t = t.replace(/&/g, '');
216
+ t = t.replace(/and/g, '');
217
+ t = t.replace(/entry/g, '');
218
+ t = t.replace(/\s+/g, '');
219
+ return t.replace(/[^a-z0-9]/g, '');
220
+ };
221
+
222
+ if (fd && fd.length) {
223
+ let kv = fd.find(k => k && String(k.key).toLowerCase() === String(field).toLowerCase());
224
+ if (kv) value = kv.value;
225
+ if (value === null || value === undefined || value === '') {
226
+ const fieldNorm = norm(field);
227
+ const pathName = Array.isArray(path) ? path[path.length -1] : String(path);
228
+ const pathNorm = norm(pathName);
229
+ kv = fd.find(k => k && (norm(k.key) === fieldNorm || norm(k.key) === pathNorm || norm(k.key).includes(fieldNorm) || fieldNorm.includes(norm(k.key))));
230
+ if (kv) value = kv.value;
231
+ }
232
+ }
233
+
234
+ if ((value === null || value === undefined || value === '') && sc && sc.formData && typeof sc.formData === 'object') {
235
+ if (sc.formData[field] !== undefined) value = sc.formData[field];
236
+ else {
237
+ const fieldNorm = (s: any) => s === null || s === undefined ? '' : String(s).toLowerCase().replace(/[^a-z0-9]/g, '');
238
+ const target = fieldNorm(field);
239
+ for (const k of Object.keys(sc.formData)) {
240
+ if (fieldNorm(k) === target || k.toLowerCase() === field.toLowerCase() || k.toLowerCase().includes(field.toLowerCase())) {
241
+ value = sc.formData[k];
242
+ break;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ } catch (e) {
248
+ // ignore
249
+ }
250
+ }
251
+
252
+ // Normalize and format
253
+ if (value === null || value === undefined || value === '') return '—';
254
+
255
+ // Date formatting
256
+ if ((this.dateTimeFields && this.dateTimeFields.has(field)) || (this.dateFields && this.dateFields.has(field))) {
257
+ const d = new Date(value);
258
+ if (!isNaN(d.getTime())) {
259
+ if (this.dateFields && this.dateFields.has(field)) return d.toISOString().slice(0,10);
260
+ return d.toLocaleString();
261
+ }
262
  }
263
+
264
+ if (typeof value === 'object') return this.formatFormValue(value);
265
+ return value;
266
  }
267
 
268
  getValue(obj: any, key: string): any {
269
+ const v = obj && obj[key] !== undefined ? obj[key] : undefined;
270
+ if (v === null || v === undefined || v === '') return '—';
271
+ if (typeof v === 'object') return this.formatFormValue(v);
272
+ if ((this.dateTimeFields && this.dateTimeFields.has(key)) || (this.dateFields && this.dateFields.has(key))) {
273
+ const d = new Date(v);
274
+ if (!isNaN(d.getTime())) {
275
+ if (this.dateFields && this.dateFields.has(key)) return d.toISOString().slice(0,10);
276
+ return d.toLocaleString();
277
+ }
278
+ }
279
+ return v;
280
+ }
281
+
282
+ // Helpers for handling stored formData
283
+ isFormDataArray(fd: any): boolean {
284
+ return Array.isArray(fd) && fd.length >0 && fd.every((item: any) => item && Object.prototype.hasOwnProperty.call(item, 'key'));
285
+ }
286
+
287
+ getFormDataArray(caseObj: any): Array<{ key: string; value: any }> {
288
+ if (!caseObj || !caseObj.formData) return [];
289
+ const fd = caseObj.formData;
290
+ if (this.isFormDataArray(fd)) return fd as Array<{ key: string; value: any }>;
291
+ if (typeof fd === 'object') return Object.keys(fd).map(k => ({ key: k, value: fd[k] }));
292
+ return [{ key: 'value', value: fd }];
293
+ }
294
+
295
+ formatFormValue(value: any): string {
296
+ if (value === null || value === undefined || value === '') return '—';
297
+ if (typeof value === 'object') {
298
+ try { return JSON.stringify(value, null,2); } catch { return String(value); }
299
+ }
300
+ return String(value);
301
  }
302
 
303
  constructor(private caseStore: CaseStoreService, private router: Router) { }
 
315
 
316
  filteredCases: PoliceCase[] = [];
317
 
318
+ // selection state
319
+ allSelected: boolean = false;
320
+
321
  ngOnInit(): void {
322
+ // Subscribe to case store so UI updates automatically when new cases are added/updated
323
+ if (typeof this.caseStore.getCases$ === 'function') {
324
+ this.casesSub = this.caseStore.getCases$().subscribe((cases: PoliceCase[]) => {
325
+ this.cases = cases || [];
326
+ this.cases.forEach((c: any) => { if (c.selected === undefined) c.selected = false; });
327
+ this.populateFilterOptions();
328
+ this.applyFilters();
329
+ this.applySort();
330
+ });
331
+ } else {
332
+ // Fallback for older API
333
+ this.load();
334
+ this.populateFilterOptions();
335
+ this.applyFilters();
336
+ this.applySort();
337
+ }
338
+ }
339
+
340
+ ngOnDestroy(): void {
341
+ if (this.casesSub) this.casesSub.unsubscribe();
342
  }
343
 
344
  load(): void {
345
  this.cases = this.caseStore.getPoliceCases();
346
+ this.cases.forEach((c: any) => { if (c.selected === undefined) c.selected = false; });
347
  this.populateFilterOptions();
348
  this.applyFilters();
349
  }
350
 
351
+ toggleSelectAll(event: Event): void {
352
+ const checked = (event.target as HTMLInputElement).checked;
353
+ this.allSelected = checked;
354
+ this.rows.forEach((c: any) => c.selected = checked);
355
+ }
356
+
357
  populateFilterOptions() {
358
  this.crimeTypes = [...new Set(this.cases.map(c => c.crime).filter(Boolean))] as string[];
359
  this.statusTypes = [...new Set(this.cases.map(c => c.status).filter(Boolean))] as string[];
 
362
  }
363
 
364
  applyFilters() {
365
+ // First, filter by dropdowns
366
+ let filtered = this.cases.filter(c =>
367
  (!this.filterCrimeType || c.crime === this.filterCrimeType) &&
368
  (!this.filterStatus || c.status === this.filterStatus) &&
369
  (!this.filterLocation || c.police?.address === this.filterLocation) &&
370
  (!this.filterOfficer || c.police?.name === this.filterOfficer)
371
  );
372
+ // Then, filter by search query
373
+ const s = (this.q || '').toLowerCase();
374
+ if (s) {
375
+ filtered = filtered.filter(c =>
376
+ (c.caseId || '').toString().toLowerCase().includes(s) ||
377
+ (c.crime || '').toLowerCase().includes(s) ||
378
+ (c.police?.address || '').toLowerCase().includes(s) ||
379
+ (c.status || '').toLowerCase().includes(s) ||
380
+ (c.police?.name || '').toLowerCase().includes(s)
381
+ );
382
+ }
383
+ this.filteredCases = filtered;
384
+ this.currentPage =1; // Reset to first page on filter
385
+ this.applySort();
386
  }
387
 
388
  resetFilters() {
 
414
  this.sortKey = key;
415
  this.sortDir = key === 'dateTime' ? 'desc' : 'asc';
416
  }
417
+ this.applySort();
418
+ }
419
+ isAsc(key: typeof this.sortKey) {
420
+ return this.sortKey === key && this.sortDir === 'asc';
421
+ }
422
+ isDesc(key: typeof this.sortKey) {
423
+ return this.sortKey === key && this.sortDir === 'desc';
424
  }
 
 
425
  ariaSort(key: typeof this.sortKey) {
426
  return this.sortKey === key ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
427
  }
428
 
429
+ applySort() {
430
+ const key = this.sortKey;
431
+ const dir = this.sortDir;
432
+ this.filteredCases.sort((a, b) => {
433
+ let aVal: any, bVal: any;
434
+ switch (key) {
435
+ case 'caseId':
436
+ aVal = a.caseId || '';
437
+ bVal = b.caseId || '';
438
+ break;
439
+ case 'crime':
440
+ aVal = a.crime || '';
441
+ bVal = b.crime || '';
442
+ break;
443
+ case 'dateTime':
444
+ aVal = a.dateTime ? new Date(a.dateTime).getTime() :0;
445
+ bVal = b.dateTime ? new Date(b.dateTime).getTime() :0;
446
+ break;
447
+ case 'location':
448
+ aVal = a.police?.address || '';
449
+ bVal = b.police?.address || '';
450
+ break;
451
+ case 'status':
452
+ aVal = a.status || '';
453
+ bVal = b.status || '';
454
+ break;
455
+ case 'Investigation Officer':
456
+ aVal = a.police?.name || '';
457
+ bVal = b.police?.name || '';
458
+ break;
459
+ default:
460
+ aVal = '';
461
+ bVal = '';
462
+ }
463
+ if (aVal < bVal) return dir === 'asc' ? -1 :1;
464
+ if (aVal > bVal) return dir === 'asc' ?1 : -1;
465
+ return 0;
466
+ });
467
  }
468
 
469
  // Update your table to use filteredCases instead of rows
 
486
  }
487
 
488
  editCase(c: PoliceCase, i: number): void {
489
+ // Navigate to /infopage/:id for editing and pass prefill data in navigation state
490
+ const prefill: Record<string, any> = {};
491
+ try {
492
+ // If case.formData is stored as array of {key,value}
493
+ const fd = (c as any).formData;
494
+ if (fd && Array.isArray(fd)) {
495
+ (fd as Array<any>).forEach(kv => { if (kv && kv.key) prefill[kv.key] = kv.value; });
496
+ } else if (fd && typeof fd === 'object') {
497
+ Object.assign(prefill, fd as Record<string, any>);
498
+ }
499
+ // Also include mapped top-level properties for convenience
500
+ if (c.caseId) prefill['Case ID'] = c.caseId;
501
+ if (c.crime) prefill['Crime Type'] = c.crime;
502
+ if (c.dateTime) prefill['Date & Time (Entry)'] = c.dateTime;
503
+ if (c.police && c.police.address) prefill['Location'] = c.police.address;
504
+ if (c.police && c.police.name) prefill['Investigating Officer'] = c.police.name;
505
+ if (c.accused && c.accused.name) prefill['Suspect Name'] = c.accused.name;
506
+ } catch (e) {
507
+ // ignore
508
+ }
509
+
510
+ this.router.navigate(['/infopage', c.caseId], { state: { from: 'record', returnId: c.caseId, prefillFormData: prefill, case: c } });
511
+ }
512
+
513
+ // resolve origin dynamically from current router url
514
+ private resolveOrigin(): string {
515
+ try {
516
+ const cur = this.router.url || '';
517
+ if (cur.includes('/case-details')) return 'case-details';
518
+ if (cur.includes('/record')) return 'record';
519
+ } catch {}
520
+ return 'record';
521
+ }
522
+
523
+ navigateToCaseDetails(c: PoliceCase): void {
524
+ if (!c || !c.caseId) return;
525
+ const origin = this.resolveOrigin();
526
+ // navigate to new summary page — include source so summary can navigate back
527
+ const from = origin;
528
+ this.router.navigate(['/case-details-summary-page', c.caseId], { queryParams: { from, returnId: c.caseId }, state: { case: c, from, returnId: c.caseId } });
529
+ }
530
+
531
+ viewSummary(caseId: string) {
532
+ const origin = this.resolveOrigin();
533
+ this.router.navigate(['/case-details-summary-page', caseId], { queryParams: { from: origin, returnId: caseId }, state: { from: origin, returnId: caseId } });
534
  }
535
 
536
  onModernSearch() {
 
621
  goToDetect(caseId: string): void {
622
  this.router.navigate(['/py-detect'], { state: { caseId } });
623
  }
624
+
625
+ navigateBackToInfoPage(): void {
626
+ this.router.navigate(['/infopage']);
627
+ }
628
+
629
+ logout(): void {
630
+ // Implement your logout logic here (clear session, etc.)
631
+ // For now, just redirect to home/login
632
+ window.location.href = '/';
633
+ }
634
  }
src/app/services/pydetect.service.ts ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface SessionData {
2
+ sessionId: string;
3
+ caseData?: CaseData;
4
+ accusedData?: AccusedData;
5
+ evidence?: EvidenceItem[];
6
+ questions?: any[];
7
+ responses?: any[];
8
+ notes?: any[];
9
+ report?: any;
10
+ }
11
+ import { Injectable } from '@angular/core';
12
+ import { HttpClient, HttpHeaders } from '@angular/common/http';
13
+ import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
14
+ import { delay, tap, catchError } from 'rxjs/operators';
15
+ import { environment } from '../../environments/environment';
16
+ // Request investigation questions based on Brief Description
17
+
18
+ export interface CaseData {
19
+ caseId: string;
20
+ caseType: string;
21
+ crimeCategory: string;
22
+ crimeSubtype: string;
23
+ description: string;
24
+ location: string;
25
+ dateTime: string;
26
+ urgency: string;
27
+ officerName: string;
28
+ badgeNumber: string;
29
+ department: string;
30
+ contactInfo: string;
31
+ }
32
+
33
+ export interface AccusedData {
34
+ name: string;
35
+ age: string;
36
+ gender: string;
37
+ address: string;
38
+ occupation: string;
39
+ contactNumber: string;
40
+ relationship: string;
41
+ background: string;
42
+ previousRecords: string;
43
+ }
44
+
45
+ export interface EvidenceItem {
46
+ type: string;
47
+ description: string;
48
+ location: string;
49
+ collectedBy: string;
50
+ dateCollected: string;
51
+ chainOfCustody: string;
52
+ significance: string;
53
+ }
54
+
55
+ @Injectable({
56
+ providedIn: 'root'
57
+ })
58
+ export class PyDetectService {
59
+ private baseUrl = environment.pyDetectApiUrl;
60
+ private httpOptions = {
61
+ headers: new HttpHeaders({
62
+ 'Content-Type': 'application/json'
63
+ })
64
+ };
65
+
66
+ // Session management
67
+ private currentSessionSubject = new BehaviorSubject<SessionData | null>(null);
68
+ public currentSession$ = this.currentSessionSubject.asObservable();
69
+
70
+ // Voice settings
71
+ private voiceEnabledSubject = new BehaviorSubject<boolean>(true);
72
+ public voiceEnabled$ = this.voiceEnabledSubject.asObservable();
73
+
74
+ constructor(private http: HttpClient) {
75
+ // Initialize voice settings from localStorage
76
+ const savedVoiceEnabled = localStorage.getItem('pydetect_voice_enabled');
77
+ if (savedVoiceEnabled !== null) {
78
+ this.voiceEnabledSubject.next(JSON.parse(savedVoiceEnabled));
79
+ }
80
+ }
81
+
82
+ // Request investigation questions based on Brief Description
83
+
84
+ // ============ SESSION MANAGEMENT ============
85
+
86
+ // Start a new session
87
+ startSession(briefDescription?: string): Observable<any> {
88
+ const payload: any = {};
89
+ if (briefDescription) payload.brief_description = briefDescription;
90
+ return this.http.post(`${this.baseUrl}/start_session`, payload, this.httpOptions)
91
+ .pipe(
92
+ tap(response => {
93
+ const responseData = response as any;
94
+ const sessionData: SessionData = {
95
+ sessionId: responseData.session_id || this.generateSessionId(),
96
+ caseData: undefined,
97
+ accusedData: undefined,
98
+ evidence: [],
99
+ questions: [],
100
+ responses: [],
101
+ notes: [],
102
+ report: undefined
103
+ };
104
+ this.currentSessionSubject.next(sessionData);
105
+ this.saveSessionToStorage(sessionData);
106
+ }),
107
+ catchError(this.handleError)
108
+ );
109
+ }
110
+
111
+ // Get current session
112
+ getCurrentSession(): SessionData | null {
113
+ return this.currentSessionSubject.value;
114
+ }
115
+
116
+ // Update session data
117
+ updateSession(updates: Partial<SessionData>): void {
118
+ const current = this.currentSessionSubject.value;
119
+ if (current) {
120
+ const updated = { ...current, ...updates };
121
+ this.currentSessionSubject.next(updated);
122
+ this.saveSessionToStorage(updated);
123
+ }
124
+ }
125
+
126
+ // ============ CASE MANAGEMENT ============
127
+
128
+ // Submit complete case details
129
+ submitCaseDetails(sessionId: string, caseData: CaseData, briefDescription?: string): Observable<any> {
130
+ const payload: any = {
131
+ session_id: sessionId,
132
+ case_data: caseData,
133
+ timestamp: new Date().toISOString()
134
+ };
135
+ // ...existing code...
136
+ return this.http.post(`${this.baseUrl}/submit_case`, payload, this.httpOptions)
137
+ .pipe(
138
+ tap(response => {
139
+ this.updateSession({ caseData: caseData });
140
+ }),
141
+ catchError(this.handleError)
142
+ );
143
+ }
144
+
145
+ // ============ BODY LANGUAGE EXPLANATION ============
146
+
147
+ /**
148
+ * Fetches body language explanation for a given cue from backend.
149
+ * @param cue The body language cue to explain
150
+ * @returns Observable<{ explanation: string }>
151
+ */
152
+ bodyLanguageExplain(cue: string): Observable<{ meaning?: string; explanation?: string }> {
153
+ const payload = { cue };
154
+ return this.http.post<{ meaning?: string; explanation?: string }>(`${this.baseUrl}/body_language_explain`, payload, this.httpOptions)
155
+ .pipe(catchError((error): Observable<{ meaning?: string; explanation?: string }> => {
156
+ let errorMessage = 'An unknown error occurred';
157
+ if (error.error instanceof ErrorEvent) {
158
+ errorMessage = `Client Error: ${error.error.message}`;
159
+ } else {
160
+ errorMessage = `Server Error: ${error.status} - ${error.message}`;
161
+ }
162
+ // Return an object with explanation only for error case
163
+ return of({ explanation: errorMessage });
164
+ }));
165
+ }
166
+
167
+ // Submit accused details
168
+ submitAccused(sessionId: string, accused: AccusedData, additionalData?: any): Observable<any> {
169
+ const currentSession = this.getCurrentSession();
170
+
171
+ const payload = {
172
+ session_id: sessionId,
173
+ accused: accused,
174
+ crime: additionalData?.crime || currentSession?.caseData?.crimeCategory || '',
175
+ profile: currentSession?.caseData || additionalData?.profile || {},
176
+ evidence: currentSession?.evidence || additionalData?.evidence || [],
177
+ timestamp: new Date().toISOString(),
178
+ ...additionalData
179
+ };
180
+
181
+ return this.http.post(`${this.baseUrl}/submit_accused`, payload, this.httpOptions)
182
+ .pipe(
183
+ tap(response => {
184
+ this.updateSession({ accusedData: accused });
185
+ }),
186
+ catchError(this.handleError)
187
+ );
188
+ }
189
+
190
+ // ============ EVIDENCE MANAGEMENT ============
191
+
192
+ // Submit evidence
193
+ submitEvidence(sessionId: string, evidence: EvidenceItem[]): Observable<any> {
194
+ const payload = {
195
+ session_id: sessionId,
196
+ evidence: evidence.map(item => ({
197
+ ...item,
198
+ timestamp: new Date().toISOString(),
199
+ evidence_id: this.generateEvidenceId()
200
+ }))
201
+ };
202
+
203
+ return this.http.post(`${this.baseUrl}/submit_evidence`, payload, this.httpOptions)
204
+ .pipe(
205
+ tap(response => {
206
+ const currentSession = this.getCurrentSession();
207
+ const updatedEvidence = [...(currentSession?.evidence || []), ...evidence];
208
+ this.updateSession({ evidence: updatedEvidence });
209
+ }),
210
+ catchError(this.handleError)
211
+ );
212
+ }
213
+
214
+ // Add single evidence item
215
+ addEvidenceItem(sessionId: string, evidenceItem: EvidenceItem): Observable<any> {
216
+ return this.submitEvidence(sessionId, [evidenceItem]);
217
+ }
218
+
219
+ // ============ INVESTIGATION NOTES ============
220
+
221
+ // Add investigation note
222
+ addNote(sessionId: string, note: string, category: string = 'general'): Observable<any> {
223
+ const noteData = {
224
+ session_id: sessionId,
225
+ note: note,
226
+ timestamp: new Date().toISOString(),
227
+ category: category,
228
+ note_id: this.generateNoteId(),
229
+ officer: this.getCurrentSession()?.caseData?.officerName || 'Unknown Officer'
230
+ };
231
+
232
+ return this.http.post(`${this.baseUrl}/add_note`, noteData, this.httpOptions)
233
+ .pipe(
234
+ tap(response => {
235
+ const currentSession = this.getCurrentSession();
236
+ const updatedNotes = [...(currentSession?.notes || []), noteData];
237
+ this.updateSession({ notes: updatedNotes });
238
+ }),
239
+ catchError(this.handleError)
240
+ );
241
+ }
242
+
243
+ // ============ AI QUESTIONING SYSTEM ============
244
+
245
+ // Get context-aware AI questions
246
+ askQuestion(sessionId: string, crimeType?: string, briefDescription?: string): Observable<any> {
247
+ // ...existing code...
248
+ const queryParams = [`session_id=${encodeURIComponent(sessionId)}`];
249
+ // Always send crimeType, even if empty
250
+ queryParams.push(`crime_type=${encodeURIComponent(crimeType ?? '')}`);
251
+ // Always send briefDescription, even if empty
252
+ queryParams.push(`brief_description=${encodeURIComponent(briefDescription ?? '')}`);
253
+ const queryString = queryParams.join('&');
254
+ return this.http.get(`${this.baseUrl}/ask_question?${queryString}`, this.httpOptions)
255
+ .pipe(catchError(this.handleError));
256
+ }
257
+
258
+ // Submit response to AI question
259
+ submitResponse(sessionId: string, text: string, questionId?: string, timing?: {
260
+ answer_start_at?: number;
261
+ answer_end_at?: number;
262
+ duration_ms?: number;
263
+ mode?: string;
264
+ }): Observable<any> {
265
+ const responseData: any = {
266
+ session_id: sessionId,
267
+ text: text,
268
+ question_id: questionId,
269
+ timestamp: new Date().toISOString(),
270
+ response_id: this.generateResponseId()
271
+ };
272
+ if (timing) {
273
+ if (timing.answer_start_at) responseData.answer_start_at = timing.answer_start_at;
274
+ if (timing.answer_end_at) responseData.answer_end_at = timing.answer_end_at;
275
+ if (typeof timing.duration_ms === 'number') responseData.duration_ms = timing.duration_ms;
276
+ if (timing.mode) responseData.mode = timing.mode;
277
+ }
278
+ return this.http.post(`${this.baseUrl}/submit_response`, responseData, this.httpOptions)
279
+ .pipe(
280
+ tap(response => {
281
+ const currentSession = this.getCurrentSession();
282
+ const updatedResponses = [...(currentSession?.responses || []), responseData];
283
+ this.updateSession({ responses: updatedResponses });
284
+ }),
285
+ catchError(this.handleError)
286
+ );
287
+ }
288
+
289
+ // Stream a single face frame for nonverbal analysis
290
+ faceFrame(sessionId: string, frameDataUrl: string): Observable<any> {
291
+ const payload = { session_id: sessionId, frame: frameDataUrl };
292
+ return this.http.post(`${this.baseUrl}/face_frame`, payload, this.httpOptions)
293
+ .pipe(catchError(this.handleError));
294
+ }
295
+
296
+ // ============ REPORTING SYSTEM ============
297
+
298
+ // Get comprehensive report
299
+ getReport(sessionId: string, reportType: string = 'complete'): Observable<any> {
300
+ return this.http.get(`${this.baseUrl}/get_report/${sessionId}?type=${reportType}`, this.httpOptions)
301
+ .pipe(
302
+ tap(response => {
303
+ this.updateSession({ report: response });
304
+ }),
305
+ catchError(this.handleError)
306
+ );
307
+ }
308
+
309
+ // Generate summary report
310
+ generateSummary(sessionId: string): Observable<any> {
311
+ const currentSession = this.getCurrentSession();
312
+ const summaryData = {
313
+ session_id: sessionId,
314
+ case_data: currentSession?.caseData,
315
+ accused_data: currentSession?.accusedData,
316
+ evidence_count: currentSession?.evidence?.length || 0,
317
+ questions_answered: currentSession?.responses?.length || 0,
318
+ notes_count: currentSession?.notes?.length || 0,
319
+ timestamp: new Date().toISOString()
320
+ };
321
+
322
+ return this.http.post(`${this.baseUrl}/generate_summary`, summaryData, this.httpOptions)
323
+ .pipe(catchError(this.handleError));
324
+ }
325
+
326
+ // ============ VOICE FUNCTIONALITY ============
327
+
328
+ // Toggle voice functionality
329
+ toggleVoice(): void {
330
+ const current = this.voiceEnabledSubject.value;
331
+ this.voiceEnabledSubject.next(!current);
332
+ localStorage.setItem('pydetect_voice_enabled', JSON.stringify(!current));
333
+ }
334
+
335
+ // Check if voice is enabled
336
+ isVoiceEnabled(): boolean {
337
+ return this.voiceEnabledSubject.value;
338
+ }
339
+
340
+ // ============ AUTHENTICATION ============
341
+
342
+ // Sign in
343
+ signIn(email: string, password: string): Observable<any> {
344
+ return this.http.post(`${this.baseUrl}/sign-in`, {
345
+ email: email,
346
+ password: password
347
+ }, this.httpOptions).pipe(catchError(this.handleError));
348
+ }
349
+
350
+ // Sign up
351
+ signUp(name: string, email: string, password: string, role: string = 'investigator'): Observable<any> {
352
+ return this.http.post(`${this.baseUrl}/sign-up`, {
353
+ name: name,
354
+ email: email,
355
+ password: password,
356
+ role: role
357
+ }, this.httpOptions).pipe(catchError(this.handleError));
358
+ }
359
+
360
+ // Health check
361
+ healthCheck(): Observable<any> {
362
+ return this.http.get(`${this.baseUrl}/health`, this.httpOptions)
363
+ .pipe(catchError(this.handleError));
364
+ }
365
+
366
+ // ============ UTILITY METHODS ============
367
+
368
+ // Generate unique session ID
369
+ private generateSessionId(): string {
370
+ return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
371
+ }
372
+
373
+ // Generate unique evidence ID
374
+ private generateEvidenceId(): string {
375
+ return 'evidence_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
376
+ }
377
+
378
+ // Generate unique note ID
379
+ private generateNoteId(): string {
380
+ return 'note_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
381
+ }
382
+
383
+ // Generate unique response ID
384
+ private generateResponseId(): string {
385
+ return 'response_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
386
+ }
387
+
388
+ // Save session to localStorage
389
+ private saveSessionToStorage(session: SessionData): void {
390
+ try {
391
+ localStorage.setItem('pydetect_current_session', JSON.stringify(session));
392
+ } catch (error) {
393
+ }
394
+ }
395
+
396
+ // Load session from localStorage
397
+ loadSessionFromStorage(): SessionData | null {
398
+ try {
399
+ const stored = localStorage.getItem('pydetect_current_session');
400
+ if (stored) {
401
+ const session = JSON.parse(stored);
402
+ this.currentSessionSubject.next(session);
403
+ return session;
404
+ }
405
+ } catch (error) {
406
+ }
407
+ return null;
408
+ }
409
+
410
+ // Clear current session
411
+ clearSession(): void {
412
+ this.currentSessionSubject.next(null);
413
+ localStorage.removeItem('pydetect_current_session');
414
+ }
415
+
416
+ // Error handling
417
+ private handleError(error: any): Observable<never> {
418
+
419
+ let errorMessage = 'An unknown error occurred';
420
+ if (error.error instanceof ErrorEvent) {
421
+ errorMessage = `Client Error: ${error.error.message}`;
422
+ } else {
423
+ errorMessage = `Server Error: ${error.status} - ${error.message}`;
424
+ }
425
+
426
+ return throwError(() => new Error(errorMessage));
427
+ }
428
+
429
+ // ============ DATA VALIDATION ============
430
+
431
+ // Validate case data
432
+ validateCaseData(caseData: Partial<CaseData>): { isValid: boolean; errors: string[] } {
433
+ const errors: string[] = [];
434
+
435
+ if (!caseData.caseId) errors.push('Case ID is required');
436
+ if (!caseData.crimeCategory) errors.push('Crime category is required');
437
+ if (!caseData.description) errors.push('Description is required');
438
+ if (!caseData.officerName) errors.push('Officer name is required');
439
+ if (!caseData.badgeNumber) errors.push('Badge number is required');
440
+
441
+ return {
442
+ isValid: errors.length === 0,
443
+ errors: errors
444
+ };
445
+ }
446
+
447
+ // Validate accused data
448
+ validateAccusedData(accusedData: Partial<AccusedData>): { isValid: boolean; errors: string[] } {
449
+ const errors: string[] = [];
450
+
451
+ if (!accusedData.name) errors.push('Accused name is required');
452
+ if (!accusedData.age) errors.push('Age is required');
453
+ if (!accusedData.gender) errors.push('Gender is required');
454
+
455
+ return {
456
+ isValid: errors.length === 0,
457
+ errors: errors
458
+ };
459
+ }
460
+
461
+ // ============ STATISTICS & ANALYTICS ============
462
+
463
+ // Get session statistics
464
+ getSessionStats(): any {
465
+ const session = this.getCurrentSession();
466
+ if (!session) return null;
467
+
468
+ return {
469
+ sessionId: session.sessionId,
470
+ questionsAnswered: session.responses?.length || 0,
471
+ evidenceItems: session.evidence?.length || 0,
472
+ notesAdded: session.notes?.length || 0,
473
+ hasAccusedData: !!session.accusedData,
474
+ hasCaseData: !!session.caseData,
475
+ hasReport: !!session.report
476
+ };
477
+ }
478
+
479
+ // Request investigation questions from backend
480
+ getInvestigationQuestions(sessionId: string, crimeType: string, briefDescription: string): Observable<any> {
481
+ return this.http.get(
482
+ `${this.baseUrl}/ask_question?session_id=${sessionId}&crime_type=${encodeURIComponent(crimeType)}&brief_description=${encodeURIComponent(briefDescription)}`,
483
+ this.httpOptions
484
+ );
485
+ }
486
+ }
src/app/shared/case-store.service.ts CHANGED
@@ -1,25 +1,26 @@
1
  import { Injectable } from '@angular/core';
 
2
 
3
  export interface PoliceCase {
4
  // Optional metadata used by your UI
5
  caseId?: string;
6
  dateTime?: string;
7
  status?: 'Open' | 'Under Investigation' | 'Closed' | 'Archived';
8
- crime: string;
9
- police: {
10
- name: string;
11
- station: string;
12
- address: string;
13
- pincode: string;
14
- dutyPerson: string;
15
- modeOfCrime: string;
16
  information?: string;
17
  };
18
- accused: {
19
- name: string;
20
- age: string | number;
21
- gender: string;
22
- address: string;
23
  occupation?: string;
24
  };
25
  lastUpdated?: string;
@@ -29,21 +30,48 @@ export interface PoliceCase {
29
  verifiedBy?: string;
30
  briefDescription?: string;
31
  caseCategory?: string;
 
 
 
 
32
  }
33
 
34
  @Injectable({ providedIn: 'root' })
35
  export class CaseStoreService {
36
  private readonly storageKey = 'py_detect_police_cases';
37
  private cases: PoliceCase[] = [];
 
 
 
 
 
 
 
38
 
39
  constructor() {
40
  this.load();
41
  }
42
 
43
  /** Create (newest first) */
 
 
 
 
 
 
 
 
 
44
  addPoliceCase(c: PoliceCase): void {
 
 
 
 
 
45
  this.cases.unshift(c);
46
  this.save();
 
 
47
  }
48
 
49
  /** Read */
@@ -53,9 +81,20 @@ export class CaseStoreService {
53
 
54
  /** Update by array index */
55
  updatePoliceCaseAt(index: number, updated: PoliceCase): void {
56
- if (index >= 0 && index < this.cases.length) {
 
57
  this.cases[index] = updated;
58
  this.save();
 
 
 
 
 
 
 
 
 
 
59
  }
60
  }
61
 
@@ -63,8 +102,10 @@ export class CaseStoreService {
63
  updatePoliceCaseById(caseId: string, updated: PoliceCase): void {
64
  const idx = this.cases.findIndex(c => c.caseId === caseId);
65
  if (idx !== -1) {
 
66
  this.cases[idx] = updated;
67
  this.save();
 
68
  }
69
  }
70
 
@@ -77,6 +118,14 @@ export class CaseStoreService {
77
  const suspect = (formValue && formValue.suspect) || {};
78
  const notes = (formValue && formValue.notes) || {};
79
 
 
 
 
 
 
 
 
 
80
  const mapped: PoliceCase = {
81
  caseId: crime.caseId || '',
82
  dateTime: crime.dateTime || '',
@@ -97,7 +146,9 @@ export class CaseStoreService {
97
  gender: suspect.gender || '—',
98
  address: suspect.address || '—',
99
  occupation: suspect.alias || ''
100
- }
 
 
101
  };
102
 
103
  this.addPoliceCase(mapped);
@@ -109,8 +160,8 @@ export class CaseStoreService {
109
  const suspect = (formValue && formValue.suspect) || {};
110
  const notes = (formValue && formValue.notes) || {};
111
  const mapped: PoliceCase = {
112
- caseId: crime.caseId || '',
113
- dateTime: crime.dateTime || '',
114
  status: notes.status || 'Open',
115
  crime: crime.crimeType || 'Unknown',
116
  police: {
@@ -132,16 +183,91 @@ export class CaseStoreService {
132
  reportedBy: crime.reportedBy || '',
133
  verifiedBy: notes.verifiedBy || '',
134
  briefDescription: crime.briefDescription || '',
 
 
 
 
 
 
 
 
 
 
135
  };
 
136
  const idx = this.cases.findIndex(c => c.caseId === mapped.caseId);
137
  if (idx !== -1) {
138
- this.cases[idx] = mapped;
 
139
  this.save();
 
 
140
  } else {
141
  this.addPoliceCase(mapped);
142
  }
143
  }
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  /** Persist to localStorage (safe to keep; remove if not needed) */
146
  private save(): void {
147
  try { localStorage.setItem(this.storageKey, JSON.stringify(this.cases)); } catch { }
@@ -156,7 +282,7 @@ export class CaseStoreService {
156
  this.cases = [];
157
  }
158
  // Seed a default case if none exist (for development/testing)
159
- if (this.cases.length === 0) {
160
  this.cases = [
161
  {
162
  caseId: 'CASE-001',
@@ -174,14 +300,17 @@ export class CaseStoreService {
174
  },
175
  accused: {
176
  name: 'Jane Smith',
177
- age: 30,
178
  gender: 'Female',
179
  address: '456 Side Rd, City',
180
  occupation: 'Unemployed'
181
- }
 
182
  }
183
  ];
184
  this.save();
185
  }
 
 
186
  }
187
  }
 
1
  import { Injectable } from '@angular/core';
2
+ import { BehaviorSubject, Observable } from 'rxjs';
3
 
4
  export interface PoliceCase {
5
  // Optional metadata used by your UI
6
  caseId?: string;
7
  dateTime?: string;
8
  status?: 'Open' | 'Under Investigation' | 'Closed' | 'Archived';
9
+ crime?: string;
10
+ police?: {
11
+ name?: string;
12
+ station?: string;
13
+ address?: string;
14
+ pincode?: string;
15
+ dutyPerson?: string;
16
+ modeOfCrime?: string;
17
  information?: string;
18
  };
19
+ accused?: {
20
+ name?: string;
21
+ age?: string | number;
22
+ gender?: string;
23
+ address?: string;
24
  occupation?: string;
25
  };
26
  lastUpdated?: string;
 
30
  verifiedBy?: string;
31
  briefDescription?: string;
32
  caseCategory?: string;
33
+ // Store raw form data from infopage so all entered fields can be displayed
34
+ // Use array of key/value pairs for safe rendering
35
+ formData?: Array<{ key: string; value: any }> | Record<string, any>;
36
+ selected?: boolean; // <-- Add this line for table selection
37
  }
38
 
39
  @Injectable({ providedIn: 'root' })
40
  export class CaseStoreService {
41
  private readonly storageKey = 'py_detect_police_cases';
42
  private cases: PoliceCase[] = [];
43
+ // Observable subject to broadcast changes
44
+ private casesSubject = new BehaviorSubject<PoliceCase[]>([]);
45
+
46
+ /** Observable for components to subscribe to case list updates */
47
+ getCases$(): Observable<PoliceCase[]> {
48
+ return this.casesSubject.asObservable();
49
+ }
50
 
51
  constructor() {
52
  this.load();
53
  }
54
 
55
  /** Create (newest first) */
56
+ private getNextCaseId(): string {
57
+ const max = this.cases
58
+ .map(c => c.caseId)
59
+ .map(id => parseInt((id || '').replace('CASE-', ''),10))
60
+ .filter(n => !isNaN(n))
61
+ .reduce((a, b) => Math.max(a, b),0);
62
+ return `CASE-${(max +1).toString().padStart(3, '0')}`;
63
+ }
64
+
65
  addPoliceCase(c: PoliceCase): void {
66
+ if (!c.caseId) {
67
+ c.caseId = this.getNextCaseId();
68
+ }
69
+ c.lastUpdated = new Date().toISOString();
70
+ c.verifiedBy = c.verifiedBy || '';
71
  this.cases.unshift(c);
72
  this.save();
73
+ this.debugLogCasesOperation('addPoliceCase', c);
74
+ this.casesSubject.next(this.cases.slice());
75
  }
76
 
77
  /** Read */
 
81
 
82
  /** Update by array index */
83
  updatePoliceCaseAt(index: number, updated: PoliceCase): void {
84
+ if (index >=0 && index < this.cases.length) {
85
+ updated.lastUpdated = new Date().toISOString();
86
  this.cases[index] = updated;
87
  this.save();
88
+ this.casesSubject.next(this.cases.slice());
89
+ }
90
+ }
91
+
92
+ /** Delete by array index */
93
+ deletePoliceCaseAt(index: number): void {
94
+ if (index >=0 && index < this.cases.length) {
95
+ this.cases.splice(index,1);
96
+ this.save();
97
+ this.casesSubject.next(this.cases.slice());
98
  }
99
  }
100
 
 
102
  updatePoliceCaseById(caseId: string, updated: PoliceCase): void {
103
  const idx = this.cases.findIndex(c => c.caseId === caseId);
104
  if (idx !== -1) {
105
+ updated.lastUpdated = new Date().toISOString();
106
  this.cases[idx] = updated;
107
  this.save();
108
+ this.casesSubject.next(this.cases.slice());
109
  }
110
  }
111
 
 
118
  const suspect = (formValue && formValue.suspect) || {};
119
  const notes = (formValue && formValue.notes) || {};
120
 
121
+ // Merge raw inputs so we capture everything entered on the Info page
122
+ const mergedRaw = {
123
+ ...(formValue && formValue.formData ? formValue.formData : (formValue || {})),
124
+ ...crime,
125
+ ...suspect,
126
+ ...notes
127
+ };
128
+
129
  const mapped: PoliceCase = {
130
  caseId: crime.caseId || '',
131
  dateTime: crime.dateTime || '',
 
146
  gender: suspect.gender || '—',
147
  address: suspect.address || '—',
148
  occupation: suspect.alias || ''
149
+ },
150
+ // store raw form data so UI can display all entered fields
151
+ formData: this.convertToKeyValue(mergedRaw || {})
152
  };
153
 
154
  this.addPoliceCase(mapped);
 
160
  const suspect = (formValue && formValue.suspect) || {};
161
  const notes = (formValue && formValue.notes) || {};
162
  const mapped: PoliceCase = {
163
+ caseId: crime.caseId || (formValue && formValue['Case ID']) || '',
164
+ dateTime: crime.dateTime || '' ,
165
  status: notes.status || 'Open',
166
  crime: crime.crimeType || 'Unknown',
167
  police: {
 
183
  reportedBy: crime.reportedBy || '',
184
  verifiedBy: notes.verifiedBy || '',
185
  briefDescription: crime.briefDescription || '',
186
+ // Save full raw form data. Merge wrapper fields so everything entered is preserved.
187
+ formData: this.convertToKeyValue(
188
+ {
189
+ ...(formValue && formValue.formData ? formValue.formData : (formValue || {})),
190
+ ...crime,
191
+ ...suspect,
192
+ ...notes,
193
+ ...(formValue && formValue.legal ? formValue.legal : {})
194
+ }
195
+ )
196
  };
197
+
198
  const idx = this.cases.findIndex(c => c.caseId === mapped.caseId);
199
  if (idx !== -1) {
200
+ // Merge existing object to preserve other metadata where possible
201
+ this.cases[idx] = { ...this.cases[idx], ...mapped };
202
  this.save();
203
+ this.debugLogCasesOperation('updatePoliceCase', mapped);
204
+ this.casesSubject.next(this.cases.slice());
205
  } else {
206
  this.addPoliceCase(mapped);
207
  }
208
  }
209
 
210
+ /**
211
+ * Convert an object (or already key/value array) into an array of {key, value}
212
+ */
213
+ private convertToKeyValue(data: any): Array<{ key: string; value: any }> {
214
+ if (!data) return [];
215
+ if (Array.isArray(data)) {
216
+ // assume already in the desired shape
217
+ return data.map(item => {
218
+ if (item && typeof item === 'object' && 'key' in item) return item;
219
+ return { key: String(item), value: item };
220
+ });
221
+ }
222
+
223
+ const result: Array<{ key: string; value: any }> = [];
224
+
225
+ const isPlainObject = (v: any) => v && typeof v === 'object' && !(v instanceof Date) && !(v instanceof File) && !Array.isArray(v);
226
+
227
+ const recurse = (obj: any, prefix = '') => {
228
+ if (obj === null || obj === undefined) return;
229
+ if (typeof obj !== 'object' || obj instanceof Date || obj instanceof File) {
230
+ result.push({ key: prefix || 'value', value: obj });
231
+ return;
232
+ }
233
+
234
+ if (Array.isArray(obj)) {
235
+ // store arrays as JSON string for readability
236
+ result.push({ key: prefix || 'value', value: JSON.stringify(obj) });
237
+ return;
238
+ }
239
+
240
+ for (const k of Object.keys(obj)) {
241
+ const v = obj[k];
242
+ const newKey = prefix ? `${prefix}.${k}` : k;
243
+ if (isPlainObject(v)) {
244
+ recurse(v, newKey);
245
+ } else if (Array.isArray(v)) {
246
+ // arrays -> stringify
247
+ result.push({ key: newKey, value: JSON.stringify(v) });
248
+ } else {
249
+ result.push({ key: newKey, value: v });
250
+ }
251
+ }
252
+ };
253
+
254
+ if (typeof data === 'object') {
255
+ recurse(data, '');
256
+ return result;
257
+ }
258
+
259
+ // primitive
260
+ return [{ key: 'value', value: data }];
261
+ }
262
+
263
+ // Simple debug helper to log saved cases (can be removed later)
264
+ private debugLogCasesOperation(label: string, mapped?: PoliceCase) {
265
+ try {
266
+ console.log(`[CaseStore] ${label}`, mapped || null);
267
+ console.log('[CaseStore] current cases:', JSON.parse(localStorage.getItem(this.storageKey) || '[]'));
268
+ } catch { }
269
+ }
270
+
271
  /** Persist to localStorage (safe to keep; remove if not needed) */
272
  private save(): void {
273
  try { localStorage.setItem(this.storageKey, JSON.stringify(this.cases)); } catch { }
 
282
  this.cases = [];
283
  }
284
  // Seed a default case if none exist (for development/testing)
285
+ if (this.cases.length ===0) {
286
  this.cases = [
287
  {
288
  caseId: 'CASE-001',
 
300
  },
301
  accused: {
302
  name: 'Jane Smith',
303
+ age:30,
304
  gender: 'Female',
305
  address: '456 Side Rd, City',
306
  occupation: 'Unemployed'
307
+ },
308
+ formData: []
309
  }
310
  ];
311
  this.save();
312
  }
313
+ // Emit current cases to subscribers
314
+ this.casesSubject.next(this.cases.slice());
315
  }
316
  }
src/app/validationpage/validationpage.component.css CHANGED
@@ -1,901 +1,1190 @@
1
  /* Modern UI header styles from infopage */
2
  .site-header {
3
- background: #011329;
4
- box-shadow: 0 2px 12px #38bdf844;
5
- margin-bottom: 0;
6
- position: relative;
7
- z-index: 10;
8
- padding-bottom: 0;
 
 
 
 
 
 
 
9
  }
10
 
11
  /* Validation Report Template Styles */
12
  .validation-template-main {
13
- margin-top: 32px;
14
- width: 100%;
15
- display: flex;
16
- justify-content: center;
17
  }
 
18
  .validation-template-flex {
19
- display: flex;
20
- flex-direction: row;
21
- max-width: 1200px;
22
- width: 100%;
23
- gap: 32px;
 
 
24
  }
 
 
 
 
 
 
 
 
 
25
  .validation-template-left {
26
- display: flex;
27
- flex-direction: column;
28
- align-items: center;
29
- justify-content: flex-start;
30
- gap: 32px;
31
- min-width: 180px;
32
  }
 
33
  .validation-circle {
34
- width: 140px;
35
- height: 140px;
36
- border-radius: 50%;
37
- display: flex;
38
- flex-direction: column;
39
- align-items: center;
40
- justify-content: center;
41
- box-shadow: 0 2px 16px #38bdf844;
42
- margin-bottom: 8px;
43
  }
 
 
44
  .validation-circle.truth {
45
  background: linear-gradient(135deg, #bae6fd 0%, #38bdf8 100%);
46
  }
 
47
  .validation-circle.inconsistency {
48
  background: linear-gradient(135deg, #fee2e2 0%, #991b1b 100%);
49
  }
 
50
  .circle-value {
51
- font-size: 2.4rem;
52
- font-weight: 900;
53
- color: #2563eb;
54
- margin-bottom: 8px;
55
  }
 
56
  .validation-circle.inconsistency .circle-value {
57
- color: #991b1b;
58
  }
 
59
  .circle-label {
60
- font-size: 1rem;
61
- color: #6366f1;
62
- text-align: center;
63
  }
 
64
  .validation-template-right {
65
- flex: 1;
66
- display: flex;
67
- flex-direction: column;
68
- gap: 24px;
69
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  .validation-section {
71
- background: #fff;
72
- border-radius: 12px;
73
- box-shadow: 0 2px 16px #2563eb22;
74
- margin-bottom: 0;
75
- padding-bottom: 0;
76
  }
 
77
  .section-header {
78
- font-size: 1.15rem;
79
- font-weight: 700;
80
- padding: 12px 24px;
81
- border-radius: 12px 12px 0 0;
82
- letter-spacing: 1px;
83
  }
 
84
  .details-header {
85
- background: #fbbf24;
86
- color: #23272b;
87
  }
 
88
  .incident-header {
89
- background: #ef4444;
90
- color: #fff;
91
  }
 
92
  .followup-header {
93
- background: #14b8a6;
94
- color: #fff;
95
  }
 
96
  .section-table {
97
- width: 100%;
98
- display: flex;
99
- flex-direction: column;
100
- padding: 0 24px 18px 24px;
101
  }
 
102
  .section-row {
103
- display: flex;
104
- border-bottom: 1px solid #e5e7eb;
105
- padding: 8px 0;
106
- }
107
- .section-row:last-child {
108
- border-bottom: none;
109
  }
 
 
 
 
 
110
  .section-cell {
111
- flex: 1;
112
- font-size: 1rem;
113
- color: #23272b;
114
- padding-right: 16px;
115
- word-break: break-word;
116
  }
117
- .section-cell[colspan="3"] {
118
- flex: 3;
119
- }
120
- @media (max-width: 900px) {
121
- .validation-template-flex {
122
- flex-direction: column;
123
- align-items: center;
124
- }
125
- .validation-template-right {
126
- width: 100%;
127
- }
 
 
 
128
  }
129
 
130
  .header-inner {
131
- display: flex;
132
- align-items: center;
133
- justify-content: flex-start;
134
- padding: 18px 32px 0 32px;
135
- position: relative;
136
  }
137
 
138
  .logo-cluster {
139
- display: flex;
140
- align-items: center;
141
- gap: 18px;
142
  }
143
 
144
  .logo-img-header {
145
- width: 54px;
146
- height: 54px;
147
- border-radius: 50%;
148
- background: #fff;
149
- box-shadow: 0 2px 8px rgba(0,0,0,0.18);
150
- padding: 4px;
151
- margin-top: -6px;
152
- margin-bottom: 1vh;
153
  }
154
 
155
  .py-detect-title-header {
156
- font-size: 2.1rem;
157
- font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
158
- font-weight: 900;
159
- letter-spacing: 6px;
160
- color: #38bdf8;
161
- display: flex;
162
- align-items: center;
163
- gap: 2px;
164
- margin-bottom: 1.5vh;
165
- }
166
-
167
- .py-detect-title-header .py-letter.p {
168
- color: #e3f6ff;
169
- text-shadow: 0 0 6px #38bdf8;
170
- }
171
-
172
- .py-detect-title-header .py-letter.y {
173
- color: #38bdf8;
174
- text-shadow: 0 0 6px #38bdf8;
175
- }
176
-
177
- .py-detect-title-header .py-shape {
178
- color: #e3f6ff;
179
- background: #e3f6ff;
180
- text-shadow: 0 0 6px #38bdf8;
181
- box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
182
- border: 2px solid #23272b;
183
- width: 18px;
184
- height: 4px;
185
- display: inline-block;
186
- margin: 0 8px;
187
- border-radius: 2px;
188
- }
189
-
190
- .py-detect-title-header .py-letter.d {
191
- color: #e3f6ff;
192
- text-shadow: 0 0 6px #38bdf8;
193
- }
194
-
195
- .py-detect-title-header .py-letter.e {
196
- color: #38bdf8;
197
- text-shadow: 0 0 6px #38bdf8;
198
- }
199
-
200
- .py-detect-title-header .py-letter.t {
201
- color: #e3f6ff;
202
- text-shadow: 0 0 6px #38bdf8;
203
- }
204
-
205
- .py-detect-title-header .py-letter.e2 {
206
- color: #38bdf8;
207
- text-shadow: 0 0 6px #38bdf8;
208
- }
209
-
210
- .py-detect-title-header .py-letter.c {
211
- color: #e3f6ff;
212
- text-shadow: 0 0 6px #38bdf8;
213
- }
214
-
215
- .py-detect-title-header .py-letter.t2 {
216
- color: #38bdf8;
217
- text-shadow: 0 0 6px #38bdf8;
218
- }
219
 
 
 
 
 
220
 
221
- /* Footer */
222
- footer {
223
- background: linear-gradient(to right, #011022, #01030a);
224
- color: #fff;
225
- text-align: center;
226
- padding: 10px 0px;
227
- position: static;
228
- width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  }
230
 
231
  /* Professional, modern look for validation results */
232
  .validation-result-container {
233
- display: flex;
234
- flex-direction: column;
235
- align-items: center;
236
- justify-content: flex-start;
237
- background: #f6f8fa;
238
- position: relative;
239
- padding-bottom: 64px; /* Ensure space for footer */
240
- min-height: 0;
241
- }
242
-
243
- h2 {
244
- font-size: 2.4rem;
245
- font-weight: 800;
246
- color: #2563eb;
247
- margin-top: 40px;
248
- margin-bottom: 28px;
249
- letter-spacing: 2px;
250
- text-align: center;
251
- background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
252
- -webkit-background-clip: text;
253
- -webkit-text-fill-color: transparent;
254
- text-shadow: 0 2px 12px #38bdf844;
255
- border-bottom: 2px solid #38bdf8;
256
- padding-bottom: 12px;
257
  }
258
 
 
259
  .percentage-box {
260
- background: rgba(30,41,59,0.92);
261
- border-radius: 18px;
262
- box-shadow: 0 8px 32px rgba(30,41,59,0.48), 0 0 24px #38bdf844;
263
- border: 3px solid #38bdf8;
264
- padding: 48px 64px;
265
- min-width: 340px;
266
- max-width: 480px;
267
- display: flex;
268
- flex-direction: column;
269
- align-items: center;
270
- gap: 32px;
271
  }
272
 
273
- .percentage-box p {
274
- font-size: 2.2rem;
275
- font-weight: 700;
276
- color: #bae6fd;
277
- margin: 0;
278
- letter-spacing: 1px;
279
- text-shadow: 0 0 8px #38bdf888;
280
- }
281
 
282
- .percentage-box p strong {
283
- color: #38bdf8;
284
- font-size: 2.3rem;
285
- font-weight: 900;
286
- margin-right: 12px;
287
- }
 
 
 
288
 
289
- /* Add a subtle animation for result appearance */
290
- .validation-result-container {
291
- animation: fadeInUp 0.8s cubic-bezier(.39,.58,.57,1) both;
 
 
 
 
 
 
 
 
 
 
 
292
  }
 
 
 
 
 
293
 
294
- @keyframes fadeInUp {
295
- 0% {
296
- opacity: 0;
297
- transform: translateY(40px);
298
- }
299
- 100% {
300
- opacity: 1;
301
- transform: translateY(0);
302
- }
 
 
 
 
 
303
  }
304
 
305
- .global-bottom-left-btn {
306
- position: fixed;
307
- left: 32px;
308
- bottom: 32px;
309
- z-index: 1000;
310
- margin: 0;
311
- box-shadow: 0 0 32px #38bdf888, 0 2px 24px #22222288;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  }
313
 
314
- .back-btn {
315
- font-family: 'Montserrat', 'Poppins', 'Arial Black', Arial, sans-serif;
316
- font-size: 1.05rem;
317
- font-weight: 700;
318
- letter-spacing: 2px;
319
- background: linear-gradient(90deg, #38bdf8 0%, #23272b 100%);
320
- color: #e3f6ff;
321
- box-shadow: 0 2px 16px #38bdf888;
322
- border: none;
323
- border-radius: 12px;
324
- padding: 0.55rem 1.3rem;
325
- margin: 0 0.3rem;
326
- cursor: pointer;
327
- transition: background 0.4s, box-shadow 0.4s, color 0.3s, transform 0.2s;
328
- position: fixed;
329
- left: 24px;
330
- bottom: 24px;
331
- z-index: 1000;
332
- overflow: hidden;
333
- animation: buttonPulse 1.2s infinite alternate;
334
  }
335
 
336
- .back-btn:hover {
337
- background: linear-gradient(90deg, #23272b 0%, #38bdf8 100%);
338
- color: #bae6fd;
339
- box-shadow: 0 2px 24px #bae6fd88;
340
- animation: buttonPulse 0.7s;
 
 
 
 
 
 
 
 
 
 
341
  }
342
 
343
- @keyframes buttonPulse {
344
- 0% { box-shadow: 0 2px 16px #38bdf888; transform: scale(1); }
345
- 100% { box-shadow: 0 2px 32px #bae6fd88; transform: scale(1.07); }
 
 
 
 
 
 
 
 
 
346
  }
347
 
348
  /* Summary card for investigation evaluation */
349
  .summary-card {
350
- margin-top: 32px;
351
- background: rgba(56, 189, 248, 0.08);
352
- border-radius: 14px;
353
- border: 2px solid rgba(99, 102, 241, 0.15);
354
- box-shadow: 0 0 24px rgba(99, 102, 241, 0.12);
355
- padding: 24px 32px;
356
- max-width: 420px;
357
- text-align: left;
358
- display: flex;
359
- flex-direction: column;
360
- align-items: flex-start;
361
  }
 
362
  .summary-title {
363
- font-size: 1.25rem;
364
- font-weight: 700;
365
- color: #2563eb;
366
- margin-bottom: 10px;
367
- letter-spacing: 1px;
368
  }
 
369
  .summary-text {
370
- font-size: 1.08rem;
371
- color: #23272b;
372
- font-weight: 500;
373
- margin: 0;
374
  }
375
 
376
  /* Status chips/badges for result */
377
  .status-chip {
378
- display: inline-block;
379
- font-size: 0.85rem;
380
- padding: 4px 10px;
381
- border-radius: 8px;
382
- font-weight: 600;
383
- }
384
- .status-chip.active { background: #dcfce7; color: #15803d; }
385
- .status-chip.archived { background: #fee2e2; color: #991b1b; }
 
 
 
 
 
 
 
 
386
  /* Modal styles for report summary */
387
  .modal-overlay {
388
- position: fixed;
389
- top: 0; left: 0; right: 0; bottom: 0;
390
- background: rgba(30,41,59,0.65);
391
- z-index: 2000;
392
- display: flex;
393
- align-items: center;
394
- justify-content: center;
395
- animation: fadeInUp 0.4s;
 
 
 
396
  }
 
397
  .modal-content {
398
- background: #fff;
399
- border-radius: 18px;
400
- box-shadow: 0 8px 32px #38bdf844, 0 2px 16px #6366f144;
401
- padding: 36px 44px 28px 44px;
402
- min-width: 340px;
403
- max-width: 480px;
404
- color: #23272b;
405
- position: relative;
406
- outline: none;
407
  }
 
408
  .modal-title {
409
- font-size: 2rem;
410
- font-weight: 800;
411
- color: #2563eb;
412
- margin-bottom: 18px;
413
- letter-spacing: 1px;
414
  }
 
415
  .modal-section {
416
- margin-bottom: 18px;
417
  }
 
418
  .modal-close {
419
- position: absolute;
420
- top: 18px;
421
- right: 24px;
422
- background: none;
423
- border: none;
424
- font-size: 2rem;
425
- color: #2563eb;
426
- cursor: pointer;
427
- z-index: 10;
428
  }
 
429
  .report-actions, .modal-section h3 {
430
- margin-top: 18px;
431
  }
 
 
432
  .report-btn {
433
  background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
434
  color: #fff;
435
- font-weight: 700;
436
  border: none;
437
- border-radius: 999px;
438
- padding: 0.5rem 1.4rem;
439
- margin-right: 12px;
440
- margin-bottom: 8px;
441
  cursor: pointer;
442
- box-shadow: 0 2px 16px #38bdf888;
443
  display: inline-flex;
444
  align-items: center;
445
- gap: 8px;
446
- font-size: 1rem;
447
  transition: background 0.2s, box-shadow 0.2s;
448
  }
 
449
  .report-btn:hover {
450
  background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%);
451
  color: #bae6fd;
452
- box-shadow: 0 2px 24px #bae6fd88;
453
  }
 
454
  .icon-report::before {
455
- content: "\1F4C4";
456
- font-size: 1.2em;
457
- margin-right: 4px;
458
  }
 
459
  .icon-download::before {
460
- content: "\1F4BE";
461
- font-size: 1.2em;
462
- margin-right: 4px;
463
  }
 
464
  .icon-email::before {
465
- content: "\2709";
466
- font-size: 1.2em;
467
- margin-right: 4px;
468
  }
 
469
  .modal-content h3 {
470
- font-size: 1.1rem;
471
- color: #2563eb;
472
- font-weight: 700;
473
- margin-bottom: 6px;
474
  }
 
475
  .modal-content p {
476
- font-size: 1rem;
477
- color: #23272b;
478
- margin-bottom: 0;
479
  }
480
 
481
  /* Dashboard header styles */
 
482
  .dashboard-header {
483
  background: linear-gradient(90deg, rgba(30,41,59,0.92) 0%, #38bdf8 100%);
484
- padding: 32px 0 24px 0;
485
  color: #fff;
486
- box-shadow: 0 2px 16px #2563eb44;
487
  position: relative;
488
  }
 
489
  .dashboard-header-content {
490
- display: flex;
491
- align-items: center;
492
- justify-content: center;
493
- max-width: 1200px;
494
- margin: 0 auto;
495
- padding: 0 32px;
496
  }
 
497
  .dashboard-logo {
498
- width: 64px;
499
- height: 64px;
500
- border-radius: 50%;
501
- background: #fff;
502
- box-shadow: 0 2px 8px #38bdf844;
503
- margin-right: 24px;
504
  }
 
505
  .dashboard-title-block {
506
- display: flex;
507
- flex-direction: column;
508
- align-items: flex-start;
509
  }
 
510
  .dashboard-title {
511
- font-size: 2.2rem;
512
- font-weight: 900;
513
- letter-spacing: 2px;
514
- color: #fff;
515
  }
 
516
  .dashboard-date {
517
- font-size: 1.1rem;
518
- color: #bae6fd;
519
- margin-top: 4px;
520
  }
 
521
  .header-btns-right {
522
- display: flex;
523
- align-items: center;
524
- gap: 16px;
525
- }
526
-
527
- /* Main dashboard content */
528
- .dashboard-main {
529
- background: #f6f8fa;
530
- min-height: 100vh;
531
- padding: 32px 0 0 0;
532
- }
533
- .dashboard-cards {
534
- display: flex;
535
- flex-wrap: wrap;
536
- gap: 32px;
537
- max-width: 1200px;
538
- margin: 0 auto;
539
- justify-content: center;
540
- }
541
- .dashboard-card {
542
- background: #fff;
543
- border-radius: 18px;
544
- box-shadow: 0 4px 24px #2563eb22;
545
- padding: 32px 28px;
546
- min-width: 280px;
547
- max-width: 340px;
548
- flex: 1 1 320px;
549
- display: flex;
550
- flex-direction: column;
551
- align-items: center;
552
  }
553
- .dashboard-card-title {
554
- font-size: 1.25rem;
555
- font-weight: 700;
556
- color: #2563eb;
557
- margin-bottom: 18px;
558
- letter-spacing: 1px;
559
- }
560
- .dashboard-chart-card {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  align-items: center;
562
- justify-content: center;
 
 
 
563
  }
564
- .dashboard-chart-circle {
565
- width: 120px;
566
- height: 120px;
567
- border-radius: 50%;
568
- background: linear-gradient(135deg, #bae6fd 0%, #38bdf8 100%);
569
- display: flex;
570
- align-items: center;
571
- justify-content: center;
572
- margin-bottom: 12px;
573
- box-shadow: 0 2px 16px #38bdf844;
574
- }
575
- .dashboard-circle-value {
576
- font-size: 2.2rem;
577
- font-weight: 900;
578
- color: #2563eb;
579
- text-shadow: 0 2px 12px #38bdf844;
580
- }
581
- .dashboard-card-label {
582
- font-size: 1rem;
583
- color: #6366f1;
584
- margin-top: 8px;
585
- }
586
- .dashboard-details-card {
587
- align-items: flex-start;
588
- }
589
- .dashboard-details-list {
590
- width: 100%;
591
- }
592
- .dashboard-details-row {
593
- display: flex;
594
- justify-content: space-between;
595
- align-items: center;
596
- margin-bottom: 14px;
597
  }
598
- .dashboard-details-label {
599
- font-size: 1rem;
600
- color: #23272b;
601
- font-weight: 600;
 
602
  }
603
- .dashboard-details-value {
604
- font-size: 1rem;
605
- color: #2563eb;
606
- font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  }
608
- .dashboard-actions-card {
609
- align-items: flex-start;
 
 
 
 
 
 
 
 
 
 
 
610
  }
611
 
612
- /* Session Overview Card */
613
- .session-overview-card {
614
- background: #fbbf24;
615
- border-radius: 12px;
616
- box-shadow: 0 2px 12px #fbbf2444;
617
- margin: 32px auto 18px auto;
618
- max-width: 700px;
619
- padding: 18px 32px 12px 32px;
620
  }
621
- .session-header {
622
- font-size: 1.15rem;
623
- font-weight: 700;
624
- color: #23272b;
625
- margin-bottom: 10px;
626
- }
627
- .session-fields {
628
- display: flex;
629
- flex-wrap: wrap;
630
- gap: 18px 32px;
631
- }
632
- .session-field {
633
- min-width: 220px;
634
- font-size: 1rem;
635
- color: #23272b;
636
- }
637
- .field-label {
638
- font-weight: 600;
639
- color: #23272b;
640
- }
641
- .field-value {
642
- font-weight: 500;
643
- color: #2563eb;
644
- }
645
-
646
- /* AI Analysis Summary */
647
- .ai-summary-section {
648
- background: #fff;
649
- border-radius: 12px;
650
- box-shadow: 0 2px 16px #2563eb22;
651
- margin: 0 auto 24px auto;
652
- max-width: 900px;
653
- padding: 24px 32px;
654
- }
655
- .ai-summary-header {
656
- font-size: 1.2rem;
657
- font-weight: 700;
658
- color: #2563eb;
659
- margin-bottom: 18px;
660
  }
661
- .ai-metrics-grid {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
  display: grid;
663
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
664
- gap: 18px 32px;
665
- }
666
- .ai-metric {
667
- display: flex;
668
- flex-direction: column;
669
- align-items: flex-start;
670
- background: #f6f8fa;
671
- border-radius: 10px;
672
- padding: 16px 18px;
673
- box-shadow: 0 2px 8px #2563eb11;
674
- }
675
- .metric-label {
676
- font-size: 1rem;
677
- font-weight: 600;
678
- color: #23272b;
679
- margin-bottom: 8px;
680
- }
681
- .metric-ring {
682
- font-size: 2rem;
683
- font-weight: 900;
684
- border-radius: 50%;
685
- padding: 18px 0;
686
- width: 90px;
687
- text-align: center;
688
- margin-bottom: 6px;
689
- }
690
- .metric-blue {
691
- background: linear-gradient(135deg, #bae6fd 0%, #38bdf8 100%);
692
- color: #2563eb;
693
- box-shadow: 0 2px 8px #38bdf844;
694
  }
695
- .metric-cyan {
696
- background: linear-gradient(135deg, #a7f3d0 0%, #38bdf8 100%);
697
- color: #0e7490;
 
 
 
 
 
 
 
 
 
 
698
  }
699
- .metric-red {
700
- background: linear-gradient(135deg, #fee2e2 0%, #991b1b 100%);
701
- color: #991b1b;
 
702
  }
703
- .metric-bar {
704
- font-size: 1rem;
705
- font-weight: 700;
706
- color: #2563eb;
707
- margin-bottom: 6px;
708
  }
709
- .metric-verdict-box {
710
- background: #fef9c3;
711
- color: #f59e42;
712
- font-weight: 700;
713
- border-radius: 8px;
714
- padding: 8px 12px;
715
- margin-top: 6px;
716
- box-shadow: 0 2px 8px #fbbf2444;
717
- }
718
-
719
- /* AI Observations Section */
720
- .ai-observations-section {
721
- background: #14b8a6;
722
- border-radius: 12px;
723
- box-shadow: 0 2px 16px #14b8a644;
724
- margin: 0 auto 24px auto;
725
- max-width: 900px;
726
- padding: 24px 32px;
727
- color: #fff;
728
  }
729
- .observations-header {
730
- font-size: 1.15rem;
731
- font-weight: 700;
732
- margin-bottom: 12px;
733
- display: flex;
734
- align-items: center;
735
- gap: 8px;
736
  }
737
- .ai-icon {
738
- font-size: 1.3rem;
739
- }
740
- .observations-note {
741
- background: #fff;
742
- color: #134e4a;
743
- border-radius: 8px;
744
- padding: 14px 18px;
745
- font-size: 1rem;
746
- font-weight: 500;
747
- box-shadow: 0 2px 8px #14b8a644;
748
- }
749
-
750
- /* Audio–Video Metrics Section */
751
- .audio-video-metrics-section {
752
- background: #fff;
753
- border-radius: 12px;
754
- box-shadow: 0 2px 16px #2563eb22;
755
- margin: 0 auto 24px auto;
756
- max-width: 900px;
757
- padding: 24px 32px;
758
- }
759
- .metrics-header {
760
- font-size: 1.15rem;
761
- font-weight: 700;
762
- color: #2563eb;
763
- margin-bottom: 12px;
764
- }
765
- .metrics-graphs {
766
- display: flex;
767
- flex-wrap: wrap;
768
- gap: 18px;
769
- }
770
- .graph-placeholder {
771
- background: #f6f8fa;
772
- border-radius: 8px;
773
- color: #6366f1;
774
- font-size: 1rem;
775
- font-weight: 600;
776
- padding: 18px 24px;
777
- min-width: 220px;
778
- flex: 1;
779
- text-align: center;
780
- box-shadow: 0 2px 8px #2563eb11;
781
- }
782
-
783
- /* Final Outcome Section */
784
- .final-outcome-section {
785
- background: linear-gradient(90deg, #fbbf24 0%, #fef9c3 100%);
786
- border-radius: 12px;
787
- box-shadow: 0 2px 16px #fbbf2444;
788
- margin: 0 auto 32px auto;
789
- max-width: 900px;
790
- padding: 24px 32px;
791
- }
792
- .outcome-header {
793
- font-size: 1.15rem;
794
- font-weight: 700;
795
- color: #f59e42;
796
- margin-bottom: 12px;
797
- }
798
- .outcome-fields {
799
- display: flex;
800
- flex-wrap: wrap;
801
- gap: 18px 32px;
802
- margin-bottom: 18px;
803
- }
804
- .outcome-field {
805
- min-width: 220px;
806
- font-size: 1rem;
807
- color: #23272b;
808
- }
809
- .outcome-field .field-label {
810
- font-weight: 600;
811
- color: #23272b;
812
- }
813
- .outcome-field .field-value {
814
- font-weight: 500;
815
- color: #2563eb;
816
- }
817
- .outcome-field .field-value.warning {
818
- color: #991b1b;
819
- font-weight: 700;
820
  }
821
- .outcome-actions {
822
- display: flex;
823
- gap: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
824
  }
825
- .report-btn {
826
- background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
827
- color: #fff;
828
- font-weight: 700;
829
- border: none;
830
- border-radius: 999px;
831
- padding: 0.5rem 1.4rem;
832
- margin-bottom: 8px;
833
- cursor: pointer;
834
- box-shadow: 0 2px 16px #38bdf888;
835
- font-size: 1rem;
836
- transition: background 0.2s, box-shadow 0.2s;
837
  }
838
- .report-btn:hover {
839
- background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%);
840
- color: #bae6fd;
841
- box-shadow: 0 2px 24px #bae6fd88;
842
- }
843
- .investigation-outcome-card {
844
- width: 100%;
845
- max-width: 420px;
846
- margin: 0 auto;
847
- background: #fff;
848
- border-radius: 18px;
849
- box-shadow: 0 4px 24px #2563eb22;
850
- padding: 32px 28px;
851
- display: flex;
852
- flex-direction: column;
853
- align-items: center;
854
  }
855
- .outcome-fields-grid {
856
- display: grid;
857
- grid-template-columns: 1fr 1.2fr;
858
- gap: 18px 12px;
859
- width: 100%;
860
- margin-top: 8px;
861
- }
862
- .outcome-field-row {
863
- display: contents;
864
- }
865
- .outcome-label {
866
- font-size: 1rem;
867
- color: #23272b;
868
- font-weight: 600;
869
- text-align: right;
870
- padding-right: 12px;
871
- }
872
- .outcome-value {
873
- font-size: 1rem;
874
- color: #2563eb;
875
- font-weight: 700;
876
- text-align: left;
877
  }
878
- .status-chip.active {
879
- background: #dcfce7;
880
- color: #15803d;
881
- border-radius: 8px;
882
- padding: 4px 10px;
883
- font-weight: 700;
 
 
 
 
 
 
 
 
 
 
884
  }
885
- .status-chip.archived {
886
- background: #fee2e2;
887
- color: #991b1b;
888
- border-radius: 8px;
889
- padding: 4px 10px;
890
- font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  }
892
- .summary-value {
893
- grid-column: span 2;
894
- background: #f6f8fa;
895
- color: #991b1b;
896
- border-radius: 8px;
897
- padding: 8px 12px;
898
- font-size: 0.98rem;
899
- font-weight: 600;
900
- margin-top: 6px;
901
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /* Modern UI header styles from infopage */
2
  .site-header {
3
+ background: #011329;
4
+ box-shadow:0 2px 12px #38bdf844;
5
+ margin-bottom:0;
6
+ position: relative;
7
+ z-index:10;
8
+ padding-bottom:0;
9
+ }
10
+
11
+ /* CSS variables for positioning */
12
+ :root {
13
+ --validation-result-top:140px; /* distance from top for the result container */
14
+ --header-question-height:64px; /* approximate height of the header-question-summary box */
15
+ --header-question-gap:12px; /* gap between question box and result container */
16
  }
17
 
18
  /* Validation Report Template Styles */
19
  .validation-template-main {
20
+ margin-top:32px;
21
+ width:100%;
22
+ display: flex;
23
+ justify-content: flex-start; /* moved to left side */
24
  }
25
+
26
  .validation-template-flex {
27
+ display: grid;
28
+ /* increased left column width from360px to420px to give more room for case summary */
29
+ grid-template-columns:420px 1fr;
30
+ gap:28px;
31
+ align-items: start;
32
+ max-width:1200px;
33
+ margin:0; /* remove auto centering so layout sits on the left */
34
  }
35
+
36
+ .validation-side-left {
37
+ width:420px; /* increased from360px */
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap:16px;
41
+ align-items: stretch;
42
+ }
43
+
44
  .validation-template-left {
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ justify-content: flex-start;
49
+ gap:32px;
50
+ min-width:180px;
51
  }
52
+
53
  .validation-circle {
54
+ width:140px;
55
+ height:140px;
56
+ border-radius:50%;
57
+ display: flex;
58
+ flex-direction: column;
59
+ align-items: center;
60
+ justify-content: center;
61
+ box-shadow:0 2px 16px #38bdf844;
62
+ margin-bottom:8px;
63
  }
64
+
65
+
66
  .validation-circle.truth {
67
  background: linear-gradient(135deg, #bae6fd 0%, #38bdf8 100%);
68
  }
69
+
70
  .validation-circle.inconsistency {
71
  background: linear-gradient(135deg, #fee2e2 0%, #991b1b 100%);
72
  }
73
+
74
  .circle-value {
75
+ font-size:2.4rem;
76
+ font-weight:900;
77
+ color: #2563eb;
78
+ margin-bottom:8px;
79
  }
80
+
81
  .validation-circle.inconsistency .circle-value {
82
+ color: #991b1b;
83
  }
84
+
85
  .circle-label {
86
+ font-size:1rem;
87
+ color: #6366f1;
88
+ text-align: center;
89
  }
90
+
91
  .validation-template-right {
92
+ flex:1;
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap:24px;
96
  }
97
+
98
+ /* Action buttons row below the main validation template */
99
+ .validation-actions {
100
+ display:flex;
101
+ gap:35px;
102
+ justify-content:flex-end;
103
+ align-items:center;
104
+ padding:18px 24px;
105
+ max-width:1200px;
106
+ margin:-9px 0 24px 42px; /* align with main content area (left column420px) */
107
+ }
108
+
109
+ .action-btn {
110
+ display:inline-flex;
111
+ align-items:center;
112
+ gap:8px;
113
+ background:#fff;
114
+ border:1px solid rgba(2,24,64,0.06);
115
+ padding:6px 10px;
116
+ border-radius:8px;
117
+ cursor:pointer;
118
+ font-weight:700;
119
+ box-shadow:0 8px 18px rgba(2,24,64,0.04);
120
+ }
121
+
122
+ .action-icon {
123
+ width:14px;
124
+ height:14px;
125
+ border-radius:999px;
126
+ display:inline-block;
127
+ }
128
+
129
+ .action-icon.blue { background:#1e40af; }
130
+ .action-icon.purple { background:#7c3aed; }
131
+ .action-icon.green { background:#059669; }
132
+
133
+ .action-btn.action-download .action-label { color:#0f172a; }
134
+ .action-btn.action-email .action-label { color:#0f172a; }
135
+ .action-btn.action-reanalyze .action-label { color:#0f172a; }
136
+
137
+ .action-btn:hover { transform:translateY(-2px); box-shadow:012px32px rgba(2,24,64,0.06); }
138
+
139
+ @media (max-width:900px) {
140
+ .validation-actions { justify-content:center; margin-left:0; }
141
+ }
142
+
143
  .validation-section {
144
+ background: #fff;
145
+ border-radius:12px;
146
+ box-shadow:0 2px 16px #2563eb22;
147
+ margin-bottom:0;
148
+ padding-bottom:0;
149
  }
150
+
151
  .section-header {
152
+ font-size:1.15rem;
153
+ font-weight:700;
154
+ padding:12px 24px;
155
+ border-radius:12px 12px 0 0;
156
+ letter-spacing:1px;
157
  }
158
+
159
  .details-header {
160
+ background: #fbbf24;
161
+ color: #23272b;
162
  }
163
+
164
  .incident-header {
165
+ background: #ef4444;
166
+ color: #fff;
167
  }
168
+
169
  .followup-header {
170
+ background: #14b8a6;
171
+ color: #fff;
172
  }
173
+
174
  .section-table {
175
+ width:100%;
176
+ display: flex;
177
+ flex-direction: column;
178
+ padding:0 24px 18px 24px;
179
  }
180
+
181
  .section-row {
182
+ display: flex;
183
+ border-bottom:1px solid #e5e7eb;
184
+ padding:8px 0;
 
 
 
185
  }
186
+
187
+ .section-row:last-child {
188
+ border-bottom: none;
189
+ }
190
+
191
  .section-cell {
192
+ flex:1;
193
+ font-size:1rem;
194
+ color: #23272b;
195
+ padding-right:16px;
196
+ word-break: break-word;
197
  }
198
+
199
+ .section-cell[colspan="3"] {
200
+ flex:3;
201
+ }
202
+
203
+ @media (max-width:900px) {
204
+ .validation-template-flex {
205
+ flex-direction: column;
206
+ align-items: center;
207
+ }
208
+
209
+ .validation-template-right {
210
+ width:100%;
211
+ }
212
  }
213
 
214
  .header-inner {
215
+ display: flex;
216
+ align-items: center;
217
+ justify-content: space-between;
218
+ padding:18px 32px 0 32px;
219
+ position: relative;
220
  }
221
 
222
  .logo-cluster {
223
+ display: flex;
224
+ align-items: center;
225
+ gap:18px;
226
  }
227
 
228
  .logo-img-header {
229
+ width:54px;
230
+ height:54px;
231
+ border-radius:50%;
232
+ background: #fff;
233
+ box-shadow:0 2px 8px rgba(0,0,0,0.18);
234
+ padding:4px;
235
+ margin-top: -6px;
236
+ margin-bottom:1vh;
237
  }
238
 
239
  .py-detect-title-header {
240
+ font-size:2.1rem;
241
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
242
+ font-weight:900;
243
+ letter-spacing:6px;
244
+ color: #38bdf8;
245
+ display: flex;
246
+ align-items: center;
247
+ gap:2px;
248
+ margin-bottom:1.5vh;
249
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ .py-detect-title-header .py-letter.p {
252
+ color: #e3f6ff;
253
+ text-shadow:006px #38bdf8;
254
+ }
255
 
256
+ .py-detect-title-header .py-letter.y {
257
+ color: #38bdf8;
258
+ text-shadow:006px #38bdf8;
259
+ }
260
+
261
+ .py-detect-title-header .py-shape {
262
+ color: #e3f6ff;
263
+ background: #e3f6ff;
264
+ text-shadow:006px #38bdf8;
265
+ box-shadow:006px #38bdf8,002px #fff;
266
+ border:2px solid #23272b;
267
+ width:18px;
268
+ height:4px;
269
+ display: inline-block;
270
+ margin:08px;
271
+ border-radius:2px;
272
+ }
273
+
274
+ .py-detect-title-header .py-letter.d {
275
+ color: #e3f6ff;
276
+ text-shadow:006px #38bdf8;
277
+ }
278
+
279
+ .py-detect-title-header .py-letter.e {
280
+ color: #38bdf8;
281
+ text-shadow:006px #38bdf8;
282
+ }
283
+
284
+ .py-detect-title-header .py-letter.t {
285
+ color: #e3f6ff;
286
+ text-shadow:006px #38bdf8;
287
+ }
288
+
289
+ .py-detect-title-header .py-letter.e2 {
290
+ color: #38bdf8;
291
+ text-shadow:006px #38bdf8;
292
+ }
293
+
294
+ .py-detect-title-header .py-letter.c {
295
+ color: #e3f6ff;
296
+ text-shadow:006px #38bdf8;
297
+ }
298
+
299
+ .py-detect-title-header .py-letter.t2 {
300
+ color: #38bdf8;
301
+ text-shadow:006px #38bdf8;
302
+ }
303
+
304
+ .footer {
305
+ background: linear-gradient(to right, #011022, #01030a);
306
+ color: #fff;
307
+ text-align: center;
308
+ padding:10px 0px;
309
+ position: fixed;
310
+ left:0;
311
+ bottom:0;
312
+ width:100%;
313
+ z-index:100;
314
+ margin-top:0;
315
+ }
316
+
317
+ /* Back button styling — gradient and subtle animation to match page design */
318
+ .back-btn {
319
+ background: linear-gradient(90deg,#38bdf8,#2563eb);
320
+ color: #fff;
321
+ border: none;
322
+ border-radius:12px;
323
+ padding:8px 14px;
324
+ font-size:0.98rem;
325
+ font-weight:800;
326
+ letter-spacing:0.6px;
327
+ cursor: pointer;
328
+ box-shadow:0 6px 18px rgba(56,189,248,0.12);
329
+ transition: transform 220ms cubic-bezier(.2,.9,.2,1), box-shadow 220ms ease, filter 220ms ease;
330
+ display: inline-flex;
331
+ gap:8px;
332
+ align-items: center;
333
+ justify-content: center;
334
+ position: relative;
335
+ overflow: hidden;
336
+ margin-bottom:16px;
337
  }
338
 
339
  /* Professional, modern look for validation results */
340
  .validation-result-container {
341
+ position: sticky;
342
+ right:0;
343
+ top:110px;
344
+ width:360px;
345
+ background: linear-gradient(180deg, #ffffff, #f1fbff);
346
+ padding:18px;
347
+ border-radius:12px;
348
+ box-shadow:0 12px 36px rgba(3,102,214,0.06);
349
+ z-index:60;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  }
351
 
352
+ /* adjust percentage-box border color so it reads on the lighter container */
353
  .percentage-box {
354
+ background: rgba(3,102,214,0.06);
355
+ border-radius:14px;
356
+ box-shadow:0 8px 32px rgba(30,41,59,0.06),0018px rgba(56,189,248,0.04);
357
+ border:1px solid rgba(56,189,248,0.08);
358
+ padding:20px 26px; /* reduced */
359
+ min-width:260px;
360
+ max-width:320px;
361
+ display: flex;
362
+ flex-direction: column;
363
+ align-items: center;
364
+ gap:12px;
365
  }
366
 
367
+ /* Radial diagram sizing inside the compact box */
368
+ .radial-chart-wrapper { width:212px; height:177px; display:flex; align-items:center; justify-content:center; }
369
+ .radial-number { font-size:1.2rem; }
 
 
 
 
 
370
 
371
+ /* Radial animation styles */
372
+ .radial-chart-wrapper {
373
+ width:212px;
374
+ height:177px;
375
+ display: flex;
376
+ align-items: center;
377
+ justify-content: center;
378
+ }
379
+ .radial-svg { width:100%; height:124%; overflow:visible; }
380
 
381
+ .radial-bg { stroke: rgba(0,0,0,0.06); }
382
+ .radial-anim { transition: stroke-dashoffset 3000ms cubic-bezier(.2,.9,.2,1); transform-origin: center; }
383
+
384
+ /* Color the animated strokes to match labels */
385
+ .outer-fg { stroke: #6366f1; }
386
+ .middle-fg { stroke: #059669; }
387
+ .inner-fg { stroke: #ef4444; }
388
+
389
+ /* Color the radial percentage values per modality: audio (blue), video (green), verified (red) */
390
+ .radial-values .radial-item:nth-child(1) .radial-number {
391
+ color: #2563eb; /* blue for audio */
392
+ }
393
+ .radial-values .radial-item:nth-child(2) .radial-number {
394
+ color: #059669; /* green for video */
395
  }
396
+ .radial-values .radial-item:nth-child(3) .radial-number {
397
+ color: #ef4444; /* red for verified */
398
+ }
399
+
400
+ /* Ensure circles use correct stroke-dasharray values via binding; initial dashoffset set in component to circumference (hidden) */
401
 
402
+ .radial-values { display:flex; flex-direction:row; gap:53px; }
403
+ .radial-item { display:flex; gap:10px; align-items:center; }
404
+ .radial-icon { width:28px; height:28px; display:flex; align-items:center; justify-content:center; border-radius:999px; }
405
+ .icon-audio { background: linear-gradient(90deg,#eef2ff,#e0f2fe); color:#0550ff; padding:4px; border-radius:6px; }
406
+ .icon-video { background: linear-gradient(90deg,#ecfdf5,#d1fae5); color:#059669; padding:4px; border-radius:6px; }
407
+ .icon-verified { background: linear-gradient(90deg,#fff1f2,#fee2e2); color:#ef4444; padding:4px; border-radius:6px; }
408
+
409
+ .radial-number { font-size:1.4rem; font-weight:800; color:#0f172a; }
410
+ .radial-label { font-size:0.95rem; color:#475569; }
411
+ .radial-detail { font-size:0.9rem; color:#6b7280; margin-top:4px; }
412
+
413
+ /* Reduced motion */
414
+ @media (prefers-reduced-motion: reduce) {
415
+ .radial-anim { transition: none !important; }
416
  }
417
 
418
+ /* Ensure layout is responsive: on small screens keep it centered and full-width */
419
+ @media (max-width:900px) {
420
+ .validation-result-container {
421
+ position: static;
422
+ right: auto;
423
+ top: auto;
424
+ width:100%;
425
+ max-width:100%;
426
+ padding:18px 12px;
427
+ border-radius:12px;
428
+ margin:0 12px 18px 12px;
429
+ }
430
+
431
+ .percentage-box {
432
+ min-width: auto;
433
+ max-width:100%;
434
+ padding:18px;
435
+ }
436
+
437
+ .radial-chart-wrapper {
438
+ width:212px;
439
+ height:177px;
440
+ }
441
  }
442
 
443
+ .validation-template-left .validation-result-container {
444
+ position: relative;
445
+ right: auto;
446
+ top: auto;
447
+ width:100%;
448
+ max-width:900px;
449
+ margin:12px 0 4px 0;
450
+ padding:16px;
451
+ border-radius:12px;
452
+ box-shadow:0 8px 32px rgba(3,102,214,0.06);
453
+ background: linear-gradient(180deg, #ffffff, #f7fbff);
 
 
 
 
 
 
 
 
 
454
  }
455
 
456
+ .validation-template-left .percentage-box { max-width:100%; min-width: auto; padding:16px; }
457
+ .radial-chart-wrapper { width:212px; height:177px; }
458
+
459
+ /* Responsive adjustments: ensure left panel width is respected on smaller screens */
460
+ @media (max-width:1200px) {
461
+ .validation-template-flex { grid-template-columns:48%1fr; }
462
+ .validation-side-left { width:48%; }
463
+ }
464
+ @media (max-width:1000px) {
465
+ .validation-template-right { display: none; }
466
+ .validation-template-flex { grid-template-columns:1fr; }
467
+ .validation-template-left .radial-chart-wrapper {
468
+ width:212px;
469
+ height:177px;
470
+ }
471
  }
472
 
473
+ /* Footer */
474
+ footer {
475
+ background: linear-gradient(to right, #011022, #01030a);
476
+ color: #fff;
477
+ text-align: center;
478
+ padding:10px 0px;
479
+ position: fixed;
480
+ left:0;
481
+ bottom:0;
482
+ width:100%;
483
+ z-index:100;
484
+ margin-top:0;
485
  }
486
 
487
  /* Summary card for investigation evaluation */
488
  .summary-card {
489
+ margin-top:32px;
490
+ background: rgba(56,189,248,0.08);
491
+ border-radius:12px;
492
+ border:2px solid rgba(99,102,241,0.15);
493
+ box-shadow:0024px rgba(99,102,241,0.12);
494
+ padding:24px 32px;
495
+ max-width:420px;
496
+ text-align: left;
497
+ display: flex;
498
+ flex-direction: column;
499
+ align-items: flex-start;
500
  }
501
+
502
  .summary-title {
503
+ font-size:1.25rem;
504
+ font-weight:700;
505
+ color: #2563eb;
506
+ margin-bottom:10px;
507
+ letter-spacing:1px;
508
  }
509
+
510
  .summary-text {
511
+ font-size:1.08rem;
512
+ color: #23272b;
513
+ font-weight:500;
514
+ margin:0;
515
  }
516
 
517
  /* Status chips/badges for result */
518
  .status-chip {
519
+ display: inline-block;
520
+ font-size:0.85rem;
521
+ padding:4px 10px;
522
+ border-radius:8px;
523
+ font-weight:600;
524
+ }
525
+
526
+ .status-chip.active {
527
+ background: #dcfce7;
528
+ color: #15803d;
529
+ }
530
+
531
+ .status-chip.archived {
532
+ background: #fee2e2;
533
+ color: #991b1b;
534
+ }
535
  /* Modal styles for report summary */
536
  .modal-overlay {
537
+ position: fixed;
538
+ top:0;
539
+ left:0;
540
+ right:0;
541
+ bottom:0;
542
+ background: rgba(30,41,59,0.65);
543
+ z-index:2000;
544
+ display: flex;
545
+ align-items: center;
546
+ justify-content: center;
547
+ animation: fadeInUp0.4s;
548
  }
549
+
550
  .modal-content {
551
+ background: #fff;
552
+ border-radius:18px;
553
+ box-shadow:0 8px 32px #38bdf844,0 2px 16px #6366f144;
554
+ padding:36px 44px 28px 44px;
555
+ min-width:340px;
556
+ max-width:480px;
557
+ color: #23272b;
558
+ position: relative;
559
+ outline: none;
560
  }
561
+
562
  .modal-title {
563
+ font-size:2rem;
564
+ font-weight:800;
565
+ color: #2563eb;
566
+ margin-bottom:18px;
567
+ letter-spacing:1px;
568
  }
569
+
570
  .modal-section {
571
+ margin-bottom:18px;
572
  }
573
+
574
  .modal-close {
575
+ position: absolute;
576
+ top:18px;
577
+ right:24px;
578
+ background: none;
579
+ border: none;
580
+ font-size:2rem;
581
+ color: #2563eb;
582
+ cursor: pointer;
583
+ z-index:10;
584
  }
585
+
586
  .report-actions, .modal-section h3 {
587
+ margin-top:18px;
588
  }
589
+
590
+
591
  .report-btn {
592
  background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
593
  color: #fff;
594
+ font-weight:700;
595
  border: none;
596
+ border-radius:999px;
597
+ padding:0.5rem 1.4rem;
598
+ margin-right:12px;
599
+ margin-bottom:8px;
600
  cursor: pointer;
601
+ box-shadow:0 2px 16px #38bdf888;
602
  display: inline-flex;
603
  align-items: center;
604
+ gap:8px;
605
+ font-size:1rem;
606
  transition: background 0.2s, box-shadow 0.2s;
607
  }
608
+
609
  .report-btn:hover {
610
  background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%);
611
  color: #bae6fd;
612
+ box-shadow:0 2px 24px #bae6fd88;
613
  }
614
+
615
  .icon-report::before {
616
+ content: "\1F4C4";
617
+ font-size:1.2em;
618
+ margin-right:4px;
619
  }
620
+
621
  .icon-download::before {
622
+ content: "\1F4BE";
623
+ font-size:1.2em;
624
+ margin-right:4px;
625
  }
626
+
627
  .icon-email::before {
628
+ content: "\2709";
629
+ font-size:1.2em;
630
+ margin-right:4px;
631
  }
632
+
633
  .modal-content h3 {
634
+ font-size:1.1rem;
635
+ color: #2563eb;
636
+ font-weight:700;
637
+ margin-bottom:6px;
638
  }
639
+
640
  .modal-content p {
641
+ font-size:1rem;
642
+ color: #23272b;
643
+ margin-bottom:0;
644
  }
645
 
646
  /* Dashboard header styles */
647
+
648
  .dashboard-header {
649
  background: linear-gradient(90deg, rgba(30,41,59,0.92) 0%, #38bdf8 100%);
650
+ padding:12px 0 10px 0;
651
  color: #fff;
652
+ box-shadow:0 2px 16px #2563eb44;
653
  position: relative;
654
  }
655
+
656
  .dashboard-header-content {
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: center;
660
+ max-width:1200px;
661
+ margin:0 auto;
662
+ padding:032px;
663
  }
664
+
665
  .dashboard-logo {
666
+ width:64px;
667
+ height:64px;
668
+ border-radius:50%;
669
+ background: #fff;
670
+ box-shadow:0 2px 8px #38bdf844;
671
+ margin-right:24px;
672
  }
673
+
674
  .dashboard-title-block {
675
+ display: flex;
676
+ flex-direction: column;
677
+ align-items: flex-start;
678
  }
679
+
680
  .dashboard-title {
681
+ font-size:1.45rem;
682
+ font-weight:900;
683
+ letter-spacing:2px;
684
+ color: #fff;
685
  }
686
+
687
  .dashboard-date {
688
+ font-size:0.95rem;
689
+ color: #bae6fd;
690
+ margin-top:2px;
691
  }
692
+
693
  .header-btns-right {
694
+ display: flex;
695
+ align-items: center;
696
+ gap:16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  }
698
+
699
+ /* Top hero bar */
700
+ .hero-bar {
701
+ width:100%;
702
+ background: linear-gradient(90deg, #64748b, #00d4ff);
703
+ color: #e6f2ff;
704
+ padding:0px 0;
705
+ box-shadow:0 8px 36px #bae6fd;
706
+ }
707
+ .hero-inner {
708
+ max-width:1200px;
709
+ margin:0 auto;
710
+ display: flex;
711
+ align-items: center;
712
+ justify-content: space-between;
713
+ gap:12px;
714
+ padding:024px;
715
+ }
716
+ .hero-title h1 {
717
+ margin:0;
718
+ font-size:1.6rem;
719
+ font-weight:900;
720
+ letter-spacing:1px;
721
+ background: linear-gradient(90deg, #fff, #cceeff);
722
+ -webkit-background-clip: text;
723
+ background-clip: text;
724
+ -webkit-text-fill-color: transparent;
725
+ color: transparent; /* fallback for browsers respecting color */
726
+ }
727
+ .hero-sub { color: #bcd9f8; font-size:0.95rem; margin-top:4px; }
728
+ .hero-actions { display:flex; align-items:center; gap:8px; }
729
+
730
+ /* Ensure hero-inner is a positioning context for absolute button */
731
+ .hero-inner { position: relative; }
732
+
733
+ /* Place the More details pill at the right-side corner of the hero bar */
734
+ .hero-more-btn {
735
+ position: absolute;
736
+ right: 150px;
737
+ top: 50%;
738
+ transform: translateY(-50%);
739
+ z-index: 30;
740
+ /* keep existing visual styles */
741
+ background: linear-gradient(90deg, #0ea5ff, #23272b);
742
+ color: #fff;
743
+ border: none;
744
+ padding: 6px 12px;
745
+ border-radius: 999px;
746
+ display: inline-flex;
747
  align-items: center;
748
+ gap: 8px;
749
+ font-weight: 700;
750
+ cursor: pointer;
751
+ box-shadow: 0 6px 18px rgba(37,99,235,0.18);
752
  }
753
+
754
+ .hero-more-btn:hover { transform: translateY(-52%); box-shadow:0 10px 26px rgba(37,99,235,0.22); }
755
+
756
+ /* Ensure hero title and actions appear inline and centered as a group */
757
+ .hero-bar .hero-inner { justify-content: center !important; position: relative; }
758
+ .hero-bar .hero-title { text-align: center; }
759
+ .hero-bar .hero-actions {
760
+ position: static !important;
761
+ display: flex;
762
+ gap:8px;
763
+ align-items: center;
764
+ margin-left:16px; /* space between title and actions */
765
+ }
766
+ .hero-bar .hero-more-btn {
767
+ right: -285px;
768
+ top: auto !important;
769
+ transform: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  }
771
+
772
+ /* Responsive: keep actions inline on small screens but allow wrapping */
773
+ @media (max-width:900px) {
774
+ .hero-bar .hero-inner { flex-direction: column; gap:12px; }
775
+ .hero-bar .hero-actions { margin-left:0; }
776
  }
777
+
778
+ /* Layout containers (left content / right analytics) */
779
+ .validation-template-main { padding:18px 20px; }
780
+ .validation-template-flex { display: grid; grid-template-columns:360px 1fr; gap:28px; align-items: start; max-width:1200px; margin:0 auto; }
781
+
782
+ /* Summary cards grid */
783
+ .summary-cards { display: grid; grid-template-columns: repeat(2, minmax(220px,1fr)); gap:16px; width:100%; }
784
+ .summary-card { padding:14px; border-radius:12px; min-height:100px; display:flex; flex-direction:column; gap:8px; justify-content:center; }
785
+ .summary-card .card-top { font-weight:700; color:#075985; font-size:0.95rem; }
786
+ .summary-card .card-value { font-size:1.25rem; font-weight:800; color:#0f3b72; }
787
+ .summary-card .card-sub { font-size:0.9rem; color:#64748b; }
788
+
789
+ /* Metrics pane */
790
+ .metrics-pane { width:100%; margin-top:18px; }
791
+ .metrics-title { font-size:1.05rem; color:#075985; margin-bottom:10px; font-weight:800; }
792
+ .metrics-bars { display:flex; flex-direction:column; gap:10px; }
793
+ .metrics-row-item { display:flex; align-items:center; gap:12px; }
794
+ .metrics-label {
795
+ width:120px;
796
+ font-weight: 700;
797
+ color: #475569;}
798
+ .metrics-bar-bg { flex:1; background:#e6eef8; border-radius:12px; height:12px; overflow:hidden; }
799
+ .metrics-bar-fill { height:100%; width:0%; background: linear-gradient(90deg,#38bdf8,#2aa89f); border-radius:12px; transition: width 0.9s cubic-bezier(.2,.9,.2,1); }
800
+ .metrics-bar-fill.cyan { background: linear-gradient(90deg,#60a5fa,#06b6d4); }
801
+ .metrics-bar-fill.amber { background: linear-gradient(90deg,#fbbf24,#f97316); }
802
+ .metrics-value { width:56px; text-align:right; font-weight:700; color:#075985; }
803
+
804
+ /* Case summary refinements */
805
+ .case-summary-section { padding:18px; background: rgba(255,255,255,0.95); border-radius:12px; box-shadow:0 8px 32px rgba(2,6,23,0.04); margin-top:18px; width:100%; max-width:100%; }
806
+ .case-summary-grid { display:flex; gap:16px; align-items:flex-start; }
807
+ .case-summary-fields { display:flex; flex-direction:column; gap:6px; color:#334155; margin-bottom:12px; }
808
+ .case-summary-text { color:#334155; flex:1; margin-top:12px; }
809
+
810
+ /* Ensure card-modal-case defaults align with wider left panel when present */
811
+ .card-modal-case {
812
+ --w:34%; /* default flex width for the card row version */
813
+ --h: calc(100vh -160px);
814
+ min-width:300px;
815
+ align-self: flex-start;
816
  }
817
+
818
+ /* Validation results when moved into left column */
819
+ .validation-template-left .validation-result-container {
820
+ position: relative;
821
+ right: auto;
822
+ top: auto;
823
+ width:100%;
824
+ max-width:900px;
825
+ margin:12px 0 4px 0;
826
+ padding:16px;
827
+ border-radius:12px;
828
+ box-shadow:0 8px 32px rgba(3,102,214,0.06);
829
+ background: linear-gradient(180deg, #ffffff, #f7fbff);
830
  }
831
 
832
+ /* small tweak: ensure radial box scales within left column */
833
+ .validation-template-left .percentage-box { max-width:100%; min-width: auto; padding:16px; }
834
+ .radial-chart-wrapper {
835
+ width:212px;
836
+ height:177px;
 
 
 
837
  }
838
+
839
+ /* Keep original right-column behaviour but lower priority (in case other pages use it) */
840
+ .validation-result-container { position: sticky; top:110px; right:0; width:360px; }
841
+
842
+ /* Horizontal main row to place validation results and cards side-by-side */
843
+ .main-row { display:flex; gap:20px; align-items:flex-start; width:100%; }
844
+ .cards-col { flex:1; display:flex; flex-direction:column; gap:16px; }
845
+
846
+ /* Make validation-result-container take intrinsic width and let cards-col expand */
847
+ .validation-result-container { width:auto; max-width:420px; }
848
+
849
+ /* Align summary-cards to be a vertical stack in cards-col */
850
+ .summary-cards { display:flex; flex-direction:column; gap:16px; }
851
+ .summary-card { max-width:100%; }
852
+
853
+ @media (max-width:1000px) {
854
+ .main-row { flex-direction:column; }
855
+ .validation-result-container { max-width:100%; }
856
+ .cards-col { width:100%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  }
858
+
859
+ /* Cards row: horizontal layout to fit page width */
860
+ .cards-row { display:flex; gap:16px; align-items:flex-start; width:100%; justify-content:flex-start; grid-column:1 / -1; justify-self: start; margin-left:0; padding-left:0; position: relative; left:0; }
861
+ .card-modal { background: linear-gradient(120deg,#ffffff,#f6faff); border-radius:12px; box-shadow:0 8px 32px rgba(2,6,23,0.04); padding:12px; display:flex; flex-direction:column; }
862
+
863
+ /* Make all card-modals expand to fill available horizontal space evenly */
864
+ .cards-row .card-modal { flex:110; min-width:350px; height: calc(100vh -200px); overflow:hidden; }
865
+
866
+ /* Remove previous rigid per-card max-widths (selectors kept for compatibility) */
867
+ .card-modal-case, .card-modal-metrics, .card-modal-results, .card-modal-summary { /* sized by .cards-row .card-modal */ }
868
+
869
+ .card-modal-header { font-weight:800; color:#075985; margin-bottom:8px; }
870
+ .card-modal-body { flex:1; overflow:h; padding-right:8px; }
871
+ .card-modal-footer { display:flex; gap:8px; justify-content:flex-end; margin-top:8px; }
872
+
873
+ /* Specific inner layouts */
874
+ .results-horizontal { display:flex; gap:12px; align-items:center; }
875
+ .results-values { display:flex; flex-direction:row; gap:12px; }
876
+
877
+ /* Summary grid inside summary card modal */
878
+ .summary-grid {
879
  display: grid;
880
+ grid-template-columns: repeat(2, minmax(0,1fr));
881
+ gap: 12px;
882
+ margin-top: -8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  }
884
+
885
+ /* Ensure metrics bars keep original look */
886
+ .metrics-bars { display:flex; flex-direction:column; gap:10px; }
887
+ .metrics-row-item { display:flex; align-items:center; gap:12px; }
888
+
889
+ /* Hide scrollbars where possible and ensure page height fits viewport */
890
+ :host {
891
+ margin:0;
892
+ padding:0;
893
+ width:100%;
894
+ height:100%;
895
+ /* Hide both horizontal and vertical scrollbars for this validation page only */
896
+ overflow: hidden;
897
  }
898
+
899
+ .validation-template-main {
900
+ height: calc(100vh -120px);
901
+ overflow: hidden;
902
  }
903
+
904
+ .validation-template-flex {
905
+ height:100%;
906
+ display: flex;
907
+ flex-direction: column;
908
  }
909
+
910
+ .cards-row .card-modal-body {
911
+ max-height: calc(100vh -260px);
912
+ overflow-y: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
913
  }
914
+
915
+
916
+ /* Responsive: stack cards vertically on small screens */
917
+ @media (max-width:1200px) {
918
+ .card-modal-case, .card-modal-metrics, .card-modal-results, .card-modal-summary { flex:1145%; max-width:48%; }
 
 
919
  }
920
+ @media (max-width:900px) {
921
+ .cards-row { flex-direction:column; }
922
+ .card-modal-case, .card-modal-metrics, .card-modal-results, .card-modal-summary { width:100%; max-width:100%; min-width:unset; height:auto; }
923
+ .card-modal-body { overflow: visible; max-height: none; }
924
+ .validation-template-main { height: auto; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  }
926
+
927
+ /* Animations and transitions */
928
+ .card-glass { /* existing definitions remain applied */ }
929
+
930
+ /* small touch: animate radial numbers and card entries */
931
+ .summary-card { transform: translateY(6px); animation: enterCard0.48s cubic-bezier(.2,.9,.2,1) both; }
932
+ @keyframes enterCard { from { opacity:0; transform: translateY(12px) scale(0.995);} to { opacity:1; transform: translateY(0) scale(1);} }
933
+
934
+ /* Ensure metrics fills update smoothly on load */
935
+ .metrics-bar-fill { will-change: width; }
936
+
937
+ /* ensure footer won't overlap sticky right panel on small screens */
938
+ @media (max-width:600px) { footer { position: static; } }
939
+
940
+ /* place pre-question summary inside validation header at top-right */
941
+ .validation-header {
942
+ position: relative;
943
+ }
944
+ .validation-header .header-question-summary {
945
+ position: absolute;
946
+ right:24px;
947
+ /* place inside the header (top-right) */
948
+ top:18px;
949
+ background: linear-gradient(90deg, #ffffff, #f1fbff);
950
+ border:1px solid rgba(3,102,214,0.06);
951
+ padding:8px 12px;
952
+ border-radius:10px;
953
+ box-shadow:0 6px 18px rgba(3,102,214,0.06);
954
+ display: flex;
955
+ flex-direction: column;
956
+ align-items: center;
957
+ min-width:160px;
958
+ z-index:60;
959
  }
960
+
961
+ .validation-header .header-question-summary .question-summary-title {
962
+ font-size:1rem;
963
+ font-weight:800;
964
+ color: #075985;
965
+ margin-bottom:6px;
 
 
 
 
 
 
966
  }
967
+
968
+ .validation-header .header-question-summary .action-btn {
969
+ padding:6px 10px;
970
+ font-size:0.95rem;
971
+ border-radius:8px;
 
 
 
 
 
 
 
 
 
 
 
972
  }
973
+
974
+ @media (max-width:900px) {
975
+ .validation-header .header-question-summary {
976
+ position: static;
977
+ margin-top:12px;
978
+ right: auto;
979
+ top: auto;
980
+ transform: none;
981
+ min-width: auto;
982
+ }
 
 
 
 
 
 
 
 
 
 
 
 
983
  }
984
+
985
+ /* Horizontal main row to place validation results and cards side-by-side */
986
+ .main-row { display:flex; gap:20px; align-items:flex-start; width:100%; }
987
+ .cards-col { flex:1; display:flex; flex-direction:column; gap:16px; }
988
+
989
+ /* Make validation-result-container take intrinsic width and let cards-col expand */
990
+ .validation-result-container { width:auto; max-width:420px; }
991
+
992
+ /* Align summary-cards to be a vertical stack in cards-col */
993
+ .summary-cards { display:flex; flex-direction:column; gap:16px; }
994
+ .summary-card { max-width:100%; }
995
+
996
+ @media (max-width:1000px) {
997
+ .main-row { flex-direction:column; }
998
+ .validation-result-container { max-width:100%; }
999
+ .cards-col { width:100%; }
1000
  }
1001
+
1002
+ /* Cards row: horizontal layout to fit page width */
1003
+ .cards-row { display:flex; gap:16px; align-items:flex-start; width:100%; justify-content:flex-start; grid-column:1 / -1; justify-self: start; margin-left:0; padding-left:0; position: relative; left:0; }
1004
+ .card-modal { background: linear-gradient(120deg,#ffffff,#f6faff); border-radius:12px; box-shadow:0 8px 32px rgba(2,6,23,0.04); padding:12px; display:flex; flex-direction:column; }
1005
+
1006
+ /* Make all card-modals expand to fill available horizontal space evenly */
1007
+ .cards-row .card-modal { flex:110; min-width:200px; height: calc(100vh -200px); overflow:hidden; }
1008
+
1009
+ /* Remove previous rigid per-card max-widths (selectors kept for compatibility) */
1010
+ .card-modal-case, .card-modal-metrics, .card-modal-results, .card-modal-summary { /* sized by .cards-row .card-modal */ }
1011
+
1012
+ .card-modal-header { font-weight:800; color:#075985; margin-bottom:8px; }
1013
+ .card-modal-body { flex:1; overflow:auto; padding-right:8px; }
1014
+ .card-modal-footer { display:flex; gap:8px; justify-content:flex-end; margin-top:8px; }
1015
+
1016
+ .cards-row {
1017
+ /* Use CSS Grid for a clean3-column layout */
1018
+ display: grid;
1019
+ grid-template-columns: repeat(2, minmax(0,1fr)); /*3 columns on wide screens */
1020
+ gap:20px;
1021
+ align-items: start;
1022
+ justify-items: stretch;
1023
+ width:153%;
1024
+ margin-left:-340px; /* ensure it's flush to left */
1025
+ }
1026
+
1027
+ /* Responsive grid breakpoints */
1028
+ @media (max-width:1200px) {
1029
+ .cards-row {
1030
+ grid-template-columns: repeat(2, minmax(0,1fr)); /*2 columns on medium screens */
1031
+ }
1032
  }
1033
+ @media (max-width:800px) {
1034
+ .cards-row {
1035
+ grid-template-columns:1fr; /* stack on small screens */
1036
+ }
 
 
 
 
 
1037
  }
1038
+
1039
+ /* Base card styling used by all dashboard cards */
1040
+ .case-summary-card,
1041
+ .metrics-card,
1042
+ .result-chart-card,
1043
+ .summary-card {
1044
+ /* Per-card variables (can be set inline or with additional classes):
1045
+ --card-offset-x: shift left/right (example: "-12px" or "8px")
1046
+ --card-margin-left: extra margin-left if preferred (example: "8px")
1047
+ --card-width: explicit width (e.g. "100%" or "480px"); grid keeps this flexible
1048
+ --card-height: explicit height (e.g. "480px")
1049
+ */
1050
+ --card-offset-x:0px;
1051
+ --card-margin-left:0px;
1052
+ --card-width: auto;
1053
+ --card-height:0px;
1054
+
1055
+ background: linear-gradient(180deg, #ffffff, #f7fbff);
1056
+ border-radius:12px;
1057
+ box-shadow:0 10px 30px rgba(2,6,23,0.06);
1058
+ padding:16px;
1059
+ box-sizing: border-box;
1060
+
1061
+ /* Allow nudging via transform (preferred) and margin as fallback */
1062
+ transform: translateX(var(--card-offset-x));
1063
+ margin-left: var(--card-margin-left);
1064
+
1065
+ /* Width/height controls - width in grid context should usually be "auto" or100% */
1066
+ width: var(--card-width);
1067
+ min-width:0; /* important for grid children to allow shrinking */
1068
+ height: var(--card-height);
1069
+
1070
+ /* Smooth animations when position/size changes */
1071
+ transition: transform 280ms cubic-bezier(.2,.9,.2,1),
1072
+ margin 280ms cubic-bezier(.2,.9,.2,1),
1073
+ width 340ms cubic-bezier(.2,.9,.2,1),
1074
+ height 340ms cubic-bezier(.2,.9,.2,1),
1075
+ box-shadow 220ms ease;
1076
+
1077
+ /* Ensure content can scroll internally if height is constrained */
1078
+ display: flex;
1079
+ flex-direction: column;
1080
+ overflow: hidden;
1081
+ }
1082
+
1083
+ /* Content area inside card that may scroll if content is taller than allowed height */
1084
+ .case-summary-card .card-body,
1085
+ .metrics-card .card-body,
1086
+ .result-chart-card .card-body,
1087
+ .summary-card .card-body {
1088
+ overflow: auto;
1089
+ padding-right:8px;
1090
+ }
1091
+
1092
+ /* Utility classes for quick nudges (small adjustments) */
1093
+ .nudge-left { --card-offset-x: -12px; }
1094
+ .nudge-right { --card-offset-x:12px; }
1095
+ .nudge-more-left { --card-offset-x: -24px; }
1096
+ .nudge-more-right { --card-offset-x:24px; }
1097
+
1098
+ /* Utility classes to increase card size */
1099
+ .card-tall { --card-height:560px; }
1100
+ .card-taller { --card-height:680px; }
1101
+ .card-wide { /* span two columns in the grid */
1102
+ grid-column: span2; /* makes the card occupy two columns */
1103
+ }
1104
+
1105
+ /* Hover elevation effect to indicate interactivity */
1106
+ .case-summary-card:hover,
1107
+ .metrics-card:hover,
1108
+ .result-chart-card:hover,
1109
+ .summary-card:hover {
1110
+ box-shadow:0 18px 40px rgba(2,6,23,0.10);
1111
+ transform: translateX(calc(var(--card-offset-x) *1.2)) translateY(-6px);
1112
+ }
1113
+
1114
+ /* Focus or active state for keyboard users */
1115
+ .case-summary-card:focus,
1116
+ .metrics-card:focus,
1117
+ .result-chart-card:focus,
1118
+ .summary-card:focus {
1119
+ outline:3px solid rgba(59,130,246,0.12);
1120
+ outline-offset:4px;
1121
+ }
1122
+
1123
+ /* Example specific defaults (can be overridden inline or with utility classes)
1124
+ - These make the case summary slightly wider by default and metrics a bit narrower
1125
+ */
1126
+ .case-summary-card { --card-width: auto; --card-height:0px; }
1127
+ .metrics-card { --card-width: auto; --card-height:0px; }
1128
+ .result-chart-card { --card-width: auto; --card-height:0px; }
1129
+ .summary-card { --card-width: auto; --card-height:0px; }
1130
+
1131
+ /* Ensure card content areas handle internal layout gracefully */
1132
+ .card-title { font-weight:800; color: #075985; margin-bottom:8px; }
1133
+ .card-subtitle { color: #64748b; font-size:0.95rem; margin-bottom:12px; }
1134
+
1135
+ /* Accessibility: ensure sufficient contrast for borders/shadows when scaled */
1136
+ @media (prefers-reduced-motion: reduce) {
1137
+ .case-summary-card,
1138
+ .metrics-card,
1139
+ .result-chart-card,
1140
+ .summary-card {
1141
+ transition: none;
1142
+ }
1143
+ }
1144
+
1145
+ /* End of dashboard card layout controls */
1146
+
1147
+ /* Scoped: Remove internal scrollbar only for the Validation Results card */
1148
+ .card-modal-results, .card-modal-results .card-modal-body {
1149
+ /* Prevent internal scrollbars from appearing inside the results card */
1150
+ overflow: visible !important;
1151
+ max-height: none !important;
1152
+ }
1153
+
1154
+ /* Hide WebKit scrollbars if any still appear inside results card */
1155
+ .card-modal-results::-webkit-scrollbar, .card-modal-results .card-modal-body::-webkit-scrollbar {
1156
+ width:0px;
1157
+ height:0px;
1158
+ display: none;
1159
+ }
1160
+
1161
+ /* Hide scrollbars in Firefox/IE for results card */
1162
+ .card-modal-results, .card-modal-results .card-modal-body {
1163
+ -ms-overflow-style: none; /* IE and Edge */
1164
+ scrollbar-width: none; /* Firefox */
1165
+ }
1166
+
1167
+ /* Case summary heading */
1168
+ .case-summary-heading {
1169
+ font-weight:800;
1170
+ color: #075985;
1171
+ margin-bottom:8px;
1172
+ font-size:1rem;
1173
+ }
1174
+
1175
+ /* Ensure the Py-Detect Summary heading has spacing above it when present */
1176
+ .case-summary-heading.pydetect { margin-top:6px; }
1177
+
1178
+ /* Nudge the Summary card slightly upward to better align with screenshot */
1179
+ .card-modal-summary {
1180
+ transform: translateY(-12px);
1181
+ /* ensure smooth transition and promote to its own layer */
1182
+ will-change: transform;
1183
+ z-index:5;
1184
+ }
1185
+
1186
+ @media (max-width:900px) {
1187
+ /* remove nudge on small screens to prevent overlap */
1188
+ .card-modal-summary { transform: none; z-index: auto; }
1189
+ }
1190
+
src/app/validationpage/validationpage.component.html CHANGED
@@ -1,203 +1,220 @@
1
- <!-- Back to Case Details button at the top -->
2
- <button class="back-btn" (click)="navigateBackToCaseDetails()">
3
- <span class="back-icon">←</span> Back to Case Details
4
- </button>
5
-
6
  <!-- Modern UI header with logo and PyDetect title -->
7
  <div class="site-header">
8
- <div class="header-inner">
9
- <div class="logo-cluster">
10
- <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
11
- <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
12
- </span>
13
- <div class="py-detect-title-header">
14
- <span class="py-letter p">P</span>
15
- <span class="py-letter y">Y</span>
16
- <span class="py-shape"></span>
17
- <span class="py-letter d">D</span>
18
- <span class="py-letter e">E</span>
19
- <span class="py-letter t">T</span>
20
- <span class="py-letter e2">E</span>
21
- <span class="py-letter c">C</span>
22
- <span class="py-letter t2">T</span>
23
- </div>
24
- </div>
25
- </div>
 
 
 
 
 
26
  </div>
27
 
28
- <!-- Dashboard-style header -->
29
- <div class="dashboard-header">
30
- <div class="dashboard-header-content">
31
-
32
- <div class="dashboard-title-block">
33
- <div class="dashboard-title">Investigation Report</div>
34
- <div class="dashboard-date">{{ reportDate || 'June 2024' }}</div>
35
- </div>
36
- <div class="header-btns-right">
37
- <button class="back-btn" (click)="navigateBackToPyDetect()">
38
- <span class="back-icon">←</span> Back to Investigation
39
- </button>
40
- </div>
41
- </div>
42
- </div>
 
 
 
 
43
 
44
- <!-- Main dashboard content -->
45
- <div class="dashboard-main">
46
- <div class="dashboard-cards">
47
- <!-- Circular chart card -->
48
- <div class="dashboard-card dashboard-chart-card">
49
- <div class="dashboard-card-title">Truth Consistency</div>
50
- <div class="dashboard-chart-circle">
51
- <!-- Placeholder for chart, can use SVG or library later -->
52
- <div class="dashboard-circle-value">{{ truePercentage }}%</div>
53
- </div>
54
- <div class="dashboard-card-label">Consistency Index</div>
55
- </div>
56
- <!-- Circular chart card -->
57
- <div class="dashboard-card dashboard-chart-card">
58
- <div class="dashboard-card-title">Inconsistency Index</div>
59
- <div class="dashboard-chart-circle">
60
- <!-- Placeholder for chart, can use SVG or library later -->
61
- <div class="dashboard-circle-value">{{ truePercentage }}%</div>
62
- </div>
63
- <div class="dashboard-card-label">InConsistency Index</div>
64
- </div>
65
-
66
- <!-- Actions card -->
67
- <div class="dashboard-card dashboard-actions-card">
68
- <div class="dashboard-card-title">Actions</div>
69
- <button class="report-btn" (click)="downloadPDF()">
70
- <span class="icon-download"></span> Download PDF
71
- </button>
72
- <button class="report-btn" (click)="emailReport()">
73
- <span class="icon-email"></span> Email Report
74
- </button>
75
- <button class="report-btn" (click)="reAnalyze()">Re-Analyze Audio/Video</button>
76
- </div>
77
- <!-- Details card -->
78
- <div class="dashboard-card dashboard-details-card investigation-outcome-card">
79
- <div class="dashboard-card-title">Investigation Outcome</div>
80
- <div class="outcome-fields-grid">
81
- <div class="outcome-field-row">
82
- <span class="outcome-label">Status:</span>
83
- <span class="outcome-value">
84
- <span *ngIf="truePercentage >= 80" class="status-chip active">🟩 Consistent</span>
85
- <span *ngIf="truePercentage < 80" class="status-chip archived">��� Inconsistent</span>
86
- </span>
87
- </div>
88
- <div class="outcome-field-row">
89
- <span class="outcome-label">Investigation Confidence Score:</span>
90
- <span class="outcome-value">92%</span>
91
- </div>
92
- <div class="outcome-field-row">
93
- <span class="outcome-label">Dominant Emotion Detected:</span>
94
- <span class="outcome-value">Calm</span>
95
- </div>
96
- <div class="outcome-field-row">
97
- <span class="outcome-label">Response Clarity:</span>
98
- <span class="outcome-value">84% Clear</span>
99
- </div>
100
- <div class="outcome-field-row">
101
- <span class="outcome-label">Speech Tone Analysis:</span>
102
- <span class="outcome-value">Neutral: 60%, Tense: 40%</span>
103
- </div>
104
- <div class="outcome-field-row">
105
- <span class="outcome-label">Summary:</span>
106
- <span class="outcome-value summary-value">
107
- The investigation did not meet validation criteria. Further review advised.
108
- </span>
109
- </div>
110
- </div>
111
- </div>
112
- </div>
113
- </div>
114
 
115
- <!-- Session Overview -->
116
- <div class="session-overview-card">
117
- <div class="session-header">Session Overview</div>
118
- <div class="session-fields">
119
- <div class="session-field"><span class="field-label">Session ID:</span> <span class="field-value">INT-2025-007</span></div>
120
- <div class="session-field"><span class="field-label">Suspect Name:</span> <span class="field-value">Ajay Kumar</span></div>
121
- <div class="session-field"><span class="field-label">Investigation Officer:</span> <span class="field-value">Ganesh R.</span></div>
122
- <div class="session-field"><span class="field-label">Session Duration:</span> <span class="field-value">00:42:18</span></div>
123
- <div class="session-field"><span class="field-label">AI Model Version:</span> <span class="field-value">Py-Detect AI 2.0</span></div>
124
- <div class="session-field"><span class="field-label">Date Analyzed:</span> <span class="field-value">2025-10-15</span></div>
125
- </div>
126
- </div>
127
 
128
- <!-- AI Analysis Summary -->
129
- <div class="ai-summary-section">
130
- <div class="ai-summary-header">AI Analysis Summary</div>
131
- <div class="ai-metrics-grid">
132
- <div class="ai-metric metric-consistency">
133
- <div class="metric-label">Truth Consistency Index</div>
134
- <div class="metric-ring metric-blue">72%</div>
135
- </div>
136
- <div class="ai-metric metric-inconsistency">
137
- <div class="metric-label">Inconsistency Index</div>
138
- <div class="metric-ring metric-red">28%</div>
139
- </div>
140
- <div class="ai-metric metric-emotion">
141
- <div class="metric-label">Emotional State Distribution</div>
142
- <div class="metric-bar">Calm – 60%, Nervous – 30%, Angry – 10%</div>
143
- </div>
144
- <div class="ai-metric metric-clarity">
145
- <div class="metric-label">Response Clarity</div>
146
- <div class="metric-ring metric-cyan">85% clear</div>
147
- </div>
148
- <div class="ai-metric metric-eyecontact">
149
- <div class="metric-label">Eye-Contact Frequency</div>
150
- <div class="metric-ring metric-blue">68% consistent</div>
151
- </div>
152
- <div class="ai-metric metric-confidence">
153
- <div class="metric-label">Speech Confidence Level</div>
154
- <div class="metric-ring metric-cyan">75%</div>
155
- </div>
156
- <div class="ai-metric metric-verdict">
157
- <div class="metric-label">Overall AI Verdict</div>
158
- <div class="metric-verdict-box">Partially Consistent / Needs Review</div>
159
- </div>
160
- </div>
161
- </div>
162
 
163
- <!-- AI Observations and Insights -->
164
- <div class="ai-observations-section">
165
- <div class="observations-header"><span class="ai-icon">🤖</span> AI Observations & Insights</div>
166
- <div class="observations-note">
167
- <strong>Observation Summary:</strong><br>
168
- During questioning, the suspect showed hesitation when discussing the time of the incident.<br>
169
- Eye movement frequency decreased by 25% during key questions.<br>
170
- Speech tone remained steady, indicating partial honesty.<br>
171
- <strong>Recommendation:</strong> Further questioning advised for financial motive discussion.
172
- </div>
173
- </div>
 
174
 
175
- <!-- Audio–Video Metrics (Graphs Placeholder) -->
176
- <div class="audio-video-metrics-section">
177
- <div class="metrics-header">Audio–Video Metrics</div>
178
- <div class="metrics-graphs">
179
- <div class="graph-placeholder">Waveform & Emotion Timeline (Graph)</div>
180
- <div class="graph-placeholder">Confidence vs Time (Graph)</div>
181
- <div class="graph-placeholder">Facial Micro-Expression (Graph)</div>
182
- </div>
183
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- <!-- Final Outcome Summary -->
186
- <div class="final-outcome-section">
187
- <div class="outcome-header">Final Outcome Summary</div>
188
- <div class="outcome-fields">
189
- <div class="outcome-field"><span class="field-label">AI Verdict:</span> <span class="field-value warning">⚠️ Requires Further Verification</span></div>
190
- <div class="outcome-field"><span class="field-label">Confidence Score:</span> <span class="field-value">82%</span></div>
191
- <div class="outcome-field"><span class="field-label">Recommended Action:</span> <span class="field-value">Conduct follow-up questioning with corroborating evidence C-2025-A.</span></div>
192
- <div class="outcome-field"><span class="field-label">Report Generated By:</span> <span class="field-value">Py-Detect AI System</span></div>
193
- </div>
194
- <div class="outcome-actions">
195
- <button class="report-btn" (click)="downloadPDF()">Download Report (PDF)</button>
196
- <button class="report-btn" (click)="emailReport()">Email Summary</button>
197
- <button class="report-btn" (click)="reAnalyze()">Re-Analyze Audio/Video</button>
198
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  </div>
200
 
201
  <footer>
202
- <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
203
  </footer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!-- Modern UI header with logo and PyDetect title -->
2
  <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
+ </div>
20
+ <div class="header-actions-right">
21
+ <button class="back-btn" (click)="navigateBackToPyDetect()">
22
+ <span class="back-icon">←</span> Back to Investigation Page
23
+ </button>
24
+ </div>
25
+ </div>
26
  </div>
27
 
28
+ <!-- Hero / Top summary bar -->
29
+ <header class="hero-bar">
30
+ <div class="hero-inner">
31
+ <div class="hero-title">
32
+ <h1>Investigation Validation Summary</h1>
33
+ <div class="hero-sub">Snapshot of the current investigation — concise, actionable</div>
34
+ </div>
35
+ <div class="hero-actions">
36
+ <div class="progress-badge">Analysis: <strong>100% Complete</strong></div>
37
+ <button class="icon-btn" title="Settings">⚙️</button>
38
+ <button class="icon-btn" title=" Report">🔍</button>
39
+ <!-- More details pill button -->
40
+ <button class="hero-more-btn" (click)="goToQuestionSummary()" title="More details">
41
+ <span class="hero-more-icon">🛈</span>
42
+ <span class="hero-more-label">More details</span>
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </header>
47
 
48
+ <div class="validation-template-main">
49
+ <div class="validation-template-flex">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ <!-- Single horizontal row containing independent card-modals -->
52
+ <div class="cards-row">
 
 
 
 
 
 
 
 
 
 
53
 
54
+ <!-- Case Summary Card (card-modal) -->
55
+ <article class="card-modal card-modal-case">
56
+ <header class="card-modal-header">Case Summary</header>
57
+ <div class="card-modal-body">
58
+ <div class="case-summary-fields">
59
+ <div>Case ID: <b>CASE-007</b></div>
60
+ <div>Officer: <b>Ganesh</b></div>
61
+ <div>Date: <b>2025-10-15</b></div>
62
+ <div>Suspect: <b>Jeeva</b></div>
63
+ </div>
64
+ <div class="case-summary-text">
65
+ <div class="case-summary-heading">Investigation Summary</div>
66
+ The suspect displayed calm emotions overall but showed minor inconsistency in hand gestures. <br />
67
+ <b>Recommendation:</b> Conduct a short follow-up session.
68
+ </div>
69
+ </div>
70
+ </article>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ <!-- Metrics Card -->
73
+ <article class="card-modal card-modal-metrics">
74
+ <header class="card-modal-header">Audio / Video Metrics</header>
75
+ <div class="card-modal-body">
76
+ <div class="metrics-bars">
77
+ <div class="metrics-row-item">
78
+ <div class="metrics-label">Speech</div>
79
+ <div class="metrics-bar-bg">
80
+ <div class="metrics-bar-fill" [style.width.%]="audioMetric1"></div>
81
+ </div>
82
+ <div class="metrics-value">{{ audioMetric1 }}%</div>
83
+ </div>
84
 
85
+ <div class="metrics-row-item">
86
+ <div class="metrics-label">Eye Contact</div>
87
+ <div class="metrics-bar-bg">
88
+ <div class="metrics-bar-fill cyan" [style.width.%]="audioMetric2"></div>
89
+ </div>
90
+ <div class="metrics-value">{{ audioMetric2 }}%</div>
91
+ </div>
92
+
93
+ <div class="metrics-row-item">
94
+ <div class="metrics-label">Emotion</div>
95
+ <div class="metrics-bar-bg">
96
+ <div class="metrics-bar-fill amber" [style.width.%]="audioMetric3"></div>
97
+ </div>
98
+ <div class="metrics-value">{{ audioMetric3 }}%</div>
99
+ </div>
100
+
101
+ <div class="metrics-row-item">
102
+ <div class="metrics-label">Clarity</div>
103
+ <div class="metrics-bar-bg">
104
+ <div class="metrics-bar-fill" [style.width.%]="audioMetric4"></div>
105
+ </div>
106
+ <div class="metrics-value">{{ audioMetric4 }}%</div>
107
+ </div>
108
+
109
+ <div class="metrics-row-item">
110
+ <div class="metrics-label">Confidence</div>
111
+ <div class="metrics-bar-bg">
112
+ <div class="metrics-bar-fill" [style.width.%]="audioMetric5"></div>
113
+ </div>
114
+ <div class="metrics-value">{{ audioMetric5 }}%</div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </article>
119
 
120
+ <!-- Validation Results Card -->
121
+ <article class="card-modal card-modal-results">
122
+ <header class="card-modal-header">Validation Results</header>
123
+ <div class="card-modal-body results-horizontal">
124
+ <div class="radial-chart-wrapper" aria-hidden="true">
125
+ <svg viewBox="0 0 220 220" preserveAspectRatio="xMidYMid meet" class="radial-svg" role="img" aria-label="Validation radial chart">
126
+ <g transform="translate(110,110) rotate(-90)">
127
+ <circle [attr.r]="r1" class="radial-bg outer" stroke-width="10" fill="none" [attr.stroke-dasharray]="circumference1"></circle>
128
+ <circle [attr.r]="r1" class="radial-anim outer-fg" stroke-width="10" fill="none" [attr.stroke-dasharray]="circumference1" [attr.stroke-dashoffset]="offset1" stroke-linecap="round"></circle>
129
+
130
+ <circle [attr.r]="r2" class="radial-bg middle" stroke-width="10" fill="none" [attr.stroke-dasharray]="circumference2"></circle>
131
+ <circle [attr.r]="r2" class="radial-anim middle-fg" stroke-width="10" fill="none" [attr.stroke-dasharray]="circumference2" [attr.stroke-dashoffset]="offset2" stroke-linecap="round"></circle>
132
+
133
+ <circle [attr.r]="r3" class="radial-bg inner" stroke-width="10" fill="none" [attr.stroke-dasharray]="circumference3"></circle>
134
+ <circle [attr.r]="r3" class="radial-anim inner-fg" stroke-width="10" fill="none" [attr.stroke-dasharray]="circumference3" [attr.stroke-dashoffset]="offset3" stroke-linecap="round"></circle>
135
+ </g>
136
+ </svg>
137
+ </div>
138
+
139
+ <div class="radial-values horizontal-values">
140
+ <div class="radial-item">
141
+ <div class="radial-icon" title="Audio"><span class="icon-audio">🔊</span></div>
142
+ <div>
143
+ <div class="radial-number">{{ audioAnalysisScore }}%</div>
144
+ <div class="radial-label">Audio-Analysis</div>
145
+ <div class="radial-detail">Truthness: <strong>{{ audioTruthness }}%</strong></div>
146
+ </div>
147
+ </div>
148
+ <div class="radial-item">
149
+ <div class="radial-icon" title="Video"><span class="icon-video">🎥</span></div>
150
+ <div>
151
+ <div class="radial-number">{{ videoAnalysisScore }}%</div>
152
+ <div class="radial-label">Video-Analysis</div>
153
+ <div class="radial-detail">Truthness: <strong>{{ videoTruthness }}%</strong></div>
154
+ </div>
155
+ </div>
156
+ <div class="radial-item">
157
+ <div class="radial-icon" title="Verified"><span class="icon-verified">✔️</span></div>
158
+ <div>
159
+ <div class="radial-number">{{ verifiedScore }}%</div>
160
+ <div class="radial-label">Verified-Scores</div>
161
+ <div class="radial-detail">Overall Truth Probability : <strong>{{ verifiedConfidence }}%</strong></div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </article>
167
+
168
+ <!-- Summary Card (combined) -->
169
+ <article class="card-modal card-modal-summary">
170
+ <header class="card-modal-header">Summary</header>
171
+ <div class="card-modal-body summary-grid">
172
+ <div class="summary-card card-glass">
173
+ <div class="card-top">Verdict / Status</div>
174
+ <div class="card-value status-badge status-badge-green">Consistent</div>
175
+ <div class="card-sub">Verdict derived from multi-modal analysis</div>
176
+ </div>
177
+ <div class="summary-card card-glass">
178
+ <div class="card-top">Investigation Confidence</div>
179
+ <div class="card-value confidence-percentage">92%</div>
180
+ <div class="card-sub">Overall system confidence</div>
181
+ </div>
182
+ <div class="summary-card card-glass">
183
+ <div class="card-top">Dominant Emotion</div>
184
+ <div class="card-value emotion-emoji">😌 Calm</div>
185
+ <div class="card-sub">Detected from vocal and facial cues</div>
186
+ </div>
187
+ <div class="summary-card card-glass">
188
+ <div class="card-top">Session Overview</div>
189
+ <div class="card-value">Audio {{ audioAnalysisScore }}% · Video {{ videoAnalysisScore }}% · Truth {{ verifiedConfidence }}%</div>
190
+ <div class="card-sub">Quick breakdown of modal scores</div>
191
+ </div>
192
+ </div>
193
+ </article>
194
+
195
+ </div>
196
+
197
+ </div>
198
  </div>
199
 
200
  <footer>
201
+ <p>©2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
202
  </footer>
203
+
204
+ <!-- Action buttons: Download / Email / Re-Analyze placed below the main template -->
205
+ <div class="validation-actions" aria-hidden="false">
206
+ <button class="action-btn action-download" (click)="downloadPDF()" aria-label="Download Report">
207
+ <span class="action-icon blue" aria-hidden="true"></span>
208
+ <span class="action-label">Download Report</span>
209
+ </button>
210
+
211
+ <button class="action-btn action-email" (click)="emailReport()" aria-label="Email to Supervisor">
212
+ <span class="action-icon purple" aria-hidden="true"></span>
213
+ <span class="action-label">Email to Supervisor</span>
214
+ </button>
215
+
216
+ <button class="action-btn action-reanalyze" (click)="reAnalyze()" aria-label="Re-Analyze Audio/Video">
217
+ <span class="action-icon green" aria-hidden="true"></span>
218
+ <span class="action-label">Re-Analyze Audio/Video</span>
219
+ </button>
220
+ </div>
src/app/validationpage/validationpage.component.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Component } from '@angular/core';
2
  import { Router } from '@angular/router';
 
3
  // For PDF generation
4
  // import jsPDF from 'jspdf';
5
 
@@ -8,19 +9,194 @@ import { Router } from '@angular/router';
8
  templateUrl: './validationpage.component.html',
9
  styleUrls: ['./validationpage.component.css']
10
  })
11
- export class ValidationpageComponent {
12
- truePercentage: number = 0;
13
- falsePercentage: number = 0;
14
  modalOpen: boolean = false;
15
  reportDate: string = new Date().toLocaleString('default', { month: 'long', year: 'numeric' });
16
 
17
- constructor(private router: Router) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  const nav = this.router.getCurrentNavigation();
19
  const state = nav?.extras?.state as { truePercentage?: number; falsePercentage?: number };
20
  this.truePercentage = state?.truePercentage ?? 0;
21
  this.falsePercentage = state?.falsePercentage ?? 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  navigateBackToCaseDetails() {
26
  this.router.navigate(['/case-details']);
@@ -35,7 +211,6 @@ export class ValidationpageComponent {
35
  }
36
 
37
  downloadPDF() {
38
-
39
  // Example: Use jsPDF to generate PDF
40
  // const doc = new jsPDF();
41
  // doc.text('Investigation Report', 10, 10);
@@ -62,4 +237,22 @@ export class ValidationpageComponent {
62
  // TODO: Implement re-analysis logic
63
  alert('Re-Analyze Audio/Video functionality to be implemented.');
64
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
 
1
+ import { Component, OnInit } from '@angular/core';
2
  import { Router } from '@angular/router';
3
+ import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
4
  // For PDF generation
5
  // import jsPDF from 'jspdf';
6
 
 
9
  templateUrl: './validationpage.component.html',
10
  styleUrls: ['./validationpage.component.css']
11
  })
12
+ export class ValidationpageComponent implements OnInit {
13
+ truePercentage: number = 72;
14
+ falsePercentage: number = 28;
15
  modalOpen: boolean = false;
16
  reportDate: string = new Date().toLocaleString('default', { month: 'long', year: 'numeric' });
17
 
18
+
19
+ // Dashboard metrics (real-time from CaseStoreService)
20
+ sessionCount: number = 0;
21
+ officerCount: number = 0;
22
+ suspectCount: number = 0;
23
+ avgSessionDuration: string = '';
24
+ analysisCount: number = 0;
25
+ consistencyIndex: number = 0;
26
+ audioVideoCount: number = 0;
27
+ reportCount: number = 0;
28
+
29
+ // Donut chart (officer session distribution)
30
+ officer1Percent: number = 0;
31
+ officer2Percent: number = 0;
32
+ officer3Percent: number = 0;
33
+
34
+ // Audio/Video metrics (current values will be animated)
35
+ audioMetric1: number = 0; // Speech
36
+ audioMetric2: number = 0; // Eye Contact
37
+ audioMetric3: number = 0; // Emotion
38
+ audioMetric4: number = 0; // Clarity
39
+ audioMetric5: number = 0; // Confidence
40
+
41
+ // Targets for animations
42
+ private audioMetric1Target = 0;
43
+ private audioMetric2Target = 0;
44
+ private audioMetric3Target = 0;
45
+ private audioMetric4Target = 0;
46
+ private audioMetric5Target = 0;
47
+
48
+ // Outcome distribution
49
+ outcome1: number = 0; // Consistent
50
+ outcome2: number = 0; // Needs Review
51
+ outcome3: number = 0; // Inconsistent
52
+
53
+ // Track which section/tab is selected
54
+ selectedSection: string = 'Dashboard';
55
+
56
+ // Analysis overview scores (for radial diagram) - displayed values
57
+ audioAnalysisScore: number = 0;
58
+ videoAnalysisScore: number = 0;
59
+ verifiedScore: number = 0;
60
+
61
+ // Analysis targets (final percentages to animate to)
62
+ audioAnalysisTarget: number = 0;
63
+ videoAnalysisTarget: number = 0;
64
+ verifiedTarget: number = 0;
65
+
66
+ // New: truthness/confidence fields used by template
67
+ audioTruthness: number = 74;
68
+ videoTruthness: number = 72;
69
+ verifiedConfidence: number = 73;
70
+
71
+ // SVG circle radii and computed values
72
+ r1 = 80; // outer (blue)
73
+ r2 = 64; // middle (green)
74
+ r3 = 48; // inner (red)
75
+ circumference1 = 2 * Math.PI * this.r1;
76
+ circumference2 = 2 * Math.PI * this.r2;
77
+ circumference3 = 2 * Math.PI * this.r3;
78
+ // stroke-dashoffset values (start hidden = full circumference)
79
+ offset1 = this.circumference1;
80
+ offset2 = this.circumference2;
81
+ offset3 = this.circumference3;
82
+
83
+ constructor(private router: Router, private caseStore: CaseStoreService) {
84
+ // Get latest cases
85
+ const cases = this.caseStore.getPoliceCases();
86
+ this.sessionCount = cases.length;
87
+ this.officerCount = Array.from(new Set(cases.map(c => c.police?.name))).length;
88
+ this.suspectCount = Array.from(new Set(cases.map(c => c.accused?.name))).length;
89
+ this.analysisCount = cases.length;
90
+ this.reportCount = cases.length;
91
+ // Dummy: avgSessionDuration
92
+ this.avgSessionDuration = '00:42:18';
93
+ // Dummy: audio/video count
94
+ this.audioVideoCount = cases.length;
95
+ // Dummy: consistencyIndex (use a fallback value, or derive from available case data)
96
+ this.consistencyIndex = Math.round((cases.reduce((sum, c) => sum + 72, 0) / (cases.length || 1)));
97
+ // Donut chart: officer session distribution
98
+ const officerSessions = cases.reduce((acc, c) => {
99
+ const name = c.police?.name || 'Unknown';
100
+ acc[name] = (acc[name] || 0) + 1;
101
+ return acc;
102
+ }, {} as { [name: string]: number });
103
+ const officerNames = Object.keys(officerSessions);
104
+ const totalSessions = cases.length || 1;
105
+ this.officer1Percent = Math.round((officerSessions[officerNames[0]] || 0) * 100 / totalSessions);
106
+ this.officer2Percent = Math.round((officerSessions[officerNames[1]] || 0) * 100 / totalSessions);
107
+ this.officer3Percent = Math.round((officerSessions[officerNames[2]] || 0) * 100 / totalSessions);
108
+ // Dummy: audio/video metric targets
109
+ this.audioMetric1Target = 68;
110
+ this.audioMetric2Target = 75;
111
+ this.audioMetric3Target = 60;
112
+ this.audioMetric4Target = 85;
113
+ this.audioMetric5Target = 82;
114
+ // Dummy: outcome distribution
115
+ this.outcome1 = 60;
116
+ this.outcome2 = 25;
117
+ this.outcome3 = 15;
118
+ // true/false percentage from router state
119
  const nav = this.router.getCurrentNavigation();
120
  const state = nav?.extras?.state as { truePercentage?: number; falsePercentage?: number };
121
  this.truePercentage = state?.truePercentage ?? 0;
122
  this.falsePercentage = state?.falsePercentage ?? 0;
123
+
124
+ // compute overview targets (do NOT set displayed scores yet)
125
+ this.computeOverviewScores();
126
+ }
127
+
128
+ ngOnInit(): void {
129
+ // Start load animations
130
+ this.animateLoad();
131
+ }
132
+
133
+ private animateLoad() {
134
+ const duration = 3000; // ms — slow 3s animation per request
135
+ const start = performance.now();
136
+
137
+ const animate = (now: number) => {
138
+ const t = Math.min(1, (now - start) / duration);
139
+ // ease-out
140
+ const ease = 1 - Math.pow(1 - t, 3);
141
+
142
+ // animate metric bars from their targets
143
+ this.audioMetric1 = Math.round(this.audioMetric1Target * ease);
144
+ this.audioMetric2 = Math.round(this.audioMetric2Target * ease);
145
+ this.audioMetric3 = Math.round(this.audioMetric3Target * ease);
146
+ this.audioMetric4 = Math.round(this.audioMetric4Target * ease);
147
+ this.audioMetric5 = Math.round(this.audioMetric5Target * ease);
148
+
149
+ // interpolate displayed radial scores from0 to targets
150
+ this.audioAnalysisScore = Math.round(this.audioAnalysisTarget * ease);
151
+ this.videoAnalysisScore = Math.round(this.videoAnalysisTarget * ease);
152
+ this.verifiedScore = Math.round(this.verifiedTarget * ease);
153
+
154
+ // Also update the truthness/confidence displayed details
155
+ this.audioTruthness = this.audioAnalysisScore;
156
+ this.videoTruthness = this.videoAnalysisScore;
157
+ this.verifiedConfidence = this.verifiedScore;
158
+
159
+ // update SVG offsets so stroke animates from full circumference to target offset
160
+ this.offset1 = Math.round(this.circumference1 * (1 - this.audioAnalysisScore / 100));
161
+ this.offset2 = Math.round(this.circumference2 * (1 - this.videoAnalysisScore / 100));
162
+ this.offset3 = Math.round(this.circumference3 * (1 - this.verifiedScore / 100));
163
+
164
+ if (t < 1) {
165
+ requestAnimationFrame(animate);
166
+ }
167
+ };
168
+
169
+ // ensure offsets start hidden (full circumference)
170
+ this.offset1 = this.circumference1;
171
+ this.offset2 = this.circumference2;
172
+ this.offset3 = this.circumference3;
173
+
174
+ requestAnimationFrame(animate);
175
  }
176
 
177
+ computeOverviewScores() {
178
+ // Audio analysis target: average of audio metric targets
179
+ const audioVals = [this.audioMetric1Target, this.audioMetric2Target, this.audioMetric3Target, this.audioMetric4Target, this.audioMetric5Target].filter(v => typeof v === 'number');
180
+ this.audioAnalysisTarget = audioVals.length ? Math.round(audioVals.reduce((a, b) => a + b, 0) / audioVals.length) : 0;
181
+
182
+ // Video analysis target: use consistencyIndex as proxy or derive from other available metrics
183
+ this.videoAnalysisTarget = this.consistencyIndex || Math.round((this.audioMetric2Target + this.audioMetric3Target + this.audioMetric4Target) / 3);
184
+
185
+ // Verified target: derive from truePercentage if available, else average of audio/video targets
186
+ const v = this.truePercentage || Math.round((this.audioAnalysisTarget + this.videoAnalysisTarget) / 2);
187
+ this.verifiedTarget = Math.round(v);
188
+
189
+ // Do not set displayed scores or offsets here; animateLoad will handle the animated transition
190
+ }
191
+
192
+ percentToOffset(percent: number, circumference: number) {
193
+ const pct = Math.max(0, Math.min(100, percent));
194
+ return Math.round(circumference * (1 - pct / 100));
195
+ }
196
+
197
+ selectSection(section: string) {
198
+ this.selectedSection = section;
199
+ }
200
 
201
  navigateBackToCaseDetails() {
202
  this.router.navigate(['/case-details']);
 
211
  }
212
 
213
  downloadPDF() {
 
214
  // Example: Use jsPDF to generate PDF
215
  // const doc = new jsPDF();
216
  // doc.text('Investigation Report', 10, 10);
 
237
  // TODO: Implement re-analysis logic
238
  alert('Re-Analyze Audio/Video functionality to be implemented.');
239
  }
240
+
241
+ // Navigate to question summary page
242
+ goToQuestionSummary() {
243
+ this.router.navigate(['/question-summary']);
244
+ }
245
+
246
+ getMetricGradient(metricValue: number): string {
247
+ const p = Math.max(0, Math.min(100, Math.round(metricValue)));
248
+ if (p >= 80) return 'linear-gradient(90deg, #10b981, #34d399)'; // green
249
+ if (p >= 60) return 'linear-gradient(90deg, #3b82f6, #60a5fa)'; // blue
250
+ if (p >= 40) return 'linear-gradient(90deg, #f59e0b, #f97316)'; // amber
251
+ return 'linear-gradient(90deg, #ef4444, #fb7185)'; // red
252
+ }
253
+
254
+ getMetricTextColor(metricValue: number): string {
255
+ const p = Math.max(0, Math.min(100, Math.round(metricValue)));
256
+ return p >= 50 ? '#ffffff' : '#0f172a';
257
+ }
258
  }
src/app/view-details-page/view-details-page.component.css ADDED
@@ -0,0 +1,750 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
2
+
3
+ :root {
4
+ --bg-dark: #011329;
5
+ --accent: #008cff;
6
+ --accent-strong: #006bb3;
7
+ --accent-soft: #a8dcff;
8
+ --accent-emerald: #0ea5a4;
9
+ --muted: #6b7280;
10
+ --muted-2: #94a3b8;
11
+ --card-bg: #ffffff;
12
+ --card-border: #e6f2ff;
13
+ --card-border-hover: rgba(0,140,255,0.18);
14
+ --soft-blue: rgba(3,102,214,0.06);
15
+ --shadow-strong: 08px24px rgba(3,102,214,0.08);
16
+ --gap: 18px;
17
+ --transition-fast: 160ms;
18
+ --transition-medium: 280ms;
19
+ --transition-slow: 420ms;
20
+ }
21
+
22
+ /* Modern UI header styles from infopage */
23
+ .site-header {
24
+ background: #011329;
25
+ box-shadow: 0 2px 12px #38bdf844;
26
+ margin-bottom: 0;
27
+ position: relative;
28
+ z-index: 10;
29
+ padding-bottom: 0;
30
+ }
31
+
32
+ .header-inner {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ padding: 18px 32px 0 32px;
37
+ position: relative;
38
+ }
39
+
40
+ .logo-cluster {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 18px;
44
+ }
45
+
46
+ .logo-img-header {
47
+ width: 54px;
48
+ height: 54px;
49
+ border-radius: 50%;
50
+ background: #fff;
51
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
52
+ padding: 4px;
53
+ margin-top: -6px;
54
+ margin-bottom: 1vh;
55
+ }
56
+
57
+ .py-detect-title-header {
58
+ font-size: 2.1rem;
59
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
60
+ font-weight: 900;
61
+ letter-spacing: 6px;
62
+ color: #38bdf8;
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 2px;
66
+ margin-bottom: 1.5vh;
67
+ }
68
+
69
+ .py-detect-title-header .py-letter.p {
70
+ color: #e3f6ff;
71
+ text-shadow: 0 0 6px #38bdf8;
72
+ }
73
+
74
+ .py-detect-title-header .py-letter.y {
75
+ color: #38bdf8;
76
+ text-shadow: 0 0 6px #38bdf8;
77
+ }
78
+
79
+ .py-detect-title-header .py-shape {
80
+ color: #e3f6ff;
81
+ background: #e3f6ff;
82
+ text-shadow: 0 0 6px #38bdf8;
83
+ box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
84
+ border: 2px solid #23272b;
85
+ width: 18px;
86
+ height: 4px;
87
+ display: inline-block;
88
+ margin: 0 8px;
89
+ border-radius: 2px;
90
+ }
91
+
92
+ .py-detect-title-header .py-letter.d {
93
+ color: #e3f6ff;
94
+ text-shadow: 0 0 6px #38bdf8;
95
+ }
96
+
97
+ .py-detect-title-header .py-letter.e {
98
+ color: #38bdf8;
99
+ text-shadow: 0 0 6px #38bdf8;
100
+ }
101
+
102
+ .py-detect-title-header .py-letter.t {
103
+ color: #e3f6ff;
104
+ text-shadow: 0 0 6px #38bdf8;
105
+ }
106
+
107
+ .py-detect-title-header .py-letter.e2 {
108
+ color: #38bdf8;
109
+ text-shadow: 0 0 6px #38bdf8;
110
+ }
111
+
112
+ .py-detect-title-header .py-letter.c {
113
+ color: #e3f6ff;
114
+ text-shadow: 0 0 6px #38bdf8;
115
+ }
116
+
117
+ .py-detect-title-header .py-letter.t2 {
118
+ color: #38bdf8;
119
+ text-shadow: 0 0 6px #38bdf8;
120
+ }
121
+ /* Overall layout spacing */
122
+ .details-layout {
123
+ display: flex;
124
+ gap: 28px;
125
+ align-items: flex-start;
126
+ padding: 18px 20px 80px;
127
+ background: linear-gradient(120deg, #f7fbff, #fafcff);
128
+ }
129
+
130
+ .details-sidebar {
131
+ width: 270px;
132
+ background: linear-gradient(180deg,#091427,#0b1a33);
133
+ border-radius: 18px;
134
+ padding: 28px 18px;
135
+ box-shadow: 0 12px 36px rgba(2,24,64,0.06);
136
+ color: #fff;
137
+ }
138
+
139
+ .sidebar-section-heading {
140
+ padding: 10px 12px;
141
+ border-radius: 10px;
142
+ background: rgba(255,255,255,0.02);
143
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
144
+ font-weight: 700;
145
+ }
146
+
147
+ .sidebar-btn-group {
148
+ margin-top: 12px;
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 10px;
152
+ }
153
+
154
+ .sidebar-btn2 {
155
+ padding: 12px;
156
+ border-radius: 10px;
157
+ text-align: left;
158
+ font-weight: 700;
159
+ transition: background var(--transition-fast) ease, transform var(--transition-fast) ease;
160
+ }
161
+
162
+ .sidebar-btn2:hover {
163
+ background: rgba(255,255,255,0.02);
164
+ transform: translateY(-1px);
165
+ }
166
+
167
+ /* Color and animation enhancements for clearer visual hierarchy */
168
+ .details-sidebar .sidebar-btn2 {
169
+ color: var(--muted-2);
170
+ background: transparent;
171
+ border-left: 4px solid transparent;
172
+ }
173
+
174
+ .details-sidebar .sidebar-btn2:hover {
175
+ background: rgba(255,255,255,0.02);
176
+ color: #e6f7ff;
177
+ transform: translateY(-1px);
178
+ }
179
+
180
+ .details-sidebar .sidebar-btn2.active {
181
+ background: linear-gradient(90deg, rgba(0,140,255,0.06), rgba(56,189,248,0.03));
182
+ color: #e8f8ff;
183
+ border-left-color: var(--accent);
184
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
185
+ }
186
+
187
+ /* Main card */
188
+ .indigo-main-card {
189
+ background: var(--card-bg);
190
+ border-radius: 18px;
191
+ padding: 24px;
192
+ box-shadow: var(--shadow-strong);
193
+ border: 1px solid var(--soft-blue);
194
+ width: 100%;
195
+ max-width: calc(100% -320px);
196
+ box-sizing: border-box;
197
+ overflow: auto;
198
+ transition: box-shadow var(--transition-medium) ease, transform var(--transition-medium) ease;
199
+ }
200
+
201
+ .indigo-main-card:hover {
202
+ box-shadow: 0 18px 48px rgba(3,102,214,0.08);
203
+ transform: translateY(-2px);
204
+ }
205
+
206
+ .indigo-main-card.wide {
207
+ background: #f7fbff;
208
+ border-radius: 18px;
209
+ box-shadow: 0 8px 32px rgba(56,189,248,0.10);
210
+ padding: 32px 32px 28px 32px;
211
+ max-width: 1600px;
212
+ margin: 32px auto 0 auto;
213
+ width: 100%;
214
+ }
215
+
216
+ .indigo-main-card,
217
+ .indigo-main-card.wide {
218
+ overflow-x: visible !important;
219
+ overflow-y: visible !important;
220
+ }
221
+
222
+ .case-header {
223
+ padding: 8px 6px 14px 6px;
224
+ }
225
+
226
+ .case-qa {
227
+ display: flex;
228
+ flex-direction: column;
229
+ gap: 8px;
230
+ font-weight: 600;
231
+ color: #0b3b72;
232
+ }
233
+
234
+ .case-qa strong {
235
+ color: #0b3b72;
236
+ }
237
+
238
+ /* Layout toggle */
239
+ .layout-toggle-row {
240
+ display: flex;
241
+ align-items: center;
242
+ gap: 12px;
243
+ margin-bottom: 12px;
244
+ }
245
+
246
+ .toggle-btn {
247
+ padding: 6px 10px;
248
+ border-radius: 8px;
249
+ border: 1px solid rgba(2,24,64,0.06);
250
+ background: transparent;
251
+ cursor: pointer;
252
+ transition: all var(--transition-fast) ease;
253
+ }
254
+
255
+ .toggle-btn:hover {
256
+ transform: translateY(-2px);
257
+ box-shadow: 0 6px 12px rgba(2,24,64,0.04);
258
+ }
259
+
260
+ .toggle-btn.active {
261
+ background: var(--accent);
262
+ color: #fff;
263
+ box-shadow: 0 6px 16px rgba(3,102,214,0.12);
264
+ }
265
+
266
+ /* Metrics card appearance */
267
+ .metrics-card {
268
+ background: #fff;
269
+ border-radius: 18px;
270
+ padding: 32px 28px 28px 28px;
271
+ box-shadow: 0 4px 24px rgba(56,189,248,0.10);
272
+ border: 1.5px solid #e6f2ff;
273
+ min-height: 320px;
274
+ display: flex;
275
+ flex-direction: column;
276
+ justify-content: flex-start;
277
+ align-items: flex-start;
278
+ width: 100%;
279
+ margin-bottom: 0;
280
+ }
281
+
282
+ .metrics-card-heading {
283
+ background: #f7fbff;
284
+ padding: 14px 24px;
285
+ border-radius: 14px;
286
+ display: inline-block;
287
+ color: #232a3d;
288
+ font-size: 1.22rem;
289
+ font-weight: 700;
290
+ margin-bottom: 22px;
291
+ box-shadow: 0 2px 12px #e0e7ff;
292
+ text-align: left;
293
+ }
294
+
295
+ .metrics-card-heading.audio-analysis,
296
+ .metrics-card-heading.video-analysis {
297
+ color: #2563eb !important;
298
+ }
299
+
300
+ .metric-row {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
304
+ padding: 14px 10px;
305
+ gap: 18px;
306
+ border-bottom: 1px solid #e6eaf3;
307
+ font-size: 1.12rem;
308
+ background: none;
309
+ }
310
+
311
+ .metric-label, .metric-name {
312
+ background: none;
313
+ border: none;
314
+ color: #232a3d;
315
+ font-weight: 600;
316
+ padding: 0;
317
+ margin-bottom: 0;
318
+ font-size: 1.04rem;
319
+ box-shadow: none;
320
+ display: inline-block;
321
+ text-align: left;
322
+ }
323
+ .metric-value {
324
+ color: #05204a;
325
+ font-weight: 900;
326
+ text-align: right;
327
+ font-size: 1.18rem;
328
+ }
329
+
330
+ /* Responsive tweaks for smaller screens */
331
+ @media (max-width: 1200px) {
332
+ .audio-cards-row {
333
+ grid-template-columns: 1fr 1fr;
334
+ gap: 24px;
335
+ }
336
+ .indigo-main-card.wide {
337
+ padding: 18px 8px 12px 8px;
338
+ max-width: 100%;
339
+ }
340
+ }
341
+ @media (max-width: 900px) {
342
+ .audio-cards-row {
343
+ flex-direction: column;
344
+ gap: 18px;
345
+ align-items: stretch;
346
+ }
347
+ .audio-cards-row .metrics-card {
348
+ max-width: 100%;
349
+ min-width: 0;
350
+ }
351
+ }
352
+ @media (max-width: 700px) {
353
+ .audio-cards-row {
354
+ grid-template-columns: 1fr;
355
+ gap: 18px;
356
+ }
357
+ .metrics-card {
358
+ min-height: 180px;
359
+ padding: 16px 8px;
360
+ }
361
+ }
362
+
363
+ /* Ensure four cards are always displayed in a single horizontal row */
364
+ .audio-cards-row {
365
+ display: flex;
366
+ flex-direction: row;
367
+ gap: 32px;
368
+ justify-content: center;
369
+ align-items: stretch;
370
+ width: 100%;
371
+ margin-top: 0;
372
+ }
373
+ .audio-cards-row .metrics-card {
374
+ flex: 1 1 0;
375
+ max-width: 340px;
376
+ min-width: 220px;
377
+ }
378
+
379
+ /* Fix: Make audio cards row fill available horizontal space and align cards in a single row */
380
+ .metrics-grid {
381
+ display: grid;
382
+ grid-template-columns: repeat(4,1fr);
383
+ gap: 32px;
384
+ }
385
+
386
+ .metrics-col {
387
+ display: flex;
388
+ flex-direction: column;
389
+ gap: 10px;
390
+ min-width: 0;
391
+ }
392
+
393
+ .metrics-card-heading {
394
+ font-size: 1.08rem;
395
+ color: var(--accent);
396
+ }
397
+
398
+ .metrics-list-plain {
399
+ list-style: none;
400
+ padding: 0;
401
+ margin: 0;
402
+ }
403
+
404
+ .metrics-list-plain li {
405
+ display: flex;
406
+ justify-content: space-between;
407
+ align-items: center;
408
+ padding: 8px 6px;
409
+ border-bottom: 1px dashed rgba(3,102,214,0.04);
410
+ }
411
+
412
+ .metrics-list-plain li .value {
413
+ min-width: 72px;
414
+ text-align: right;
415
+ color: #0b3b72;
416
+ font-weight: 700;
417
+ }
418
+
419
+ /* Multiple cards layout */
420
+ .metrics-grid-multiple {
421
+ display: flex;
422
+ gap: 18px;
423
+ flex-wrap: wrap;
424
+ }
425
+
426
+ .metrics-grid-multiple .metrics-card {
427
+ min-width: 311px;
428
+ flex: auto;
429
+ transition: transform var(--transition-fast) ease, box-shadow var(--transition-fast) ease;
430
+ }
431
+
432
+ .metrics-grid-multiple .metrics-card:hover {
433
+ transform: translateY(-6px);
434
+ box-shadow: 0 20px 40px rgba(3,102,214,0.08);
435
+ }
436
+
437
+ /* Charts and table styles */
438
+ .charts-row {
439
+ display: flex;
440
+ gap: 16px;
441
+ margin-top: 18px;
442
+ flex-wrap: wrap;
443
+ }
444
+
445
+ .chart-card {
446
+ flex: 11420px;
447
+ background: #fff;
448
+ border-radius: 12px;
449
+ padding: 12px;
450
+ border: 1px solid var(--soft-blue);
451
+ box-shadow: 0 4px 12px rgba(3,102,214,0.04);
452
+ }
453
+
454
+ .simple-bar {
455
+ width: 100%;
456
+ height: 180px;
457
+ display: block;
458
+ }
459
+
460
+ .summary-table {
461
+ width: 100%;
462
+ border-collapse: collapse;
463
+ font-size: 0.95rem;
464
+ }
465
+
466
+ .summary-table th, .summary-table td {
467
+ border: 1px solid rgba(224,231,239,0.6);
468
+ padding: 8px 12px;
469
+ text-align: left;
470
+ }
471
+
472
+ .summary-table thead th {
473
+ background: #fbfdff;
474
+ color: var(--accent);
475
+ font-weight: 700;
476
+ }
477
+
478
+ .summary-table tbody tr:nth-child(even) {
479
+ background: #fbfbff;
480
+ }
481
+
482
+ /* Audio / video summary rows */
483
+ .audio-summary-row {
484
+ display: flex;
485
+ gap: 16px;
486
+ margin-top: 8px;
487
+ flex-wrap: wrap;
488
+ }
489
+
490
+ .audio-summary-row .summary-item {
491
+ font-weight: 700;
492
+ color: #0b3b72;
493
+ padding: 6px 8px;
494
+ border-radius: 8px;
495
+ background: rgba(56,189,248,0.04);
496
+ transition: background var(--transition-fast) ease, transform var(--transition-fast) ease;
497
+ }
498
+
499
+ .audio-summary-row .summary-item:hover {
500
+ background: rgba(56,189,248,0.08);
501
+ transform: translateY(-3px);
502
+ }
503
+
504
+ /* Scrollable details area for cards that allow internal scrolling */
505
+ .audio-card-body, .audio-details-card, .video-details-card, .metrics-card-body {
506
+ max-height: 340px;
507
+ overflow: auto;
508
+ }
509
+
510
+ .audio-card-body::-webkit-scrollbar, .audio-details-card::-webkit-scrollbar, .video-details-card::-webkit-scrollbar {
511
+ height: 8px;
512
+ width: 8px;
513
+ }
514
+
515
+ .audio-card-body::-webkit-scrollbar-thumb, .audio-details-card::-webkit-scrollbar-thumb, .video-details-card::-webkit-scrollbar-thumb {
516
+ background: rgba(2,24,64,0.12);
517
+ border-radius: 6px;
518
+ }
519
+
520
+ /* Back button styling (matches page accent and animated glow) */
521
+ .back-btn {
522
+ display: inline-flex;
523
+ align-items: center;
524
+ gap: 8px;
525
+ background: linear-gradient(90deg, rgba(0,140,255,0.14), rgba(3,102,214,0.10));
526
+ color: #01243a;
527
+ font-weight: 800;
528
+ padding: 8px 14px;
529
+ border-radius: 10px;
530
+ border: 1px solid rgba(3,102,214,0.12);
531
+ box-shadow: 0 6px 18px rgba(3,102,214,0.06);
532
+ cursor: pointer;
533
+ transition: transform var(--transition-fast) ease, box-shadow var(--transition-fast) ease, border-color var(--transition-fast) ease;
534
+ }
535
+
536
+ .back-btn:hover {
537
+ transform: translateY(-3px);
538
+ box-shadow: 0 14px 30px rgba(3,102,214,0.12);
539
+ border-color: rgba(0,140,255,0.22);
540
+ }
541
+
542
+ .back-btn:active {
543
+ transform: translateY(-1px) scale(0.995);
544
+ }
545
+
546
+ .back-btn:focus {
547
+ outline: none;
548
+ box-shadow: 0004px rgba(0,140,255,0.12);
549
+ }
550
+
551
+ /* small icon styling */
552
+ .back-icon {
553
+ display: inline-block;
554
+ font-weight: 900;
555
+ color: var(--accent-strong);
556
+ transform: translateX(-1px);
557
+ }
558
+
559
+ /* subtle pulse animation to draw attention when on verification page */
560
+ @keyframes backPulse {
561
+ 0% {
562
+ box-shadow: 0 6px 18px rgba(3,102,214,0.06);
563
+ }
564
+
565
+ 50% {
566
+ box-shadow: 0 20px 40px rgba(3,102,214,0.08);
567
+ }
568
+
569
+ 100% {
570
+ box-shadow: 0 6px 18px rgba(3,102,214,0.06);
571
+ }
572
+ }
573
+
574
+ /* Apply pulse conditionally — you can add `.pulse` class in template if needed */
575
+ .back-btn.pulse {
576
+ animation: backPulse3.6s ease-in-out infinite;
577
+ }
578
+
579
+ /* Dark mode inverse for header area */
580
+ .site-header .back-btn {
581
+ background: linear-gradient(90deg, #38bdf8, rgba(255, 255, 255, 0.02));
582
+ color: #e6f7ff;
583
+ border: 1px solid rgba(255,255,255,0.04);
584
+ box-shadow: 0 6px 18px rgba(0,0,0,0.18);
585
+ margin-bottom: 16px;
586
+ }
587
+
588
+ .site-header .back-btn:hover {
589
+ box-shadow: 0 14px 30px rgba(0,0,0,0.28);
590
+ transform: translateY(-2px);
591
+ }
592
+
593
+ .nav-btn {
594
+ padding: 8px 12px;
595
+ border-radius: 8px;
596
+ border: 1px solid rgba(2,24,64,0.06);
597
+ background: linear-gradient(90deg, rgba(0,140,255,0.06), rgba(56,189,248,0.03));
598
+ color: #044a91;
599
+ font-weight: 700;
600
+ cursor: pointer;
601
+ transition: transform 160ms cubic-bezier(.2,.8,.2,1), box-shadow 160ms cubic-bezier(.2,.8,.2,1), background 220ms ease;
602
+ }
603
+
604
+ .nav-btn:hover:not([disabled]) {
605
+ transform: translateY(-3px);
606
+ box-shadow:0 12px 22px rgba(3,102,214,0.10);
607
+ background: linear-gradient(90deg, rgba(0,140,255,0.12), rgba(56,189,248,0.06));
608
+ }
609
+
610
+ .nav-btn[disabled] {
611
+ opacity:0.5;
612
+ cursor: default;
613
+ transform: none;
614
+ box-shadow: none;
615
+ }
616
+
617
+ /* During click/navigation animation we keep transform but remove any shadow to avoid background glow */
618
+ .nav-btn.prev-anim {
619
+ transform: translateX(-6px) scale(0.98);
620
+ box-shadow: none !important;
621
+ background: linear-gradient(90deg, rgba(3,102,214,0.14), rgba(56,189,248,0.06));
622
+ }
623
+
624
+ .nav-btn.next-anim {
625
+ transform: translateX(6px) scale(0.98);
626
+ box-shadow: none !important;
627
+ background: linear-gradient(90deg, rgba(3,102,214,0.14), rgba(56,189,248,0.06));
628
+ }
629
+
630
+ /* Also ensure active state does not show shadow */
631
+ .nav-btn:active {
632
+ box-shadow: none !important;
633
+ }
634
+
635
+ /* Footer */
636
+ footer {
637
+ background: linear-gradient(to right, #011022, #01030a);
638
+ color: #fff;
639
+ text-align: center;
640
+ padding: 10px 0px;
641
+ position: fixed;
642
+ left: 0;
643
+ bottom: 0;
644
+ width: 100%;
645
+ z-index: 100;
646
+ margin-top: 0;
647
+ }
648
+
649
+ /* Indigo container style for Verified Scores (refined to match screenshot) */
650
+ .indigo-main-card.verified-scores {
651
+ background: linear-gradient(180deg,#f7fbff, #ffffff);
652
+ border-radius: 18px;
653
+ padding: 20px;
654
+ box-shadow: 0 14px 40px rgba(3,102,214,0.06), inset 0 1px 0 rgba(255,255,255,0.6);
655
+ border: 1px solid rgba(3,102,214,0.08);
656
+ margin-bottom: 18px;
657
+ position: relative;
658
+ overflow: auto;
659
+ }
660
+
661
+ /* subtle divider under the main heading inside the indigo container */
662
+ .indigo-main-card.verified-scores > .metrics-card-heading {
663
+ color: #2563eb;
664
+ font-size: 1.08rem;
665
+ margin-bottom: 10px;
666
+ padding: 10px 8px;
667
+ border-radius: 8px;
668
+ display: inline-block;
669
+ }
670
+
671
+ .indigo-main-card.verified-scores .metrics-card-heading + .metrics-grid-card {
672
+ margin-top: 12px;
673
+ }
674
+
675
+ /* thin top rule under the overall heading (full width) */
676
+ .indigo-main-card.verified-scores .heading-divider {
677
+ content: '';
678
+ display: block;
679
+ height: 1px;
680
+ background: rgba(6,30,70,0.06);
681
+ margin: 12px 0 18px 0;
682
+ }
683
+
684
+ /* custom scrollbar styling to match screenshot cyan thumb */
685
+ .indigo-main-card.verified-scores::-webkit-scrollbar {
686
+ width: 12px;
687
+ }
688
+
689
+ .indigo-main-card.verified-scores::-webkit-scrollbar-track {
690
+ background: rgba(0,0,0,0.03);
691
+ border-radius: 8px;
692
+ }
693
+
694
+ .indigo-main-card.verified-scores::-webkit-scrollbar-thumb {
695
+ background: linear-gradient(180deg,#00e0ff,#00c0f0);
696
+ border-radius: 8px 5px 0 9px;
697
+ }
698
+
699
+ /* ensure inner validation card blends and keeps same rounded edges */
700
+ .metrics-card.validation-card {
701
+ background: rgba(250,252,255,0.98);
702
+ border-radius: 12px;
703
+ padding: 18px;
704
+ border: 1px solid rgba(3,102,214,0.04);
705
+ }
706
+
707
+ /* Make the three-column grid appear with soft separators similar to screenshot */
708
+
709
+ .metrics-grid {
710
+ display: grid;
711
+ grid-template-columns: repeat(4, 1fr);
712
+ gap: 32px;
713
+ }
714
+
715
+ .metrics-grid .metrics-col {
716
+ padding: 12px;
717
+ background: rgba(255,255,255,0.6);
718
+ border-radius: 10px;
719
+ }
720
+
721
+ .metrics-grid .metrics-col .metrics-card-heading {
722
+ background: rgba(3,102,214,0.03);
723
+ padding: 8px 10px;
724
+ border-radius: 8px;
725
+ }
726
+
727
+ /* Small tweak: emphasize metric separators inside the grid */
728
+ .metrics-grid .metric-row {
729
+ border-bottom: 1px solid rgba(6,30,70,0.04);
730
+ }
731
+
732
+ /* Scoped: remove internal scrollbar only for the video details metrics card */
733
+ .video-details-card {
734
+ /* allow content to expand and avoid internal scroll */
735
+ overflow: visible !important;
736
+ max-height: none !important;
737
+ }
738
+
739
+ /* Hide any WebKit scrollbars that might still appear inside the video card */
740
+ .video-details-card::-webkit-scrollbar {
741
+ width:0px;
742
+ height:0px;
743
+ display: none;
744
+ }
745
+
746
+ /* Hide scrollbars in Firefox/IE for the video card */
747
+ .video-details-card {
748
+ -ms-overflow-style: none; /* IE and Edge */
749
+ scrollbar-width: none; /* Firefox */
750
+ }
src/app/view-details-page/view-details-page.component.html ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- Modern UI header with logo and PyDetect title -->
2
+ <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+
9
+ <div class="py-detect-title-header">
10
+ <span class="py-letter p">P</span>
11
+ <span class="py-letter y">Y</span>
12
+ <span class="py-shape"></span>
13
+ <span class="py-letter d">D</span>
14
+ <span class="py-letter e">E</span>
15
+ <span class="py-letter t">T</span>
16
+ <span class="py-letter e2">E</span>
17
+ <span class="py-letter c">C</span>
18
+ <span class="py-letter t2">T</span>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="header-actions-right">
23
+ <button class="back-btn" (click)="goBack()">
24
+ <span class="back-icon">←</span> Back
25
+ </button>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="details-layout">
31
+ <aside class="details-sidebar modern-sidebar">
32
+ <div class="sidebar-section-heading">Analysis Modules</div>
33
+ <div class="sidebar-btn-group">
34
+ <button class="sidebar-btn2" [class.active]="activeTab === 'audio'" (click)="setTab('audio')">
35
+ <span class="sidebar-icon material-icons">Audio Analysis</span>
36
+
37
+ </button>
38
+ <button class="sidebar-btn2" [class.active]="activeTab === 'video'" (click)="setTab('video')">
39
+ <span class="sidebar-icon material-icons">Video Analysis</span>
40
+
41
+ </button>
42
+ <button class="sidebar-btn2" [class.active]="activeTab === 'validation'" (click)="setTab('validation')">
43
+ <span class="sidebar-icon material-icons">verified Scores</span>
44
+
45
+ </button>
46
+ </div>
47
+
48
+ </aside>
49
+
50
+ <main class="details-main">
51
+ <div class="indigo-main-card wide" [@fadeInTab]>
52
+
53
+ <!-- Case header (only show Question/Answer) -->
54
+ <div class="case-header" *ngIf="selectedQuestion || (questions && questions.length > 0)">
55
+ <div class="case-qa">
56
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
57
+ <div style="flex:1">
58
+ <div><strong>Question:</strong> {{ selectedQuestion?.question || questions[0]?.question || '—' }}</div>
59
+ <div><strong>Answer:</strong> {{ selectedQuestion?.answer || questions[0]?.answer || '—' }}</div>
60
+ </div>
61
+
62
+ <!-- Question counter -->
63
+ <div style="display:flex;flex-direction:column;align-items:flex-end;min-width:140px;margin:8px;">
64
+ <div class="qa-counter" aria-live="polite" style="font-weight:700;color:#075985;">
65
+ Question
66
+ {{ getCurrentIndex() >= 0 ? (getCurrentIndex() + 1) : (selectedQuestion ? 1 : 0) }}
67
+ of {{ questions.length || 0 }}
68
+ </div>
69
+ </div>
70
+
71
+ <div style="display:flex;align-items:center;gap:8px;">
72
+ <button class="nav-btn"
73
+ (click)="prevQuestion()"
74
+ [disabled]="!hasPrev()"
75
+ [class.prev-anim]="navAnimating === 'prev'">
76
+ Previous
77
+ </button>
78
+ <button class="nav-btn"
79
+ (click)="nextQuestion()"
80
+ [disabled]="!hasNext()"
81
+ [class.next-anim]="navAnimating === 'next'">
82
+ Next
83
+ </button>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ <hr />
88
+ </div>
89
+
90
+ <!-- If no questions for this case show fallback -->
91
+ <div *ngIf="(!questions || questions.length === 0)" class="empty-message">
92
+ No question records available for Case ID {{ caseId }}.
93
+ </div>
94
+
95
+ <!-- Audio tab -->
96
+ <div *ngIf="questions && questions.length > 0 && activeTab === 'audio'">
97
+ <div *ngFor="let q of (selectedQuestion ? [selectedQuestion] : questions); let i = index"
98
+ class="audio-analysis-card metrics-card audio-card"
99
+ [@cardFade]>
100
+
101
+ <div class="metrics-card-heading audio-analysis">Audio Analysis</div>
102
+
103
+ <div class="metrics-list">
104
+ <ng-container *ngIf="layoutMode === 'single'">
105
+ <div class="metrics-grid-card">
106
+ <div class="metrics-grid">
107
+ <div class="metrics-card metrics-col">
108
+ <div class="metrics-card-heading">Core Metrics</div>
109
+ <div *ngFor="let m of coreMetrics" class="metric-row">
110
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
111
+ {{ m.label }}
112
+ </button>
113
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
114
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
115
+ </div>
116
+ </div>
117
+
118
+ <div class="metrics-card metrics-col">
119
+ <div class="metrics-card-heading">Stress & Tone</div>
120
+ <div *ngFor="let m of stressToneMetrics" class="metric-row">
121
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
122
+ {{ m.label }}
123
+ </button>
124
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
125
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="metrics-card metrics-col">
130
+ <div class="metrics-card-heading">Speech Behaviour</div>
131
+ <div *ngFor="let m of speechBehaviourMetrics" class="metric-row">
132
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
133
+ {{ m.label }}
134
+ </button>
135
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
136
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="metrics-card metrics-col">
141
+ <div class="metrics-card-heading">Advanced (optional)</div>
142
+ <div *ngFor="let m of advancedMetrics" class="metric-row">
143
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
144
+ {{ m.label }}
145
+ </button>
146
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
147
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </ng-container>
153
+
154
+ <ng-container *ngIf="layoutMode === 'multiple'">
155
+ <div class="metrics-grid-multiple">
156
+ <div class="metrics-card"
157
+ *ngFor="let group of [coreMetrics, stressToneMetrics, speechBehaviourMetrics, advancedMetrics]">
158
+ <div class="metrics-card-heading">
159
+ {{
160
+ group === coreMetrics ? 'Core Metrics' :
161
+ group === stressToneMetrics ? 'Stress & Tone' :
162
+ group === speechBehaviourMetrics ? 'Speech Behaviour' :
163
+ 'Advanced (optional)'
164
+ }}
165
+ </div>
166
+
167
+ <div *ngFor="let m of group" class="metric-row">
168
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
169
+ {{ m.label }}
170
+ </button>
171
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
172
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ </ng-container>
177
+ </div>
178
+
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Video tab -->
183
+ <div *ngIf="questions && questions.length > 0 && activeTab === 'video'">
184
+ <div *ngFor="let q of (selectedQuestion ? [selectedQuestion] : questions); let i = index"
185
+ class="video-analysis-card metrics-card video-card"
186
+ [@cardFade]>
187
+
188
+ <div class="metrics-card-heading video-analysis">Video Analysis</div>
189
+
190
+ <ng-container *ngIf="layoutMode === 'single'">
191
+ <div class="metrics-grid-card">
192
+ <div class="metrics-grid">
193
+ <div class="metrics-card metrics-col">
194
+ <div class="metrics-card-heading">Core Metrics</div>
195
+ <div *ngFor="let m of videoCoreMetrics" class="metric-row">
196
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
197
+ {{ m.label }}
198
+ </button>
199
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
200
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
201
+ </div>
202
+ </div>
203
+
204
+ <div class="metrics-card metrics-col">
205
+ <div class="metrics-card-heading">Behavioural & Psychological</div>
206
+ <div *ngFor="let m of videoBehaviourMetrics" class="metric-row">
207
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
208
+ {{ m.label }}
209
+ </button>
210
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
211
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
212
+ </div>
213
+ </div>
214
+
215
+ <div class="metrics-card metrics-col">
216
+ <div class="metrics-card-heading">Advanced (optional)</div>
217
+ <div *ngFor="let m of videoAdvancedMetrics" class="metric-row">
218
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
219
+ {{ m.label }}
220
+ </button>
221
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
222
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </ng-container>
228
+
229
+ <ng-container *ngIf="layoutMode === 'multiple'">
230
+ <div class="metrics-grid-multiple">
231
+ <div class="metrics-card"
232
+ *ngFor="let group of [videoCoreMetrics, videoBehaviourMetrics, videoAdvancedMetrics]">
233
+ <div class="metrics-card-heading">
234
+ {{
235
+ group === videoCoreMetrics ? 'Core Metrics' :
236
+ group === videoBehaviourMetrics ? 'Behavioural & Psychological' :
237
+ 'Advanced (optional)'
238
+ }}
239
+ </div>
240
+
241
+ <div *ngFor="let m of group" class="metric-row">
242
+ <button class="metric-name metric-label" (click)="toggleTooltip(m.key)">
243
+ {{ m.label }}
244
+ </button>
245
+ <div class="metric-value">{{ getMetricValue(q, m.key) }}</div>
246
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </ng-container>
251
+
252
+ </div>
253
+ </div>
254
+
255
+ <!-- Validation tab -->
256
+ <div *ngIf="questions && questions.length > 0 && activeTab === 'validation'">
257
+ <div *ngFor="let q of (selectedQuestion ? [selectedQuestion] : questions); let i = index"
258
+ class="validation-analysis-wrap">
259
+
260
+ <div class="indigo-main-card verified-scores" [@cardFade]>
261
+ <div class="metrics-card-heading">Verified Scores</div>
262
+
263
+ <ng-container *ngIf="layoutMode === 'single'">
264
+ <div class="verified-scores-grid">
265
+ <!-- First row: three cards -->
266
+ <div class="verified-scores-row" style="display:flex;gap:16px;margin-bottom:16px;">
267
+ <div class="metrics-card verified-score-card" style="flex:1;min-width:220px;">
268
+ <div class="metrics-card-heading" style="font-weight:600;">Physical Expression</div>
269
+ <div class="metric-value" style="font-weight:700;">Neutral, Low hand, Moderate leg, 2 detected</div>
270
+ </div>
271
+ <div class="metrics-card verified-score-card" style="flex:1;min-width:220px;">
272
+ <div class="metrics-card-heading" style="font-weight:600;">Physical Score (%)</div>
273
+ <div class="metric-value" style="font-weight:700;">2%</div>
274
+ </div>
275
+ <div class="metrics-card verified-score-card" style="flex:1;min-width:220px;">
276
+ <div class="metrics-card-heading" style="font-weight:600;">Voice Expression</div>
277
+ <div class="metric-value" style="font-weight:700;">Stress 68, Conf Moderate, Sent -45%, Delay 3.1 sec</div>
278
+ </div>
279
+ </div>
280
+ <!-- Second row: two cards -->
281
+ <div class="verified-scores-row" style="display:flex;gap:16px;">
282
+ <div class="metrics-card verified-score-card" style="flex:1;min-width:220px;">
283
+ <div class="metrics-card-heading" style="font-weight:600;">Voice Score (%)</div>
284
+ <div class="metric-value" style="font-weight:700;">23</div>
285
+ </div>
286
+ <div class="metrics-card verified-score-card" style="flex:1;min-width:220px;">
287
+ <div class="metrics-card-heading" style="font-weight:600;">Truth Probability (%)</div>
288
+ <div class="metric-value" style="font-weight:700;">78%</div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </ng-container>
293
+
294
+ <ng-container *ngIf="layoutMode === 'multiple'">
295
+ <div class="metrics-grid-multiple">
296
+ <div class="metrics-card" *ngFor="let m of videoFinalMetrics">
297
+ <div class="metrics-card-heading" *ngIf="m.label !== 'Overall Score (%)'">
298
+ {{ m.label }}
299
+ </div>
300
+
301
+ <div class="metric-row" *ngIf="m.label !== 'Overall Score (%)'">
302
+ <div class="metric-value">{{ getFinalMetricValue(q, m.key) }}</div>
303
+ </div>
304
+
305
+ <div class="tooltip" *ngIf="shownTooltip === m.key">{{ m.desc }}</div>
306
+ </div>
307
+ </div>
308
+ </ng-container>
309
+
310
+ </div>
311
+ </div>
312
+ </div>
313
+
314
+ </div>
315
+ </main>
316
+
317
+ <footer>
318
+ <p>©2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
319
+ </footer>
320
+ </div>
src/app/view-details-page/view-details-page.component.ts ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Component } from '@angular/core';
2
+ import { ActivatedRoute, Router } from '@angular/router';
3
+ import { QuestionDataService } from '../question-data.service';
4
+ import { trigger, transition, style, animate } from '@angular/animations';
5
+ import { CASE_DATA } from '../data/case-data';
6
+
7
+ @Component({
8
+ selector: 'app-view-details-page',
9
+ templateUrl: './view-details-page.component.html',
10
+ styleUrls: ['./view-details-page.component.css'],
11
+ animations: [
12
+ trigger('fadeInTab', [
13
+ transition(':enter', [
14
+ style({ opacity: 0, transform: 'translateY(16px)' }),
15
+ animate('500ms cubic-bezier(.4,0,.2,1)', style({ opacity: 1, transform: 'translateY(0)' }))
16
+ ])
17
+ ]),
18
+ // cardFade used for individual metric cards when they enter/leave
19
+ trigger('cardFade', [
20
+ transition(':enter', [
21
+ style({ opacity: 0, transform: 'translateY(8px) scale(0.98)' }),
22
+ // duration300ms, delay60ms
23
+ animate('300ms 60ms cubic-bezier(.2,.8,.2,1)', style({ opacity: 1, transform: 'translateY(0) scale(1)' }))
24
+ ]),
25
+ transition(':leave', [
26
+ animate('180ms cubic-bezier(.4,0,.2,1)', style({ opacity: 0, transform: 'translateY(8px) scale(0.98)' }))
27
+ ])
28
+ ])
29
+ ]
30
+ })
31
+ export class ViewDetailsPageComponent {
32
+ activeTab: 'audio' | 'video' | 'validation' = 'audio';
33
+
34
+ // When navigated from question summary with an index -> selectedQuestion is shown
35
+ selectedQuestion: any = null;
36
+
37
+ // When navigated by caseId -> questions contains all questions for the case
38
+ caseId: string = '';
39
+ caseDetails: any = null;
40
+ questions: any[] = [];
41
+
42
+ // prefer service data; fallback to static CASE_DATA
43
+ private sourceData: any[] = CASE_DATA;
44
+
45
+ // layout mode persistence: 'single' or 'multiple'
46
+ layoutMode: 'single' | 'multiple' = 'single';
47
+
48
+ // tooltip handling
49
+ shownTooltip: string | null = null;
50
+
51
+ // Audio metric groups (use key for binding and label for display)
52
+ coreMetrics = [
53
+ { key: 'truthProbability', label: 'Truth Probability (%)', desc: 'AI-estimated likelihood the spoken response is truthful.' },
54
+ { key: 'dominantEmotion', label: 'Dominant Emotion', desc: 'Primary emotion (Calm, Nervous, Defensive, Angry, Sad).' },
55
+ { key: 'emotion', label: 'Emotion', desc: 'Detected emotion labels or values for the utterance.' },
56
+ { key: 'duration', label: 'Duration', desc: 'Length of the spoken response or recording.' },
57
+ { key: 'confidence', label: 'Confidence Level', desc: 'High / Moderate / Low (based on tone steadiness).' },
58
+ { key: 'speechRate', label: 'Speech Rate (WPM)', desc: 'Words per minute. Faster or slower speech under stress.' },
59
+ { key: 'sentiment', label: 'Sentiment Score', desc: 'Positive / Negative / Neutral tone.' }
60
+ ];
61
+
62
+ stressToneMetrics = [
63
+ { key: 'pitchStability', label: 'Pitch Stability (Hz variation)', desc: 'Measures vocal frequency fluctuation.' },
64
+ { key: 'stressLevel', label: 'Stress Level (%)', desc: 'Based on amplitude variation & tone sharpness.' },
65
+ { key: 'blinkRate', label: 'Blink Rate', desc: 'Blinks per minute — often rises under stress.' },
66
+ { key: 'energyLevel', label: 'Energy Level (dB)', desc: 'Average vocal energy / loudness.' },
67
+ { key: 'voiceTremor', label: 'Voice Tremor Index', desc: 'Detects micro-shakes in tone.' }
68
+ ];
69
+
70
+ speechBehaviourMetrics = [
71
+ { key: 'responseDelay', label: 'Response Delay (sec)', desc: 'Time between question end and answer start.' },
72
+ { key: 'pausesPerMinute', label: 'Pauses per Minute', desc: 'Number of noticeable silences.' },
73
+ { key: 'disfluencyRate', label: 'Disfluency Rate', desc: '“Uh”, “um”, or stuttering frequency.' },
74
+ { key: 'articulationClarity', label: 'Articulation Clarity', desc: 'Pronunciation sharpness.' },
75
+ { key: 'eyeContact', label: 'Eye Contact', desc: 'Estimate of eye contact during response (if available).' }
76
+ ];
77
+
78
+ advancedMetrics = [
79
+ { key: 'spectralTilt', label: 'Spectral Tilt', desc: 'Balance between low/high frequency energy.' },
80
+ { key: 'formantShifts', label: 'Formant Shifts (F1, F2)', desc: 'Resonance changes in vocal tract.' },
81
+ { key: 'prosodyScore', label: 'Prosody Score', desc: 'Rhythm + intonation smoothness.' },
82
+ { key: 'emotionStability', label: 'Emotion Stability Index', desc: 'Consistency of emotion across phrases.' }
83
+ ];
84
+
85
+ // Video metric groups (Core, Behavioural, Advanced) — only these metrics per user request
86
+ videoCoreMetrics = [
87
+ { key: 'facialEmotion', label: 'Facial Emotion Detection', desc: 'Classifies visible emotions (Calm, Angry, Nervous, Sad, Confused, Fearful).' },
88
+ { key: 'eyeContactConsistency', label: 'Eye Contact Consistency (%)', desc: 'Percentage of time the subject maintains eye contact.' },
89
+ { key: 'blinkRate', label: 'Blink Rate (per minute)', desc: 'Blink frequency; increased blinking may indicate nervousness.' },
90
+ { key: 'headMovement', label: 'Head Movement Analysis', desc: 'Detects nods, shakes, or tilts.' },
91
+ { key: 'bodyMovementIndex', label: 'Body Movement Index', desc: 'Tracks posture shifts, fidgeting, or restlessness.' },
92
+ { key: 'handMovementFreq', label: 'Hand Movement Frequency', desc: 'Detects gesturing or hiding hands.' },
93
+ { key: 'microExpressionScore', label: 'Facial Micro-Expression Score', desc: 'AI-based confidence in identifying suppressed emotions.' }
94
+ ];
95
+
96
+ videoBehaviourMetrics = [
97
+ { key: 'confidenceLevel', label: 'Confidence Level (%)', desc: 'Derived from posture, facial stability, and gestures.' },
98
+ { key: 'stressLevel', label: 'Stress Level (%)', desc: 'Combines facial tension + movement instability.' },
99
+ { key: 'emotionShiftTimeline', label: 'Emotion Shift Timeline', desc: 'Tracks emotion changes throughout questioning.' }
100
+ ];
101
+
102
+ videoAdvancedMetrics = [
103
+ { key: 'gazeDeviation', label: 'Gaze Deviation Angle', desc: 'Measures deviation of eye direction from interviewer.' },
104
+ { key: 'facialTempMap', label: 'Facial Temperature Map (IR)', desc: 'Detects heat changes around nose/forehead (IR optional).' },
105
+ { key: 'postureStability', label: 'Posture Stability Index', desc: 'Monitors torso movement variance.' }
106
+ ];
107
+
108
+ // Final verified metrics used in Validation tab (only these metrics)
109
+ videoFinalMetrics = [
110
+ { key: 'physicalExpression', label: 'Physical Expression', desc: 'Summary of visible cues: posture, gestures, micro-expressions.' },
111
+ { key: 'physicalScore', label: 'Physical Score (%)', desc: 'Overall body-language consistency and stability score.' },
112
+ { key: 'voiceExpression', label: 'Voice Expression', desc: 'Combined emotional tone summary.' },
113
+ { key: 'voiceScore', label: 'Voice Score (%)', desc: 'Confidence and emotional steadiness derived from tone and speech.' },
114
+ { key: 'truthProbability', label: 'Truth Probability (%)', desc: 'Weighted average score combining both voice and video indicators.' },
115
+
116
+ ];
117
+
118
+ constructor(private route: ActivatedRoute, private router: Router, private questionDataService: QuestionDataService) {
119
+ // load persisted layout
120
+ const saved = localStorage.getItem('metricsLayoutMode');
121
+ if (saved === 'single' || saved === 'multiple') this.layoutMode = saved;
122
+
123
+ this.route.paramMap.subscribe(params => {
124
+ const idParam = params.get('id');
125
+ const caseParam = params.get('caseId');
126
+
127
+ const svcQuestions = this.questionDataService.getQuestions() || [];
128
+ const rawData = svcQuestions.length ? svcQuestions : this.sourceData;
129
+ // normalize so each item has `question` (fallback to `text`)
130
+ const data = rawData.map(q => ({ ...(q || {}), question: q?.question || q?.text || '' }));
131
+ this.caseDetails = this.questionDataService.getCaseDetails() || null;
132
+
133
+ if (idParam !== null) {
134
+ const idx = Number(idParam);
135
+ this.selectedQuestion = data[idx] || null;
136
+ this.caseId = this.selectedQuestion?.caseId || this.caseDetails?.caseId || '';
137
+ // keep full data list so Previous/Next navigate across all questions
138
+ this.questions = data;
139
+ this.activeTab = 'audio';
140
+ } else if (caseParam) {
141
+ this.caseId = caseParam;
142
+ this.questions = data.filter(q => q.caseId === this.caseId);
143
+ this.selectedQuestion = this.questions[0] || null;
144
+ } else {
145
+ this.questions = data;
146
+ this.selectedQuestion = this.questions[0] || null;
147
+ this.caseId = this.selectedQuestion?.caseId || '';
148
+ }
149
+ });
150
+ }
151
+
152
+ toggleLayout(mode: 'single' | 'multiple') {
153
+ this.layoutMode = mode;
154
+ localStorage.setItem('metricsLayoutMode', mode);
155
+ }
156
+
157
+ toggleTooltip(key: string) {
158
+ this.shownTooltip = this.shownTooltip === key ? null : key;
159
+ }
160
+
161
+ // helper to read metric value from question object and format
162
+ getMetricValue(q: any, key: string): string {
163
+ if (!q) return '—';
164
+ // truthProbability uses existing formatter
165
+ if (key === 'truthProbability') return this.formatTruthProbability(q);
166
+ const val = q[key];
167
+ if (val === undefined || val === null) return '—';
168
+ if (typeof val === 'number') {
169
+ // append % for keys that represent percent-like metrics
170
+ if (/Level|Score|Probability|Rate|Tremor|Stability/i.test(key)) return val + '%';
171
+ return String(val);
172
+ }
173
+ return String(val);
174
+ }
175
+
176
+ setTab(tab: 'audio' | 'video' | 'validation') {
177
+ this.activeTab = tab;
178
+ }
179
+
180
+ goBack() {
181
+ // navigate back to question summary list
182
+ this.router.navigate(['/question-summary']);
183
+ }
184
+
185
+ navigateHome() {
186
+ window.location.href = '/home';
187
+ }
188
+
189
+ // helper methods for validation calculations
190
+ getPhysicalExpressionSummary(q: any): string {
191
+ const parts: string[] = [];
192
+ if (!q) return '—';
193
+ if (q.posture) parts.push(q.posture);
194
+ if (q.handMovement) parts.push(q.handMovement + ' hand');
195
+ if (q.legMovement) parts.push(q.legMovement + ' leg');
196
+ if (q.microExpressions) parts.push(q.microExpressions);
197
+ return parts.length ? parts.join(', ') : '—';
198
+ }
199
+
200
+ getPhysicalScore(q: any): string {
201
+ if (!q) return '—';
202
+ const scores: number[] = [];
203
+ if (typeof q.handMovement === 'number') scores.push(q.handMovement);
204
+ if (typeof q.legMovement === 'number') scores.push(q.legMovement);
205
+ const match = (q.microExpressions || '').match(/(\d+)/);
206
+ if (match) scores.push(Number(match[1]));
207
+ if (!scores.length) return '—';
208
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + '%';
209
+ }
210
+
211
+ getVoiceExpressionSummary(q: any): string {
212
+ if (!q) return '—';
213
+ const parts: string[] = [];
214
+ if (q.stressLevel !== undefined) parts.push('Stress ' + q.stressLevel);
215
+ if (q.confidence) parts.push('Conf ' + q.confidence);
216
+ if (q.sentiment) parts.push('Sent ' + this.getSentimentPercent(q.sentiment));
217
+ if (q.responseDelay) parts.push('Delay ' + q.responseDelay);
218
+ return parts.length ? parts.join(', ') : '—';
219
+ }
220
+
221
+ getVoiceScore(q: any): string {
222
+ if (!q) return '—';
223
+ const scores: number[] = [];
224
+ if (typeof q.stressLevel === 'number') scores.push(q.stressLevel);
225
+ if (typeof q.confidence === 'number') scores.push(q.confidence);
226
+ else if (q.confidence === 'High') scores.push(90);
227
+ else if (q.confidence === 'Moderate') scores.push(60);
228
+ else if (q.confidence === 'Low') scores.push(30);
229
+ if (!scores.length) return '—';
230
+ return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) + '%';
231
+ }
232
+
233
+ getOverallScore(q: any): string {
234
+ const phys = this.getPhysicalScore(q);
235
+ const voice = this.getVoiceScore(q);
236
+ const physNum = parseInt(phys as any);
237
+ const voiceNum = parseInt(voice as any);
238
+ if (isNaN(physNum) && isNaN(voiceNum)) return '—';
239
+ if (isNaN(physNum)) return voice;
240
+ if (isNaN(voiceNum)) return phys;
241
+ return Math.round((physNum + voiceNum) / 2) + '%';
242
+ }
243
+
244
+ getSentimentPercent(sentiment: string): string {
245
+ if (!sentiment) return '';
246
+ const match = sentiment.match(/([+-]?\d*\.?\d+)/);
247
+ if (match) {
248
+ const value = parseFloat(match[1]);
249
+ const percent = Math.round(value * 100);
250
+ return (percent > 0 ? '+' : '') + percent + '%';
251
+ }
252
+ return sentiment;
253
+ }
254
+
255
+ // New helper: compute lie percentage from truthProbability (string like '78%')
256
+ getLiePercent(q: any): string {
257
+ if (!q) return '—';
258
+ const tp = q.truthProbability;
259
+ if (tp === undefined || tp === null) return '—';
260
+ // allow number or string with %
261
+ let num = NaN;
262
+ if (typeof tp === 'number') num = tp;
263
+ else if (typeof tp === 'string') {
264
+ const m = tp.match(/(\d+)/);
265
+ if (m) num = Number(m[1]);
266
+ }
267
+ if (isNaN(num)) return '—';
268
+ const lie = Math.max(0, 100 - num);
269
+ return lie + '%';
270
+ }
271
+
272
+ // helper to format truthProbability for display in template
273
+ formatTruthProbability(q: any): string {
274
+ if (!q) return '—';
275
+ const tp = q.truthProbability;
276
+ if (tp === undefined || tp === null) return '—';
277
+ if (typeof tp === 'number') return tp + '%';
278
+ if (typeof tp === 'string') {
279
+ // if already contains %, return as-is, else append
280
+ if (tp.includes('%')) return tp;
281
+ const m = tp.match(/(\d+)/);
282
+ if (m) return m[1] + '%';
283
+ return tp;
284
+ }
285
+ return String(tp);
286
+ }
287
+
288
+ // helper to get display value for final verified metrics
289
+ getFinalMetricValue(q: any, key: string): string {
290
+ if (!q) return '—';
291
+ switch (key) {
292
+ case 'physicalExpression':
293
+ return q?.physicalExpression || this.getPhysicalExpressionSummary(q) || '—';
294
+ case 'physicalScore':
295
+ return q?.physicalScore || this.getPhysicalScore(q);
296
+ case 'voiceExpression':
297
+ return q?.voiceExpression || this.getVoiceExpressionSummary(q);
298
+ case 'voiceScore':
299
+ return q?.voiceScore || this.getVoiceScore(q);
300
+ case 'truthProbability':
301
+ return this.formatTruthProbability(q);
302
+ case 'overallScore':
303
+ return q?.overallScore || this.getOverallScore(q);
304
+ default:
305
+ return this.getMetricValue(q, key);
306
+ }
307
+ }
308
+
309
+ // navigation helpers for Previous / Next buttons
310
+ public getCurrentIndex(): number {
311
+ if (!this.selectedQuestion || !this.questions || !this.questions.length) return -1;
312
+ // try reference match first
313
+ let idx = this.questions.indexOf(this.selectedQuestion);
314
+ if (idx >=0) return idx;
315
+ // fallback to matching by caseId + question text
316
+ idx = this.questions.findIndex(q => q && this.selectedQuestion && q.caseId === this.selectedQuestion.caseId && (q.question === this.selectedQuestion.question || q.text === this.selectedQuestion.question));
317
+ return idx;
318
+ }
319
+
320
+ hasPrev(): boolean {
321
+ const idx = this.getCurrentIndex();
322
+ return idx >0;
323
+ }
324
+
325
+ hasNext(): boolean {
326
+ const idx = this.getCurrentIndex();
327
+ return idx >=0 && idx < this.questions.length -1;
328
+ }
329
+
330
+ prevQuestion() {
331
+ const idx = this.getCurrentIndex();
332
+ if (idx >0) {
333
+ const newIdx = idx -1;
334
+ // play click animation briefly before navigating
335
+ this.navAnimating = 'prev';
336
+ this.shownTooltip = null;
337
+ setTimeout(() => {
338
+ this.router.navigate(['/view-details', newIdx]);
339
+ },140);
340
+ // clear animation state after it finishes
341
+ setTimeout(() => { this.navAnimating = null; },420);
342
+ }
343
+ }
344
+
345
+ nextQuestion() {
346
+ const idx = this.getCurrentIndex();
347
+ if (idx >=0 && idx < this.questions.length -1) {
348
+ const newIdx = idx +1;
349
+ this.navAnimating = 'next';
350
+ this.shownTooltip = null;
351
+ setTimeout(() => {
352
+ this.router.navigate(['/view-details', newIdx]);
353
+ },140);
354
+ setTimeout(() => { this.navAnimating = null; },420);
355
+ }
356
+ }
357
+
358
+ // transient animation state for nav buttons ('prev' | 'next' | null)
359
+ navAnimating: 'prev' | 'next' | null = null;
360
+ }
src/assets/google-logo.svg ADDED
src/environments/environment.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const environment = {
2
+ production: false,
3
+ pyDetectApiUrl: 'http://127.0.0.1:5002'
4
+ };
src/styles.css CHANGED
@@ -5,10 +5,9 @@ body, html {
5
  background: url('/assets/background.jpg') no-repeat center center fixed;
6
  background-size: cover;
7
  transition: background-color 0.3s ease, color 0.3s ease;
 
8
  }
9
 
10
-
11
-
12
  /* Theme Support */
13
  body.dark-theme {
14
  background-color: #0a0e16;
 
5
  background: url('/assets/background.jpg') no-repeat center center fixed;
6
  background-size: cover;
7
  transition: background-color 0.3s ease, color 0.3s ease;
8
+ overflow-x: hidden !important;
9
  }
10
 
 
 
11
  /* Theme Support */
12
  body.dark-theme {
13
  background-color: #0a0e16;