Oviya commited on
Commit
6f093ab
·
1 Parent(s): 55e4706

casedetailspage-update

Browse files
src/app/case-details-page/case-details-page.component.css CHANGED
@@ -1,5 +1,9 @@
1
  @import '../recordpage/recordpage.component.css';
2
 
 
 
 
 
3
  body, main.content {
4
  background: #f4f6fa;
5
  min-height: 100vh;
@@ -144,10 +148,10 @@ h3 {
144
  margin-bottom: 12px;
145
  }
146
 
147
- p {
148
  margin: 4px 0;
149
  color: #334155;
150
- }
151
 
152
  hr {
153
  margin: 24px 0 0 0;
@@ -421,6 +425,15 @@ hr {
421
  max-width: 320px;
422
  }
423
 
 
 
 
 
 
 
 
 
 
424
  @media (max-width: 700px) {
425
  .actions .btn {
426
  min-width: 120px;
@@ -449,19 +462,49 @@ hr {
449
  }
450
  }
451
 
452
- /* Make all table columns equal width */
453
- .records {
454
- table-layout: fixed;
455
  width: 100%;
 
 
 
 
456
  }
457
- .records th,
458
- .records td {
459
- width: 16.66%; /* 6 columns, 100/6 = 16.66% */
460
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  vertical-align: middle;
462
- white-space: nowrap;
463
- overflow: hidden;
464
- text-overflow: ellipsis;
 
 
 
 
 
 
 
 
 
 
465
  }
466
 
467
  .custom-active-cases-label {
@@ -475,4 +518,469 @@ hr {
475
  right: 32vw;
476
  }
477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
 
1
  @import '../recordpage/recordpage.component.css';
2
 
3
+ body, html {
4
+ overflow: auto !important;
5
+ }
6
+
7
  body, main.content {
8
  background: #f4f6fa;
9
  min-height: 100vh;
 
148
  margin-bottom: 12px;
149
  }
150
 
151
+ /*p {
152
  margin: 4px 0;
153
  color: #334155;
154
+ }*/
155
 
156
  hr {
157
  margin: 24px 0 0 0;
 
425
  max-width: 320px;
426
  }
427
 
428
+ .next-action-col {
429
+ text-align: left;
430
+ min-width: 120px;
431
+ padding-left: 0;
432
+ padding-right: 0;
433
+ font-weight: 500;
434
+ color: #2563eb;
435
+ }
436
+
437
  @media (max-width: 700px) {
438
  .actions .btn {
439
  min-width: 120px;
 
462
  }
463
  }
464
 
465
+ /* table layout for case details page */
466
+ .record-table {
 
467
  width: 100%;
468
+ table-layout: auto;
469
+ border-collapse: collapse;
470
+ overflow-x: hidden;
471
+ box-sizing: border-box;
472
  }
473
+
474
+ .record-table th, .record-table td {
475
+ padding-left: 12px;
476
+ padding-right: 12px;
477
+ word-break: break-word;
478
+ white-space: normal;
479
+ box-sizing: border-box;
480
+ border-bottom: 1px solid #e5e7eb; /* consistent row border */
481
+ }
482
+
483
+ .record-table tr:last-child td {
484
+ border-bottom: none;
485
+ }
486
+
487
+ th.actions, td.actions {
488
+ text-align: left;
489
+ padding-left: 0;
490
+ padding-right: 0;
491
+ }
492
+
493
+ td.actions {
494
  vertical-align: middle;
495
+ }
496
+
497
+ .icon-btn.view {
498
+ margin: 0;
499
+ padding: 0;
500
+ background: none;
501
+ border: none;
502
+ cursor: pointer;
503
+ color: #2563eb;
504
+ font-size: 1.4em;
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: flex-start;
508
  }
509
 
510
  .custom-active-cases-label {
 
518
  right: 32vw;
519
  }
520
 
521
+ .filter-bar select,
522
+ .filter-bar .filter-date {
523
+ padding: 6px 18px;
524
+ border-radius: 6px;
525
+ border: 1.5px solid #cbd5e1;
526
+ font-size: 1em;
527
+ color: #2563eb;
528
+ background: #fff;
529
+ font-weight: 600;
530
+ outline: none;
531
+ transition: border 0.15s;
532
+ min-width: 160px;
533
+ margin-right: 4px;
534
+ }
535
+ .filter-bar select:focus,
536
+ .filter-bar .filter-date:focus {
537
+ border: 1.5px solid #38bdf8;
538
+ }
539
+ .date-group {
540
+ display: inline-flex;
541
+ align-items: center;
542
+ font-family: inherit;
543
+ color: #a86a00;
544
+ font-size: 1.08em;
545
+ font-weight: 500;
546
+ margin-right: 8px;
547
+ gap: 2px;
548
+ }
549
+ .date-label {
550
+ margin: 0 4px 0 4px;
551
+ display: inline-flex;
552
+ align-items: center;
553
+ gap: 2px;
554
+ }
555
+ .calendar-ico {
556
+ font-size: 1.1em;
557
+ margin-left: 2px;
558
+ vertical-align: middle;
559
+ }
560
+ .filter-date {
561
+ border: none;
562
+ background: transparent;
563
+ color: #a86a00;
564
+ font-size: 1em;
565
+ font-weight: 600;
566
+ outline: none;
567
+ min-width: 90px;
568
+ margin-left: 2px;
569
+ }
570
+ .filter-date::-webkit-input-placeholder { color: #a86a00; }
571
+ .filter-date:-moz-placeholder { color: #a86a00; }
572
+ .filter-date::-moz-placeholder { color: #a86a00; }
573
+ .filter-date:-ms-input-placeholder { color: #a86a00; }
574
+
575
+ .fullpage-details {
576
+ width: 100vw;
577
+ min-height: 100vh;
578
+ background: #f8fafc;
579
+ padding: 32px 0 32px 0;
580
+ position: relative;
581
+ z-index: 1;
582
+ }
583
+ .details-content {
584
+ max-width: 1200px;
585
+ margin: 0 auto;
586
+ background: #fff;
587
+ border-radius: 18px;
588
+ box-shadow: 0 8px 32px rgba(30,41,59,0.12);
589
+ padding: 32px 48px;
590
+ }
591
+ .btn.back-btn {
592
+ margin: 0 0 24px 0;
593
+ background: #64748b;
594
+ color: #fff;
595
+ border-radius: 8px;
596
+ font-weight: 600;
597
+ font-size: 1.1rem;
598
+ padding: 10px 28px;
599
+ border: none;
600
+ cursor: pointer;
601
+ }
602
+
603
+ .fullpage-popup-overlay {
604
+ position: fixed;
605
+ top: 0; left: 0; right: 0; bottom: 0;
606
+ width: 100vw;
607
+ height: 100vh;
608
+ background: #f8fafc;
609
+ z-index: 9999;
610
+ display: flex;
611
+ align-items: flex-start;
612
+ justify-content: center;
613
+ overflow-y: auto;
614
+ overscroll-behavior: contain;
615
+ }
616
+ body.popup-open {
617
+ overflow: hidden !important;
618
+ }
619
+
620
+ .fullpage-popup-content {
621
+ background: #fff;
622
+ border-radius: 18px;
623
+ box-shadow: 0 8px 32px rgba(30,41,59,0.18);
624
+ margin: 40px 0;
625
+ max-width: 1200px;
626
+ width: 90vw;
627
+ min-height: 80vh;
628
+ padding: 32px 48px;
629
+ position: relative;
630
+ }
631
+
632
+ /* --- Section Card Styles --- */
633
+ .details-section-card {
634
+ background: #fff;
635
+ border-radius: 22px;
636
+ box-shadow: 0 4px 24px rgba(30,41,59,0.10);
637
+ border-left: 6px solid #38bdf8;
638
+ border-right: 2px solid #e0f2fe;
639
+ padding: 32px 36px 32px 36px;
640
+ margin-bottom: 0;
641
+ margin-top: 32px;
642
+ animation: fadeInUp 0.6s cubic-bezier(.23,1.01,.32,1) both;
643
+ }
644
+ @keyframes fadeInUp {
645
+ from { opacity: 0; transform: translateY(32px); }
646
+ to { opacity: 1; transform: translateY(0); }
647
+ }
648
+
649
+ .section-title {
650
+ font-size: 1.5rem;
651
+ font-weight: 700;
652
+ color: #2563eb;
653
+ margin-bottom: 18px;
654
+ letter-spacing: 0.5px;
655
+ }
656
+
657
+ .subgroup-pills {
658
+ display: flex;
659
+ gap: 18px;
660
+ margin-bottom: 28px;
661
+ flex-wrap: wrap;
662
+ }
663
+ .subgroup-pills button {
664
+ background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
665
+ color: #fff;
666
+ font-weight: 700;
667
+ border: none;
668
+ border-radius: 22px;
669
+ padding: 10px 32px;
670
+ font-size: 1.08em;
671
+ box-shadow: 0 2px 12px rgba(56,189,248,0.13);
672
+ cursor: pointer;
673
+ transition: background 0.18s, box-shadow 0.18s, transform 0.18s;
674
+ outline: none;
675
+ margin-bottom: 4px;
676
+ }
677
+ .subgroup-pills button.active,
678
+ .subgroup-pills button:focus {
679
+ background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%);
680
+ box-shadow: 0 4px 16px rgba(56,189,248,0.18);
681
+ transform: translateY(-2px) scale(1.04);
682
+ }
683
+
684
+ .fields-table-2col {
685
+ display: flex;
686
+ flex-direction: row;
687
+ gap: 0;
688
+ margin-top: 8px;
689
+ margin-bottom: 8px;
690
+ }
691
+ .fields-col {
692
+ display: flex;
693
+ flex-direction: column;
694
+ gap: 6px;
695
+ }
696
+ .fields-col-labels {
697
+ min-width: 260px;
698
+ text-align: left;
699
+ }
700
+ .fields-col-values {
701
+ min-width: 220px;
702
+ text-align: right;
703
+ }
704
+ .field-label {
705
+ color: #22223b;
706
+ font-weight: 500;
707
+ font-size: 1.08em;
708
+ padding: 2px 0;
709
+ }
710
+ .field-value {
711
+ color: #22223b;
712
+ font-weight: 700;
713
+ font-size: 1.08em;
714
+ padding: 2px 0;
715
+ letter-spacing: 0.5px;
716
+ }
717
+
718
+ /* --- Card Container and Title --- */
719
+ .case-details-title {
720
+ font-size: 2.2rem;
721
+ font-weight: 700;
722
+ color: #22223b;
723
+ margin-bottom: 32px;
724
+ margin-left: 8px;
725
+ display: flex;
726
+ align-items: center;
727
+ justify-content: space-between;
728
+ position: relative;
729
+ }
730
+
731
+ .btn.close-btn {
732
+ background: none;
733
+ color: #64748b;
734
+ border: none;
735
+ font-size: 2.2rem;
736
+ font-weight: 700;
737
+ cursor: pointer;
738
+ position: absolute;
739
+ right: 0;
740
+ top: 0;
741
+ line-height: 1;
742
+ padding: 0 16px;
743
+ transition: color 0.15s;
744
+ }
745
+ .btn.close-btn:hover {
746
+ color: #2563eb;
747
+ }
748
+
749
+ .btn.close-btn-bottom {
750
+ position: absolute;
751
+ right: 32px;
752
+ top: 32px;
753
+ background: #2563eb;
754
+ color: #fff;
755
+ border: none;
756
+ border-radius: 50%;
757
+ width: 48px;
758
+ height: 48px;
759
+ font-size: 2.2rem;
760
+ font-weight: 700;
761
+ box-shadow: 0 4px 16px rgba(56,189,248,0.18);
762
+ cursor: pointer;
763
+ z-index: 10001;
764
+ display: flex;
765
+ align-items: center;
766
+ justify-content: center;
767
+ transition: background 0.18s, color 0.18s, box-shadow 0.18s;
768
+ }
769
+ .btn.close-btn-bottom:hover {
770
+ background: #38bdf8;
771
+ color: #22223b;
772
+ }
773
+
774
+ .progress-col {
775
+ text-align: left;
776
+ min-width: 90px;
777
+ padding-left: 0;
778
+ padding-right: 0;
779
+ }
780
+
781
+ .progress-dot {
782
+ width: 18px;
783
+ height: 18px;
784
+ border-radius: 50%;
785
+ display: inline-block;
786
+ margin-right: 6px;
787
+ vertical-align: middle;
788
+ box-shadow: 0 2px 8px rgba(56,189,248,0.10);
789
+ }
790
+ .progress-dot.blue {
791
+ background: linear-gradient(135deg, #2563eb 0%, #38bdf8 100%);
792
+ }
793
+ .progress-dot.green {
794
+ background: linear-gradient(135deg, #34d399 0%, #6ee7b7 100%);
795
+ }
796
+ .progress-check {
797
+ font-size: 1.2em;
798
+ color: #34d399;
799
+ background: #d1fae5;
800
+ border-radius: 4px;
801
+ padding: 2px 4px;
802
+ display: inline-block;
803
+ margin-right: 6px;
804
+ vertical-align: middle;
805
+ }
806
+ .progress-value {
807
+ font-weight: 700;
808
+ font-size: 1em;
809
+ color: #22223b;
810
+ vertical-align: middle;
811
+ }
812
+
813
+ .priority-pill {
814
+ display: inline-flex;
815
+ align-items: center;
816
+ font-weight: bold;
817
+ font-size: 0.98em;
818
+ padding: 2px 12px 2px 8px;
819
+ border-radius: 16px;
820
+ margin-right: 2px;
821
+ letter-spacing: 0.02em;
822
+ }
823
+ .priority-high {
824
+ background: #fee2e2;
825
+ color: #b91c1c;
826
+ }
827
+ .priority-medium {
828
+ background: #fef9c3;
829
+ color: #ca8a04;
830
+ }
831
+ .priority-low {
832
+ background: #dcfce7;
833
+ color: #15803d;
834
+ }
835
+
836
+ .evidence-upload-section {
837
+ margin-top: 32px;
838
+ padding: 16px;
839
+ background: #f3f4f6;
840
+ border-radius: 12px;
841
+ box-shadow: 0 1px 4px rgba(0,0,0,0.04);
842
+ }
843
+ .evidence-upload-section h3 {
844
+ margin-bottom: 12px;
845
+ font-size: 1.1em;
846
+ color: #2563eb;
847
+ }
848
+ .evidence-list {
849
+ margin-top: 10px;
850
+ }
851
+ .evidence-file {
852
+ display: flex;
853
+ align-items: center;
854
+ font-size: 0.98em;
855
+ margin-bottom: 6px;
856
+ color: #374151;
857
+ }
858
+ .evidence-file i {
859
+ margin-right: 8px;
860
+ color: #2563eb;
861
+ }
862
+
863
+
864
+ /* Footer */
865
+ footer {
866
+ background: linear-gradient(to right, #011022, #01030a);
867
+ color: #fff;
868
+ text-align: center;
869
+ padding: 10px 0px;
870
+ position: relative;
871
+ bottom: 0;
872
+ left: 0;
873
+ width: 100%;
874
+ margin-top: 40px;
875
+ }
876
+
877
+ /* Additional Styles for Go Detect button/icon */
878
+ .icon-btn.detect {
879
+ background: #e3f2fd;
880
+ color: #1976d2;
881
+ border-radius: 50%;
882
+ box-shadow: 0 2px 8px rgba(25, 118, 210, 0.15);
883
+ margin-left: 4px;
884
+ position: relative;
885
+ transition: box-shadow 0.2s, transform 0.2s;
886
+ }
887
+ .icon-btn.detect:enabled:hover {
888
+ box-shadow: 0 4px 16px rgba(25, 118, 210, 0.25);
889
+ transform: scale(1.08);
890
+ }
891
+ .icon-btn.detect i {
892
+ font-size: 1.35em;
893
+ animation: detectPulse 1.2s infinite;
894
+ }
895
+ @keyframes detectPulse {
896
+ 0% { transform: scale(1); color: #1976d2; }
897
+ 50% { transform: scale(1.25); color: #42a5f5; }
898
+ 100% { transform: scale(1); color: #1976d2; }
899
+ }
900
+
901
+ .header-actions {
902
+ display: flex;
903
+ align-items: center;
904
+ gap: 16px;
905
+ }
906
+ .header-btn.detect-header-btn {
907
+ background: #1976d2;
908
+ color: #fff;
909
+ border: none;
910
+ border-radius: 8px;
911
+ padding: 8px 20px;
912
+ font-size: 1.08em;
913
+ font-weight: 500;
914
+ box-shadow: 0 2px 8px rgba(25, 118, 210, 0.10);
915
+ cursor: pointer;
916
+ transition: background 0.2s, transform 0.2s;
917
+ display: flex;
918
+ align-items: center;
919
+ gap: 8px;
920
+ position: relative;
921
+ }
922
+ .header-btn.detect-header-btn:hover {
923
+ background: #1565c0;
924
+ transform: scale(1.04);
925
+ }
926
+ .header-btn.detect-header-btn i {
927
+ font-size: 1.2em;
928
+ animation: detectPulse 1.2s infinite;
929
+ }
930
+
931
+ /* Enhanced styles for the Go Detect button */
932
+ .detect-btn {
933
+ background: linear-gradient(90deg, #1976d2 0%, #38bdf8 100%);
934
+ color: #fff;
935
+ border: none;
936
+ border-radius: 24px;
937
+ font-weight: 700;
938
+ font-size: 1.08em;
939
+ padding: 10px 28px;
940
+ box-shadow: 0 2px 12px rgba(25, 118, 210, 0.13);
941
+ cursor: pointer;
942
+ transition: background 0.18s, box-shadow 0.18s, transform 0.18s;
943
+ outline: none;
944
+ margin-left: 4px;
945
+ position: relative;
946
+ overflow: hidden;
947
+ z-index: 1;
948
+ animation: detectBtnPulse 1.6s infinite alternate;
949
+ }
950
+ .detect-btn:enabled:hover,
951
+ .detect-btn:focus {
952
+ background: linear-gradient(90deg, #38bdf8 0%, #1976d2 100%);
953
+ box-shadow: 0 4px 24px rgba(56,189,248,0.18);
954
+ transform: scale(1.06);
955
+ }
956
+ @keyframes detectBtnPulse {
957
+ 0% { box-shadow: 0 2px 12px rgba(25, 118, 210, 0.13); }
958
+ 100% { box-shadow: 0 8px 32px rgba(56,189,248,0.22); }
959
+ }
960
+ .detect-btn::after {
961
+ content: '';
962
+ position: absolute;
963
+ left: 0; top: 0;
964
+ width: 100%; height: 100%;
965
+ border-radius: 24px;
966
+ background: linear-gradient(90deg, rgba(56,189,248,0.12) 0%, rgba(25,118,210,0.08) 100%);
967
+ opacity: 0.5;
968
+ z-index: -1;
969
+ pointer-events: none;
970
+ animation: detectBtnGlow 2.2s infinite alternate;
971
+ }
972
+ @keyframes detectBtnGlow {
973
+ 0% { opacity: 0.5; }
974
+ 100% { opacity: 0.9; }
975
+ }
976
+
977
+
978
+
979
+
980
+
981
+
982
+
983
+
984
+
985
+
986
 
src/app/case-details-page/case-details-page.component.html CHANGED
@@ -43,30 +43,69 @@
43
  <div class="summary-label">Closed</div>
44
  <div class="summary-value">{{ closedCases }}</div>
45
  </div>
 
 
 
 
46
  </div>
47
 
48
  <div class="record-meta" style="padding: 8px 24px 0 24px; color: #6b7280; font-size: 0.98em;">
49
  {{ filteredCases.length }} items • Updated a few seconds ago
50
  </div>
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  <table class="record-table">
53
  <thead>
54
  <tr>
55
  <th>#</th>
56
  <th>Case ID</th>
 
57
  <th>Status</th>
58
  <th>Crime Type</th>
59
  <th>Date &amp; Time</th>
60
  <th>Location</th>
61
  <th>Suspect Name</th>
62
  <th>Last Updated</th>
63
- <th>Actions</th>
 
 
64
  </tr>
65
  </thead>
66
  <tbody>
67
- <tr *ngFor="let c of filteredCases; let i = index">
68
- <td>{{ i + 1 }}</td>
69
- <td class="mono">{{ c.caseId || '—' }}</td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  <td>
71
  <span class="status-label"
72
  [ngClass]="{
@@ -82,38 +121,182 @@
82
  <td>{{ c.police.address || '—' }}</td>
83
  <td>{{ c.accused.name || '—' }}</td>
84
  <td>{{ c.lastUpdated ? (c.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  <td class="actions">
86
- <button type="button" class="icon-btn view" (click)="openDetails(c)" title="View Details">
87
  <i class="fas fa-eye"></i>
88
  </button>
 
 
 
 
 
 
 
 
 
89
  </td>
90
  </tr>
91
- <tr *ngIf="filteredCases.length === 0">
92
- <td colspan="9" class="empty">No records found.</td>
93
  </tr>
94
  </tbody>
95
  </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </div>
97
 
98
- <!-- Modal white blur overlay for background -->
99
- <div class="modal-blur-overlay" *ngIf="showDetails"></div>
100
- <div class="modal" *ngIf="showDetails" role="dialog" aria-modal="true" aria-labelledby="detailsTitle">
101
- <div class="modal-header">
102
- <h2 id="detailsTitle">Case Details</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
- <div class="modal-body" *ngIf="selectedCase as sc">
105
- <div class="detail-row"><span>Case ID</span><b>{{ sc.caseId || '—' }}</b></div>
106
- <div class="detail-row"><span>Status</span><b>{{ sc.status || '' }}</b></div>
107
- <div class="detail-row"><span>Crime Type</span><b>{{ sc.crime || '' }}</b></div>
108
- <div class="detail-row"><span>Date & Time</span><b>{{ sc.dateTime ? (sc.dateTime | date:'M/d/yyyy HH:mm') : '—' }}</b></div>
109
- <div class="detail-row"><span>Location</span><b>{{ sc.police.address || '—' }}</b></div>
110
- <div class="detail-row"><span>Suspect Name</span><b>{{ sc.accused.name || '—' }}</b></div>
111
- <div class="detail-row"><span>Last Updated</span><b>{{ sc.lastUpdated ? (sc.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</b></div>
112
- <hr />
113
- <div class="detail-row"><span>Notes / Explanation</span><b>{{ sc.police.information || '—' }}</b></div>
114
  </div>
115
- <div class="modal-footer">
116
- <button type="button" class="btn" (click)="closeDetails()">Close</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  </div>
118
  </div>
119
 
 
 
 
 
 
 
43
  <div class="summary-label">Closed</div>
44
  <div class="summary-value">{{ closedCases }}</div>
45
  </div>
46
+ <div class="summary-card review">
47
+ <div class="summary-label">Pending Review</div>
48
+ <div class="summary-value">{{ reviewCases }}</div>
49
+ </div>
50
  </div>
51
 
52
  <div class="record-meta" style="padding: 8px 24px 0 24px; color: #6b7280; font-size: 0.98em;">
53
  {{ filteredCases.length }} items • Updated a few seconds ago
54
  </div>
55
 
56
+ <div class="filter-bar">
57
+ <select [(ngModel)]="filterStatus">
58
+ <option value="">Status</option>
59
+ <option *ngFor="let status of statusTypes">{{ status }}</option>
60
+ </select>
61
+ <select [(ngModel)]="filterCrimeType">
62
+ <option value="">Crime Type</option>
63
+ <option *ngFor="let type of crimeTypes">{{ type }}</option>
64
+ </select>
65
+ <button (click)="applyFilters()">Apply</button>
66
+ <button (click)="resetFilters()">Reset</button>
67
+ </div>
68
+
69
+ <!-- Table/list view always visible -->
70
  <table class="record-table">
71
  <thead>
72
  <tr>
73
  <th>#</th>
74
  <th>Case ID</th>
75
+ <th>Priority</th>
76
  <th>Status</th>
77
  <th>Crime Type</th>
78
  <th>Date &amp; Time</th>
79
  <th>Location</th>
80
  <th>Suspect Name</th>
81
  <th>Last Updated</th>
82
+ <th class="progress-col">Progress</th>
83
+ <th class="next-action-col">Next Action</th>
84
+ <th class="actions">Actions</th>
85
  </tr>
86
  </thead>
87
  <tbody>
88
+ <tr *ngFor="let c of rows, let i = index">
89
+ <td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
90
+ <td class="mono">
91
+ <a (click)="openDetails(c)" style="cursor:pointer; color:#2563eb; text-decoration:underline;">
92
+ {{ c.caseId || '—' }}
93
+ </a>
94
+ </td>
95
+ <td>
96
+ <ng-container [ngSwitch]="getCasePriority(c)">
97
+ <span *ngSwitchCase="'High'" class="priority-pill priority-high" title="High Priority">
98
+ 🔴 High
99
+ </span>
100
+ <span *ngSwitchCase="'Medium'" class="priority-pill priority-medium" title="Medium Priority">
101
+ 🟡 Medium
102
+ </span>
103
+ <span *ngSwitchCase="'Low'" class="priority-pill priority-low" title="Low Priority">
104
+ 🟢 Low
105
+ </span>
106
+ <span *ngSwitchDefault style="color:#6b7280;">—</span>
107
+ </ng-container>
108
+ </td>
109
  <td>
110
  <span class="status-label"
111
  [ngClass]="{
 
121
  <td>{{ c.police.address || '—' }}</td>
122
  <td>{{ c.accused.name || '—' }}</td>
123
  <td>{{ c.lastUpdated ? (c.lastUpdated | date:'M/d/yyyy HH:mm') : '—' }}</td>
124
+ <td class="progress-col">
125
+ <ng-container [ngSwitch]="getProgressValue(c)">
126
+ <ng-container *ngSwitchCase="100">
127
+ <span class="progress-check">&#x2714;</span>
128
+ <span class="progress-value">100%</span>
129
+ </ng-container>
130
+ <ng-container *ngSwitchCase="75">
131
+ <span class="progress-dot green"></span>
132
+ <span class="progress-value">75%</span>
133
+ </ng-container>
134
+ <ng-container *ngSwitchDefault>
135
+ <span class="progress-dot blue"></span>
136
+ <span class="progress-value">{{ getProgressValue(c) }}%</span>
137
+ </ng-container>
138
+ </ng-container>
139
+ </td>
140
+ <td class="next-action-col">
141
+ {{ getNextActionMessage(c) }}
142
+ </td>
143
  <td class="actions">
144
+ <button type="button" class="icon-btn view" (click)="openDetails(c)" title="View Case Details" aria-label="View Case Details">
145
  <i class="fas fa-eye"></i>
146
  </button>
147
+ <button type="button" class="detect-btn"
148
+ [disabled]="!c.caseId"
149
+ (click)="c.caseId && goToDetect(c.caseId)"
150
+ title="Go Detect" aria-label="Go Detect">
151
+ Go Detect
152
+ </button>
153
+ <button *ngIf="isInvestigator()" type="button" class="icon-btn upload" (click)="showEvidencePanel(c)" title="Upload Evidence" aria-label="Upload Evidence">
154
+ <i class="fas fa-upload"></i>
155
+ </button>
156
  </td>
157
  </tr>
158
+ <tr *ngIf="rows.length === 0">
159
+ <td colspan="12" class="empty">No records found.</td>
160
  </tr>
161
  </tbody>
162
  </table>
163
+
164
+ <!-- Pagination Controls -->
165
+ <div class="pagination-controls" style="display:flex;justify-content:center;align-items:center;margin:20px 0;gap:10px;">
166
+ <style>
167
+ .pagination-controls button {
168
+ border: none;
169
+ background: #f3f4f6;
170
+ color: #333;
171
+ border-radius: 8px;
172
+ padding: 0 16px;
173
+ min-width: 40px;
174
+ min-height: 40px;
175
+ font-size: 1.1em;
176
+ font-weight: 500;
177
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
178
+ transition: background 0.2s, color 0.2s, transform 0.2s;
179
+ cursor: pointer;
180
+ outline: none;
181
+ }
182
+ .pagination-controls button:hover:not(:disabled),
183
+ .pagination-controls button:focus:not(:disabled) {
184
+ background: #e3eafe;
185
+ color: #1976d2;
186
+ transform: scale(1.08);
187
+ }
188
+ .pagination-controls button.active {
189
+ background: #1976d2;
190
+ color: #fff;
191
+ font-weight: bold;
192
+ box-shadow: 0 0 0 2px #90caf9;
193
+ animation: pulseActive 1s infinite;
194
+ }
195
+ @keyframes pulseActive {
196
+ 0% { box-shadow: 0 0 0 2px #90caf9; }
197
+ 50% { box-shadow: 0 0 0 6px #90caf9; }
198
+ 100% { box-shadow: 0 0 0 2px #90caf9; }
199
+ }
200
+ .pagination-controls span {
201
+ font-size: 1.2em;
202
+ color: #888;
203
+ padding: 0 8px;
204
+ }
205
+ </style>
206
+ <button (click)="prevPage()" [disabled]="currentPage === 1">«</button>
207
+ <ng-container *ngFor="let page of getPagination()">
208
+ <button *ngIf="page !== '...'" (click)="goToPage(page)" [class.active]="currentPage === page">{{ page }}</button>
209
+ <span *ngIf="page === '...'">...</span>
210
+ </ng-container>
211
+ <button (click)="nextPage()" [disabled]="currentPage === totalPages">»</button>
212
+ </div>
213
  </div>
214
 
215
+ <!-- Results summary and page size selector -->
216
+ <div style="display:flex;align-items:center;justify-content:flex-start;gap:24px;margin-bottom:16px;">
217
+ <span style="font-size:1.1em;">Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span>
218
+ <select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)" style="padding:4px 12px;border-radius:8px;font-size:1em;">
219
+ <option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option>
220
+ </select>
221
+ </div>
222
+
223
+ <!-- Evidence Upload Section for Investigators only, below the table -->
224
+ <div *ngIf="isInvestigator() && selectedCase" class="evidence-upload-section">
225
+ <h3>Upload Evidence for Case: {{ selectedCase.caseId }}</h3>
226
+ <input type="file" multiple (change)="onEvidenceUpload($event)" />
227
+ <div class="evidence-list" *ngIf="uploadedEvidence.length">
228
+ <div *ngFor="let file of uploadedEvidence" class="evidence-file">
229
+ <i class="fas fa-file-upload"></i> {{ file.name }}
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Evidence Upload Panel for selected case -->
235
+ <div *ngIf="isInvestigator() && evidencePanelCase && evidencePanelCase.caseId" class="evidence-upload-section">
236
+ <hr class="evidence-hr" />
237
+ <div class="evidence-title">
238
+ <i class="fas fa-folder-open evidence-folder"></i>
239
+ Evidence Upload for Case: {{ evidencePanelCase.caseId }}
240
  </div>
241
+ <hr class="evidence-hr" />
242
+ <div class="evidence-type-tabs">
243
+ <button [class.active]="evidenceType === 'Document'" (click)="setEvidenceType('Document')">Document</button>
244
+ <button [class.active]="evidenceType === 'Photo'" (click)="setEvidenceType('Photo')">Photo</button>
 
 
 
 
 
 
245
  </div>
246
+ <div class="evidence-actions">
247
+ <label class="evidence-file-label">
248
+ [Choose File]
249
+ <input type="file" multiple (change)="onEvidenceFileSelectType($event)" style="display:none;" />
250
+ </label>
251
+ </div>
252
+ <hr class="evidence-hr" />
253
+ <div class="evidence-list-block">
254
+ <div class="evidence-list-title">Uploaded Evidence ({{ evidenceType }}):</div>
255
+ <div *ngFor="let file of evidenceFiles[evidencePanelCase.caseId][evidenceType]" class="evidence-file-row">
256
+ <i [ngClass]="getEvidenceIcon(file.name)" class="evidence-file-icon"></i>
257
+ <span class="evidence-file-name">{{ file.name }}</span>
258
+ <a class="evidence-view-link" href="#" (click)="viewEvidenceFile(file)">(View)</a>
259
+ </div>
260
+ </div>
261
+ <hr class="evidence-hr" />
262
+ </div>
263
+
264
+ <!-- Full-page overlay for details (no evidence upload here) -->
265
+ <div *ngIf="selectedCase" class="fullpage-popup-overlay">
266
+ <div class="fullpage-popup-content">
267
+ <div class="case-details-title">Case Details</div>
268
+ <div class="details-sections">
269
+ <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
270
+ <div class="details-section-card">
271
+ <div class="section-title">{{ sections[sectionKey].title }}</div>
272
+ <div class="subgroup-pills">
273
+ <button *ngFor="let subgroup of getSubgroups(sectionKey)"
274
+ [class.active]="selectedSubgroup[sectionKey] === subgroup"
275
+ (click)="selectSubgroup(sectionKey, subgroup)">
276
+ {{ subgroup }}
277
+ </button>
278
+ </div>
279
+ <div class="fields-table-2col">
280
+ <div class="fields-col fields-col-labels">
281
+ <div class="field-label" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
282
+ {{ field }}
283
+ </div>
284
+ </div>
285
+ <div class="fields-col fields-col-values">
286
+ <div class="field-value" *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
287
+ {{ getFieldValue(selectedCase, sectionKey, field) }}
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </ng-container>
293
+ </div>
294
+ <button class="btn close-btn-bottom" (click)="closeDetails()" title="Close">&times;</button>
295
  </div>
296
  </div>
297
 
298
+ <!-- Footer from provided design -->
299
+ <footer>
300
+ <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
301
+ </footer>
302
+ <!-- End of record-card -->
src/app/case-details-page/case-details-page.component.ts CHANGED
@@ -2,6 +2,8 @@ import { Component, OnInit } from '@angular/core';
2
  import { Router } from '@angular/router';
3
  import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
4
 
 
 
5
  @Component({
6
  selector: 'app-case-details-page',
7
  templateUrl: './case-details-page.component.html',
@@ -9,11 +11,95 @@ import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
9
  })
10
  export class CaseDetailsPageComponent implements OnInit {
11
  cases: PoliceCase[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  showDetails = false;
13
  selectedCase: PoliceCase | null = null;
14
  username: string = '';
15
  q: string = '';
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  constructor(private caseStore: CaseStoreService, private router: Router) {}
18
 
19
  ngOnInit(): void {
@@ -22,9 +108,19 @@ export class CaseDetailsPageComponent implements OnInit {
22
  }
23
 
24
  get filteredCases(): PoliceCase[] {
25
- if (!this.q?.trim()) return this.cases;
 
 
 
 
 
 
 
 
 
 
26
  const qLower = this.q.trim().toLowerCase();
27
- return this.cases.filter(c =>
28
  (c.caseId || '').toLowerCase().includes(qLower) ||
29
  (c.status || '').toLowerCase().includes(qLower) ||
30
  (c.crime || '').toLowerCase().includes(qLower) ||
@@ -43,21 +139,314 @@ export class CaseDetailsPageComponent implements OnInit {
43
  return this.cases.filter(c => c.status === 'Closed').length;
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
46
  openDetails(c: PoliceCase): void {
47
  this.selectedCase = c;
48
  this.showDetails = true;
 
49
  }
50
 
51
  closeDetails(): void {
52
  this.showDetails = false;
53
  this.selectedCase = null;
 
54
  }
55
 
56
- goToDetect(): void {
57
- window.location.href = '/py-detect';
58
  }
59
 
60
  navigateHome(): void {
61
  this.router.navigate(['/']);
62
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
 
2
  import { Router } from '@angular/router';
3
  import { CaseStoreService, PoliceCase } from '../shared/case-store.service';
4
 
5
+ type EvidenceType = 'Document' | 'Photo';
6
+
7
  @Component({
8
  selector: 'app-case-details-page',
9
  templateUrl: './case-details-page.component.html',
 
11
  })
12
  export class CaseDetailsPageComponent implements OnInit {
13
  cases: PoliceCase[] = [];
14
+
15
+ // Pagination
16
+ currentPage: number = 1;
17
+ pageSize: number = 5;
18
+ pageSizeOptions: number[] = [5, 10, 20, 50];
19
+ get totalPages(): number {
20
+ return Math.ceil(this.filteredCases.length / this.pageSize) || 1;
21
+ }
22
+ get pagedCases(): PoliceCase[] {
23
+ const start = (this.currentPage - 1) * this.pageSize;
24
+ return this.filteredCases.slice(start, start + this.pageSize);
25
+ }
26
+ setPage(page: number) {
27
+ if (page < 1 || page > this.totalPages) return;
28
+ this.currentPage = page;
29
+ }
30
+ nextPage() { this.setPage(this.currentPage + 1); }
31
+ prevPage() { this.setPage(this.currentPage - 1); }
32
+ onPageSizeChange(size: number) {
33
+ this.pageSize = size;
34
+ this.currentPage = 1;
35
+ }
36
+ get resultsStart(): number {
37
+ return this.filteredCases.length === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1;
38
+ }
39
+ get resultsEnd(): number {
40
+ return Math.min(this.currentPage * this.pageSize, this.filteredCases.length);
41
+ }
42
+ get resultsTotal(): number {
43
+ return this.filteredCases.length;
44
+ }
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 = '';
54
+ filterDateTo: string = '';
55
+
56
+ sections: any = {
57
+ crime: {
58
+ title: 'Crime Details',
59
+ subgroups: {
60
+ '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'],
61
+ 'Location & People': ['Location', 'Jurisdiction / PS', 'Scene Type', 'Reported By', 'Reported Contact', 'Witness Count', 'Victim Name', 'Victim Contact', 'Victim Summary', 'Suspected Offender Known?', 'Suspect Link'],
62
+ 'Offence & Context': ['Legal Sections / Charges', 'Offence Category', 'Offence Description', 'Suspected Motive', 'Confirmed Motive', 'Weapon Involved', 'Property Loss / Damage'],
63
+ 'Evidence & Scene': ['Evidence Collected', 'Forensic Tests Required', 'Scene Condition', 'Photos / Video?', 'CCTV Present?', 'CCTV Sources / IDs', 'Physical Evidence (list)', 'Chain of Custody?', 'Digital Evidence', 'Evidence Storage Reference'],
64
+ 'Operational Notes': ['Investigating Officer', 'Duty Person', 'Supervising Officer', 'Patrol Notes', 'Arrest Made', 'Arrest Location', 'Initial Actions Taken', 'riskLevel', 'Confidentiality'],
65
+ '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'],
66
+ 'Remark': ['Remark']
67
+ }
68
+ },
69
+ suspect: {
70
+ title: 'Suspect Details',
71
+ subgroups: {
72
+ 'Identity': ['Suspect ID', 'Suspect Name', 'Alias / Nickname', 'Age', 'Gender', 'Nationality', 'Nationality ID / Passport Number', 'Languages', 'Address', 'Known Aliases', 'Government ID'],
73
+ 'Physical Description': ['Height (cm)', 'Weight (kg)', 'Build', 'Hair Color', 'Eye Color', 'Distinguishing Marks', 'Tattoo Details', 'Scar Details', 'Photo Upload'],
74
+ 'Background': ['Employment', 'Education', 'Occupation', 'Company', 'Workplace Address', 'Marital Status', 'Known Habits', 'Known Financial Details'],
75
+ 'Known Associates': ['Associate Names', 'Gang Affiliation', 'Family Connections', 'Social Media Handles'],
76
+ 'Prior Records': ['Criminal History', 'Prior Arrests', 'Probation/Parole Status'],
77
+ 'Remark': ['Remark']
78
+ }
79
+ },
80
+ notes: {
81
+ title: 'Evidence and Documents',
82
+ subgroups: {
83
+ 'Investigation Notes': ['Initial Findings', 'Detailed Notes', 'Status', 'Version History / Updates'],
84
+ 'Evidence Files': ['Evidence Photos', 'Evidence Videos', 'Evidence Documents'],
85
+ 'Links and Recommendation': ['Links to Evidence', 'Final Recommendations'],
86
+ 'Remark': ['Remark']
87
+ }
88
+ }
89
+ };
90
+
91
+ selectedSubgroup: any = {
92
+ crime: 'Identification & Timing',
93
+ suspect: 'Identity',
94
+ notes: 'Investigation Notes'
95
+ };
96
+
97
+ uploadedEvidence: File[] = [];
98
+
99
+ evidencePanelCase: PoliceCase | null = null;
100
+ evidenceType: EvidenceType = 'Document';
101
+ evidenceFiles: { [caseId: string]: { Document: File[]; Photo: File[] } } = {};
102
+
103
  constructor(private caseStore: CaseStoreService, private router: Router) {}
104
 
105
  ngOnInit(): void {
 
108
  }
109
 
110
  get filteredCases(): PoliceCase[] {
111
+ let filtered = this.cases;
112
+
113
+ if (this.filterStatus) {
114
+ filtered = filtered.filter(c => c.status === this.filterStatus);
115
+ }
116
+ if (this.filterCrimeType) {
117
+ filtered = filtered.filter(c => c.crime === this.filterCrimeType);
118
+ }
119
+ // Add date filtering logic here if needed
120
+
121
+ if (!this.q?.trim()) return filtered;
122
  const qLower = this.q.trim().toLowerCase();
123
+ return filtered.filter(c =>
124
  (c.caseId || '').toLowerCase().includes(qLower) ||
125
  (c.status || '').toLowerCase().includes(qLower) ||
126
  (c.crime || '').toLowerCase().includes(qLower) ||
 
139
  return this.cases.filter(c => c.status === 'Closed').length;
140
  }
141
 
142
+ get reviewCases(): number {
143
+ return this.cases.filter(c => c.status === 'Under Investigation').length;
144
+ }
145
+
146
+ get statusTypes(): string[] {
147
+ return Array.from(new Set(this.cases.map(c => c.status).filter(s => !!s))) as string[];
148
+ }
149
+ get crimeTypes(): string[] {
150
+ return Array.from(new Set(this.cases.map(c => c.crime).filter(s => !!s))) as string[];
151
+ }
152
+
153
  openDetails(c: PoliceCase): void {
154
  this.selectedCase = c;
155
  this.showDetails = true;
156
+ document.body.classList.add('popup-open');
157
  }
158
 
159
  closeDetails(): void {
160
  this.showDetails = false;
161
  this.selectedCase = null;
162
+ document.body.classList.remove('popup-open');
163
  }
164
 
165
+ goToDetect(caseId: string): void {
166
+ this.router.navigate(['/py-detect'], { state: { caseId } });
167
  }
168
 
169
  navigateHome(): void {
170
  this.router.navigate(['/']);
171
  }
172
+
173
+ applyFilters() {
174
+ // No-op: filteredCases getter will use filters
175
+ this.currentPage = 1;
176
+ }
177
+ resetFilters() {
178
+ this.filterStatus = '';
179
+ this.filterCrimeType = '';
180
+ this.filterDateFrom = '';
181
+ this.filterDateTo = '';
182
+ this.q = '';
183
+ this.currentPage = 1;
184
+ }
185
+ getSubgroups(sectionKey: string): string[] {
186
+ return Object.keys(this.sections[sectionKey].subgroups);
187
+ }
188
+
189
+ getFieldsForSubgroup(sectionKey: string, subgroup: string): string[] {
190
+ return this.sections[sectionKey].subgroups[subgroup] || [];
191
+ }
192
+
193
+ selectSubgroup(sectionKey: string, subgroup: string) {
194
+ this.selectedSubgroup[sectionKey] = subgroup;
195
+ }
196
+
197
+ getFieldValue(sc: any, sectionKey: string, field: string): any {
198
+ // Map field labels to PoliceCase property paths (copied from recordpage.component.ts)
199
+ const fieldMap: { [key: string]: string | string[] } = {
200
+ // Crime Details
201
+ 'Case ID': 'caseId',
202
+ 'FIR / Ref #': 'firRef',
203
+ 'Crime Type': 'crime',
204
+ 'Case Category': 'caseCategory',
205
+ 'Date & Time (Entry)': 'dateTime',
206
+ 'Occurred From': 'occurredFrom',
207
+ 'Occurred To': 'occurredTo',
208
+ 'Time Reported': 'timeReported',
209
+ 'Time Discovered': 'timeDiscovered',
210
+ 'Country': 'country',
211
+ 'State': 'state',
212
+ 'District': 'district',
213
+ 'Number of Victims': 'numberOfVictims',
214
+ 'Brief Description': 'briefDescription',
215
+ 'Location': ['police', 'address'],
216
+ 'Jurisdiction / PS': 'jurisdiction',
217
+ 'Scene Type': 'sceneType',
218
+ 'Reported By': 'reportedBy',
219
+ 'Reported Contact': 'reportedContact',
220
+ 'Witness Count': 'witnessCount',
221
+ 'Victim Name': 'victimName',
222
+ 'Victim Contact': 'victimContact',
223
+ 'Victim Summary': 'victimSummary',
224
+ 'Suspected Offender Known?': 'suspectedOffenderKnown',
225
+ 'Suspect Link': 'suspectLink',
226
+ 'Legal Sections / Charges': 'legalSections',
227
+ 'Offence Category': 'offenceCategory',
228
+ 'Offence Description': 'offenceDescription',
229
+ 'Suspected Motive': 'suspectedMotive',
230
+ 'Confirmed Motive': 'confirmedMotive',
231
+ 'Weapon Involved': 'weaponInvolved',
232
+ 'Property Loss / Damage': 'propertyLoss',
233
+ 'Evidence Collected': 'evidenceCollected',
234
+ 'Forensic Tests Required': 'forensicTestsRequired',
235
+ 'Scene Condition': 'sceneCondition',
236
+ 'Photos / Video?': 'photosVideo',
237
+ 'CCTV Present?': 'cctvPresent',
238
+ 'CCTV Sources / IDs': 'cctvSources',
239
+ 'Physical Evidence (list)': 'physicalEvidence',
240
+ 'Chain of Custody?': 'chainOfCustody',
241
+ 'Digital Evidence': 'digitalEvidence',
242
+ 'Evidence Storage Reference': 'evidenceStorageReference',
243
+ 'Investigating Officer': ['police', 'name'],
244
+ 'Duty Person': ['police', 'dutyPerson'],
245
+ 'Supervising Officer': ['police', 'supervisingOfficer'],
246
+ 'Patrol Notes': ['police', 'patrolNotes'],
247
+ 'Arrest Made': 'arrestMade',
248
+ 'Arrest Location': 'arrestLocation',
249
+ 'Initial Actions Taken': 'initialActionsTaken',
250
+ 'riskLevel': 'riskLevel',
251
+ 'Confidentiality': 'confidentiality',
252
+ 'Biometric / Forensic IDs': 'biometricIds',
253
+ 'DNA Ref ID': 'dnaRefId',
254
+ 'Fingerprint ID': 'fingerprintId',
255
+ 'Case Status': 'status',
256
+ 'Linked Cases': 'linkedCases',
257
+ 'arrestCount': 'arrestCount',
258
+ 'Case Priority': 'casePriority',
259
+ 'Follow-up Date': 'followUpDate',
260
+ 'Court Case ID': 'courtCaseId',
261
+ 'Next Hearing Date': 'nextHearingDate',
262
+ 'Final Summary': 'finalSummary',
263
+ 'Remark': 'remark',
264
+ // Suspect Details
265
+ 'Suspect ID': ['accused', 'suspectId'],
266
+ 'Suspect Name': ['accused', 'name'],
267
+ 'Alias / Nickname': ['accused', 'alias'],
268
+ 'Age': ['accused', 'age'],
269
+ 'Gender': ['accused', 'gender'],
270
+ 'Nationality': ['accused', 'nationality'],
271
+ 'Nationality ID / Passport Number': ['accused', 'passportNumber'],
272
+ 'Languages': ['accused', 'languages'],
273
+ 'Address': ['accused', 'address'],
274
+ 'Known Aliases': ['accused', 'knownAliases'],
275
+ 'Government ID': ['accused', 'governmentId'],
276
+ 'Height (cm)': ['accused', 'height'],
277
+ 'Weight (kg)': ['accused', 'weight'],
278
+ 'Build': ['accused', 'build'],
279
+ 'Hair Color': ['accused', 'hairColor'],
280
+ 'Eye Color': ['accused', 'eyeColor'],
281
+ 'Distinguishing Marks': ['accused', 'distinguishingMarks'],
282
+ 'Tattoo Details': ['accused', 'tattooDetails'],
283
+ 'Scar Details': ['accused', 'scarDetails'],
284
+ 'Photo Upload': ['accused', 'photoUpload'],
285
+ 'Employment': ['accused', 'employment'],
286
+ 'Education': ['accused', 'education'],
287
+ 'Occupation': ['accused', 'occupation'],
288
+ 'Company': ['accused', 'company'],
289
+ 'Workplace Address': ['accused', 'workplaceAddress'],
290
+ 'Marital Status': ['accused', 'maritalStatus'],
291
+ 'Known Habits': ['accused', 'knownHabits'],
292
+ 'Known Financial Details': ['accused', 'knownFinancialDetails'],
293
+ 'Associate Names': ['accused', 'associateNames'],
294
+ 'Gang Affiliation': ['accused', 'gangAffiliation'],
295
+ 'Family Connections': ['accused', 'familyConnections'],
296
+ 'Social Media Handles': ['accused', 'socialMediaHandles'],
297
+ 'Criminal History': ['accused', 'criminalHistory'],
298
+ 'Prior Arrests': ['accused', 'priorArrests'],
299
+ 'Probation/Parole Status': ['accused', 'probationStatus'],
300
+ // Notes/Evidence
301
+ 'Initial Findings': ['police', 'information'],
302
+ 'Detailed Notes': ['notes', 'detailedNotes'],
303
+ 'Status': 'status',
304
+ 'Version History / Updates': ['notes', 'versionHistory'],
305
+ 'Evidence Photos': ['legal', 'evidencePhotos'],
306
+ 'Evidence Videos': ['legal', 'evidenceVideos'],
307
+ 'Evidence Documents': ['legal', 'evidenceDocuments'],
308
+ 'Links to Evidence': ['legal', 'linksToEvidence'],
309
+ 'Final Recommendations': ['legal', 'finalRecommendations'],
310
+ 'Witness Statements': ['legal', 'witnessStatements'],
311
+ 'Confessions': ['legal', 'confessions'],
312
+ // Audit Fields
313
+ 'Created By': 'createdBy',
314
+ 'Creation Date': 'creationDate',
315
+ 'Last Updated': 'lastUpdated',
316
+ 'Verified By': 'verifiedBy'
317
+ };
318
+
319
+ const path = fieldMap[field] || field;
320
+ if (Array.isArray(path)) {
321
+ let value = sc;
322
+ for (const p of path) {
323
+ if (value && value[p] !== undefined) value = value[p];
324
+ else return '—';
325
+ }
326
+ return value !== undefined && value !== null && value !== '' ? value : '—';
327
+ } else {
328
+ return sc[path] !== undefined && sc[path] !== null && sc[path] !== '' ? sc[path] : '—';
329
+ }
330
+ }
331
+
332
+ getProgressValue(caseObj: any): number {
333
+ let total = 0, filled = 0;
334
+ ['crime', 'suspect', 'notes'].forEach(sectionKey => {
335
+ const subgroups = this.getSubgroups(sectionKey);
336
+ subgroups.forEach(subgroup => {
337
+ const fields = this.getFieldsForSubgroup(sectionKey, subgroup);
338
+ total += fields.length;
339
+ fields.forEach(field => {
340
+ const value = this.getFieldValue(caseObj, sectionKey, field);
341
+ if (value && value !== '—') filled++;
342
+ });
343
+ });
344
+ });
345
+ return total ? Math.round((filled / total) * 100) : 0;
346
+ }
347
+ getNextActionMessage(c: any): string {
348
+ // Example logic based on status and progress
349
+ const progress = this.getProgressValue(c);
350
+ if (c.status === 'Closed' || progress === 100) {
351
+ return 'Case completed';
352
+ }
353
+ if (c.status === 'Under Review' || progress >= 75) {
354
+ return 'Await supervisor approval';
355
+ }
356
+ if (c.status === 'Open' || progress < 75) {
357
+ return 'Collect evidence';
358
+ }
359
+ return '—';
360
+ }
361
+ getCasePriority(c: PoliceCase): 'High' | 'Medium' | 'Low' {
362
+ // Automatic logic based on crime type and status
363
+ const highCrimes = ['Murder', 'Robbery'];
364
+ const mediumCrimes = ['Theft', 'Assault'];
365
+ if (highCrimes.includes(c.crime) || c.status === 'Open') {
366
+ return 'High';
367
+ }
368
+ if (mediumCrimes.includes(c.crime) || c.status === 'Under Investigation') {
369
+ return 'Medium';
370
+ }
371
+ return 'Low';
372
+ }
373
+
374
+ isInvestigator(): boolean {
375
+ // Simple role check: username contains 'investigator' (customize as needed)
376
+ return this.username.toLowerCase().includes('investigator');
377
+ }
378
+
379
+ onEvidenceUpload(event: any): void {
380
+ const files = Array.from(event.target.files) as File[];
381
+ this.uploadedEvidence = [...this.uploadedEvidence, ...files];
382
+ // You can add logic to save evidence to the case or backend here
383
+ }
384
+
385
+ showEvidencePanel(c: PoliceCase): void {
386
+ if (!c.caseId) return;
387
+ this.evidencePanelCase = c;
388
+ this.evidenceType = 'Document';
389
+ if (!this.evidenceFiles[c.caseId]) {
390
+ this.evidenceFiles[c.caseId] = { Document: [], Photo: [] };
391
+ }
392
+ }
393
+
394
+ setEvidenceType(type: EvidenceType): void {
395
+ this.evidenceType = type;
396
+ }
397
+
398
+ onEvidenceFileSelectType(event: any): void {
399
+ const files = Array.from(event.target.files) as File[];
400
+ if (this.evidencePanelCase && this.evidencePanelCase.caseId) {
401
+ this.evidenceFiles[this.evidencePanelCase.caseId][this.evidenceType].push(...files);
402
+ }
403
+ }
404
+
405
+ getEvidenceIcon(filename: string): string {
406
+ if (filename.match(/\.(jpg|jpeg|png|gif)$/i)) return 'fas fa-camera';
407
+ if (filename.match(/\.(mp4|avi|mov)$/i)) return 'fas fa-film';
408
+ if (filename.match(/\.(pdf)$/i)) return 'fas fa-file-pdf';
409
+ return 'fas fa-file';
410
+ }
411
+
412
+ viewEvidenceFile(file: File): void {
413
+ const url = URL.createObjectURL(file);
414
+ window.open(url, '_blank');
415
+ }
416
+
417
+ get rows(): PoliceCase[] {
418
+ return this.pagedCases;
419
+ }
420
+
421
+ // Helper for pagination display
422
+ getPagination(): (number | string)[] {
423
+ const pages: (number | string)[] = [];
424
+ const total = this.totalPages;
425
+ if (total <= 7) {
426
+ for (let i = 1; i <= total; i++) pages.push(i);
427
+ } else {
428
+ if (this.currentPage <= 4) {
429
+ for (let i = 1; i <= 5; i++) pages.push(i);
430
+ pages.push('...');
431
+ pages.push(total);
432
+ } else if (this.currentPage >= total - 3) {
433
+ pages.push(1);
434
+ pages.push('...');
435
+ for (let i = total - 4; i <= total; i++) pages.push(i);
436
+ } else {
437
+ pages.push(1);
438
+ pages.push('...');
439
+ for (let i = this.currentPage - 1; i <= this.currentPage + 1; i++) pages.push(i);
440
+ pages.push('...');
441
+ pages.push(total);
442
+ }
443
+ }
444
+ return pages;
445
+ }
446
+
447
+ goToPage(page: string | number) {
448
+ if (typeof page === 'number') {
449
+ this.setPage(page);
450
+ }
451
+ }
452
  }
src/app/case-store.service.ts CHANGED
@@ -143,6 +143,7 @@ export class CaseStoreService {
143
  this.addPoliceCase(mapped);
144
  }
145
 
 
146
  /** Persist to localStorage (safe to keep; remove if not needed) */
147
  private save(): void {
148
  try { localStorage.setItem(this.storageKey, JSON.stringify(this.cases)); } catch { }
 
143
  this.addPoliceCase(mapped);
144
  }
145
 
146
+
147
  /** Persist to localStorage (safe to keep; remove if not needed) */
148
  private save(): void {
149
  try { localStorage.setItem(this.storageKey, JSON.stringify(this.cases)); } catch { }
src/app/homepage/homepage.component.css CHANGED
@@ -39,138 +39,115 @@ body, html {
39
  left: 0;
40
  z-index: 30;
41
  }
42
-
43
- .logo {
44
- width: 120px;
45
- height: 120px;
46
- margin-left: 0;
47
- margin-top: 0;
48
- position: absolute;
49
- left: 0;
50
- top: 0;
51
- z-index: 40;
52
- }
53
-
54
- .logo-fixed {
55
  position: fixed;
56
  top: 0;
57
  left: 0;
58
- width: 120px;
59
- height: 120px;
60
  z-index: 100;
61
- margin: 0;
62
  }
63
 
64
- /* Add gap between logo and browser border */
65
- .logo-img {
66
- width: 6vw;
67
- position: fixed;
68
- top: 21px;
69
- left: 36px;
70
- z-index: 300;
71
- margin: 0;
72
- background: #ffffff;
73
- max-width: 4.2vw;
74
- min-width: 56px;
75
- height: auto;
76
- border-radius: 50%;
77
- padding: 4px;
78
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
79
- transition: transform .25s ease;
80
  }
81
 
82
- /* Move Py-Detect text further down by increasing top value of .logo-title-row */
83
- .logo-title-row {
84
  display: flex;
85
- flex-direction: row;
86
  align-items: center;
87
- position: absolute;
88
- top: 180px; /* Increased from 120px to move text further down */
89
- left: 48px;
90
- z-index: 300;
91
- gap: 32px;
92
  justify-content: flex-start;
93
- width: 100%;
 
94
  }
95
 
96
- .py-detect-title {
97
- position: fixed;
98
- margin-left: 97px;
99
- margin-top: -228px;
100
- text-align: left;
101
- font-size: 3vw;
102
- color: #38bdf8;
 
 
 
 
 
 
 
 
 
 
 
 
103
  font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
104
  font-weight: 900;
105
  letter-spacing: 6px;
106
- background: none;
107
- border: none;
108
- box-shadow: none;
109
- min-width: unset;
110
- max-width: unset;
111
  }
112
 
113
- /* Py-Detect title: each letter with its own color */
114
- .py-detect-title .py-letter.p {
115
  color: #e3f6ff;
116
  text-shadow: 0 0 6px #38bdf8;
117
  }
118
 
119
- .py-detect-title .py-letter.y {
120
  color: #38bdf8;
121
  text-shadow: 0 0 6px #38bdf8;
122
  }
123
 
124
- .py-detect-title .py-shape {
125
  color: #e3f6ff;
126
  background: #e3f6ff;
127
  text-shadow: 0 0 6px #38bdf8;
128
  box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
129
  border: 2px solid #23272b;
 
 
 
 
 
130
  }
131
 
132
- .py-shape {
133
- display: inline-block;
134
- width: 18px;
135
- height: 4px;
136
- background: #38bdf8;
137
- margin: 0 8px;
138
- vertical-align: middle;
139
- border-radius: 2px;
140
- box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
141
- border: 2px solid #23272b;
142
- }
143
-
144
- .py-detect-title .py-letter.d {
145
  color: #e3f6ff;
146
  text-shadow: 0 0 6px #38bdf8;
147
  }
148
 
149
- .py-detect-title .py-letter.e {
150
  color: #38bdf8;
151
  text-shadow: 0 0 6px #38bdf8;
152
  }
153
 
154
- .py-detect-title .py-letter.t {
155
  color: #e3f6ff;
156
  text-shadow: 0 0 6px #38bdf8;
157
  }
158
 
159
- .py-detect-title .py-letter.e2 {
160
  color: #38bdf8;
161
  text-shadow: 0 0 6px #38bdf8;
162
  }
163
 
164
- .py-detect-title .py-letter.c {
165
  color: #e3f6ff;
166
  text-shadow: 0 0 6px #38bdf8;
167
  }
168
 
169
- .py-detect-title .py-letter.t2 {
170
  color: #38bdf8;
171
  text-shadow: 0 0 6px #38bdf8;
172
  }
173
 
 
 
174
  .center-instructions {
175
  position: absolute;
176
  top: 50%;
@@ -230,7 +207,7 @@ body, html {
230
 
231
  .auth-topright {
232
  position: fixed;
233
- top: 46px;
234
  right: 48px;
235
  z-index: 200;
236
  display: flex;
@@ -974,3 +951,36 @@ footer .social-icons {
974
  background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
975
  box-shadow: 0 4px 8px rgba(188, 24, 136, 0.3);
976
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  left: 0;
40
  z-index: 30;
41
  }
42
+ /* Modern UI header styles from infopage */
43
+ .site-header {
44
+ background: #011329;
45
+ box-shadow: 0 2px 12px #38bdf844;
46
+ margin-bottom: 0;
 
 
 
 
 
 
 
 
47
  position: fixed;
48
  top: 0;
49
  left: 0;
50
+ width: 100%;
 
51
  z-index: 100;
52
+ padding-bottom: 0;
53
  }
54
 
55
+ body {
56
+ padding-top: 80px; /* Adjust to match header height so content is not hidden */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
 
59
+ .header-inner {
 
60
  display: flex;
 
61
  align-items: center;
 
 
 
 
 
62
  justify-content: flex-start;
63
+ padding: 18px 32px 0 32px;
64
+ position: relative;
65
  }
66
 
67
+ .logo-cluster {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 18px;
71
+ }
72
+
73
+ .logo-img-header {
74
+ width: 54px;
75
+ height: 54px;
76
+ border-radius: 50%;
77
+ background: #fff;
78
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
79
+ padding: 4px;
80
+ margin-top: -6px;
81
+ margin-bottom: 1vh;
82
+ }
83
+
84
+ .py-detect-title-header {
85
+ font-size: 2.1rem;
86
  font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
87
  font-weight: 900;
88
  letter-spacing: 6px;
89
+ color: #38bdf8;
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 2px;
93
+ margin-bottom: 1.5vh;
94
  }
95
 
96
+ .py-detect-title-header .py-letter.p {
 
97
  color: #e3f6ff;
98
  text-shadow: 0 0 6px #38bdf8;
99
  }
100
 
101
+ .py-detect-title-header .py-letter.y {
102
  color: #38bdf8;
103
  text-shadow: 0 0 6px #38bdf8;
104
  }
105
 
106
+ .py-detect-title-header .py-shape {
107
  color: #e3f6ff;
108
  background: #e3f6ff;
109
  text-shadow: 0 0 6px #38bdf8;
110
  box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
111
  border: 2px solid #23272b;
112
+ width: 18px;
113
+ height: 4px;
114
+ display: inline-block;
115
+ margin: 0 8px;
116
+ border-radius: 2px;
117
  }
118
 
119
+ .py-detect-title-header .py-letter.d {
 
 
 
 
 
 
 
 
 
 
 
 
120
  color: #e3f6ff;
121
  text-shadow: 0 0 6px #38bdf8;
122
  }
123
 
124
+ .py-detect-title-header .py-letter.e {
125
  color: #38bdf8;
126
  text-shadow: 0 0 6px #38bdf8;
127
  }
128
 
129
+ .py-detect-title-header .py-letter.t {
130
  color: #e3f6ff;
131
  text-shadow: 0 0 6px #38bdf8;
132
  }
133
 
134
+ .py-detect-title-header .py-letter.e2 {
135
  color: #38bdf8;
136
  text-shadow: 0 0 6px #38bdf8;
137
  }
138
 
139
+ .py-detect-title-header .py-letter.c {
140
  color: #e3f6ff;
141
  text-shadow: 0 0 6px #38bdf8;
142
  }
143
 
144
+ .py-detect-title-header .py-letter.t2 {
145
  color: #38bdf8;
146
  text-shadow: 0 0 6px #38bdf8;
147
  }
148
 
149
+
150
+
151
  .center-instructions {
152
  position: absolute;
153
  top: 50%;
 
207
 
208
  .auth-topright {
209
  position: fixed;
210
+ top: 28px;
211
  right: 48px;
212
  z-index: 200;
213
  display: flex;
 
951
  background: linear-gradient(45deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888);
952
  box-shadow: 0 4px 8px rgba(188, 24, 136, 0.3);
953
  }
954
+
955
+ /* Sleek header auth buttons */
956
+ .auth-topright .auth-btn {
957
+ font-size: 0.95rem;
958
+ font-weight: 600;
959
+ padding: 0.35rem 1.1rem;
960
+ background: rgba(30,41,59,0.85);
961
+ color: #e3f6ff;
962
+ border: 1px solid #38bdf8;
963
+ border-radius: 8px;
964
+ box-shadow: 0 2px 8px #01132944;
965
+ transition: background 0.2s, color 0.2s, box-shadow 0.2s, border-color 0.2s;
966
+ opacity: 0.96;
967
+ }
968
+
969
+ .auth-topright .auth-btn {
970
+ animation: authPulse 2.2s ease-in-out infinite;
971
+ }
972
+
973
+ @keyframes authPulse {
974
+ 0% { box-shadow: 0 0 0px #38bdf844, 0 2px 8px #01132944; }
975
+ 50% { box-shadow: 0 0 12px #38bdf8cc, 0 2px 16px #01132944; }
976
+ 100% { box-shadow: 0 0 0px #38bdf844, 0 2px 8px #01132944; }
977
+ }
978
+
979
+ .auth-topright .auth-btn:hover {
980
+ animation: none;
981
+ background: rgba(56,189,248,0.18);
982
+ color: #bae6fd;
983
+ border-color: #bae6fd;
984
+ box-shadow: 0 4px 24px #38bdf8cc;
985
+ opacity: 1;
986
+ }
src/app/homepage/homepage.component.html CHANGED
@@ -1,24 +1,28 @@
1
- <!-- Logo and Py-Detect Title (kept as your last version) -->
2
- <div class="back-colo">
3
- <div class="logo-title-row">
4
- <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img" />
5
- <div class="py-detect-title">
6
- <span class="py-letter p">P</span>
7
- <span class="py-letter y">Y</span>
8
- <span class="py-shape"></span>
9
- <span class="py-letter d">D</span>
10
- <span class="py-letter e">E</span>
11
- <span class="py-letter t">T</span>
12
- <span class="py-letter e2">E</span>
13
- <span class="py-letter c">C</span>
14
- <span class="py-letter t2">T</span>
 
 
 
 
 
15
  </div>
16
  </div>
17
- </div>
18
 
19
  <!-- Top-right auth buttons (unchanged) -->
20
  <div class="auth-topright">
21
- <button class="auth-btn" (click)="openSignIn()">Sign In</button>
22
  <button class="auth-btn" (click)="openSignUp()">Sign Up</button>
23
  </div>
24
 
@@ -100,7 +104,7 @@
100
  <section class="how-to-use">
101
  <h2 class="section-title">How to Use Py-Detect?</h2>
102
  <p class="section-subtitle">
103
- Py-Detect supports both <strong>Admins</strong> (e.g., Inspectors) and <strong>Users</strong> (e.g., Constables)
104
  to manage cases, conduct investigations, and analyse suspects with AI.
105
  </p>
106
 
@@ -110,7 +114,7 @@
110
  <i class="fas fa-user-tie icon"></i>
111
  <h3>1. Admin Creates Case</h3>
112
  <p>
113
- The Admin (Inspector) logs in, enters case details, and assigns
114
  the investigation to specific users.
115
  </p>
116
  </div>
@@ -120,7 +124,7 @@
120
  <i class="fas fa-user-check icon"></i>
121
  <h3>2. User Views Assignment</h3>
122
  <p>
123
- Assigned Users (Constables) log in to see their cases,
124
  review case details, and prepare for investigation.
125
  </p>
126
  </div>
@@ -130,7 +134,7 @@
130
  <i class="fas fa-video icon"></i>
131
  <h3>3. Investigation Recording</h3>
132
  <p>
133
- During interrogation, the system generates AI-based questions.
134
  Video and audio responses are recorded in real time.
135
  </p>
136
  </div>
@@ -140,7 +144,7 @@
140
  <i class="fas fa-brain icon"></i>
141
  <h3>4. AI Analysis</h3>
142
  <p>
143
- Py-Detect analyses tone, body language, and facial expressions
144
  to produce a deception score and behavioural insights.
145
  </p>
146
  </div>
@@ -150,7 +154,7 @@
150
  <i class="fas fa-file-alt icon"></i>
151
  <h3>5. Report & Decision</h3>
152
  <p>
153
- A final AI-powered prediction report is generated,
154
  helping investigators make evidence-based decisions quickly.
155
  </p>
156
  </div>
 
1
+ <!-- Modern UI header with logo and PyDetect title -->
2
+ <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
+ </div>
20
  </div>
21
  </div>
 
22
 
23
  <!-- Top-right auth buttons (unchanged) -->
24
  <div class="auth-topright">
25
+ <button class="auth-btn" (click)="openSignIn()">Sign In</button>
26
  <button class="auth-btn" (click)="openSignUp()">Sign Up</button>
27
  </div>
28
 
 
104
  <section class="how-to-use">
105
  <h2 class="section-title">How to Use Py-Detect?</h2>
106
  <p class="section-subtitle">
107
+ Py-Detect supports both <strong>Admins</strong> (e.g., Inspectors) and <strong>Users</strong> (e.g., Constables)
108
  to manage cases, conduct investigations, and analyse suspects with AI.
109
  </p>
110
 
 
114
  <i class="fas fa-user-tie icon"></i>
115
  <h3>1. Admin Creates Case</h3>
116
  <p>
117
+ The Admin (Inspector) logs in, enters case details, and assigns
118
  the investigation to specific users.
119
  </p>
120
  </div>
 
124
  <i class="fas fa-user-check icon"></i>
125
  <h3>2. User Views Assignment</h3>
126
  <p>
127
+ Assigned Users (Constables) log in to see their cases,
128
  review case details, and prepare for investigation.
129
  </p>
130
  </div>
 
134
  <i class="fas fa-video icon"></i>
135
  <h3>3. Investigation Recording</h3>
136
  <p>
137
+ During interrogation, the system generates AI-based questions.
138
  Video and audio responses are recorded in real time.
139
  </p>
140
  </div>
 
144
  <i class="fas fa-brain icon"></i>
145
  <h3>4. AI Analysis</h3>
146
  <p>
147
+ Py-Detect analyses tone, body language, and facial expressions
148
  to produce a deception score and behavioural insights.
149
  </p>
150
  </div>
 
154
  <i class="fas fa-file-alt icon"></i>
155
  <h3>5. Report & Decision</h3>
156
  <p>
157
+ A final AI-powered prediction report is generated,
158
  helping investigators make evidence-based decisions quickly.
159
  </p>
160
  </div>
src/app/homepage/homepage.component.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { Component, Renderer2, OnInit, OnDestroy } from '@angular/core';
2
  import { Router } from '@angular/router';
3
  import { CommonModule } from '@angular/common';
 
4
  import { SignInComponent } from './sign-in/sign-in.component';
5
  import { SignUpComponent } from './sign-up/sign-up.component';
6
 
@@ -9,7 +10,7 @@ import { SignUpComponent } from './sign-up/sign-up.component';
9
  standalone: true,
10
  templateUrl: './homepage.component.html',
11
  styleUrls: ['./homepage.component.css'],
12
- imports: [CommonModule, SignInComponent, SignUpComponent]
13
  })
14
  export class HomepageComponent implements OnInit, OnDestroy {
15
  // Keep your existing auth modal flags (reverted)
@@ -56,4 +57,8 @@ export class HomepageComponent implements OnInit, OnDestroy {
56
  toggleInfo(info: string) {
57
  this.selectedInfo = (this.selectedInfo === info) ? null : info;
58
  }
 
 
 
 
59
  }
 
1
  import { Component, Renderer2, OnInit, OnDestroy } from '@angular/core';
2
  import { Router } from '@angular/router';
3
  import { CommonModule } from '@angular/common';
4
+ import { RouterModule } from '@angular/router';
5
  import { SignInComponent } from './sign-in/sign-in.component';
6
  import { SignUpComponent } from './sign-up/sign-up.component';
7
 
 
10
  standalone: true,
11
  templateUrl: './homepage.component.html',
12
  styleUrls: ['./homepage.component.css'],
13
+ imports: [CommonModule, RouterModule, SignInComponent, SignUpComponent]
14
  })
15
  export class HomepageComponent implements OnInit, OnDestroy {
16
  // Keep your existing auth modal flags (reverted)
 
57
  toggleInfo(info: string) {
58
  this.selectedInfo = (this.selectedInfo === info) ? null : info;
59
  }
60
+
61
+ public navigateHome(): void {
62
+ this.router.navigate(['/']);
63
+ }
64
  }
src/app/infopage/infopage.component.css CHANGED
@@ -765,7 +765,7 @@ html {
765
  /* ===== FLOATING NAVIGATION BUTTON ===== */
766
  .modern-floating-nav-btn {
767
  position: fixed;
768
- bottom: 32px;
769
  right: 32px;
770
  z-index: 2000;
771
  display: flex;
@@ -1851,7 +1851,7 @@ html {
1851
  /* ===== FLOATING NAVIGATION BUTTON ===== */
1852
  .modern-floating-nav-btn {
1853
  position: fixed;
1854
- bottom: 32px;
1855
  right: 32px;
1856
  z-index: 2000;
1857
  display: flex;
@@ -2151,3 +2151,15 @@ html {
2151
  margin-bottom: 8px;
2152
  }
2153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  /* ===== FLOATING NAVIGATION BUTTON ===== */
766
  .modern-floating-nav-btn {
767
  position: fixed;
768
+ bottom: 95px;
769
  right: 32px;
770
  z-index: 2000;
771
  display: flex;
 
1851
  /* ===== FLOATING NAVIGATION BUTTON ===== */
1852
  .modern-floating-nav-btn {
1853
  position: fixed;
1854
+ bottom: 95px;
1855
  right: 32px;
1856
  z-index: 2000;
1857
  display: flex;
 
2151
  margin-bottom: 8px;
2152
  }
2153
 
2154
+
2155
+ /* Footer */
2156
+ footer {
2157
+ background: linear-gradient(to right, #011022, #01030a);
2158
+ color: #fff;
2159
+ text-align: center;
2160
+ padding: 10px 0px;
2161
+ position: fixed;
2162
+ bottom: 0;
2163
+ left: 0;
2164
+ width: 100%;
2165
+ }
src/app/infopage/infopage.component.html CHANGED
@@ -29,6 +29,13 @@
29
  <i class="fas fa-save"></i>
30
  <span>{{ autoSaveStatus }}</span>
31
  </div>
 
 
 
 
 
 
 
32
  </div>
33
  </div>
34
  </div>
@@ -58,13 +65,13 @@
58
  </div>
59
 
60
  <!-- AI-Enhanced Job Category Style Subgroup Navigation with Section-Specific Styling -->
61
- <div class="subgroup-pills"
62
  [class.crime-section]="currentSection === 'crime'"
63
  [class.suspect-section]="currentSection === 'suspect'"
64
  [class.notes-section]="currentSection === 'notes'"
65
  [@fadeIn]>
66
  <div class="neural-mesh"></div>
67
- <div class="pill ai-subgroup-pill"
68
  *ngFor="let key of getSubgroups(); let i = index"
69
  [class.active]="key === currentSubgroup"
70
  [class.completed]="isSubgroupCompleted(key)"
@@ -83,7 +90,7 @@
83
 
84
  <!-- Job Cards Style Layout - Universal Single Card Layout for All Pages -->
85
  <div class="investigation-container">
86
-
87
  <!-- Universal Single Investigation Card -->
88
  <div class="form-card primary-card" [@cardSlide] #formCard1>
89
  <div class="card-header compact-card-header">
@@ -159,6 +166,50 @@
159
  [attr.aria-label]="'Info for ' + field">
160
  <i class="fas fa-info"></i>
161
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  </label>
163
 
164
  <!-- Dynamic Field Rendering -->
@@ -235,7 +286,7 @@
235
 
236
  <ng-template #textInput>
237
  <!-- Description fields as textarea -->
238
- <textarea *ngIf="field.toLowerCase().includes('description'); else regularInput"
239
  class="field-input"
240
  [class.compact]="isCompactField(field)"
241
  [(ngModel)]="formData[field]"
@@ -243,7 +294,10 @@
243
  [placeholder]="getFieldPlaceholder(field)"
244
  [maxlength]="getMaxLength(field)"
245
  rows="3"></textarea>
246
-
 
 
 
247
  <ng-template #regularInput>
248
  <input [type]="getInputType(field)"
249
  class="field-input"
@@ -274,10 +328,12 @@
274
 
275
  <!-- Floating Modern Navigation Button (fixed, no scroll) -->
276
  <div class="modern-floating-nav-btn">
277
- <button *ngIf="!isLastSubgroup()" type="button" class="modern-round-btn next-btn-animated" (click)="nextSubgroup()" [disabled]="!canNextSubgroup()" title="Next">
 
278
  <i class="fas fa-arrow-right"></i>
279
  </button>
280
- <button *ngIf="isLastSubgroup()" type="button" class="modern-round-btn submit-btn-animated" (click)="submitCurrentSection()" title="Submit">
 
281
  <i class="fas fa-paper-plane"></i>
282
  </button>
283
  </div>
@@ -294,3 +350,9 @@
294
  </div>
295
  </div>
296
 
 
 
 
 
 
 
 
29
  <i class="fas fa-save"></i>
30
  <span>{{ autoSaveStatus }}</span>
31
  </div>
32
+ <!-- View Records Button -->
33
+ <button class="view-records-btn" style="margin-left:16px;vertical-align:middle;"
34
+ type="button"
35
+ (click)="goToRecords()">
36
+ <i class="fas fa-folder-open"></i>
37
+ <span>View Records</span>
38
+ </button>
39
  </div>
40
  </div>
41
  </div>
 
65
  </div>
66
 
67
  <!-- AI-Enhanced Job Category Style Subgroup Navigation with Section-Specific Styling -->
68
+ <div class="subgroup-pills"
69
  [class.crime-section]="currentSection === 'crime'"
70
  [class.suspect-section]="currentSection === 'suspect'"
71
  [class.notes-section]="currentSection === 'notes'"
72
  [@fadeIn]>
73
  <div class="neural-mesh"></div>
74
+ <div class="pill ai-subgroup-pill"
75
  *ngFor="let key of getSubgroups(); let i = index"
76
  [class.active]="key === currentSubgroup"
77
  [class.completed]="isSubgroupCompleted(key)"
 
90
 
91
  <!-- Job Cards Style Layout - Universal Single Card Layout for All Pages -->
92
  <div class="investigation-container">
93
+
94
  <!-- Universal Single Investigation Card -->
95
  <div class="form-card primary-card" [@cardSlide] #formCard1>
96
  <div class="card-header compact-card-header">
 
166
  [attr.aria-label]="'Info for ' + field">
167
  <i class="fas fa-info"></i>
168
  </button>
169
+ <!-- Microphone icon for Remark field -->
170
+ <ng-container *ngIf="field === 'Remark'">
171
+ <span style="display:inline-flex;align-items:center;position:relative;">
172
+ <button type="button"
173
+ (mouseenter)="showMicPopup = true"
174
+ (mouseleave)="showMicPopup = false"
175
+ (click)="toggleRecording()"
176
+ style="background:none;border:none;outline:none;cursor:pointer;padding:0;margin-left:6px;">
177
+ <i class="fas fa-microphone"
178
+ [ngStyle]="{
179
+ color: isRecording ? '#e74c3c' : '#3498db',
180
+ animation: 'micPulse 1s infinite',
181
+ fontSize: '1.2em',
182
+ transition: 'color 0.2s'
183
+ }"
184
+ style="transition:color 0.2s;">
185
+ </i>
186
+ </button>
187
+ <!-- Popup on hover -->
188
+ <div *ngIf="showMicPopup"
189
+ style="position:absolute;top:120%;left:0;z-index:10;background:#fff;border:1px solid #ccc;padding:8px 12px;border-radius:6px;box-shadow:0 2px 8px rgba(0,0,0,0.12);white-space:nowrap;min-width:160px;">
190
+ Click to record your remark.<br>Release to stop.<br>Recording is {{ isRecording ? 'ON' : 'OFF' }}.
191
+ </div>
192
+ <!-- Inline animation keyframes -->
193
+ <style>
194
+ @keyframes micPulse {
195
+ 0% {
196
+ transform: scale(1);
197
+ filter: drop-shadow(0 0 0 #e74c3c);
198
+ }
199
+
200
+ 50% {
201
+ transform: scale(1.2);
202
+ filter: drop-shadow(0 0 8px #e74c3c);
203
+ }
204
+
205
+ 100% {
206
+ transform: scale(1);
207
+ filter: drop-shadow(0 0 0 #e74c3c);
208
+ }
209
+ }
210
+ </style>
211
+ </span>
212
+ </ng-container>
213
  </label>
214
 
215
  <!-- Dynamic Field Rendering -->
 
286
 
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]"
 
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;">
299
+ Recording... Speak now.
300
+ </div>
301
  <ng-template #regularInput>
302
  <input [type]="getInputType(field)"
303
  class="field-input"
 
328
 
329
  <!-- Floating Modern Navigation Button (fixed, no scroll) -->
330
  <div class="modern-floating-nav-btn">
331
+ <!-- Next button appears on all pages except the last Remark page under notes -->
332
+ <button *ngIf="!(isLastSubgroup() && currentSection === 'notes' && currentSubgroup === 'Remark')" type="button" class="modern-round-btn next-btn-animated" (click)="nextSubgroup()" [disabled]="!canNextSubgroup()" title="Next">
333
  <i class="fas fa-arrow-right"></i>
334
  </button>
335
+ <!-- Submit button only on last Remark page under notes -->
336
+ <button *ngIf="isLastSubgroup() && currentSection === 'notes' && currentSubgroup === 'Remark'" type="button" class="modern-round-btn submit-btn-animated" (click)="submitCurrentSection()" title="Submit">
337
  <i class="fas fa-paper-plane"></i>
338
  </button>
339
  </div>
 
350
  </div>
351
  </div>
352
 
353
+ <!-- Footer from provided design -->
354
+ <footer>
355
+ <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
356
+ </footer>
357
+
358
+
src/app/infopage/infopage.component.ts CHANGED
@@ -2,7 +2,7 @@ import { Component, HostListener, ElementRef, ViewChild, AfterViewInit, OnInit,
2
  import { trigger, state, style, transition, animate } from '@angular/animations';
3
  import { Subject, debounceTime, takeUntil } from 'rxjs';
4
  import { Router } from '@angular/router';
5
- import { CaseStoreService } from '../case-store.service';
6
 
7
  @Component({
8
  selector: 'app-infopage',
@@ -52,6 +52,8 @@ import { CaseStoreService } from '../case-store.service';
52
  export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
53
  showRemarkModal: boolean = false;
54
  showSubmitPopup: boolean = false;
 
 
55
  constructor(private router: Router, private caseStore: CaseStoreService) {}
56
  // Core state
57
  currentSection: 'crime' | 'suspect' | 'notes' = 'crime';
@@ -886,7 +888,15 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
886
  getOptions(field: string): string[] | undefined {
887
  if (field === 'Country') return this.countries;
888
  if (field === 'State') return (this.selectedValues['Country'] === 'India' || !this.selectedValues['Country']) ? this.indiaStates : [];
889
- if (field === 'District') return this.selectedValues['State'] === 'Tamil Nadu' ? this.tamilNaduDistricts : [];
 
 
 
 
 
 
 
 
890
  return this.selectOptions[field];
891
  }
892
 
@@ -990,9 +1000,18 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
990
  }
991
 
992
  getNextSubgroup(): string {
993
- const list = this.getSubgroups();
994
- const currentIndex = list.indexOf(this.currentSubgroup);
995
- return currentIndex < list.length - 1 ? list[currentIndex + 1] : '';
 
 
 
 
 
 
 
 
 
996
  }
997
 
998
  isLastSubgroup(): boolean {
@@ -1000,30 +1019,50 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
1000
  return list.indexOf(this.currentSubgroup) === list.length - 1;
1001
  }
1002
 
1003
- isFirstSubgroup(): boolean {
1004
- return this.getSubgroups().indexOf(this.currentSubgroup) === 0;
1005
- }
1006
-
1007
- canPrevSubgroup(): boolean {
1008
- return !this.isFirstSubgroup();
1009
- }
1010
-
1011
  canNextSubgroup(): boolean {
1012
- return !this.isLastSubgroup();
 
 
 
 
1013
  }
1014
 
1015
- prevSubgroup(): void {
1016
- if (!this.canPrevSubgroup()) return;
1017
- const list = this.getSubgroups();
1018
- const i = list.indexOf(this.currentSubgroup);
1019
- this.setSubgroup(list[i - 1]);
 
 
 
 
 
 
 
 
 
 
 
 
1020
  }
1021
 
1022
- nextSubgroup(): void {
1023
- if (!this.canNextSubgroup()) return;
1024
- const list = this.getSubgroups();
1025
- const i = list.indexOf(this.currentSubgroup);
1026
- this.setSubgroup(list[i + 1]);
 
 
 
 
 
 
 
 
 
 
 
 
1027
  }
1028
 
1029
  submitCurrentSection(): void {
@@ -1039,7 +1078,7 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
1039
 
1040
  this.performAutoSave();
1041
 
1042
- // Map flat formData to nested structure expected by addFromInfoForm
1043
  const crime = {
1044
  caseId: this.formData['Case ID'] || '',
1045
  dateTime: this.formData['Date & Time (Entry)'] || '',
@@ -1048,6 +1087,7 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
1048
  victimName: this.formData['Victim Name'] || '',
1049
  caseCategory: this.formData['Case Category'] || '',
1050
  reportedBy: this.formData['Reported By'] || '',
 
1051
  'FIR / Ref #': this.formData['FIR / Ref #'] || '',
1052
  'Occurred From': this.formData['Occurred From'] || '',
1053
  'Occurred To': this.formData['Occurred To'] || '',
@@ -1064,7 +1104,8 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
1064
  const notes = {
1065
  status: this.formData['Case Status'] || this.formData['Status'] || 'Open',
1066
  officerInCharge: this.formData['Investigating Officer'] || '',
1067
- initialFindings: this.formData['Initial Findings'] || ''
 
1068
  };
1069
  const legal = {
1070
  witnessStatements: this.formData['Witness Statements'] || '',
@@ -1072,7 +1113,7 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
1072
  evidence: this.uploadedFiles['Evidence Files'] || []
1073
  };
1074
 
1075
- this.caseStore.addFromInfoForm({ crime, suspect, notes, legal });
1076
  // Show popup first
1077
  this.showSubmitPopup = true;
1078
  }
@@ -1142,4 +1183,15 @@ export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
1142
  trackByField(index: number, field: string): string {
1143
  return field;
1144
  }
 
 
 
 
 
 
 
 
 
 
 
1145
  }
 
2
  import { trigger, state, style, transition, animate } from '@angular/animations';
3
  import { Subject, debounceTime, takeUntil } from 'rxjs';
4
  import { Router } from '@angular/router';
5
+ import { CaseStoreService } from '../shared/case-store.service';
6
 
7
  @Component({
8
  selector: 'app-infopage',
 
52
  export class InfopageComponent implements OnInit, AfterViewInit, OnDestroy {
53
  showRemarkModal: boolean = false;
54
  showSubmitPopup: boolean = false;
55
+ showMicPopup: boolean = false;
56
+ isRecording: boolean = false;
57
  constructor(private router: Router, private caseStore: CaseStoreService) {}
58
  // Core state
59
  currentSection: 'crime' | 'suspect' | 'notes' = 'crime';
 
888
  getOptions(field: string): string[] | undefined {
889
  if (field === 'Country') return this.countries;
890
  if (field === 'State') return (this.selectedValues['Country'] === 'India' || !this.selectedValues['Country']) ? this.indiaStates : [];
891
+ if (field === 'District') {
892
+ if (this.selectedValues['State'] === 'Tamil Nadu') {
893
+ return this.tamilNaduDistricts;
894
+ } else if (this.selectedValues['State']) {
895
+ return [];
896
+ } else {
897
+ return [];
898
+ }
899
+ }
900
  return this.selectOptions[field];
901
  }
902
 
 
1000
  }
1001
 
1002
  getNextSubgroup(): string {
1003
+ const subgroups = this.getSubgroups();
1004
+ const currentIndex = subgroups.indexOf(this.currentSubgroup);
1005
+ if (currentIndex < subgroups.length - 1) {
1006
+ return subgroups[currentIndex + 1];
1007
+ }
1008
+ // If last subgroup, return first subgroup of next section if exists
1009
+ const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1010
+ if (sectionIndex < this.sectionKeys.length - 1) {
1011
+ const nextSection = this.sectionKeys[sectionIndex + 1];
1012
+ return Object.keys(this.sections[nextSection].subgroups)[0];
1013
+ }
1014
+ return '';
1015
  }
1016
 
1017
  isLastSubgroup(): boolean {
 
1019
  return list.indexOf(this.currentSubgroup) === list.length - 1;
1020
  }
1021
 
 
 
 
 
 
 
 
 
1022
  canNextSubgroup(): boolean {
1023
+ // Enable Next on last subgroup if not notes section
1024
+ if (this.isLastSubgroup()) {
1025
+ return !(this.currentSection === 'notes' && this.currentSubgroup === 'Remark');
1026
+ }
1027
+ return true;
1028
  }
1029
 
1030
+ nextSubgroup(): void {
1031
+ const subgroups = this.getSubgroups();
1032
+ const currentIndex = subgroups.indexOf(this.currentSubgroup);
1033
+ // If not last subgroup, go to next subgroup
1034
+ if (currentIndex < subgroups.length - 1) {
1035
+ this.setSubgroup(subgroups[currentIndex + 1]);
1036
+ return;
1037
+ }
1038
+ // If last subgroup, go to first subgroup of next section (if exists)
1039
+ const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1040
+ if (sectionIndex < this.sectionKeys.length - 1) {
1041
+ const nextSection = this.sectionKeys[sectionIndex + 1];
1042
+ this.currentSection = nextSection;
1043
+ this.currentSubgroup = Object.keys(this.sections[nextSection].subgroups)[0];
1044
+ this.showHelpFor = null;
1045
+ this.triggerAutoSave();
1046
+ }
1047
  }
1048
 
1049
+ prevSubgroup(): void {
1050
+ const subgroups = this.getSubgroups();
1051
+ const currentIndex = subgroups.indexOf(this.currentSubgroup);
1052
+ if (currentIndex > 0) {
1053
+ this.setSubgroup(subgroups[currentIndex - 1]);
1054
+ return;
1055
+ }
1056
+ // If first subgroup, go to last subgroup of previous section (if exists)
1057
+ const sectionIndex = this.sectionKeys.indexOf(this.currentSection);
1058
+ if (sectionIndex > 0) {
1059
+ const prevSection = this.sectionKeys[sectionIndex - 1];
1060
+ const prevSubgroups = Object.keys(this.sections[prevSection].subgroups);
1061
+ this.currentSection = prevSection;
1062
+ this.currentSubgroup = prevSubgroups[prevSubgroups.length - 1];
1063
+ this.showHelpFor = null;
1064
+ this.triggerAutoSave();
1065
+ }
1066
  }
1067
 
1068
  submitCurrentSection(): void {
 
1078
 
1079
  this.performAutoSave();
1080
 
1081
+ // Map flat formData to nested structure expected by addOrUpdateFromInfoForm
1082
  const crime = {
1083
  caseId: this.formData['Case ID'] || '',
1084
  dateTime: this.formData['Date & Time (Entry)'] || '',
 
1087
  victimName: this.formData['Victim Name'] || '',
1088
  caseCategory: this.formData['Case Category'] || '',
1089
  reportedBy: this.formData['Reported By'] || '',
1090
+ briefDescription: this.formData['Brief Description'] || '',
1091
  'FIR / Ref #': this.formData['FIR / Ref #'] || '',
1092
  'Occurred From': this.formData['Occurred From'] || '',
1093
  'Occurred To': this.formData['Occurred To'] || '',
 
1104
  const notes = {
1105
  status: this.formData['Case Status'] || this.formData['Status'] || 'Open',
1106
  officerInCharge: this.formData['Investigating Officer'] || '',
1107
+ initialFindings: this.formData['Initial Findings'] || '',
1108
+ verifiedBy: this.formData['Verified By'] || ''
1109
  };
1110
  const legal = {
1111
  witnessStatements: this.formData['Witness Statements'] || '',
 
1113
  evidence: this.uploadedFiles['Evidence Files'] || []
1114
  };
1115
 
1116
+ this.caseStore.addOrUpdateFromInfoForm({ crime, suspect, notes, legal });
1117
  // Show popup first
1118
  this.showSubmitPopup = true;
1119
  }
 
1183
  trackByField(index: number, field: string): string {
1184
  return field;
1185
  }
1186
+
1187
+ toggleRecording() {
1188
+ this.isRecording = !this.isRecording;
1189
+ // Add actual recording logic here if needed
1190
+ // For now, just toggles the state
1191
+ }
1192
+
1193
+ goToRecords(): void {
1194
+ this.router.navigate(['/record']);
1195
+ }
1196
+
1197
  }
src/app/py-detect/py-detect.component.css CHANGED
@@ -240,7 +240,7 @@ body, html {
240
  align-items: flex-start;
241
  width: 100%;
242
  height: 80vh;
243
- gap: 32px;
244
  }
245
 
246
  .question-section {
@@ -256,14 +256,18 @@ body, html {
256
  }
257
 
258
  .big-question {
259
- font-size: 3.2rem;
260
- font-weight: 900;
261
- color: #38bdf8;
 
 
 
 
262
  margin-bottom: 24px;
263
- text-align: left;
264
  min-height: 100px;
265
  max-width: 700px;
266
  word-break: break-word;
 
267
  }
268
 
269
  .big-question-animated {
@@ -275,6 +279,11 @@ body, html {
275
  100% { text-shadow: 0 0 32px #38bdf8, 0 2px 24px #22222288; }
276
  }
277
 
 
 
 
 
 
278
  .video-section {
279
  flex: 1;
280
  display: flex;
@@ -323,6 +332,30 @@ body, html {
323
  animation: buttonPulse 1.2s infinite alternate;
324
  }
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  .btn-submit {
327
  position: fixed;
328
  right: 32px;
@@ -370,25 +403,522 @@ body, html {
370
  z-index: 1000;
371
  }
372
 
373
- /* Reduce Pykara logo size in py-detect topbar */
374
- header .pykara-logo-small {
375
- width: 48px;
376
- height: 48px;
377
- object-fit: contain;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  }
379
 
380
- /* Pykara logo size for consistency across homepage, py-detect, and infopage */
381
- .pykara-logo-consistent {
382
- width: 120px;
383
- height: 120px;
384
- object-fit: contain;
 
 
 
 
385
  }
386
 
387
- header .text-lg.font-semibold.tracking-wide.text-white {
388
- font-size: 2.8rem;
 
389
  font-weight: 900;
390
- letter-spacing: 8px;
391
- color: #e3f6ff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  font-family: 'Montserrat', 'Poppins', 'Arial Black', Arial, sans-serif;
393
- text-shadow: 0 0 16px #1e293b, 0 0 8px #38bdf8, 0 2px 8px #22222244;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  }
 
240
  align-items: flex-start;
241
  width: 100%;
242
  height: 80vh;
243
+ gap: 48px; /* Increased gap for more separation */
244
  }
245
 
246
  .question-section {
 
256
  }
257
 
258
  .big-question {
259
+ background: #f0f4ff;
260
+ border-radius: 18px;
261
+ box-shadow: 0 2px 16px #2563eb22;
262
+ padding: 32px 28px;
263
+ color: #23272b;
264
+ font-size: 2.2rem;
265
+ font-weight: 700;
266
  margin-bottom: 24px;
 
267
  min-height: 100px;
268
  max-width: 700px;
269
  word-break: break-word;
270
+ text-align: left;
271
  }
272
 
273
  .big-question-animated {
 
279
  100% { text-shadow: 0 0 32px #38bdf8, 0 2px 24px #22222288; }
280
  }
281
 
282
+ @keyframes questionFadeIn {
283
+ from { opacity: 0; transform: translateY(24px); }
284
+ to { opacity: 1; transform: translateY(0); }
285
+ }
286
+
287
  .video-section {
288
  flex: 1;
289
  display: flex;
 
332
  animation: buttonPulse 1.2s infinite alternate;
333
  }
334
 
335
+ .btn-start {
336
+ background: linear-gradient(90deg, #2563eb 0%, #6366f1 100%);
337
+ color: #fff;
338
+ font-size: 1.25rem;
339
+ font-weight: 700;
340
+ padding: 0.7rem 2.2rem;
341
+ border-radius: 999px;
342
+ box-shadow: 0 4px 24px #6366f188, 0 2px 8px #2563eb44;
343
+ transition: background 0.2s, color 0.2s, transform 0.2s, box-shadow 0.2s;
344
+ border: none;
345
+ outline: none;
346
+ letter-spacing: 2px;
347
+ position: relative;
348
+ overflow: hidden;
349
+ animation: buttonPulse 1.2s infinite alternate;
350
+ }
351
+
352
+ .btn-start:hover {
353
+ background: linear-gradient(90deg, #6366f1 0%, #2563eb 100%);
354
+ color: #e0e7ff;
355
+ transform: scale(1.05);
356
+ box-shadow: 0 6px 32px #6366f1cc;
357
+ }
358
+
359
  .btn-submit {
360
  position: fixed;
361
  right: 32px;
 
403
  z-index: 1000;
404
  }
405
 
406
+ /* Modern UI header styles from infopage */
407
+ .site-header {
408
+ background: #011329;
409
+ box-shadow: 0 2px 12px #38bdf844;
410
+ margin-bottom: 0;
411
+ position: relative;
412
+ z-index: 10;
413
+ padding-bottom: 0;
414
+ }
415
+
416
+ .header-inner {
417
+ display: flex;
418
+ align-items: center;
419
+ justify-content: flex-start;
420
+ padding: 18px 32px 0 32px;
421
+ position: relative;
422
+ }
423
+
424
+ .logo-cluster {
425
+ display: flex;
426
+ align-items: center;
427
+ gap: 18px;
428
  }
429
 
430
+ .logo-img-header {
431
+ width: 54px;
432
+ height: 54px;
433
+ border-radius: 50%;
434
+ background: #fff;
435
+ box-shadow: 0 2px 8px rgba(0,0,0,0.18);
436
+ padding: 4px;
437
+ margin-top: -6px;
438
+ margin-bottom: 1vh;
439
  }
440
 
441
+ .py-detect-title-header {
442
+ font-size: 2.1rem;
443
+ font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
444
  font-weight: 900;
445
+ letter-spacing: 6px;
446
+ color: #38bdf8;
447
+ display: flex;
448
+ align-items: center;
449
+ gap: 2px;
450
+ margin-bottom: 1.5vh;
451
+ }
452
+
453
+ .py-detect-title-header .py-letter.p {
454
+ color: #e3f6ff;
455
+ text-shadow: 0 0 6px #38bdf8;
456
+ }
457
+
458
+ .py-detect-title-header .py-letter.y {
459
+ color: #38bdf8;
460
+ text-shadow: 0 0 6px #38bdf8;
461
+ }
462
+
463
+ .py-detect-title-header .py-shape {
464
+ color: #e3f6ff;
465
+ background: #e3f6ff;
466
+ text-shadow: 0 0 6px #38bdf8;
467
+ box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
468
+ border: 2px solid #23272b;
469
+ width: 18px;
470
+ height: 4px;
471
+ display: inline-block;
472
+ margin: 0 8px;
473
+ border-radius: 2px;
474
+ }
475
+
476
+ .py-detect-title-header .py-letter.d {
477
+ color: #e3f6ff;
478
+ text-shadow: 0 0 6px #38bdf8;
479
+ }
480
+
481
+ .py-detect-title-header .py-letter.e {
482
+ color: #38bdf8;
483
+ text-shadow: 0 0 6px #38bdf8;
484
+ }
485
+
486
+ .py-detect-title-header .py-letter.t {
487
+ color: #e3f6ff;
488
+ text-shadow: 0 0 6px #38bdf8;
489
+ }
490
+
491
+ .py-detect-title-header .py-letter.e2 {
492
+ color: #38bdf8;
493
+ text-shadow: 0 0 6px #38bdf8;
494
+ }
495
+
496
+ .py-detect-title-header .py-letter.c {
497
+ color: #e3f6ff;
498
+ text-shadow: 0 0 6px #38bdf8;
499
+ }
500
+
501
+ .py-detect-title-header .py-letter.t2 {
502
+ color: #38bdf8;
503
+ text-shadow: 0 0 6px #38bdf8;
504
+ }
505
+
506
+ /* Footer */
507
+ footer {
508
+ background: linear-gradient(to right, #011022, #01030a);
509
+ color: #fff;
510
+ text-align: center;
511
+ padding: 10px 0px;
512
+ position: fixed;
513
+ bottom: 0;
514
+ left: 0;
515
+ width: 100%;
516
+ }
517
+
518
+
519
+ /* Left Panel */
520
+ .left-panel {
521
+ flex: 1;
522
+ display: flex;
523
+ flex-direction: column;
524
+ align-items: flex-start;
525
+ padding: 32px 24px 32px 32px;
526
+ min-width: 420px;
527
+ max-width: 700px;
528
+ }
529
+ .case-meta {
530
+ margin-bottom: 24px;
531
+ font-size: 1.1rem;
532
+ color: #6366f1;
533
+ }
534
+ .case-id-value {
535
+ color: #fbbf24;
536
+ font-weight: bold;
537
+ }
538
+ .officer-value {
539
+ color: #38bdf8;
540
+ font-weight: bold;
541
+ }
542
+ .progress-label {
543
+ color: #38bdf8;
544
+ }
545
+ .progress-value {
546
+ color: #fff;
547
+ background: #2563eb;
548
+ border-radius: 6px;
549
+ padding: 2px 8px;
550
+ font-weight: 700;
551
+ }
552
+
553
+ /* Question Card */
554
+ .question-card {
555
+ background: #f0f4ff;
556
+ border-radius: 18px;
557
+ box-shadow: 0 2px 16px #2563eb22;
558
+ padding: 32px 28px;
559
+ margin-bottom: 18px;
560
+ min-height: 120px;
561
+ width: 100%;
562
+ position: relative;
563
+ display: flex;
564
+ flex-direction: column;
565
+ align-items: flex-start;
566
+ }
567
+ .question-typewriter {
568
+ font-size: 2.1rem;
569
+ font-weight: 700;
570
+ color: #23272b;
571
+ min-height: 48px;
572
+ width: 100%;
573
+ animation: typewriter 2.2s steps(40, end) 1;
574
+ white-space: pre-line;
575
+ overflow: hidden;
576
+ border-right: 2px solid #2563eb;
577
+ }
578
+ @keyframes typewriter {
579
+ from { width: 0; }
580
+ to { width: 100%; }
581
+ }
582
+
583
+ /* Audio Status & Waveform */
584
+ .audio-status {
585
+ margin-top: 18px;
586
+ width: 100%;
587
+ display: flex;
588
+ align-items: center;
589
+ gap: 18px;
590
+ }
591
+ .recording-text {
592
+ color: #38bdf8;
593
+ font-weight: 700;
594
+ font-size: 1.1rem;
595
+ letter-spacing: 1px;
596
+ animation: pulseRecording 1.2s infinite alternate;
597
+ }
598
+ .processing-text {
599
+ color: #fbbf24;
600
+ font-weight: 700;
601
+ font-size: 1.1rem;
602
+ letter-spacing: 1px;
603
+ animation: pulseProcessing 1.2s infinite alternate;
604
+ }
605
+ @keyframes pulseRecording {
606
+ 0% { opacity: 0.7; }
607
+ 100% { opacity: 1; }
608
+ }
609
+ @keyframes pulseProcessing {
610
+ 0% { opacity: 0.7; }
611
+ 100% { opacity: 1; }
612
+ }
613
+ .waveform-animation {
614
+ width: 48px;
615
+ height: 24px;
616
+ display: inline-block;
617
+ background: none;
618
+ position: relative;
619
+ }
620
+ .waveform-animation::before {
621
+ content: '';
622
+ display: block;
623
+ width: 100%;
624
+ height: 100%;
625
+ background: url('data:image/svg+xml;utf8,<svg width="48" height="24" viewBox="0 0 48 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="10" width="4" height="4" rx="2" fill="%2338bdf8"><animate attributeName="height" values="4;16;4" dur="1s" repeatCount="indefinite"/></rect><rect x="10" y="6" width="4" height="12" rx="2" fill="%2338bdf8"><animate attributeName="height" values="12;4;12" dur="1s" repeatCount="indefinite"/></rect><rect x="18" y="10" width="4" height="4" rx="2" fill="%2338bdf8"><animate attributeName="height" values="4;16;4" dur="1s" repeatCount="indefinite"/></rect><rect x="26" y="6" width="4" height="12" rx="2" fill="%2338bdf8"><animate attributeName="height" values="12;4;12" dur="1s" repeatCount="indefinite"/></rect><rect x="34" y="10" width="4" height="4" rx="2" fill="%2338bdf8"><animate attributeName="height" values="4;16;4" dur="1s" repeatCount="indefinite"/></rect><rect x="42" y="6" width="4" height="12" rx="2" fill="%2338bdf8"><animate attributeName="height" values="12;4;12" dur="1s" repeatCount="indefinite"/></rect></svg>') no-repeat center center;
626
+ }
627
+
628
+ /* Controls Row */
629
+ .controls-row {
630
+ display: flex;
631
+ gap: 18px;
632
+ margin-top: 18px;
633
+ }
634
+ .btn-start-gradient {
635
+ background: linear-gradient(90deg, #2563eb 0%, #6366f1 100%);
636
+ color: #fff;
637
+ font-size: 1.25rem;
638
+ font-weight: 700;
639
+ padding: 0.7rem 2.2rem;
640
+ border-radius: 999px;
641
+ box-shadow: 0 4px 24px #6366f188, 0 2px 8px #2563eb44;
642
+ transition: background 0.2s, color 0.2s, transform 0.2s, box-shadow 0.2s;
643
+ border: none;
644
+ outline: none;
645
+ letter-spacing: 2px;
646
+ position: relative;
647
+ overflow: hidden;
648
+ animation: buttonPulse 1.2s infinite alternate;
649
+ }
650
+ .btn-start-gradient:hover {
651
+ background: linear-gradient(90deg, #6366f1 0%, #2563eb 100%);
652
+ color: #e0e7ff;
653
+ transform: scale(1.05);
654
+ box-shadow: 0 6px 32px #6366f1cc;
655
+ }
656
+ .btn-pause {
657
+ background: #fbbf24;
658
+ color: #23272b;
659
+ font-weight: 700;
660
+ border-radius: 999px;
661
+ padding: 0.7rem 1.5rem;
662
+ border: none;
663
+ font-size: 1.1rem;
664
+ box-shadow: 0 2px 8px #fbbf2444;
665
+ transition: background 0.2s, color 0.2s;
666
+ }
667
+ .btn-skip {
668
+ background: #e0e7ff;
669
+ color: #6366f1;
670
+ font-weight: 700;
671
+ border-radius: 999px;
672
+ padding: 0.7rem 1.5rem;
673
+ border: none;
674
+ font-size: 1.1rem;
675
+ box-shadow: 0 2px 8px #6366f144;
676
+ transition: background 0.2s, color 0.2s;
677
+ }
678
+
679
+ /* Right Panel */
680
+ .right-panel {
681
+ flex: 1;
682
+ display: flex;
683
+ flex-direction: column;
684
+ align-items: flex-start;
685
+ padding: 32px 32px 32px 0;
686
+ min-width: 400px;
687
+ max-width: 100vw;
688
+ }
689
+ .video-preview {
690
+ margin-bottom: 18px;
691
+ margin-left: 48px; /* Ensure this matches transcript-panel */
692
+ display: flex;
693
+ flex-direction: column;
694
+ align-items: center;
695
+ justify-content: center;
696
+ min-height: 285px;
697
+ background: #374151;
698
+ border: 2px dashed #64748b;
699
+ border-radius: 16px;
700
+ box-shadow: 0 2px 16px #23272b44;
701
+ padding: 32px 0;
702
+ width: 100%;
703
+ max-width: 995px;
704
+ }
705
+ .camera-video {
706
+ border-radius: 18px;
707
+ border: 4px solid #38bdf8;
708
+ box-shadow: 0 0 24px #38bdf844, 0 0 8px #1e293b88;
709
+ background: #222;
710
+ object-fit: cover;
711
+ margin-bottom: 18px;
712
+ transition: box-shadow 0.4s, border 0.4s;
713
+ }
714
+ .camera-video-large {
715
+ width: 60vw;
716
+ max-width: 800px;
717
+ height: 40vh;
718
+ max-height: 60vh;
719
+ }
720
+ .camera-inactive-block {
721
+ display: flex;
722
+ flex-direction: column;
723
+ align-items: center;
724
+ justify-content: center;
725
+ min-height: 260px;
726
+ color: #a0aec0;
727
+ }
728
+ .camera-inactive-icon {
729
+ font-size: 4rem;
730
+ color: #64748b;
731
+ margin-bottom: 18px;
732
+ }
733
+ .camera-inactive-title {
734
+ font-size: 1.4rem;
735
+ font-weight: 600;
736
+ color: #cbd5e1;
737
+ margin-bottom: 6px;
738
+ }
739
+ .camera-inactive-sub {
740
+ font-size: 1.05rem;
741
+ color: #94a3b8;
742
+ }
743
+
744
+ /* Transcript Panel */
745
+ .transcript-panel {
746
+ margin-left: 48px; /* Match video-preview for alignment */
747
+ background: #fff;
748
+ border-radius: 14px;
749
+ box-shadow: 0 2px 12px #6366f122;
750
+ padding: 18px 18px 18px 18px;
751
+ width: 100%;
752
+ max-width: 962px;
753
+ overflow: hidden;
754
+ display: flex;
755
+ flex-direction: column;
756
+ }
757
+ .transcript-title {
758
+ font-weight: 700;
759
+ color: #6366f1;
760
+ margin-bottom: 8px;
761
+ }
762
+ .transcript-scrollable {
763
+ overflow-y: auto;
764
+ max-height: 180px;
765
+ padding-right: 8px;
766
+ }
767
+ .transcript-line {
768
+ font-size: 1.05rem;
769
+ color: #23272b;
770
+ margin-bottom: 4px;
771
+ }
772
+
773
+ /* Completion Summary Modal */
774
+ .summary-modal {
775
+ position: fixed;
776
+ top: 0; left: 0; right: 0; bottom: 0;
777
+ background: rgba(30,41,59,0.85);
778
+ z-index: 9999;
779
+ display: flex;
780
+ align-items: center;
781
+ justify-content: center;
782
+ }
783
+ .summary-modal-content {
784
+ background: #fff;
785
+ border-radius: 18px;
786
+ box-shadow: 0 2px 32px #6366f1cc;
787
+ padding: 32px 40px;
788
+ min-width: 420px;
789
+ max-width: 700px;
790
+ }
791
+ .summary-qa-row {
792
+ margin-bottom: 18px;
793
+ }
794
+ .summary-question {
795
+ font-weight: 700;
796
+ color: #2563eb;
797
+ }
798
+ .summary-answer {
799
+ color: #23272b;
800
+ margin-bottom: 4px;
801
+ }
802
+ .summary-duration {
803
+ color: #6366f1;
804
+ font-size: 0.98em;
805
+ }
806
+ .btn-close-summary {
807
+ background: linear-gradient(90deg, #2563eb 0%, #6366f1 100%);
808
+ color: #fff;
809
+ font-size: 1.1rem;
810
+ font-weight: 700;
811
+ padding: 0.7rem 2.2rem;
812
+ border-radius: 999px;
813
+ box-shadow: 0 4px 24px #6366f188, 0 2px 8px #2563eb44;
814
+ border: none;
815
+ outline: none;
816
+ letter-spacing: 2px;
817
+ margin-top: 18px;
818
+ cursor: pointer;
819
+ }
820
+ .btn-close-summary:hover {
821
+ background: linear-gradient(90deg, #6366f1 0%, #2563eb 100%);
822
+ color: #e0e7ff;
823
+ transform: scale(1.05);
824
+ box-shadow: 0 6px 32px #6366f1cc;
825
+ }
826
+
827
+ /* Info Card Styles */
828
+ .info-card {
829
+ background: linear-gradient(135deg, #232b3e 60%, #232b3e 100%);
830
+ border: 2px solid #38bdf8;
831
+ border-radius: 14px;
832
+ box-shadow: 0 2px 16px #38bdf822;
833
+ padding: 32px 24px;
834
+ margin-bottom: 32px;
835
+ max-width: 520px;
836
+ text-align: center;
837
+ color: #fff;
838
+ }
839
+ .info-title {
840
+ font-size: 2rem;
841
+ font-weight: 700;
842
+ margin-bottom: 12px;
843
+ }
844
+ .info-desc {
845
+ font-size: 1.15rem;
846
+ color: #e2e8f0;
847
+ }
848
+
849
+ /* Camera Inactive Card Styles */
850
+ .camera-inactive-card {
851
+ background: linear-gradient(135deg, #232b3e 60%, #232b3e 100%);
852
+ border: 2px dashed #a0aec0;
853
+ border-radius: 14px;
854
+ box-shadow: 0 2px 16px #38bdf822;
855
+ padding: 48px 24px 32px 24px;
856
+ margin-bottom: 32px;
857
+ max-width: 600px;
858
+ min-height: 320px;
859
+ text-align: center;
860
+ color: #e2e8f0;
861
+ display: flex;
862
+ flex-direction: column;
863
+ align-items: center;
864
+ justify-content: center;
865
+ }
866
+ .camera-icon {
867
+ margin-bottom: 18px;
868
+ color: #a0aec0;
869
+ }
870
+
871
+ .button-bar {
872
+ display: flex;
873
+ flex-direction: row;
874
+ justify-content: flex-end;
875
+ align-items: center;
876
+ gap: 18px;
877
+ padding: 18px 32px 18px 32px;
878
+ box-shadow: 0 2px 12px #38bdf844;
879
+ border-bottom: 1px solid #23272b;
880
+ }
881
+ .header-btn {
882
  font-family: 'Montserrat', 'Poppins', 'Arial Black', Arial, sans-serif;
883
+ font-size: 1.05rem;
884
+ font-weight: 700;
885
+ letter-spacing: 2px;
886
+ background: #fff;
887
+ color: #2563eb;
888
+ border: 2px solid #2563eb;
889
+ border-radius: 12px;
890
+ padding: 0.55rem 1.3rem;
891
+ margin: 0 0.3rem;
892
+ cursor: pointer;
893
+ box-shadow: 0 2px 16px #38bdf888;
894
+ transition: background 0.2s, color 0.2s, box-shadow 0.2s, border 0.2s;
895
+ }
896
+ .header-btn.active, .header-btn:focus, .header-btn:hover {
897
+ background: #2563eb;
898
+ color: #fff;
899
+ border-color: #38bdf8;
900
+ box-shadow: 0 2px 24px #bae6fd88;
901
+ }
902
+ .home-btn {
903
+ background: #2563eb;
904
+ color: #fff;
905
+ border-color: #38bdf8;
906
+ box-shadow: 0 2px 24px #bae6fd88;
907
+ }
908
+ .home-btn:focus, .home-btn:hover {
909
+ background: #38bdf8;
910
+ color: #fff;
911
+ }
912
+ .start-btn {
913
+ background: #fff;
914
+ color: #2563eb;
915
+ }
916
+ .camera-btn, .voice-btn, .tts-btn {
917
+ background: #e0e7ff;
918
+ color: #2563eb;
919
+ border-color: #2563eb;
920
+ }
921
+ .tts-btn.active {
922
+ background: #2563eb;
923
+ color: #fff;
924
  }
src/app/py-detect/py-detect.component.html CHANGED
@@ -1,57 +1,116 @@
1
- <div class="h-full bg-gray-900 text-gray-200 flex flex-col">
2
- <!-- Topbar Section -->
3
- <header class="flex justify-between items-center p-4 border-b border-gray-700 bg-gray-800">
4
- <div class="flex items-center space-x-4">
5
- <!-- Logo Image and Title -->
6
- <img src="./assets/pykara-logo.png" alt="Logo" class="pykara-logo-consistent" />
7
- <div class="text-lg font-semibold tracking-wide text-white">Py-Detect</div>
 
 
 
 
 
 
 
 
 
 
 
8
  </div>
 
 
9
 
10
- <div class="flex items-center gap-4">
11
- <!-- Home Button -->
12
- <button class="home-btn" (click)="navigateHome()">Home</button>
 
 
 
 
13
 
14
- <!-- Start Button -->
15
- <button class="btn-start stylish-font" (click)="start()" [disabled]="startDisabled()" [ngClass]="{'fade-out': startDisabled()}">Start</button>
16
- <!-- Stop Button -->
17
- <button class="btn-stop stylish-font" (click)="stopAll()" [disabled]="stopDisabled()">Stop</button>
18
- <!-- Final Submit Button -->
19
- <button class="btn-submit stylish-font" (click)="submitAll()" [disabled]="submitDisabled()">Final Submit</button>
 
20
  </div>
21
- </header>
22
-
23
- <!-- Main Stage Section -->
24
- <main class="main-flex-layout">
25
- <!-- Left: Question Section -->
26
- <section class="question-section">
27
- <div *ngIf="currentQuestion()" class="text-sm text-gray-400 mb-2 stylish-font">Question {{ questionIndex() + 1 }}</div>
28
- <div class="big-question stylish-font big-question-animated" style="min-height: 64px;">
29
- <ng-container *ngIf="currentQuestion(); else showStart">
30
- {{ currentQuestion() }}
31
- </ng-container>
32
- <ng-template #showStart>
33
- <button class="btn-start stylish-font" (click)="start()" [disabled]="startDisabled()" [ngClass]="{'fade-out': startDisabled()}">Start</button>
34
- </ng-template>
35
  </div>
36
- <div class="flex items-center justify-between mt-6">
37
- <span class="badge stylish-font"
38
- [ngClass]="{
39
- 'bg-blue-600': status() === 'asking',
40
- 'bg-green-600': status() === 'listening',
41
- 'bg-yellow-600': status() === 'processing'
42
- }">
43
- {{ status() | uppercase }}
44
- </span>
45
- <span class="mic" [class.bg-green-600]="micOn()"></span>
46
- <span class="recog stylish-font" [class.bg-blue-600]="recognizerReady()">STT</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  </div>
48
- </section>
49
- <!-- Right: Camera Section -->
50
- <section class="video-section">
51
- <video #videoElement autoplay muted playsinline class="camera-video camera-video-large"></video>
52
- <div class="video-status stylish-font">{{ videoStatus }}</div>
53
- </section>
54
- </main>
55
- <!-- Back Button moved outside main to be fixed at bottom left -->
56
- <button class="back-btn global-bottom-left-btn" (click)="goToInfoPage()">Back</button>
 
 
 
 
 
 
57
  </div>
 
 
 
 
 
 
1
+ <!-- Modern UI header with logo and PyDetect title -->
2
+ <div class="site-header">
3
+ <div class="header-inner">
4
+ <div class="logo-cluster">
5
+ <span (click)="navigateHome()" style="cursor:pointer;display:flex;align-items:center;">
6
+ <img src="/assets/pykara-logo.png" alt="PyDetect Logo" class="logo-img-header" />
7
+ </span>
8
+ <div class="py-detect-title-header">
9
+ <span class="py-letter p">P</span>
10
+ <span class="py-letter y">Y</span>
11
+ <span class="py-shape"></span>
12
+ <span class="py-letter d">D</span>
13
+ <span class="py-letter e">E</span>
14
+ <span class="py-letter t">T</span>
15
+ <span class="py-letter e2">E</span>
16
+ <span class="py-letter c">C</span>
17
+ <span class="py-letter t2">T</span>
18
+ </div>
19
  </div>
20
+ </div>
21
+ </div>
22
 
23
+ <!-- Button Bar Below Header -->
24
+ <div class="button-bar">
25
+ <button class="header-btn home-btn" (click)="navigateHome()">Home</button>
26
+ <button class="header-btn camera-btn" (click)="startCamera()">Start Camera</button>
27
+ <button class="header-btn voice-btn" (click)="startVoice()">Start Voice</button>
28
+ <button class="header-btn tts-btn" [class.active]="ttsEnabled()"> <i class="fas fa-volume-up"></i> TTS On</button>
29
+ </div>
30
 
31
+ <main class="main-flex-layout">
32
+ <!-- Left Panel: Controls & Question -->
33
+ <section class="left-panel">
34
+ <div class="case-meta">
35
+ <div class="case-id">Case ID: <span class="case-id-value">{{ caseId }}</span></div>
36
+ <div class="officer">Officer: <span class="officer-value">{{ officer }}</span></div>
37
+ <div class="progress-label">Progress: <span class="progress-value">{{ progress }}%</span></div>
38
  </div>
39
+ <div class="question-card">
40
+ <!-- Show Start button if no question is active -->
41
+ <div *ngIf="!currentQuestionText">
42
+ <button class="btn-start-gradient" (click)="onStartRecording()">
43
+ <i class="fas fa-search"></i> Start Investigation
44
+ </button>
 
 
 
 
 
 
 
 
45
  </div>
46
+ <!-- Show question if active -->
47
+ <div *ngIf="currentQuestionText">
48
+ <div class="question-number" style="font-size:1.1em;color:#6366f1;font-weight:600;margin-bottom:8px;">
49
+ Question {{ currentQuestionIndex }} of {{ totalQuestions }}
50
+ </div>
51
+ <div class="question-typewriter">
52
+ {{ currentQuestionText }}
53
+ </div>
54
+ <!-- Asking popup -->
55
+ <div *ngIf="status() === 'asking'" class="popup asking-popup">
56
+ <span>Asking...</span>
57
+ </div>
58
+ <!-- Idle popup -->
59
+ <div *ngIf="status() === 'idle-wait'" class="popup idle-popup">
60
+ <span>Waiting for suspect reply...</span>
61
+ </div>
62
+ <div *ngIf="status() === 'recording'" class="recording-indicator" style="color:#38bdf8;font-weight:600;margin-top:12px;">Recording...</div>
63
+ </div>
64
+ <div class="audio-status">
65
+ <span *ngIf="isRecording" class="recording-text">Recording...</span>
66
+ <span *ngIf="isProcessing" class="processing-text">Processing Answer...</span>
67
+ <div class="waveform-animation" *ngIf="isRecording"></div>
68
+ </div>
69
+ </div>
70
+ <div class="controls-row" *ngIf="currentQuestionText">
71
+ <button class="btn-pause" (click)="pauseRecording()"><i class="fas fa-pause"></i> Pause</button>
72
+ <button class="btn-skip" (click)="skipQuestion()"><i class="fas fa-forward"></i> Skip Question</button>
73
+ </div>
74
+ </section>
75
+
76
+ <!-- Right Panel: Video + Transcript -->
77
+ <section class="right-panel">
78
+ <div class="video-preview">
79
+ <ng-container *ngIf="!videoStream">
80
+ <div class="camera-inactive-block">
81
+ <i class="fas fa-video camera-inactive-icon"></i>
82
+ <div class="camera-inactive-title">Camera Inactive</div>
83
+ <div class="camera-inactive-sub">Click "Start Camera" to begin video recording</div>
84
+ </div>
85
+ </ng-container>
86
+ <ng-container *ngIf="videoStream">
87
+ <video #videoElement autoplay muted playsinline class="camera-video camera-video-large"></video>
88
+ <div class="video-status stylish-font">{{ videoStatus }}</div>
89
+ </ng-container>
90
+ </div>
91
+ <div class="transcript-panel">
92
+ <div class="transcript-title">Transcription (Live):</div>
93
+ <div class="transcript-scrollable">
94
+ <div *ngFor="let line of transcriptLines" class="transcript-line">{{ line }}</div>
95
  </div>
96
+ </div>
97
+ </section>
98
+ </main>
99
+
100
+ <!-- Completion Summary Modal -->
101
+ <div class="summary-modal" *ngIf="showSummary">
102
+ <div class="summary-modal-content">
103
+ <h2>Interview Summary</h2>
104
+ <div *ngFor="let qa of summaryData" class="summary-qa-row">
105
+ <div class="summary-question">Q: {{ qa.question }}</div>
106
+ <div class="summary-answer">A: {{ qa.answer }}</div>
107
+ <div class="summary-duration">Audio: {{ qa.duration }}s</div>
108
+ </div>
109
+ <button class="btn-close-summary" (click)="closeSummary()">Close</button>
110
+ </div>
111
  </div>
112
+
113
+ <footer>
114
+ <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
115
+ </footer>
116
+
src/app/py-detect/py-detect.component.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component, OnDestroy, OnInit, signal, ViewChild, ElementRef } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
  import { Router, NavigationStart } from '@angular/router';
4
  import { Subscription } from 'rxjs';
@@ -31,7 +31,7 @@ type QAResult = {
31
  export class PyDetectComponent implements OnInit, OnDestroy {
32
 
33
  // ---- UI state ----
34
- status = signal<'idle' | 'asking' | 'listening' | 'processing'>('idle');
35
  autoMode = signal(true); // Auto Next is always enabled
36
  micOn = signal(false);
37
  ttsEnabled = signal(true); // Speak Questions is always enabled
@@ -49,7 +49,7 @@ export class PyDetectComponent implements OnInit, OnDestroy {
49
  // ---- Constructor with Router Injection ----
50
  private routerSubscription?: Subscription;
51
 
52
- constructor(private router: Router) {
53
  // Cancel TTS on any navigation away
54
  this.routerSubscription = this.router.events.subscribe(event => {
55
  if (event instanceof NavigationStart) {
@@ -108,6 +108,17 @@ export class PyDetectComponent implements OnInit, OnDestroy {
108
  public videoChunks: Blob[] = [];
109
  public videoAnswers: Blob[] = [];
110
 
 
 
 
 
 
 
 
 
 
 
 
111
  // Navigate back to the homepage
112
  navigateHome() {
113
  if (window.speechSynthesis) {
@@ -243,36 +254,37 @@ export class PyDetectComponent implements OnInit, OnDestroy {
243
  // 1) Get next question
244
  const q = await this.fetchNextQuestion();
245
  this.currentQuestion.set(q);
246
- this.status.set('asking');
247
  if (this.ttsEnabled()) {
248
  await this.speak(q);
249
  }
250
- this.status.set('listening');
251
- await this.startVideoRecording();
252
- const startedAt = Date.now();
253
- let silenceTimer: any;
 
254
  let userSpoke = false;
255
  this.transcriptSoFar = '';
256
  this.startRecognition('en-IN');
257
- const speechPromise = new Promise<void>((resolve) => {
 
258
  const checkSpeech = () => {
259
- if (this.transcriptSoFar.trim().length > 0) {
260
  userSpoke = true;
261
- clearTimeout(silenceTimer);
 
262
  resolve();
263
  } else {
264
- silenceTimer = setTimeout(() => resolve(), 5000);
265
  }
266
  };
267
- const poll = () => {
268
- checkSpeech();
269
- if (!userSpoke && silenceTimer && this.nextQuestionLoopRunning) setTimeout(poll, 500);
270
- };
271
- poll();
272
  });
273
- await speechPromise;
 
274
  this.stopRecognition();
275
  this.stopVideoRecording();
 
276
  const { audioUrl, avgPitchHz, avgVolume, transcript, language } =
277
  await this.captureAnswerWithAnalysis(this.maxAnswerMs);
278
  const endedAt = Date.now();
@@ -608,4 +620,153 @@ export class PyDetectComponent implements OnInit, OnDestroy {
608
  this.videoRecorder.stop();
609
  }
610
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
  }
 
1
+ import { Component, OnDestroy, OnInit, signal, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
  import { Router, NavigationStart } from '@angular/router';
4
  import { Subscription } from 'rxjs';
 
31
  export class PyDetectComponent implements OnInit, OnDestroy {
32
 
33
  // ---- UI state ----
34
+ status = signal<'idle' | 'asking' | 'idle-wait' | 'recording' | 'processing'>('idle');
35
  autoMode = signal(true); // Auto Next is always enabled
36
  micOn = signal(false);
37
  ttsEnabled = signal(true); // Speak Questions is always enabled
 
49
  // ---- Constructor with Router Injection ----
50
  private routerSubscription?: Subscription;
51
 
52
+ constructor(private router: Router, private cdr: ChangeDetectorRef) {
53
  // Cancel TTS on any navigation away
54
  this.routerSubscription = this.router.events.subscribe(event => {
55
  if (event instanceof NavigationStart) {
 
108
  public videoChunks: Blob[] = [];
109
  public videoAnswers: Blob[] = [];
110
 
111
+ // UI properties for template
112
+ progress: number = 0;
113
+ caseId: string = 'CASE-007';
114
+ officer: string = 'Ganesh';
115
+ currentQuestionText: string = '';
116
+ isRecording: boolean = false;
117
+ isProcessing: boolean = false;
118
+ transcriptLines: string[] = [];
119
+ showSummary: boolean = false;
120
+ summaryData: { question: string; answer: string; duration: number }[] = [];
121
+
122
  // Navigate back to the homepage
123
  navigateHome() {
124
  if (window.speechSynthesis) {
 
254
  // 1) Get next question
255
  const q = await this.fetchNextQuestion();
256
  this.currentQuestion.set(q);
257
+ this.status.set('asking'); // Show asking popup
258
  if (this.ttsEnabled()) {
259
  await this.speak(q);
260
  }
261
+ // After question finishes playing, show idle popup for 5 seconds
262
+ this.status.set('idle-wait'); // Show idle popup
263
+ await this.sleep(5000); // Wait for suspect reply
264
+ // Now start speech recognition only
265
+ this.status.set('recording');
266
  let userSpoke = false;
267
  this.transcriptSoFar = '';
268
  this.startRecognition('en-IN');
269
+ // Wait for user to start speaking
270
+ await new Promise<void>((resolve) => {
271
  const checkSpeech = () => {
272
+ if (this.transcriptSoFar.trim().length > 0 && !userSpoke) {
273
  userSpoke = true;
274
+ // Start video recording and analysis when user speaks
275
+ this.startVideoRecording();
276
  resolve();
277
  } else {
278
+ setTimeout(checkSpeech, 200);
279
  }
280
  };
281
+ checkSpeech();
 
 
 
 
282
  });
283
+ // Stop recognition and video recording after answer window
284
+ await this.sleep(this.maxAnswerMs);
285
  this.stopRecognition();
286
  this.stopVideoRecording();
287
+ const startedAt = Date.now();
288
  const { audioUrl, avgPitchHz, avgVolume, transcript, language } =
289
  await this.captureAnswerWithAnalysis(this.maxAnswerMs);
290
  const endedAt = Date.now();
 
620
  this.videoRecorder.stop();
621
  }
622
  }
623
+
624
+ // UI methods for template
625
+ onStartInterview() {
626
+ this.currentQuestionText = this.seedQuestions[0];
627
+ this.progress = 0;
628
+ this.isRecording = false;
629
+ this.isProcessing = false;
630
+ }
631
+ pauseRecording() {
632
+ this.isRecording = false;
633
+ // Add logic to pause recording
634
+ }
635
+ skipQuestion() {
636
+ const idx = this.seedQuestions.indexOf(this.currentQuestionText);
637
+ if (idx >= 0 && idx < this.seedQuestions.length - 1) {
638
+ this.currentQuestionText = this.seedQuestions[idx + 1];
639
+ this.progress = Math.round(((idx + 2) / this.seedQuestions.length) * 100);
640
+ } else {
641
+ this.currentQuestionText = '';
642
+ this.progress = 100;
643
+ this.showSummary = true;
644
+ }
645
+ }
646
+ closeSummary() {
647
+ this.showSummary = false;
648
+ }
649
+ onStartRecording() {
650
+ this.progress = 0;
651
+ this.transcriptLines = [];
652
+ this.showSummary = false;
653
+ this.startQuestionFlow(0);
654
+ // Play the question using TTS, then wait 2s, then start recognition
655
+ this.speakQuestion(this.seedQuestions[0], () => {
656
+ setTimeout(() => {
657
+ this.startRecognitionWithRecording(0);
658
+ }, 2000);
659
+ });
660
+ }
661
+
662
+ speakQuestion(text: string, onEnd?: () => void) {
663
+ const synth = window.speechSynthesis;
664
+ if (!synth) {
665
+ if (onEnd) onEnd();
666
+ return;
667
+ }
668
+ const utter = new SpeechSynthesisUtterance(text);
669
+ utter.lang = 'en-IN';
670
+ synth.cancel(); // Stop any previous speech
671
+ utter.onend = () => { if (onEnd) onEnd(); };
672
+ synth.speak(utter);
673
+ }
674
+
675
+ startRecognitionWithRecording(idx: number) {
676
+ const Ctor = window.webkitSpeechRecognition || window.SpeechRecognition;
677
+ if (!Ctor) return;
678
+ this.recognition = new Ctor();
679
+ this.recognition.lang = 'en-IN';
680
+ this.recognition.continuous = false;
681
+ this.recognition.interimResults = false;
682
+ let recordingStarted = false;
683
+ const startTime = Date.now();
684
+ // Timeout logic for 5 seconds if no answer
685
+ const silenceTimeout = setTimeout(() => {
686
+ if (!recordingStarted) {
687
+ this.recognition.stop();
688
+ // Do NOT set isRecording = false here
689
+ this.status.set('idle');
690
+ setTimeout(() => this.playNextQuestion(idx + 1), 5000);
691
+ }
692
+ }, 5000);
693
+ this.recognition.onresult = (event: any) => {
694
+ let finalText = '';
695
+ for (let i = event.resultIndex; i < event.results.length; i++) {
696
+ const result = event.results[i];
697
+ if (result.isFinal) {
698
+ finalText += result[0].transcript.trim();
699
+ }
700
+ }
701
+ if (!recordingStarted) {
702
+ recordingStarted = true;
703
+ clearTimeout(silenceTimeout); // Cancel timeout if answer detected
704
+ }
705
+ this.transcriptLines.push(finalText);
706
+ };
707
+ this.recognition.onend = () => {
708
+ this.isRecording = false;
709
+ this.status.set('processing');
710
+ setTimeout(() => this.playNextQuestion(idx + 1), 5000);
711
+ };
712
+ this.recognition.onerror = () => {
713
+ this.isRecording = false;
714
+ this.status.set('idle');
715
+ };
716
+ this.recognition.start();
717
+ }
718
+
719
+ playNextQuestion(idx: number) {
720
+ if (idx >= this.seedQuestions.length) {
721
+ this.currentQuestionText = '';
722
+ this.progress = 100;
723
+ this.showSummary = true;
724
+ this.status.set('idle');
725
+ this.cdr.detectChanges();
726
+ this.isRecording = false;
727
+ return;
728
+ }
729
+ this.currentQuestionText = this.seedQuestions[idx];
730
+ this.cdr.detectChanges();
731
+ this.status.set('asking'); // Set status to asking
732
+ this.isRecording = false; // Not recording yet
733
+ this.speakQuestion(this.seedQuestions[idx], () => {
734
+ // After TTS, wait 5 seconds, then start recording
735
+ setTimeout(() => {
736
+ this.status.set('recording');
737
+ this.isRecording = true;
738
+ this.startRecognitionWithRecording(idx);
739
+ }, 5000);
740
+ });
741
+ }
742
+
743
+ startQuestionFlow(idx: number) {
744
+ if (idx >= this.seedQuestions.length) {
745
+ this.currentQuestionText = '';
746
+ this.progress = 100;
747
+ this.showSummary = true;
748
+ this.status.set('idle');
749
+ return;
750
+ }
751
+ this.currentQuestionText = this.seedQuestions[idx];
752
+ this.status.set('asking');
753
+ setTimeout(() => {
754
+ this.status.set('recording');
755
+ this.isRecording = true;
756
+ this.isProcessing = false;
757
+ this.transcriptLines = [];
758
+ this.startRecognition('en-IN');
759
+ }, 1200); // 1.2s delay for "Asking..."
760
+ }
761
+
762
+ get currentQuestionIndex(): number {
763
+ return this.seedQuestions.indexOf(this.currentQuestionText) + 1;
764
+ }
765
+ get totalQuestions(): number {
766
+ return this.seedQuestions.length;
767
+ }
768
+ startVoice() {
769
+ // Add logic to start voice recording or recognition here
770
+ this.status.set('recording');
771
+ }
772
  }
src/app/recordpage/recordpage.component.css CHANGED
@@ -245,6 +245,7 @@
245
  max-width: 100vw;
246
  min-width: 1000px;
247
  }
 
248
  .table-wrap {
249
  max-width: 100vw;
250
  min-height: 400px;
@@ -257,13 +258,16 @@
257
  width: 100%;
258
  max-width: 100vw;
259
  }
 
260
  .table-wrap {
261
  min-height: 200px;
262
  }
 
263
  .padded-table-wrap {
264
  padding-left: 4px;
265
  padding-right: 4px;
266
  }
 
267
  .records-header-row {
268
  flex-direction: column;
269
  align-items: stretch;
@@ -271,10 +275,12 @@
271
  padding-left: 4px;
272
  padding-right: 4px;
273
  }
 
274
  .page-title {
275
  text-align: center;
276
  margin-right: 0;
277
  }
 
278
  .toolbar {
279
  position: static;
280
  transform: none;
@@ -324,6 +330,11 @@
324
 
325
  .records th.actions-col, .records td.actions {
326
  color: #fff;
 
 
 
 
 
327
  }
328
 
329
  .records tbody tr:last-child td {
@@ -407,14 +418,17 @@
407
  background: #e5e7eb;
408
  color: #22223b;
409
  }
 
410
  .status-open {
411
  background: #d1fae5;
412
  color: #059669;
413
  }
 
414
  .status-under {
415
  background: #dbeafe;
416
  color: #2563eb;
417
  }
 
418
  .status-closed {
419
  background: #fee2e2;
420
  color: #dc2626;
@@ -456,17 +470,18 @@
456
  border-color: #805ad5;
457
  }
458
 
459
- .btn.delete {
460
- background: #ef4444;
461
- color: #fff;
462
- border-color: #ef4444;
463
- box-shadow: 0 2px 8px #ef444422;
464
- transition: background 0.18s, color 0.18s;
465
- }
466
- .btn.delete:hover {
467
- background: #991b1b;
468
- color: #fff;
469
- }
 
470
 
471
  /* Icon buttons */
472
  .icon-btn {
@@ -484,38 +499,56 @@
484
  align-items: center;
485
  justify-content: center;
486
  }
487
- .icon-btn.view {
488
- color: #2563eb;
489
- }
490
- .icon-btn.edit {
491
- color: #7c3aed;
492
- }
493
- .icon-btn.delete {
494
- color: #ef4444;
495
- }
496
- .icon-btn:hover {
497
- background: #f0f7ff;
498
- box-shadow: 0 2px 8px #2563eb22;
499
- }
500
- .icon-btn.delete:hover {
501
- background: #fff0f0;
502
- color: #b91c1c;
503
- }
504
- .icon-btn.edit:hover {
505
- background: #f3e8ff;
506
- color: #5b21b6;
507
- }
508
- .icon-btn.view:hover {
509
- background: #e0f2fe;
510
- color: #0ea5e9;
511
- }
512
- .icon-btn.verify {
513
- color: #22c55e;
514
- }
515
- .icon-btn.verify:hover {
516
- background: #e0ffe6;
517
- color: #15803d;
518
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
519
 
520
  .empty {
521
  text-align: center;
@@ -551,6 +584,7 @@
551
  display: flex;
552
  flex-direction: column;
553
  }
 
554
  .modal-header, .modal-footer {
555
  padding: 24px 40px 18px 40px;
556
  background: #f8fafc;
@@ -558,6 +592,7 @@
558
  font-size: 1.18rem;
559
  color: #23272b;
560
  }
 
561
  .modal-body {
562
  flex: 1 1 auto;
563
  padding: 32px 48px 32px 48px;
@@ -575,33 +610,33 @@
575
  border-bottom: 1px solid #e5e7eb;
576
  }
577
 
578
- .detail-row:last-child {
579
- border-bottom: none;
580
- }
581
 
582
- .detail-row span {
583
- color: #2563eb;
584
- font-weight: 600;
585
- font-size: 1.07em;
586
- }
587
 
588
- .detail-row b {
589
- color: #23272b;
590
- font-weight: 500;
591
- font-size: 1.07em;
592
- }
593
 
594
  .detail-block {
595
  margin-top: 18px;
596
  margin-bottom: 8px;
597
  }
598
 
599
- .detail-block .label {
600
- font-weight: 700;
601
- color: #2563eb;
602
- margin-bottom: 6px;
603
- font-size: 1.09em;
604
- }
605
 
606
  .explain {
607
  white-space: pre-wrap;
@@ -624,9 +659,9 @@
624
  margin-left: 8px;
625
  }
626
 
627
- .btn:hover {
628
- background: #e0e7ef;
629
- }
630
 
631
  .records-center {
632
  display: flex;
@@ -725,11 +760,11 @@
725
  z-index: 10;
726
  }
727
 
728
- .modern-searchbar-container.compact {
729
- width: auto;
730
- margin: 0 0 0 24px;
731
- align-items: center;
732
- }
733
 
734
  .modern-searchbar-form {
735
  display: flex;
@@ -743,12 +778,12 @@
743
  position: relative;
744
  }
745
 
746
- .modern-searchbar-form.compact {
747
- height: 44px;
748
- min-width: 260px;
749
- width: 320px;
750
- padding: 0 8px 0 10px;
751
- }
752
 
753
  .modern-searchbar-icon {
754
  display: flex;
@@ -769,11 +804,11 @@
769
  height: 32px;
770
  }
771
 
772
- .modern-searchbar-input::placeholder {
773
- color: #e0e7ef;
774
- opacity: 1;
775
- font-weight: 400;
776
- }
777
 
778
  .modern-searchbar-btn {
779
  background: #fff;
@@ -790,10 +825,10 @@
790
  transition: background 0.18s, color 0.18s;
791
  }
792
 
793
- .modern-searchbar-btn:hover {
794
- background: #ede9fe;
795
- color: #4f46e5;
796
- }
797
 
798
  .modern-searchbar-form.white-bg {
799
  background: #fff !important;
@@ -805,10 +840,10 @@
805
  color: #23272b !important;
806
  }
807
 
808
- .modern-searchbar-input.white-bg::placeholder {
809
- color: #64748b !important;
810
- opacity: 1;
811
- }
812
 
813
  .modern-searchbar-icon svg {
814
  stroke: #64748b !important;
@@ -829,10 +864,10 @@
829
  transition: background 0.18s, color 0.18s;
830
  }
831
 
832
- .modern-searchbar-btn:hover {
833
- background: #38bdf8;
834
- color: #fff;
835
- }
836
 
837
 
838
  .back-colo {
@@ -865,11 +900,11 @@
865
  width: 95vw;
866
  justify-content: flex-end;
867
  }
 
868
  .modern-searchbar-form.compact {
869
  width: 100%;
870
  min-width: 0;
871
  }
872
-
873
  }
874
 
875
  /* Modern UI header styles from infopage */
@@ -881,6 +916,7 @@
881
  z-index: 10;
882
  padding-bottom: 0;
883
  }
 
884
  .header-inner {
885
  display: flex;
886
  align-items: center;
@@ -888,11 +924,13 @@
888
  padding: 18px 32px 0 32px;
889
  position: relative;
890
  }
 
891
  .logo-cluster {
892
  display: flex;
893
  align-items: center;
894
  gap: 18px;
895
  }
 
896
  .logo-img-header {
897
  width: 54px;
898
  height: 54px;
@@ -903,6 +941,7 @@
903
  margin-top: -6px;
904
  margin-bottom: 1vh;
905
  }
 
906
  .py-detect-title-header {
907
  font-size: 2.1rem;
908
  font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
@@ -915,21 +954,64 @@
915
  margin-bottom: 1.5vh;
916
  }
917
 
918
- .py-detect-title-header .py-letter.p { color: #e3f6ff; text-shadow: 0 0 6px #38bdf8; }
919
- .py-detect-title-header .py-letter.y { color: #38bdf8; text-shadow: 0 0 6px #38bdf8; }
920
- .py-detect-title-header .py-shape { color: #e3f6ff; background: #e3f6ff; text-shadow: 0 0 6px #38bdf8; box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff; border: 2px solid #23272b; width: 18px; height: 4px; display: inline-block; margin: 0 8px; border-radius: 2px; }
921
- .py-detect-title-header .py-letter.d { color: #e3f6ff; text-shadow: 0 0 6px #38bdf8; }
922
- .py-detect-title-header .py-letter.e { color: #38bdf8; text-shadow: 0 0 6px #38bdf8; }
923
- .py-detect-title-header .py-letter.t { color: #e3f6ff; text-shadow: 0 0 6px #38bdf8; }
924
- .py-detect-title-header .py-letter.e2 { color: #38bdf8; text-shadow: 0 0 6px #38bdf8; }
925
- .py-detect-title-header .py-letter.c { color: #e3f6ff; text-shadow: 0 0 6px #38bdf8; }
926
- .py-detect-title-header .py-letter.t2 { color: #38bdf8; text-shadow: 0 0 6px #38bdf8; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
 
928
  body, main.content {
929
  background: #f4f6fa;
930
  min-height: 100vh;
931
  margin: 0;
932
- padding: 0;
933
  font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
934
  }
935
 
@@ -944,6 +1026,7 @@ body, main.content {
944
  min-width: 320px;
945
  padding: 0;
946
  overflow-x: auto;
 
947
  }
948
 
949
  .record-header {
@@ -1006,9 +1089,10 @@ body, main.content {
1006
  cursor: pointer;
1007
  transition: background 0.18s;
1008
  }
1009
- .record-new-btn:hover {
1010
- background: #1e40af;
1011
- }
 
1012
 
1013
  .record-table {
1014
  width: 100%;
@@ -1017,50 +1101,52 @@ body, main.content {
1017
  background: #fff;
1018
  }
1019
 
1020
- .record-table th, .record-table td {
1021
- border-bottom: 1.5px solid #e5e7eb;
1022
- padding: 12px 16px;
1023
- text-align: left;
1024
- font-size: 1.05rem;
1025
- }
1026
 
1027
- .record-table th {
1028
- background: #f4f6fa;
1029
- color: #222b45;
1030
- font-weight: 700;
1031
- }
1032
 
1033
- .record-table tr:last-child td {
1034
- border-bottom: none;
1035
- }
1036
 
1037
- .record-table tr {
1038
- transition: background 0.15s;
1039
- }
1040
- .record-table tr:hover {
1041
- background: #f1f5f9;
1042
- }
1043
 
1044
- .record-table a {
1045
- color: #2563eb;
1046
- text-decoration: underline;
1047
- cursor: pointer;
1048
- }
1049
 
1050
- .record-table .record-status {
1051
- font-weight: 500;
1052
- border-radius: 6px;
1053
- padding: 2px 10px;
1054
- background: #f1f5f9;
1055
- color: #2563eb;
1056
- font-size: 0.98em;
1057
- }
 
 
 
 
 
 
1058
 
1059
  .vertical-sections {
1060
  display: flex;
1061
  flex-direction: column;
1062
  gap: 32px;
1063
  }
 
1064
  .horizontal-sections {
1065
  display: flex;
1066
  flex-direction: column;
@@ -1070,6 +1156,7 @@ body, main.content {
1070
  box-shadow: 0 8px 32px #2563eb22;
1071
  padding: 32px 0 32px 0;
1072
  }
 
1073
  .section-block {
1074
  background: rgba(255,255,255,0.98);
1075
  border-radius: 18px;
@@ -1082,10 +1169,12 @@ body, main.content {
1082
  border-left: 6px solid #38bdf8;
1083
  transition: box-shadow 0.2s, border 0.2s;
1084
  }
1085
- .section-block:hover {
1086
- box-shadow: 0 8px 32px #2563eb33;
1087
- border-left: 6px solid #2563eb;
1088
- }
 
 
1089
  .section-title {
1090
  font-size: 1.32em;
1091
  font-weight: 900;
@@ -1097,12 +1186,14 @@ body, main.content {
1097
  align-items: center;
1098
  gap: 10px;
1099
  }
 
1100
  .subgroup-row {
1101
  display: flex;
1102
  flex-direction: row;
1103
  gap: 36px;
1104
  overflow-x: auto;
1105
  }
 
1106
  .subgroup-block {
1107
  min-width: 260px;
1108
  flex: 1 1 0;
@@ -1118,11 +1209,13 @@ body, main.content {
1118
  border: 1.5px solid #e0e7ef;
1119
  transition: box-shadow 0.18s, border 0.18s, background 0.18s;
1120
  }
1121
- .subgroup-block:hover {
1122
- box-shadow: 0 4px 16px #38bdf855;
1123
- border: 1.5px solid #38bdf8;
1124
- background: #e0f2fe;
1125
- }
 
 
1126
  .subgroup-title {
1127
  font-size: 1.11em;
1128
  font-weight: 700;
@@ -1134,6 +1227,7 @@ body, main.content {
1134
  align-items: center;
1135
  gap: 8px;
1136
  }
 
1137
  .fields-table {
1138
  display: flex;
1139
  flex-direction: column;
@@ -1143,6 +1237,7 @@ body, main.content {
1143
  border-radius: 0;
1144
  padding: 0;
1145
  }
 
1146
  .field-row {
1147
  display: flex;
1148
  flex-direction: row;
@@ -1156,32 +1251,36 @@ body, main.content {
1156
  transition: background 0.18s;
1157
  gap: 32px;
1158
  }
1159
- .field-row:last-child {
1160
- border-bottom: none;
1161
- }
1162
- .field-row span {
1163
- color: #1e293b;
1164
- font-weight: 700;
1165
- letter-spacing: 0.2px;
1166
- min-width: 180px;
1167
- font-size: 1.08em;
1168
- margin-right: 32px;
1169
- text-align: left;
1170
- display: inline-block;
1171
- }
1172
- .field-row b {
1173
- color: #2563eb;
1174
- font-weight: 700;
1175
- word-break: break-word;
1176
- letter-spacing: 0.1px;
1177
- font-size: 1.13em;
1178
- margin-left: 8px;
1179
- text-align: left;
1180
- display: inline-block;
1181
- }
1182
- .field-row b:hover {
1183
- color: #0ea5e9;
1184
- }
 
 
 
 
1185
 
1186
  /* Stylish pills for subgroups */
1187
  .subgroup-pills {
@@ -1190,29 +1289,32 @@ body, main.content {
1190
  gap: 12px;
1191
  margin-bottom: 18px;
1192
  }
1193
- .subgroup-pills button {
1194
- background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%);
1195
- border: none;
1196
- border-radius: 20px;
1197
- padding: 7px 22px;
1198
- font-size: 1em;
1199
- color: #fff;
1200
- font-weight: 700;
1201
- cursor: pointer;
1202
- box-shadow: 0 2px 8px #2563eb22;
1203
- transition: background 0.18s, color 0.18s, box-shadow 0.18s;
1204
- outline: none;
1205
- }
1206
- .subgroup-pills button.active,
1207
- .subgroup-pills button:focus {
1208
- background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
1209
- color: #fff;
1210
- box-shadow: 0 4px 16px #38bdf855;
1211
- }
1212
- .subgroup-pills button:hover {
1213
- background: linear-gradient(90deg, #0ea5e9 0%, #2563eb 100%);
1214
- color: #fff;
1215
- }
 
 
 
1216
 
1217
  .field-card {
1218
  display: flex;
@@ -1226,26 +1328,32 @@ body, main.content {
1226
  .section-block {
1227
  padding: 18px 8vw 12px 8vw;
1228
  }
 
1229
  .subgroup-block {
1230
  min-width: 180px;
1231
  padding: 12px 8px 8px 8px;
1232
  }
 
1233
  .field-row span {
1234
  min-width: 120px;
1235
  }
1236
  }
 
1237
  @media (max-width: 600px) {
1238
  .section-block {
1239
  padding: 10px 2vw 8px 2vw;
1240
  }
 
1241
  .subgroup-block {
1242
  min-width: 120px;
1243
  padding: 8px 2px 6px 2px;
1244
  }
 
1245
  .field-row span {
1246
  min-width: 80px;
1247
  font-size: 0.98em;
1248
  }
 
1249
  .field-row b {
1250
  font-size: 1em;
1251
  }
@@ -1262,48 +1370,55 @@ body, main.content {
1262
  border-radius: 12px;
1263
  box-shadow: 0 2px 8px #2563eb11;
1264
  }
1265
- .filter-bar select {
1266
- padding: 6px 18px;
1267
- border-radius: 6px;
1268
- border: 1.5px solid #cbd5e1;
1269
- font-size: 1em;
1270
- color: #2563eb;
1271
- background: #fff;
1272
- font-weight: 600;
1273
- outline: none;
1274
- transition: border 0.15s;
1275
- }
1276
- .filter-bar select:focus {
1277
- border: 1.5px solid #38bdf8;
1278
- }
1279
- .filter-bar button {
1280
- padding: 6px 18px;
1281
- border-radius: 6px;
1282
- border: none;
1283
- font-size: 1em;
1284
- font-weight: 700;
1285
- cursor: pointer;
1286
- background: #2563eb;
1287
- color: #fff;
1288
- transition: background 0.15s;
1289
- }
1290
- .filter-bar button:hover {
1291
- background: #0ea5e9;
1292
- }
1293
- .filter-bar button:last-child {
1294
- background: #f87171;
1295
- color: #fff;
1296
- margin-left: 4px;
1297
- }
1298
- .filter-bar button:last-child:hover {
1299
- background: #ef4444;
1300
- }
 
 
 
 
 
 
1301
 
1302
  .analytics-summary {
1303
  display: flex;
1304
  gap: 32px;
1305
  margin: 18px 0 8px 0;
1306
  }
 
1307
  .summary-card {
1308
  background: #f8fafc;
1309
  border-radius: 12px;
@@ -1312,23 +1427,27 @@ body, main.content {
1312
  min-width: 120px;
1313
  text-align: center;
1314
  }
1315
- .summary-card .summary-label {
1316
- font-size: 1em;
1317
- color: #64748b;
1318
- font-weight: 600;
1319
- margin-bottom: 6px;
1320
- }
1321
- .summary-card .summary-value {
1322
- font-size: 2em;
1323
- font-weight: 900;
1324
- color: #2563eb;
1325
- }
1326
- .summary-card.open .summary-value {
1327
- color: #059669;
1328
- }
1329
- .summary-card.closed .summary-value {
1330
- color: #dc2626;
1331
- }
 
 
 
 
1332
 
1333
  .record-meta {
1334
  color: #64748b;
@@ -1340,3 +1459,16 @@ body, main.content {
1340
  }
1341
 
1342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  max-width: 100vw;
246
  min-width: 1000px;
247
  }
248
+
249
  .table-wrap {
250
  max-width: 100vw;
251
  min-height: 400px;
 
258
  width: 100%;
259
  max-width: 100vw;
260
  }
261
+
262
  .table-wrap {
263
  min-height: 200px;
264
  }
265
+
266
  .padded-table-wrap {
267
  padding-left: 4px;
268
  padding-right: 4px;
269
  }
270
+
271
  .records-header-row {
272
  flex-direction: column;
273
  align-items: stretch;
 
275
  padding-left: 4px;
276
  padding-right: 4px;
277
  }
278
+
279
  .page-title {
280
  text-align: center;
281
  margin-right: 0;
282
  }
283
+
284
  .toolbar {
285
  position: static;
286
  transform: none;
 
330
 
331
  .records th.actions-col, .records td.actions {
332
  color: #fff;
333
+ text-align: left;
334
+ padding-left: 18px;
335
+ padding-right: 18px;
336
+ width: 1%;
337
+ white-space: nowrap;
338
  }
339
 
340
  .records tbody tr:last-child td {
 
418
  background: #e5e7eb;
419
  color: #22223b;
420
  }
421
+
422
  .status-open {
423
  background: #d1fae5;
424
  color: #059669;
425
  }
426
+
427
  .status-under {
428
  background: #dbeafe;
429
  color: #2563eb;
430
  }
431
+
432
  .status-closed {
433
  background: #fee2e2;
434
  color: #dc2626;
 
470
  border-color: #805ad5;
471
  }
472
 
473
+ .btn.delete {
474
+ background: #ef4444;
475
+ color: #fff;
476
+ border-color: #ef4444;
477
+ box-shadow: 0 2px 8px #ef444422;
478
+ transition: background 0.18s, color 0.18s;
479
+ }
480
+
481
+ .btn.delete:hover {
482
+ background: #991b1b;
483
+ color: #fff;
484
+ }
485
 
486
  /* Icon buttons */
487
  .icon-btn {
 
499
  align-items: center;
500
  justify-content: center;
501
  }
502
+
503
+ .icon-btn.view {
504
+ margin: 0;
505
+ padding: 0 8px;
506
+ background: none;
507
+ border: none;
508
+ cursor: pointer;
509
+ color: #2563eb;
510
+ font-size: 1.2em;
511
+ display: inline-flex;
512
+ align-items: center;
513
+ justify-content: center;
514
+ }
515
+
516
+ .icon-btn.edit {
517
+ color: #7c3aed;
518
+ }
519
+
520
+ .icon-btn.delete {
521
+ color: #ef4444;
522
+ }
523
+
524
+ .icon-btn:hover {
525
+ background: #f0f7ff;
526
+ box-shadow: 0 2px 8px #2563eb22;
527
+ }
528
+
529
+ .icon-btn.delete:hover {
530
+ background: #fff0f0;
531
+ color: #b91c1c;
532
+ }
533
+
534
+ .icon-btn.edit:hover {
535
+ background: #f3e8ff;
536
+ color: #5b21b6;
537
+ }
538
+
539
+ .icon-btn.view:hover {
540
+ background: #e0f2fe;
541
+ color: #0ea5e9;
542
+ }
543
+
544
+ .icon-btn.verify {
545
+ color: #22c55e;
546
+ }
547
+
548
+ .icon-btn.verify:hover {
549
+ background: #e0ffe6;
550
+ color: #15803d;
551
+ }
552
 
553
  .empty {
554
  text-align: center;
 
584
  display: flex;
585
  flex-direction: column;
586
  }
587
+
588
  .modal-header, .modal-footer {
589
  padding: 24px 40px 18px 40px;
590
  background: #f8fafc;
 
592
  font-size: 1.18rem;
593
  color: #23272b;
594
  }
595
+
596
  .modal-body {
597
  flex: 1 1 auto;
598
  padding: 32px 48px 32px 48px;
 
610
  border-bottom: 1px solid #e5e7eb;
611
  }
612
 
613
+ .detail-row:last-child {
614
+ border-bottom: none;
615
+ }
616
 
617
+ .detail-row span {
618
+ color: #2563eb;
619
+ font-weight: 600;
620
+ font-size: 1.07em;
621
+ }
622
 
623
+ .detail-row b {
624
+ color: #23272b;
625
+ font-weight: 500;
626
+ font-size: 1.07em;
627
+ }
628
 
629
  .detail-block {
630
  margin-top: 18px;
631
  margin-bottom: 8px;
632
  }
633
 
634
+ .detail-block .label {
635
+ font-weight: 700;
636
+ color: #2563eb;
637
+ margin-bottom: 6px;
638
+ font-size: 1.09em;
639
+ }
640
 
641
  .explain {
642
  white-space: pre-wrap;
 
659
  margin-left: 8px;
660
  }
661
 
662
+ .btn:hover {
663
+ background: #e0e7ef;
664
+ }
665
 
666
  .records-center {
667
  display: flex;
 
760
  z-index: 10;
761
  }
762
 
763
+ .modern-searchbar-container.compact {
764
+ width: auto;
765
+ margin: 0 0 0 24px;
766
+ align-items: center;
767
+ }
768
 
769
  .modern-searchbar-form {
770
  display: flex;
 
778
  position: relative;
779
  }
780
 
781
+ .modern-searchbar-form.compact {
782
+ height: 44px;
783
+ min-width: 260px;
784
+ width: 320px;
785
+ padding: 0 8px 0 10px;
786
+ }
787
 
788
  .modern-searchbar-icon {
789
  display: flex;
 
804
  height: 32px;
805
  }
806
 
807
+ .modern-searchbar-input::placeholder {
808
+ color: #e0e7ef;
809
+ opacity: 1;
810
+ font-weight: 400;
811
+ }
812
 
813
  .modern-searchbar-btn {
814
  background: #fff;
 
825
  transition: background 0.18s, color 0.18s;
826
  }
827
 
828
+ .modern-searchbar-btn:hover {
829
+ background: #ede9fe;
830
+ color: #4f46e5;
831
+ }
832
 
833
  .modern-searchbar-form.white-bg {
834
  background: #fff !important;
 
840
  color: #23272b !important;
841
  }
842
 
843
+ .modern-searchbar-input.white-bg::placeholder {
844
+ color: #64748b !important;
845
+ opacity: 1;
846
+ }
847
 
848
  .modern-searchbar-icon svg {
849
  stroke: #64748b !important;
 
864
  transition: background 0.18s, color 0.18s;
865
  }
866
 
867
+ .modern-searchbar-btn:hover {
868
+ background: #38bdf8;
869
+ color: #fff;
870
+ }
871
 
872
 
873
  .back-colo {
 
900
  width: 95vw;
901
  justify-content: flex-end;
902
  }
903
+
904
  .modern-searchbar-form.compact {
905
  width: 100%;
906
  min-width: 0;
907
  }
 
908
  }
909
 
910
  /* Modern UI header styles from infopage */
 
916
  z-index: 10;
917
  padding-bottom: 0;
918
  }
919
+
920
  .header-inner {
921
  display: flex;
922
  align-items: center;
 
924
  padding: 18px 32px 0 32px;
925
  position: relative;
926
  }
927
+
928
  .logo-cluster {
929
  display: flex;
930
  align-items: center;
931
  gap: 18px;
932
  }
933
+
934
  .logo-img-header {
935
  width: 54px;
936
  height: 54px;
 
941
  margin-top: -6px;
942
  margin-bottom: 1vh;
943
  }
944
+
945
  .py-detect-title-header {
946
  font-size: 2.1rem;
947
  font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
 
954
  margin-bottom: 1.5vh;
955
  }
956
 
957
+ .py-detect-title-header .py-letter.p {
958
+ color: #e3f6ff;
959
+ text-shadow: 0 0 6px #38bdf8;
960
+ }
961
+
962
+ .py-detect-title-header .py-letter.y {
963
+ color: #38bdf8;
964
+ text-shadow: 0 0 6px #38bdf8;
965
+ }
966
+
967
+ .py-detect-title-header .py-shape {
968
+ color: #e3f6ff;
969
+ background: #e3f6ff;
970
+ text-shadow: 0 0 6px #38bdf8;
971
+ box-shadow: 0 0 6px #38bdf8, 0 0 2px #fff;
972
+ border: 2px solid #23272b;
973
+ width: 18px;
974
+ height: 4px;
975
+ display: inline-block;
976
+ margin: 0 8px;
977
+ border-radius: 2px;
978
+ }
979
+
980
+ .py-detect-title-header .py-letter.d {
981
+ color: #e3f6ff;
982
+ text-shadow: 0 0 6px #38bdf8;
983
+ }
984
+
985
+ .py-detect-title-header .py-letter.e {
986
+ color: #38bdf8;
987
+ text-shadow: 0 0 6px #38bdf8;
988
+ }
989
+
990
+ .py-detect-title-header .py-letter.t {
991
+ color: #e3f6ff;
992
+ text-shadow: 0 0 6px #38bdf8;
993
+ }
994
+
995
+ .py-detect-title-header .py-letter.e2 {
996
+ color: #38bdf8;
997
+ text-shadow: 0 0 6px #38bdf8;
998
+ }
999
+
1000
+ .py-detect-title-header .py-letter.c {
1001
+ color: #e3f6ff;
1002
+ text-shadow: 0 0 6px #38bdf8;
1003
+ }
1004
+
1005
+ .py-detect-title-header .py-letter.t2 {
1006
+ color: #38bdf8;
1007
+ text-shadow: 0 0 6px #38bdf8;
1008
+ }
1009
 
1010
  body, main.content {
1011
  background: #f4f6fa;
1012
  min-height: 100vh;
1013
  margin: 0;
1014
+ overflow-y: auto; /* enables scrolling */
1015
  font-family: 'Segoe UI', 'Arial', 'Roboto', sans-serif;
1016
  }
1017
 
 
1026
  min-width: 320px;
1027
  padding: 0;
1028
  overflow-x: auto;
1029
+ overflow-y: visible;
1030
  }
1031
 
1032
  .record-header {
 
1089
  cursor: pointer;
1090
  transition: background 0.18s;
1091
  }
1092
+
1093
+ .record-new-btn:hover {
1094
+ background: #1e40af;
1095
+ }
1096
 
1097
  .record-table {
1098
  width: 100%;
 
1101
  background: #fff;
1102
  }
1103
 
1104
+ .record-table th, .record-table td {
1105
+ border-bottom: 1.5px solid #e5e7eb;
1106
+ padding: 12px 16px;
1107
+ text-align: left;
1108
+ font-size: 1.05rem;
1109
+ }
1110
 
1111
+ .record-table th {
1112
+ background: #f4f6fa;
1113
+ color: #222b45;
1114
+ font-weight: 700;
1115
+ }
1116
 
1117
+ .record-table tr:last-child td {
1118
+ border-bottom: none;
1119
+ }
1120
 
1121
+ .record-table tr {
1122
+ transition: background 0.15s;
1123
+ }
 
 
 
1124
 
1125
+ .record-table tr:hover {
1126
+ background: #f1f5f9;
1127
+ }
 
 
1128
 
1129
+ .record-table a {
1130
+ color: #2563eb;
1131
+ text-decoration: underline;
1132
+ cursor: pointer;
1133
+ }
1134
+
1135
+ .record-table .record-status {
1136
+ font-weight: 500;
1137
+ border-radius: 6px;
1138
+ padding: 2px 10px;
1139
+ background: #f1f5f9;
1140
+ color: #2563eb;
1141
+ font-size: 0.98em;
1142
+ }
1143
 
1144
  .vertical-sections {
1145
  display: flex;
1146
  flex-direction: column;
1147
  gap: 32px;
1148
  }
1149
+
1150
  .horizontal-sections {
1151
  display: flex;
1152
  flex-direction: column;
 
1156
  box-shadow: 0 8px 32px #2563eb22;
1157
  padding: 32px 0 32px 0;
1158
  }
1159
+
1160
  .section-block {
1161
  background: rgba(255,255,255,0.98);
1162
  border-radius: 18px;
 
1169
  border-left: 6px solid #38bdf8;
1170
  transition: box-shadow 0.2s, border 0.2s;
1171
  }
1172
+
1173
+ .section-block:hover {
1174
+ box-shadow: 0 8px 32px #2563eb33;
1175
+ border-left: 6px solid #2563eb;
1176
+ }
1177
+
1178
  .section-title {
1179
  font-size: 1.32em;
1180
  font-weight: 900;
 
1186
  align-items: center;
1187
  gap: 10px;
1188
  }
1189
+
1190
  .subgroup-row {
1191
  display: flex;
1192
  flex-direction: row;
1193
  gap: 36px;
1194
  overflow-x: auto;
1195
  }
1196
+
1197
  .subgroup-block {
1198
  min-width: 260px;
1199
  flex: 1 1 0;
 
1209
  border: 1.5px solid #e0e7ef;
1210
  transition: box-shadow 0.18s, border 0.18s, background 0.18s;
1211
  }
1212
+
1213
+ .subgroup-block:hover {
1214
+ box-shadow: 0 4px 16px #38bdf855;
1215
+ border: 1.5px solid #38bdf8;
1216
+ background: #e0f2fe;
1217
+ }
1218
+
1219
  .subgroup-title {
1220
  font-size: 1.11em;
1221
  font-weight: 700;
 
1227
  align-items: center;
1228
  gap: 8px;
1229
  }
1230
+
1231
  .fields-table {
1232
  display: flex;
1233
  flex-direction: column;
 
1237
  border-radius: 0;
1238
  padding: 0;
1239
  }
1240
+
1241
  .field-row {
1242
  display: flex;
1243
  flex-direction: row;
 
1251
  transition: background 0.18s;
1252
  gap: 32px;
1253
  }
1254
+
1255
+ .field-row:last-child {
1256
+ border-bottom: none;
1257
+ }
1258
+
1259
+ .field-row span {
1260
+ color: #1e293b;
1261
+ font-weight: 700;
1262
+ letter-spacing: 0.2px;
1263
+ min-width: 180px;
1264
+ font-size: 1.08em;
1265
+ margin-right: 32px;
1266
+ text-align: left;
1267
+ display: inline-block;
1268
+ }
1269
+
1270
+ .field-row b {
1271
+ color: #2563eb;
1272
+ font-weight: 700;
1273
+ word-break: break-word;
1274
+ letter-spacing: 0.1px;
1275
+ font-size: 1.13em;
1276
+ margin-left: 8px;
1277
+ text-align: left;
1278
+ display: inline-block;
1279
+ }
1280
+
1281
+ .field-row b:hover {
1282
+ color: #0ea5e9;
1283
+ }
1284
 
1285
  /* Stylish pills for subgroups */
1286
  .subgroup-pills {
 
1289
  gap: 12px;
1290
  margin-bottom: 18px;
1291
  }
1292
+
1293
+ .subgroup-pills button {
1294
+ background: linear-gradient(90deg, #38bdf8 0%, #2563eb 100%);
1295
+ border: none;
1296
+ border-radius: 20px;
1297
+ padding: 7px 22px;
1298
+ font-size: 1em;
1299
+ color: #fff;
1300
+ font-weight: 700;
1301
+ cursor: pointer;
1302
+ box-shadow: 0 2px 8px #2563eb22;
1303
+ transition: background 0.18s, color 0.18s, box-shadow 0.18s;
1304
+ outline: none;
1305
+ }
1306
+
1307
+ .subgroup-pills button.active,
1308
+ .subgroup-pills button:focus {
1309
+ background: linear-gradient(90deg, #2563eb 0%, #38bdf8 100%);
1310
+ color: #fff;
1311
+ box-shadow: 0 4px 16px #38bdf855;
1312
+ }
1313
+
1314
+ .subgroup-pills button:hover {
1315
+ background: linear-gradient(90deg, #0ea5e9 0%, #2563eb 100%);
1316
+ color: #fff;
1317
+ }
1318
 
1319
  .field-card {
1320
  display: flex;
 
1328
  .section-block {
1329
  padding: 18px 8vw 12px 8vw;
1330
  }
1331
+
1332
  .subgroup-block {
1333
  min-width: 180px;
1334
  padding: 12px 8px 8px 8px;
1335
  }
1336
+
1337
  .field-row span {
1338
  min-width: 120px;
1339
  }
1340
  }
1341
+
1342
  @media (max-width: 600px) {
1343
  .section-block {
1344
  padding: 10px 2vw 8px 2vw;
1345
  }
1346
+
1347
  .subgroup-block {
1348
  min-width: 120px;
1349
  padding: 8px 2px 6px 2px;
1350
  }
1351
+
1352
  .field-row span {
1353
  min-width: 80px;
1354
  font-size: 0.98em;
1355
  }
1356
+
1357
  .field-row b {
1358
  font-size: 1em;
1359
  }
 
1370
  border-radius: 12px;
1371
  box-shadow: 0 2px 8px #2563eb11;
1372
  }
1373
+
1374
+ .filter-bar select {
1375
+ padding: 6px 18px;
1376
+ border-radius: 6px;
1377
+ border: 1.5px solid #cbd5e1;
1378
+ font-size: 1em;
1379
+ color: #2563eb;
1380
+ background: #fff;
1381
+ font-weight: 600;
1382
+ outline: none;
1383
+ transition: border 0.15s;
1384
+ }
1385
+
1386
+ .filter-bar select:focus {
1387
+ border: 1.5px solid #38bdf8;
1388
+ }
1389
+
1390
+ .filter-bar button {
1391
+ padding: 6px 18px;
1392
+ border-radius: 6px;
1393
+ border: none;
1394
+ font-size: 1em;
1395
+ font-weight: 700;
1396
+ cursor: pointer;
1397
+ background: #2563eb;
1398
+ color: #fff;
1399
+ transition: background 0.15s;
1400
+ }
1401
+
1402
+ .filter-bar button:hover {
1403
+ background: #0ea5e9;
1404
+ }
1405
+
1406
+ .filter-bar button:last-child {
1407
+ background: #f87171;
1408
+ color: #fff;
1409
+ margin-left: 4px;
1410
+ }
1411
+
1412
+ .filter-bar button:last-child:hover {
1413
+ background: #ef4444;
1414
+ }
1415
 
1416
  .analytics-summary {
1417
  display: flex;
1418
  gap: 32px;
1419
  margin: 18px 0 8px 0;
1420
  }
1421
+
1422
  .summary-card {
1423
  background: #f8fafc;
1424
  border-radius: 12px;
 
1427
  min-width: 120px;
1428
  text-align: center;
1429
  }
1430
+
1431
+ .summary-card .summary-label {
1432
+ font-size: 1em;
1433
+ color: #64748b;
1434
+ font-weight: 600;
1435
+ margin-bottom: 6px;
1436
+ }
1437
+
1438
+ .summary-card .summary-value {
1439
+ font-size: 2em;
1440
+ font-weight: 900;
1441
+ color: #2563eb;
1442
+ }
1443
+
1444
+ .summary-card.open .summary-value {
1445
+ color: #059669;
1446
+ }
1447
+
1448
+ .summary-card.closed .summary-value {
1449
+ color: #dc2626;
1450
+ }
1451
 
1452
  .record-meta {
1453
  color: #64748b;
 
1459
  }
1460
 
1461
 
1462
+ /* Footer */
1463
+ footer {
1464
+ background: linear-gradient(to right, #011022, #01030a);
1465
+ color: #fff;
1466
+ text-align: center;
1467
+ padding: 10px 0px;
1468
+ position: relative;
1469
+ bottom: 0;
1470
+ left: 0;
1471
+ width: 100%;
1472
+ margin-top: 40px;
1473
+ }
1474
+
src/app/recordpage/recordpage.component.html CHANGED
@@ -95,8 +95,8 @@
95
  </tr>
96
  </thead>
97
  <tbody>
98
- <tr *ngFor="let c of rows, let i = index">
99
- <td>{{ i + 1 }}</td>
100
  <td><a (click)="openDetails(c, i)">{{ c.caseId || '—' }}</a></td>
101
  <td>
102
  <span class="status-label"
@@ -117,16 +117,16 @@
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(i)" title="Verify">
121
  <i class="fas fa-user-check"></i>
122
  </button>
123
- <button class="icon-btn view" (click)="openDetails(c, i)" title="View">
124
  <i class="fas fa-eye"></i>
125
  </button>
126
- <button class="icon-btn edit" (click)="editCase(c, i)" title="Edit">
127
  <i class="fas fa-edit"></i>
128
  </button>
129
- <button class="icon-btn delete" (click)="deleteCase(i)" title="Delete">
130
  <i class="fas fa-trash"></i>
131
  </button>
132
  </td>
@@ -136,6 +136,76 @@
136
  </tr>
137
  </tbody>
138
  </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  </div>
140
 
141
  <!-- Modal -->
@@ -154,26 +224,37 @@
154
  <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
155
  <div class="section-block">
156
  <div class="section-title">{{ sectionKey === 'crime' ? 'Crime Details' : sectionKey === 'suspect' ? 'Suspect Details' : 'Evidence and Documents' }}</div>
157
- <div class="subgroup-pills">
158
- <button *ngFor="let subgroup of getSubgroups(sectionKey)"
159
- [class.active]="selectedSubgroup[sectionKey] === subgroup"
160
- (click)="selectSubgroup(sectionKey, subgroup)">
161
- {{ subgroup }}
162
- </button>
163
- </div>
164
- <div class="fields-grid">
165
- <ng-container *ngFor="let field of getFieldsForSubgroup(sectionKey, selectedSubgroup[sectionKey])">
166
- <div class="field-card">
167
- <span>{{ field }}</span>
168
- <b>{{ getFieldValue(sc, sectionKey, field) }}</b>
169
- </div>
170
- </ng-container>
171
- </div>
172
  </div>
173
  </ng-container>
 
 
 
 
 
 
 
 
 
 
174
  </div>
175
  </div>
176
  <div class="modal-footer">
177
  <button type="button" class="btn" (click)="closeDetails()">Close</button>
178
  </div>
179
  </div>
 
 
 
 
 
 
95
  </tr>
96
  </thead>
97
  <tbody>
98
+ <tr *ngFor="let c of rows; let i = index">
99
+ <td>{{ (currentPage - 1) * pageSize + i + 1 }}</td>
100
  <td><a (click)="openDetails(c, i)">{{ c.caseId || '—' }}</a></td>
101
  <td>
102
  <span class="status-label"
 
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>
 
136
  </tr>
137
  </tbody>
138
  </table>
139
+
140
+ <!-- Pagination Controls -->
141
+ <div class="pagination-controls" style="display:flex;justify-content:center;align-items:center;margin:20px 0;gap:10px;">
142
+ <style>
143
+ .pagination-controls button {
144
+ border: none;
145
+ background: #f3f4f6;
146
+ color: #333;
147
+ border-radius: 8px;
148
+ padding: 0 16px;
149
+ min-width: 40px;
150
+ min-height: 40px;
151
+ font-size: 1.1em;
152
+ font-weight: 500;
153
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
154
+ transition: background 0.2s, color 0.2s, transform 0.2s;
155
+ cursor: pointer;
156
+ outline: none;
157
+ }
158
+
159
+ .pagination-controls button:hover:not(:disabled),
160
+ .pagination-controls button:focus:not(:disabled) {
161
+ background: #e3eafe;
162
+ color: #1976d2;
163
+ transform: scale(1.08);
164
+ }
165
+
166
+ .pagination-controls button.active {
167
+ background: #1976d2;
168
+ color: #fff;
169
+ font-weight: bold;
170
+ box-shadow: 0 0 0 2px #90caf9;
171
+ animation: pulseActive 1s infinite;
172
+ }
173
+
174
+ @keyframes pulseActive {
175
+ 0% {
176
+ box-shadow: 0 0 0 2px #90caf9;
177
+ }
178
+
179
+ 50% {
180
+ box-shadow: 0 0 0 6px #90caf9;
181
+ }
182
+
183
+ 100% {
184
+ box-shadow: 0 0 0 2px #90caf9;
185
+ }
186
+ }
187
+
188
+ .pagination-controls span {
189
+ font-size: 1.2em;
190
+ color: #888;
191
+ padding: 0 8px;
192
+ }
193
+ </style>
194
+ <button (click)="prevPage()" [disabled]="currentPage === 1">«</button>
195
+ <ng-container *ngFor="let page of getPagination()">
196
+ <button *ngIf="page !== '...'" (click)="goToPage(page)" [class.active]="currentPage === page">{{ page }}</button>
197
+ <span *ngIf="page === '...'">...</span>
198
+ </ng-container>
199
+ <button (click)="nextPage()" [disabled]="currentPage === totalPages">»</button>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- Results summary and page size selector -->
204
+ <div style="display:flex;align-items:center;justify-content:flex-start;gap:24px;margin-bottom:16px;">
205
+ <span style="font-size:1.1em;">Results: {{ resultsStart }} - {{ resultsEnd }} of {{ resultsTotal }}</span>
206
+ <select [(ngModel)]="pageSize" (change)="onPageSizeChange(pageSize)" style="padding:4px 12px;border-radius:8px;font-size:1em;">
207
+ <option *ngFor="let size of pageSizeOptions" [value]="size">{{ size }}</option>
208
+ </select>
209
  </div>
210
 
211
  <!-- Modal -->
 
224
  <ng-container *ngFor="let sectionKey of ['crime', 'suspect', 'notes']">
225
  <div class="section-block">
226
  <div class="section-title">{{ sectionKey === 'crime' ? 'Crime Details' : sectionKey === 'suspect' ? 'Suspect Details' : 'Evidence and Documents' }}</div>
227
+ <ng-container *ngFor="let subgroup of getSubgroups(sectionKey)">
228
+ <div class="subgroup-title" style="margin:10px 0 4px 0;font-weight:600;color:#1976d2;">{{ subgroup }}</div>
229
+ <div class="fields-grid">
230
+ <ng-container *ngFor="let field of getFieldsForSubgroup(sectionKey, subgroup)">
231
+ <div class="field-card" style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;">
232
+ <span style="font-weight:500;color:#333;">{{ field }}</span>
233
+ <span style="color:#444;">{{ getFieldValue(sc, sectionKey, field) }}</span>
234
+ </div>
235
+ </ng-container>
236
+ </div>
237
+ </ng-container>
 
 
 
 
238
  </div>
239
  </ng-container>
240
+ <!-- Show all other fields from formData not in the above structure -->
241
+ <div class="section-block">
242
+ <div class="section-title">All Entered Information</div>
243
+ <div class="fields-grid">
244
+ <div class="field-card" *ngFor="let key of objectKeys(sc)" style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;">
245
+ <span style="font-weight:500;color:#333;">{{ key }}</span>
246
+ <span style="color:#444;">{{ getValue(sc, key) }}</span>
247
+ </div>
248
+ </div>
249
+ </div>
250
  </div>
251
  </div>
252
  <div class="modal-footer">
253
  <button type="button" class="btn" (click)="closeDetails()">Close</button>
254
  </div>
255
  </div>
256
+
257
+ <!-- Footer from provided design -->
258
+ <footer>
259
+ <p>© 2025 Pykara Technologies Pvt. Ltd. All rights reserved.</p>
260
+ </footer>
src/app/recordpage/recordpage.component.ts CHANGED
@@ -11,6 +11,23 @@ import { InfopageComponent } from '../infopage/infopage.component';
11
  export class RecordpageComponent implements OnInit {
12
  cases: PoliceCase[] = [];
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  // modal
15
  showDetails = false;
16
  selectedCase: PoliceCase | null = null;
@@ -227,6 +244,7 @@ export class RecordpageComponent implements OnInit {
227
  (!this.filterLocation || c.police?.address === this.filterLocation) &&
228
  (!this.filterOfficer || c.police?.name === this.filterOfficer)
229
  );
 
230
  }
231
 
232
  resetFilters() {
@@ -278,19 +296,21 @@ export class RecordpageComponent implements OnInit {
278
 
279
  // Update your table to use filteredCases instead of rows
280
  get rows(): PoliceCase[] {
281
- return this.filteredCases;
282
  }
283
 
284
  openDetails(c: PoliceCase, i: number): void {
285
  this.selectedCase = c;
286
  this.selectedIndex = i;
287
  this.showDetails = true;
 
288
  }
289
 
290
  closeDetails(): void {
291
  this.showDetails = false;
292
  this.selectedCase = null;
293
  this.selectedIndex = -1;
 
294
  }
295
 
296
  editCase(c: PoliceCase, i: number): void {
@@ -336,4 +356,54 @@ export class RecordpageComponent implements OnInit {
336
  get closedCases(): number {
337
  return this.cases.filter(c => c.status === 'Closed').length;
338
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  }
 
11
  export class RecordpageComponent implements OnInit {
12
  cases: PoliceCase[] = [];
13
 
14
+ // Pagination
15
+ currentPage: number = 1;
16
+ pageSize: number = 5; // Change to 20 if you want 20 per page
17
+ get totalPages(): number {
18
+ return Math.ceil(this.filteredCases.length / this.pageSize) || 1;
19
+ }
20
+ get pagedCases(): PoliceCase[] {
21
+ const start = (this.currentPage - 1) * this.pageSize;
22
+ return this.filteredCases.slice(start, start + this.pageSize);
23
+ }
24
+ setPage(page: number) {
25
+ if (page < 1 || page > this.totalPages) return;
26
+ this.currentPage = page;
27
+ }
28
+ nextPage() { this.setPage(this.currentPage + 1); }
29
+ prevPage() { this.setPage(this.currentPage - 1); }
30
+
31
  // modal
32
  showDetails = false;
33
  selectedCase: PoliceCase | null = null;
 
244
  (!this.filterLocation || c.police?.address === this.filterLocation) &&
245
  (!this.filterOfficer || c.police?.name === this.filterOfficer)
246
  );
247
+ this.currentPage = 1; // Reset to first page on filter
248
  }
249
 
250
  resetFilters() {
 
296
 
297
  // Update your table to use filteredCases instead of rows
298
  get rows(): PoliceCase[] {
299
+ return this.pagedCases;
300
  }
301
 
302
  openDetails(c: PoliceCase, i: number): void {
303
  this.selectedCase = c;
304
  this.selectedIndex = i;
305
  this.showDetails = true;
306
+ document.body.classList.add('modal-open');
307
  }
308
 
309
  closeDetails(): void {
310
  this.showDetails = false;
311
  this.selectedCase = null;
312
  this.selectedIndex = -1;
313
+ document.body.classList.remove('modal-open');
314
  }
315
 
316
  editCase(c: PoliceCase, i: number): void {
 
356
  get closedCases(): number {
357
  return this.cases.filter(c => c.status === 'Closed').length;
358
  }
359
+
360
+ getPagination(): (number | string)[] {
361
+ const pages: (number | string)[] = [];
362
+ const total = this.totalPages;
363
+ if (total <= 7) {
364
+ for (let i = 1; i <= total; i++) pages.push(i);
365
+ } else {
366
+ if (this.currentPage <= 4) {
367
+ for (let i = 1; i <= 5; i++) pages.push(i);
368
+ pages.push('...');
369
+ pages.push(total);
370
+ } else if (this.currentPage >= total - 3) {
371
+ pages.push(1);
372
+ pages.push('...');
373
+ for (let i = total - 4; i <= total; i++) pages.push(i);
374
+ } else {
375
+ pages.push(1);
376
+ pages.push('...');
377
+ for (let i = this.currentPage - 1; i <= this.currentPage + 1; i++) pages.push(i);
378
+ pages.push('...');
379
+ pages.push(total);
380
+ }
381
+ }
382
+ return pages;
383
+ }
384
+
385
+ goToPage(page: string | number) {
386
+ if (typeof page === 'number') {
387
+ this.setPage(page);
388
+ }
389
+ }
390
+
391
+ pageSizeOptions: number[] = [5, 10, 20, 50];
392
+ onPageSizeChange(size: number) {
393
+ this.pageSize = size;
394
+ this.currentPage = 1;
395
+ }
396
+ get resultsStart(): number {
397
+ return this.filteredCases.length === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1;
398
+ }
399
+ get resultsEnd(): number {
400
+ return Math.min(this.currentPage * this.pageSize, this.filteredCases.length);
401
+ }
402
+ get resultsTotal(): number {
403
+ return this.filteredCases.length;
404
+ }
405
+
406
+ goToDetect(caseId: string): void {
407
+ this.router.navigate(['/py-detect'], { state: { caseId } });
408
+ }
409
  }
src/app/shared/case-store.service.ts CHANGED
@@ -4,7 +4,7 @@ export interface PoliceCase {
4
  // Optional metadata used by your UI
5
  caseId?: string;
6
  dateTime?: string;
7
- status?: 'Open' | 'Under Investigation' | 'Closed';
8
  crime: string;
9
  police: {
10
  name: string;
@@ -23,6 +23,12 @@ export interface PoliceCase {
23
  occupation?: string;
24
  };
25
  lastUpdated?: string;
 
 
 
 
 
 
26
  }
27
 
28
  @Injectable({ providedIn: 'root' })
@@ -53,6 +59,15 @@ export class CaseStoreService {
53
  }
54
  }
55
 
 
 
 
 
 
 
 
 
 
56
  /**
57
  * Convenience: map Info page reactive-form value to PoliceCase and store it.
58
  * Call with the whole this.form.value from InfopageComponent.
@@ -88,6 +103,45 @@ export class CaseStoreService {
88
  this.addPoliceCase(mapped);
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  /** Persist to localStorage (safe to keep; remove if not needed) */
92
  private save(): void {
93
  try { localStorage.setItem(this.storageKey, JSON.stringify(this.cases)); } catch { }
 
4
  // Optional metadata used by your UI
5
  caseId?: string;
6
  dateTime?: string;
7
+ status?: 'Open' | 'Under Investigation' | 'Closed' | 'Archived';
8
  crime: string;
9
  police: {
10
  name: string;
 
23
  occupation?: string;
24
  };
25
  lastUpdated?: string;
26
+ nextAction?: string;
27
+ casePriority?: 'High' | 'Medium' | 'Low';
28
+ reportedBy?: string;
29
+ verifiedBy?: string;
30
+ briefDescription?: string;
31
+ caseCategory?: string;
32
  }
33
 
34
  @Injectable({ providedIn: 'root' })
 
59
  }
60
  }
61
 
62
+ /** Update by caseId */
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
+
71
  /**
72
  * Convenience: map Info page reactive-form value to PoliceCase and store it.
73
  * Call with the whole this.form.value from InfopageComponent.
 
103
  this.addPoliceCase(mapped);
104
  }
105
 
106
+ /** Add or update from Info page form */
107
+ addOrUpdateFromInfoForm(formValue: any): void {
108
+ const crime = (formValue && formValue.crime) || {};
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: {
117
+ name: notes.officerInCharge || '—',
118
+ station: '—',
119
+ address: crime.location || '—',
120
+ pincode: '',
121
+ dutyPerson: notes.officerInCharge || '—',
122
+ modeOfCrime: crime.crimeType || '—',
123
+ information: notes.initialFindings || ''
124
+ },
125
+ accused: {
126
+ name: suspect.fullName || '—',
127
+ age: suspect.age || '—',
128
+ gender: suspect.gender || '—',
129
+ address: suspect.address || '—',
130
+ occupation: suspect.alias || ''
131
+ },
132
+ reportedBy: crime.reportedBy || '',
133
+ verifiedBy: notes.verifiedBy || '',
134
+ briefDescription: crime.briefDescription || '',
135
+ };
136
+ const idx = this.cases.findIndex(c => c.caseId === mapped.caseId);
137
+ if (idx !== -1) {
138
+ this.cases[idx] = mapped;
139
+ this.save();
140
+ } else {
141
+ this.addPoliceCase(mapped);
142
+ }
143
+ }
144
+
145
  /** Persist to localStorage (safe to keep; remove if not needed) */
146
  private save(): void {
147
  try { localStorage.setItem(this.storageKey, JSON.stringify(this.cases)); } catch { }