diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f02a44e0e..09d2415c2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -65,6 +65,7 @@ import { SftModule } from './sft/sft.module'; import { DigitalModule } from './digital/digital.module'; import { LetterMgmtModule } from './letter-mgmt/letter-mgmt.module'; import { AccountsModule } from './accounts/accounts.module'; +import { ElectionModule } from './election/election.module'; import { PortalModule } from '@angular/cdk/portal'; /** Main Routing Module */ @@ -166,6 +167,7 @@ export function HttpLoaderFactory(http: HttpClient) { DigitalModule, LetterMgmtModule, AccountsModule, + ElectionModule, AppRoutingModule, NotFoundComponent, CallbackComponent, diff --git a/src/app/clerk/disbursement-review/disbursement-review.component.scss b/src/app/clerk/disbursement-review/disbursement-review.component.scss index 4364d9c8b..c3a42906e 100644 --- a/src/app/clerk/disbursement-review/disbursement-review.component.scss +++ b/src/app/clerk/disbursement-review/disbursement-review.component.scss @@ -6,78 +6,160 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// ── Variables ────────────────────────────────────────────────────────────── +$brand: #0f4f3c; +$brand-light: #e6f4f0; +$primary: #4a6cf7; +$success: #16a34a; +$success-bg: #f0fdf4; +$danger: #dc2626; +$danger-bg: #fef2f2; +$warning: #d97706; +$warning-bg: #fffbeb; +$text-dark: #0f172a; +$text-mid: #475569; +$text-soft: #94a3b8; +$border: #e2e8f0; +$bg-page: #f1f5f9; +$bg-white: #fff; +$radius: 12px; +$shadow-sm: 0 1px 4px rgb(0 0 0 / 6%); +$shadow-md: 0 4px 16px rgb(0 0 0 / 9%); + :host { display: block; } +// ── Page shell ───────────────────────────────────────────────────────────── .dri { display: flex; flex-direction: column; min-height: 100vh; - background: #f8fafc; + background: $bg-page; } +// ── Nav bar ──────────────────────────────────────────────────────────────── +.cashier-nav { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + background: $bg-white; + border-bottom: 1px solid $border; + box-shadow: $shadow-sm; + + &__item { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 7px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + border: none; + cursor: pointer; + transition: all 0.15s; + + &--primary { + background: $brand-light; + color: $brand; + } + + &--active { + background: $brand; + color: #fff; + } + } + + &__esc-hint { + margin-left: auto; + font-size: 12px; + color: $text-soft; + cursor: pointer; + padding: 6px 12px; + border-radius: 6px; + transition: all 0.15s; + font-weight: 500; + + &:hover { + background: $bg-page; + color: $text-mid; + } + } +} + +// ── Loading / empty ──────────────────────────────────────────────────────── .dri-loading { display: flex; align-items: center; gap: 12px; justify-content: center; - padding: 60px 24px; + padding: 80px 24px; font-size: 15px; - color: #64748b; + color: $text-soft; &__icon { - font-size: 28px; - width: 28px; - height: 28px; - color: #94a3b8; + font-size: 32px; + width: 32px; + height: 32px; + color: $text-soft; } } +// ── Body ─────────────────────────────────────────────────────────────────── .dri-body { - padding: 20px; + padding: 24px 20px 48px; display: flex; flex-direction: column; - gap: 16px; - max-width: 900px; + gap: 14px; + max-width: 920px; margin: 0 auto; width: 100%; } +// ── Cards ────────────────────────────────────────────────────────────────── .dri-card { - background: #fff; - border-radius: 10px; - border: 1px solid #e2e8f0; + background: $bg-white; + border-radius: $radius; + border: 1px solid $border; + box-shadow: $shadow-sm; overflow: hidden; + transition: box-shadow 0.15s; - &--header { - border-top: 3px solid #0f4f3c; + &:hover { + box-shadow: $shadow-md; } - &--rejected { - border-top: 3px solid #ef4444; + &--header { + border-left: 4px solid $brand; + box-shadow: $shadow-md; } &--approved { - border-top: 3px solid #22c55e; + border-left: 4px solid $success; + } + + &--rejected { + border-left: 4px solid $danger; } &--failed { - border-top: 3px solid #dc2626; - background: #fff5f5; + border-left: 4px solid $danger; + background: $danger-bg; } &--processing { - border-top: 3px solid #f59e0b; - background: #fffbeb; + border-left: 4px solid $warning; + background: $warning-bg; } } +// ── Header card ──────────────────────────────────────────────────────────── .dri-card-header { - padding: 16px 20px; + padding: 18px 22px; display: flex; flex-direction: column; - gap: 10px; + gap: 12px; &__left { display: flex; @@ -88,107 +170,115 @@ &__meta { display: flex; flex-wrap: wrap; - gap: 16px; - color: #64748b; + gap: 14px; + color: $text-mid; + padding-top: 4px; + border-top: 1px solid $border; } } .dri-voucher { font-family: 'Courier New', monospace; - font-size: 20px; + font-size: 22px; font-weight: 700; - color: #1e40af; + color: $brand; letter-spacing: 0.06em; + background: $brand-light; + padding: 4px 14px; + border-radius: 8px; } .dri-status { display: inline-flex; align-items: center; - padding: 3px 12px; + padding: 4px 14px; border-radius: 20px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.04em; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; &--pending { - background: #fef3c7; - color: #92400e; + background: $warning-bg; + color: $warning; + border: 1px solid #fde68a; } &--approved { - background: #dcfce7; - color: #166534; + background: $success-bg; + color: $success; + border: 1px solid #bbf7d0; } &--rejected { - background: #fee2e2; - color: #991b1b; + background: $danger-bg; + color: $danger; + border: 1px solid #fecaca; } &--processing { - background: #fef3c7; - color: #78350f; + background: $warning-bg; + color: $warning; + border: 1px solid #fde68a; } &--failed { - background: #fee2e2; - color: #7f1d1d; + background: $danger-bg; + color: $danger; + border: 1px solid #fecaca; } } -.dri-processing-text { - padding: 6px 20px 14px; - margin: 0; - font-size: 13px; - color: #78350f; -} - .dri-meta-item { display: flex; align-items: center; - gap: 4px; - font-size: 12px; + gap: 5px; + font-size: 12.5px; + color: $text-mid; + font-weight: 500; mat-icon { - font-size: 14px; - width: 14px; - height: 14px; - color: #94a3b8; + font-size: 15px; + width: 15px; + height: 15px; + color: $text-soft; } } +// ── Section title ────────────────────────────────────────────────────────── .dri-section-title { display: flex; align-items: center; - gap: 8px; - padding: 12px 20px; - font-size: 12px; - font-weight: 700; - color: #475569; + gap: 9px; + padding: 13px 22px; + font-size: 11px; + font-weight: 800; + color: $text-mid; text-transform: uppercase; - letter-spacing: 0.06em; - border-bottom: 1px solid #f1f5f9; - background: #f8fafc; + letter-spacing: 0.07em; + border-bottom: 1px solid $border; + background: linear-gradient(90deg, #f8fafc 0%, $bg-white 100%); mat-icon { font-size: 16px; width: 16px; height: 16px; - color: #0f4f3c; + color: $brand; } } +// ── Detail grid ──────────────────────────────────────────────────────────── .dri-detail-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - padding: 16px 20px; - gap: 12px 24px; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + padding: 18px 22px; + gap: 16px 28px; } .dri-detail-item { display: flex; flex-direction: column; - gap: 3px; + gap: 4px; &--full { grid-column: 1 / -1; @@ -196,46 +286,57 @@ } .dri-detail-label { - font-size: 11px; - font-weight: 600; - color: #94a3b8; + font-size: 10.5px; + font-weight: 700; + color: $text-soft; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; } .dri-detail-value { - font-size: 14px; - color: #0d1b2a; + font-size: 14.5px; + color: $text-dark; font-weight: 500; + line-height: 1.3; } .dri-amount { - font-weight: 700; - font-size: 18px; - color: #dc2626; + font-weight: 800; + font-size: 22px; + color: $danger; + letter-spacing: -0.3px; } +// ── Entries table ────────────────────────────────────────────────────────── .dri-cr-table { width: 100%; border-collapse: collapse; - font-size: 13px; + font-size: 13.5px; th, td { - padding: 10px 16px; + padding: 11px 18px; text-align: left; border-bottom: 1px solid #f1f5f9; } th { - font-size: 11px; - font-weight: 700; - color: #64748b; + font-size: 10.5px; + font-weight: 800; + color: $text-soft; text-transform: uppercase; - letter-spacing: 0.06em; + letter-spacing: 0.07em; background: #f8fafc; } + tbody tr:hover { + background: #fafbff; + } + + tbody tr:last-of-type td { + border-bottom: none; + } + .num { text-align: right; } @@ -243,63 +344,85 @@ .dri-cr-idx { width: 36px; - color: #94a3b8; + color: $text-soft; font-size: 12px; + font-weight: 600; } .dri-type-badge { display: inline-flex; align-items: center; - padding: 2px 8px; - border-radius: 10px; - background: #f1f5f9; - font-size: 11px; - font-weight: 600; - color: #475569; + padding: 3px 10px; + border-radius: 20px; + background: #eff6ff; + border: 1px solid #bfdbfe; + font-size: 10.5px; + font-weight: 700; + color: #1d4ed8; text-transform: uppercase; + letter-spacing: 0.04em; } .dri-cr-amount { font-weight: 700; - color: #166534; + color: $success; + font-size: 14px; } .dri-cr-total-row { - background: #f0fdf4; + background: $success-bg; td { border-top: 2px solid #bbf7d0; - padding: 10px 16px; + padding: 12px 18px; } } .dri-cr-total-label { font-weight: 700; - color: #166534; + color: $success; text-align: right; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.04em; } .dri-cr-total-amount { - font-weight: 700; - color: #166534; - font-size: 15px; + font-weight: 800; + color: $success; + font-size: 16px; +} + +// ── Status texts ─────────────────────────────────────────────────────────── +.dri-processing-text { + padding: 10px 22px 16px; + margin: 0; + font-size: 13.5px; + color: $warning; + font-weight: 500; } .dri-rejection-text { - padding: 14px 20px; + padding: 14px 22px; margin: 0; - font-size: 13px; - color: #dc2626; + font-size: 13.5px; + color: $danger; font-style: italic; + font-weight: 500; } .dri-txn-refs { - padding: 14px 20px; + padding: 14px 22px; margin: 0; - font-size: 12px; - color: #166534; + font-size: 13px; + color: $success; + font-weight: 600; + font-family: 'Courier New', monospace; + background: $success-bg; + border-radius: 0 0 $radius $radius; } +// ── Action buttons ───────────────────────────────────────────────────────── .dri-actions { display: flex; gap: 12px; @@ -310,15 +433,16 @@ .dri-btn { display: inline-flex; align-items: center; - gap: 6px; - border-radius: 8px; + gap: 7px; + border-radius: 10px; font-family: inherit; - font-weight: 600; + font-weight: 700; font-size: 14px; cursor: pointer; border: none; - transition: background 0.15s; - padding: 10px 20px; + transition: all 0.15s; + padding: 11px 24px; + letter-spacing: 0.01em; mat-icon { font-size: 18px; @@ -329,65 +453,81 @@ &:disabled { opacity: 0.5; cursor: not-allowed; + transform: none !important; + box-shadow: none !important; } &--approve { - background: #0f4f3c; + background: linear-gradient(135deg, $brand 0%, #1a7a5e 100%); color: #fff; + box-shadow: 0 4px 14px rgb(15 79 60 / 35%); &:hover:not(:disabled) { - background: #0a3d2e; + transform: translateY(-1px); + box-shadow: 0 6px 18px rgb(15 79 60 / 45%); } } &--reject { - background: #fee2e2; - color: #991b1b; - border: 1px solid #fca5a5; + background: $danger-bg; + color: $danger; + border: 1.5px solid #fca5a5; &:hover:not(:disabled) { - background: #fecaca; + background: #fee2e2; } } &--ghost { background: #f1f5f9; - color: #475569; + color: $text-mid; + border: 1px solid $border; &:hover:not(:disabled) { background: #e2e8f0; + color: $text-dark; } } } +// ── Reject form ──────────────────────────────────────────────────────────── .dri-reject-form { display: flex; flex-direction: column; gap: 10px; width: 100%; - max-width: 480px; + max-width: 520px; + background: $danger-bg; + border: 1.5px solid #fca5a5; + border-radius: $radius; + padding: 18px 20px; } .dri-reject-label { font-size: 12px; - font-weight: 700; - color: #991b1b; + font-weight: 800; + color: $danger; + text-transform: uppercase; + letter-spacing: 0.06em; } .dri-reject-textarea { width: 100%; padding: 10px 12px; - border: 1px solid #fca5a5; + border: 1.5px solid #fca5a5; border-radius: 8px; font-family: inherit; - font-size: 13px; + font-size: 13.5px; resize: vertical; - color: #0d1b2a; + color: $text-dark; outline: none; + background: $bg-white; box-sizing: border-box; + transition: border-color 0.15s; &:focus { - border-color: #ef4444; + border-color: $danger; + box-shadow: 0 0 0 3px rgb(220 38 38 / 10%); } } @@ -396,6 +536,8 @@ gap: 8px; } +// ── Mono ─────────────────────────────────────────────────────────────────── .cell-mono { font-family: 'Courier New', monospace; + letter-spacing: 0.02em; } diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index 3eaa4cde0..971979e4f 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -97,6 +97,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges @Input() clientTemplate: any; /** Existing client data for edit mode (null for create) */ @Input() clientData: any = null; + /** Pre-fill data from "Create as Client" on a manual family member */ + @Input() prefillData: any = null; /** Read-only mode — disables form editing until user clicks edit icon */ @Input() readOnly = false; /** Create Client Form */ @@ -415,9 +417,44 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges this.buildDependencies(); if (this.clientData) { this.prefillForEdit(); + } else if (this.prefillData) { + this.prefillFromFamilyMember(); } } + /** + * Pre-fills the General step form from a manually-entered family member + * when the user clicks "Create as Client" on the family members tab. + */ + private prefillFromFamilyMember(): void { + const d = this.prefillData; + if (!d) return; + + // Parse DOB — Fineract returns [yyyy, mm, dd] array or ISO string + let dob: Date | undefined; + if (d.dateOfBirth) { + if (Array.isArray(d.dateOfBirth)) { + dob = new Date(d.dateOfBirth[0], d.dateOfBirth[1] - 1, d.dateOfBirth[2]); + } else { + const parsed = new Date(d.dateOfBirth); + if (!isNaN(parsed.getTime())) dob = parsed; + } + } + + // Patch base fields first + this.createClientForm.patchValue({ + mobileNo: d.mobileNo || '', + ...(dob ? { dateOfBirth: dob } : {}) + }); + + // Name controls are added dynamically by buildDependencies via legalFormId — set after tick + setTimeout(() => { + if (this.createClientForm.get('firstname') && d.firstname) { + this.createClientForm.get('firstname').setValue(d.firstname); + } + }); + } + /** * Pre-fills the form with existing client data for edit mode. * Called AFTER buildDependencies so name controls already exist. diff --git a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html index 9e80a2a3c..a6b6405de 100644 --- a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html +++ b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.html @@ -33,11 +33,22 @@ - person + + {{ isLinkedClient(member) ? 'person_pin' : 'person' }} + {{ displayName(member) }} @if (getRelationship(member)) { {{ getRelationship(member) }} } + @if (isLinkedClient(member)) { + + person_pin Linked Client + + } @else { + + edit_note Manual Entry + + } @if (member.dateOfBirth) { @@ -66,14 +77,42 @@ Delete + @if (!isLinkedClient(member)) { + + } }
+ +
+
Entry Type
+
+ @if (isLinkedClient(member)) { + + person_pin Linked Existing Client + + } @else { + + edit_note Manually Entered + + } +
+
+ @if (isLinkedClient(member)) { +
+
CBS Client ID
+
+ {{ member.linkedClientCif || member.linkedClientId }} +
+
+ } @if (member.firstName) {
Name
-
{{ member.firstName }}
+
{{ displayName(member) }}
} @if (getRelationship(member)) { diff --git a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.scss b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.scss index bb1ef6dba..73766342c 100644 --- a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.scss +++ b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.scss @@ -182,6 +182,41 @@ $orange-light: #fff3e0; background: $orange-light; color: $orange; } + + &--linked { + display: inline-flex; + align-items: center; + gap: 3px; + background: $green-light; + color: $green; + border: 1px solid color.adjust($green-light, $lightness: -8%); + + mat-icon { + font-size: 12px; + height: 12px; + width: 12px; + } + } + + &--manual { + display: inline-flex; + align-items: center; + gap: 3px; + background: $gray-100; + color: $gray-700; + border: 1px solid $gray-200; + + mat-icon { + font-size: 12px; + height: 12px; + width: 12px; + } + } + } + + &__mat-icon--linked { + background: $green-light; + color: $green; } &__desc { @@ -213,6 +248,29 @@ $orange-light: #fff3e0; font-weight: 600 !important; height: 32px !important; } + + &__create-client { + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; + border-radius: 6px !important; + font-size: 12px !important; + font-weight: 700 !important; + height: 32px !important; + background: $green !important; + color: $white !important; + margin-left: auto; + + mat-icon { + font-size: 15px; + height: 15px; + width: 15px; + } + + &:hover { + background: color.adjust($green, $lightness: -8%) !important; + } + } } // Details grid @@ -269,3 +327,32 @@ $orange-light: #fff3e0; } } } + +// Entry-type pill inside expanded grid +.nom-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; + padding: 3px 10px; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + + &--linked { + background: $green-light; + color: $green; + border: 1px solid color.adjust($green-light, $lightness: -8%); + } + + &--manual { + background: $gray-100; + color: $gray-700; + border: 1px solid $gray-200; + } +} diff --git a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts index b377c45d7..b8e2be53c 100644 --- a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts +++ b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts @@ -8,7 +8,7 @@ /** Angular Imports */ import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; /** Custom Components */ @@ -60,6 +60,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; }) export class FamilyMembersTabComponent { private route = inject(ActivatedRoute); + private router = inject(Router); private clientsService = inject(ClientsService); private dialog = inject(MatDialog); private translateService = inject(TranslateService); @@ -117,7 +118,31 @@ export class FamilyMembersTabComponent { return ''; } + isLinkedClient(member: any): boolean { + return !!member.linkedClientId; + } + + createClientFromMember(member: any): void { + const prefill = { + firstname: (member.firstName || '').toString().trim(), + mobileNo: member.mobileNumber || '', + dateOfBirth: member.dateOfBirth || null, + panNumber: member.panNumber || '', + aadhaarNumber: member.aadhaarNumber || '', + familyMemberId: member.id, + parentClientId: this.clientId + }; + this.router.navigate( + [ + '/clients', + 'create' + ], + { state: { prefill } } + ); + } + displayName(member: any): string { + if (member.linkedClientName) return member.linkedClientName; return member.firstName || ''; } diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index d0a5e9db9..0d7be9314 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -115,6 +115,10 @@ export class ClientsService { return this.http.get(`/clients/${clientId}/accounts`); } + getClientAddressData(clientId: string) { + return this.http.get(`/client/${clientId}/addresses`); + } + getClientChargesData(clientId: string) { const httpParams = new HttpParams().set('pendingPayment', 'true'); return this.http.get(`/clients/${clientId}/charges`, { params: httpParams }); @@ -470,10 +474,6 @@ export class ClientsService { return this.http.get(`/fieldconfiguration/ADDRESS`); } - getClientAddressData(clientId: string) { - return this.http.get(`/client/${clientId}/addresses`); - } - /** * Documents attached to an m_client_address row. The generic document API takes * `client_addresses` as the parent-entity type — see DocumentApiResource. diff --git a/src/app/clients/create-client/create-client.component.html b/src/app/clients/create-client/create-client.component.html index 0f7579d2e..8a6360884 100644 --- a/src/app/clients/create-client/create-client.component.html +++ b/src/app/clients/create-client/create-client.component.html @@ -40,6 +40,7 @@ } diff --git a/src/app/clients/create-client/create-client.component.ts b/src/app/clients/create-client/create-client.component.ts index 222f1f385..152b51804 100644 --- a/src/app/clients/create-client/create-client.component.ts +++ b/src/app/clients/create-client/create-client.component.ts @@ -85,6 +85,8 @@ export class CreateClientComponent { selectedTabIndex = 0; createdClientId = ''; savingClient = false; + /** Pre-fill data passed from "Create as Client" on a manual family member */ + prefillData: any = null; /** Track which tabs are in edit mode (false/undefined = read-only for completed tabs) */ tabEditMode: Record = {}; @@ -109,6 +111,12 @@ export class CreateClientComponent { this.clientAddressFieldConfig = data.clientAddressFieldConfig; this.setDatatables(); }); + // Read pre-fill data set by "Create as Client" from a manual family member + const nav = this.router.getCurrentNavigation(); + const navState = nav?.extras?.state ?? (typeof history !== 'undefined' ? history.state : null); + if (navState?.prefill) { + this.prefillData = navState.prefill; + } } /** Check if a tab is in read-only mode (completed tab navigated back to) */ diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index f740caa30..d52450896 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -181,6 +181,24 @@ + + + + + how_to_vote + Election Management + + +