fix
Browse files- src/app/app-routing.module.ts +8 -0
- src/app/app.module.ts +7 -1
- src/app/case-details-page/case-details-page.component.css +69 -9
- src/app/case-details-page/case-details-page.component.html +343 -272
- src/app/case-details-page/case-details-page.component.ts +342 -14
- src/app/case-details-summary-page/case-details-summary-page.component.css +481 -0
- src/app/case-details-summary-page/case-details-summary-page.component.html +87 -0
- src/app/case-details-summary-page/case-details-summary-page.component.ts +446 -0
- src/app/data/case-data.ts +30 -0
- src/app/homepage/auth-card/auth-card.component.css +91 -0
- src/app/homepage/auth-card/auth-card.component.html +18 -0
- src/app/homepage/auth-card/auth-card.component.ts +19 -0
- src/app/homepage/auth-wrapper.component.css +13 -0
- src/app/homepage/auth-wrapper.component.ts +36 -0
- src/app/homepage/homepage.component.css +61 -0
- src/app/homepage/homepage.component.html +9 -2
- src/app/homepage/homepage.component.ts +5 -0
- src/app/homepage/sign-in/sign-in.component.css +955 -8
- src/app/homepage/sign-in/sign-in.component.html +191 -43
- src/app/homepage/sign-in/sign-in.component.ts +109 -25
- src/app/homepage/sign-in/sign-in.service.ts +1 -1
- src/app/homepage/sign-up/sign-up.component.css +560 -322
- src/app/homepage/sign-up/sign-up.component.html +133 -66
- src/app/homepage/sign-up/sign-up.component.ts +159 -21
- src/app/homepage/sign-up/sign-up.service.ts +1 -1
- src/app/infopage/infopage.component.css +6 -24
- src/app/infopage/infopage.component.html +12 -7
- src/app/infopage/infopage.component.ts +1217 -1186
- src/app/py-detect/py-detect.component.css +7 -6
- src/app/py-detect/py-detect.component.html +78 -22
- src/app/py-detect/py-detect.component.ts +543 -387
- src/app/py-detect/test-video.component.html +2 -0
- src/app/question-data.service.ts +21 -0
- src/app/question-summary-page/question-summary-page.component.css +821 -0
- src/app/question-summary-page/question-summary-page.component.html +131 -0
- src/app/question-summary-page/question-summary-page.component.ts +266 -0
- src/app/recordpage/recordpage.component.css +613 -62
- src/app/recordpage/recordpage.component.html +269 -228
- src/app/recordpage/recordpage.component.ts +252 -27
- src/app/services/pydetect.service.ts +486 -0
- src/app/shared/case-store.service.ts +150 -21
- src/app/validationpage/validationpage.component.css +1021 -732
- src/app/validationpage/validationpage.component.html +206 -189
- src/app/validationpage/validationpage.component.ts +199 -6
- src/app/view-details-page/view-details-page.component.css +750 -0
- src/app/view-details-page/view-details-page.component.html +320 -0
- src/app/view-details-page/view-details-page.component.ts +360 -0
- src/assets/google-logo.svg +9 -0
- src/environments/environment.ts +4 -0
- 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:
|
| 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:
|
| 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:
|
|
|
|
| 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:
|
| 751 |
-
right:
|
| 752 |
-
top:
|
| 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:
|
| 871 |
bottom: 0;
|
| 872 |
left: 0;
|
| 873 |
width: 100%;
|
| 874 |
-
|
|
|
|
| 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:
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</div>
|
| 22 |
|
| 23 |
<div class="record-card">
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 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 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 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">✔</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 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
</div>
|
| 222 |
|
| 223 |
<!-- Evidence Upload Section for Investigators only, below the table -->
|
| 224 |
<div *ngIf="isInvestigator() && selectedCase" class="evidence-upload-section">
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
</div>
|
| 233 |
|
| 234 |
<!-- Evidence Upload Panel for selected case -->
|
| 235 |
<div *ngIf="isInvestigator() && evidencePanelCase && evidencePanelCase.caseId" class="evidence-upload-section">
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
</div>
|
| 263 |
|
| 264 |
<!-- Full-page overlay for details (no evidence upload here) -->
|
| 265 |
<div *ngIf="selectedCase" class="fullpage-popup-overlay">
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
</div>
|
| 297 |
|
| 298 |
<!-- Footer from provided design -->
|
| 299 |
<footer>
|
| 300 |
-
|
| 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"> </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"> </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"> </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"> </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 & 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">✔</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">×</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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 341 |
for (const p of path) {
|
| 342 |
-
if (
|
| 343 |
-
else
|
|
|
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
-
|
| 346 |
} else {
|
| 347 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 385 |
return 'High';
|
| 386 |
}
|
| 387 |
-
if (mediumCrimes.includes(
|
| 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 |
-
|
| 26 |
-
<button class="auth-btn" (click)="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 151 |
-
background: #
|
| 152 |
-
color: #
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;">×</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';
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 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 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
);
|
| 66 |
}
|
| 67 |
|
| 68 |
-
|
| 69 |
-
//
|
| 70 |
-
const
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
background: #
|
| 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 |
-
|
| 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 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
background: white; /* brighter purple */
|
| 47 |
-
color: black;
|
| 48 |
}
|
| 49 |
|
| 50 |
-
.
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
-
.title {
|
| 58 |
-
|
| 59 |
-
font-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
-
.
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 74 |
display: grid;
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
font-weight: 600;
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 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 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
width: 100%;
|
| 104 |
-
background: #
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
-
.error {
|
| 109 |
-
color:
|
| 110 |
-
font-size:
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
| 115 |
width: 100%;
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
}
|
| 120 |
|
| 121 |
-
.
|
| 122 |
-
|
|
|
|
| 123 |
color: #fff;
|
| 124 |
-
|
| 125 |
-
|
| 126 |
}
|
| 127 |
|
| 128 |
-
.
|
| 129 |
-
|
| 130 |
-
color: #
|
| 131 |
-
|
| 132 |
-
margin-top: 8px;
|
| 133 |
}
|
| 134 |
|
| 135 |
-
.
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
-
.
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
}
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
.auth-box {
|
| 153 |
-
grid-template-columns: 520px 1fr;
|
| 154 |
}
|
| 155 |
}
|
| 156 |
|
| 157 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
display: flex;
|
| 159 |
align-items: center;
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
|
| 163 |
-
.
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
from {
|
| 169 |
opacity: 0;
|
| 170 |
}
|
|
@@ -174,260 +369,303 @@ select.input-field {
|
|
| 174 |
}
|
| 175 |
}
|
| 176 |
|
| 177 |
-
.
|
| 178 |
-
position:
|
| 179 |
-
|
| 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 |
-
|
| 188 |
-
|
| 189 |
}
|
| 190 |
|
| 191 |
-
.
|
| 192 |
-
|
|
|
|
| 193 |
display: flex;
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
position: absolute;
|
| 198 |
top: 0;
|
| 199 |
left: 0;
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
|
|
|
| 207 |
}
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
|
|
|
| 214 |
}
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
| 220 |
}
|
| 221 |
-
|
| 222 |
-
.
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 247 |
}
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
font-size: 2.
|
| 251 |
font-weight: 800;
|
| 252 |
-
|
| 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 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
| 269 |
}
|
| 270 |
-
|
|
|
|
|
|
|
| 271 |
color: #fff;
|
| 272 |
-
|
| 273 |
-
margin-
|
| 274 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
display: flex;
|
| 307 |
align-items: center;
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
}
|
| 312 |
-
.signup-checkbox input[type="checkbox"] {
|
| 313 |
-
margin-right: 10px;
|
| 314 |
-
accent-color: #1de9b6;
|
| 315 |
}
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
}
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
}
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
border: none;
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
font-size: 1rem;
|
| 352 |
-
font-weight: 600;
|
| 353 |
cursor: pointer;
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 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 |
-
|
| 386 |
-
z-index: 10;
|
| 387 |
-
transition: background 0.2s, color 0.2s;
|
| 388 |
-
box-shadow: 0 2px 8px #0005;
|
| 389 |
}
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
}
|
| 394 |
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
| 399 |
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
| 403 |
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 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
|
|
|
|
| 2 |
<div class="signup-header">
|
| 3 |
-
<div class="signup-logo"
|
|
|
|
|
|
|
| 4 |
</div>
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
<div class="
|
| 10 |
-
<
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
<
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">© 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">×</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 & 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">×</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 |
-
|
|
|
|
| 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 |
-
//
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 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:
|
| 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:
|
| 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:
|
|
|
|
| 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:
|
| 1201 |
-
height:
|
| 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 |
-
<
|
| 34 |
type="button"
|
| 35 |
-
(click)="goToRecords()"
|
|
|
|
|
|
|
| 36 |
<i class="fas fa-folder-open"></i>
|
| 37 |
-
<span>View Records</span>
|
| 38 |
-
</
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
})
|
| 52 |
export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 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:
|
| 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:
|
| 254 |
-
max-width:
|
| 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:
|
|
|
|
| 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:
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
</div>
|
| 73 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
//
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
// ---- TTS active flag ----
|
| 45 |
private isActive = false;
|
| 46 |
|
| 47 |
// ---- Q/A data ----
|
| 48 |
-
|
| 49 |
-
questionIndex = signal<number>(0);
|
| 50 |
log: QAResult[] = [];
|
| 51 |
|
| 52 |
|
| 53 |
// ---- Constructor with Router Injection ----
|
| 54 |
private routerSubscription?: Subscription;
|
| 55 |
|
| 56 |
-
constructor(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 94 |
-
|
| 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 |
-
|
| 103 |
-
stopDisabled = signal(true);
|
| 104 |
-
resumeDisabled = signal(true);
|
| 105 |
-
submitDisabled = signal(true);
|
| 106 |
|
| 107 |
// Add missing public methods and properties for template binding
|
| 108 |
-
|
| 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 |
-
|
| 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 |
-
|
| 322 |
|
| 323 |
-
|
| 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 |
-
|
| 383 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 478 |
-
},
|
| 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 |
-
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
};
|
| 680 |
this.videoRecorder.start();
|
|
|
|
| 681 |
}
|
| 682 |
|
| 683 |
public stopVideoRecording() {
|
| 684 |
-
|
|
|
|
| 685 |
this.videoRecorder.stop();
|
|
|
|
|
|
|
|
|
|
| 686 |
}
|
| 687 |
}
|
| 688 |
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
this.
|
| 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 |
-
//
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 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 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
}
|
|
|
|
|
|
|
| 804 |
}
|
| 805 |
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
}
|
| 816 |
-
if (this.evidenceFiles.recording) {
|
| 817 |
-
this.previousRecordings.push(this.evidenceFiles.recording.name);
|
| 818 |
-
this.evidenceFiles.recording = undefined;
|
| 819 |
}
|
| 820 |
-
this.
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 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 |
-
|
| 853 |
-
this.
|
| 854 |
-
this.
|
| 855 |
-
this.
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 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.
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
}
|
| 889 |
-
|
| 890 |
-
public closeSummary() {
|
| 891 |
-
this.showSummary = false;
|
| 892 |
}
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
}
|
|
|
|
|
|
|
|
|
|
| 915 |
}
|
| 916 |
-
}
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 921 |
}
|
|
|
|
| 922 |
|
| 923 |
-
|
| 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:
|
| 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 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 721 |
-
|
| 722 |
-
|
| 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:
|
| 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:
|
| 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: #
|
| 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: #
|
| 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: #
|
| 1102 |
}
|
| 1103 |
|
| 1104 |
.record-table th, .record-table td {
|
|
@@ -1122,9 +1426,7 @@ body, main.content {
|
|
| 1122 |
transition: background 0.15s;
|
| 1123 |
}
|
| 1124 |
|
| 1125 |
-
|
| 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: #
|
| 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-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
|
|
|
| 1420 |
}
|
| 1421 |
|
| 1422 |
-
.
|
| 1423 |
-
background:
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
}
|
| 1430 |
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 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 |
-
|
| 1445 |
-
|
| 1446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1447 |
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1451 |
|
| 1452 |
-
.
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 1469 |
-
bottom: 0;
|
| 1470 |
left: 0;
|
|
|
|
| 1471 |
width: 100%;
|
| 1472 |
-
|
|
|
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 40 |
-
<div class="
|
| 41 |
-
|
| 42 |
-
<div class="
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
</div>
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
</div>
|
| 52 |
-
</div>
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
-
<
|
| 82 |
-
<
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 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 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
box-shadow: 0 0 0 2px #90caf9;
|
| 171 |
-
animation: pulseActive 1s infinite;
|
| 172 |
-
}
|
| 173 |
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
}
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
|
|
|
| 181 |
}
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
}
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
</
|
| 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:
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 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 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
<div class="section-block">
|
| 226 |
-
<div class="section-title"
|
| 227 |
-
<
|
| 228 |
-
<div class="
|
| 229 |
-
|
| 230 |
-
<
|
| 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 |
-
|
| 258 |
-
<
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
| 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"> </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"> </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"> </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 & 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
|
| 192 |
for (const p of path) {
|
| 193 |
-
if (
|
| 194 |
-
else
|
| 195 |
}
|
| 196 |
-
|
| 197 |
} else {
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
|
|
|
|
|
|
|
|
|
| 200 |
}
|
| 201 |
|
| 202 |
getValue(obj: any, key: string): any {
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 223 |
-
this.
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 9 |
-
police
|
| 10 |
-
name
|
| 11 |
-
station
|
| 12 |
-
address
|
| 13 |
-
pincode
|
| 14 |
-
dutyPerson
|
| 15 |
-
modeOfCrime
|
| 16 |
information?: string;
|
| 17 |
};
|
| 18 |
-
accused
|
| 19 |
-
name
|
| 20 |
-
age
|
| 21 |
-
gender
|
| 22 |
-
address
|
| 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 >=
|
|
|
|
| 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 |
-
|
|
|
|
| 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 ===
|
| 160 |
this.cases = [
|
| 161 |
{
|
| 162 |
caseId: 'CASE-001',
|
|
@@ -174,14 +300,17 @@ export class CaseStoreService {
|
|
| 174 |
},
|
| 175 |
accused: {
|
| 176 |
name: 'Jane Smith',
|
| 177 |
-
age:
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
/* Validation Report Template Styles */
|
| 12 |
.validation-template-main {
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
}
|
|
|
|
| 18 |
.validation-template-flex {
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
.validation-template-left {
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
}
|
|
|
|
| 33 |
.validation-circle {
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
}
|
|
|
|
| 56 |
.validation-circle.inconsistency .circle-value {
|
| 57 |
-
|
| 58 |
}
|
|
|
|
| 59 |
.circle-label {
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
}
|
|
|
|
| 64 |
.validation-template-right {
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
.validation-section {
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
}
|
|
|
|
| 77 |
.section-header {
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
}
|
|
|
|
| 84 |
.details-header {
|
| 85 |
-
|
| 86 |
-
|
| 87 |
}
|
|
|
|
| 88 |
.incident-header {
|
| 89 |
-
|
| 90 |
-
|
| 91 |
}
|
|
|
|
| 92 |
.followup-header {
|
| 93 |
-
|
| 94 |
-
|
| 95 |
}
|
|
|
|
| 96 |
.section-table {
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
}
|
|
|
|
| 102 |
.section-row {
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
}
|
| 107 |
-
.section-row:last-child {
|
| 108 |
-
border-bottom: none;
|
| 109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
.section-cell {
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
}
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
.header-inner {
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
}
|
| 137 |
|
| 138 |
.logo-cluster {
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
}
|
| 143 |
|
| 144 |
.logo-img-header {
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
}
|
| 154 |
|
| 155 |
.py-detect-title-header {
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 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 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
}
|
| 230 |
|
| 231 |
/* Professional, modern look for validation results */
|
| 232 |
.validation-result-container {
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 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 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
}
|
| 272 |
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
color: #bae6fd;
|
| 277 |
-
margin: 0;
|
| 278 |
-
letter-spacing: 1px;
|
| 279 |
-
text-shadow: 0 0 8px #38bdf888;
|
| 280 |
-
}
|
| 281 |
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
|
| 290 |
-
.
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
}
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
}
|
| 313 |
|
| 314 |
-
.
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 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 |
-
.
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
}
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
}
|
| 347 |
|
| 348 |
/* Summary card for investigation evaluation */
|
| 349 |
.summary-card {
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
}
|
|
|
|
| 362 |
.summary-title {
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
}
|
|
|
|
| 369 |
.summary-text {
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
}
|
| 375 |
|
| 376 |
/* Status chips/badges for result */
|
| 377 |
.status-chip {
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
.status-chip.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
/* Modal styles for report summary */
|
| 387 |
.modal-overlay {
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
| 396 |
}
|
|
|
|
| 397 |
.modal-content {
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
}
|
|
|
|
| 408 |
.modal-title {
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
}
|
|
|
|
| 415 |
.modal-section {
|
| 416 |
-
|
| 417 |
}
|
|
|
|
| 418 |
.modal-close {
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
}
|
|
|
|
| 429 |
.report-actions, .modal-section h3 {
|
| 430 |
-
|
| 431 |
}
|
|
|
|
|
|
|
| 432 |
.report-btn {
|
| 433 |
background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
|
| 434 |
color: #fff;
|
| 435 |
-
font-weight:
|
| 436 |
border: none;
|
| 437 |
-
border-radius:
|
| 438 |
-
padding:
|
| 439 |
-
margin-right:
|
| 440 |
-
margin-bottom:
|
| 441 |
cursor: pointer;
|
| 442 |
-
box-shadow:
|
| 443 |
display: inline-flex;
|
| 444 |
align-items: center;
|
| 445 |
-
gap:
|
| 446 |
-
font-size:
|
| 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:
|
| 453 |
}
|
|
|
|
| 454 |
.icon-report::before {
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
}
|
|
|
|
| 459 |
.icon-download::before {
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
}
|
|
|
|
| 464 |
.icon-email::before {
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
}
|
|
|
|
| 469 |
.modal-content h3 {
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
}
|
|
|
|
| 475 |
.modal-content p {
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 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:
|
| 485 |
color: #fff;
|
| 486 |
-
box-shadow:
|
| 487 |
position: relative;
|
| 488 |
}
|
|
|
|
| 489 |
.dashboard-header-content {
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
}
|
|
|
|
| 497 |
.dashboard-logo {
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
}
|
|
|
|
| 505 |
.dashboard-title-block {
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
}
|
|
|
|
| 510 |
.dashboard-title {
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
}
|
|
|
|
| 516 |
.dashboard-date {
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
}
|
|
|
|
| 521 |
.header-btns-right {
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 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 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
align-items: center;
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
| 563 |
}
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 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 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
|
|
|
| 602 |
}
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
}
|
| 608 |
-
|
| 609 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
}
|
| 611 |
|
| 612 |
-
/*
|
| 613 |
-
.
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
margin: 32px auto 18px auto;
|
| 618 |
-
max-width: 700px;
|
| 619 |
-
padding: 18px 32px 12px 32px;
|
| 620 |
}
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
}
|
| 627 |
-
.
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
.
|
| 638 |
-
|
| 639 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
display: grid;
|
| 663 |
-
grid-template-columns: repeat(
|
| 664 |
-
gap:
|
| 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 |
-
|
| 696 |
-
|
| 697 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
}
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
|
|
|
| 702 |
}
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
}
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 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 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
align-items: center;
|
| 735 |
-
gap: 8px;
|
| 736 |
}
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
}
|
| 740 |
-
.
|
| 741 |
-
|
| 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 |
-
|
| 822 |
-
|
| 823 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
}
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 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 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 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 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 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 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
}
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
}
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
</div>
|
| 27 |
|
| 28 |
-
<!--
|
| 29 |
-
<
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
<div class="
|
| 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 |
-
<!--
|
| 116 |
-
<div class="
|
| 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 |
-
<!--
|
| 129 |
-
<
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 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 |
-
<!--
|
| 164 |
-
<
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
| 174 |
|
| 175 |
-
|
| 176 |
-
<div class="
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
<!--
|
| 186 |
-
<
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
</div>
|
| 200 |
|
| 201 |
<footer>
|
| 202 |
-
|
| 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 =
|
| 13 |
-
falsePercentage: number =
|
| 14 |
modalOpen: boolean = false;
|
| 15 |
reportDate: string = new Date().toLocaleString('default', { month: 'long', year: 'numeric' });
|
| 16 |
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|