diff --git a/src/app/core/shell/sidenav/sidenav.component.html b/src/app/core/shell/sidenav/sidenav.component.html index d52450896..05fcec760 100644 --- a/src/app/core/shell/sidenav/sidenav.component.html +++ b/src/app/core/shell/sidenav/sidenav.component.html @@ -182,22 +182,20 @@ - - - - how_to_vote - Election Management - - + + + how_to_vote + Election Management + diff --git a/src/app/core/shell/toolbar/toolbar.component.html b/src/app/core/shell/toolbar/toolbar.component.html index d885bda5d..ea21b1b2c 100644 --- a/src/app/core/shell/toolbar/toolbar.component.html +++ b/src/app/core/shell/toolbar/toolbar.component.html @@ -229,7 +229,7 @@ Governance - + + @if (c.status === 'ACTIVE') { @if (m.status === 'SCHEDULED') { - - } + } + } @@ -371,10 +385,11 @@ type="text" [(ngModel)]="meetingForm.title" placeholder="e.g. Q1 Strategy Review" - [class.input-error]="submittedMeeting && !meetingForm.title?.trim()" + maxlength="200" + [class.input-error]="submittedMeeting && meetingErrors['title']" /> - @if (submittedMeeting && !meetingForm.title?.trim()) { - Title is required + @if (submittedMeeting && meetingErrors['title']) { + {{ meetingErrors['title'] }} } @@ -385,10 +400,11 @@ - @if (submittedMeeting && !meetingForm.meetingDate) { - Date is required + @if (submittedMeeting && meetingErrors['meetingDate']) { + {{ meetingErrors['meetingDate'] }} }
@@ -430,7 +446,15 @@
- + + @if (submittedMeeting && meetingErrors['meetingLink']) { + {{ meetingErrors['meetingLink'] }} + }
@@ -464,10 +488,31 @@
} - +
- - + +
+ @for (item of meetingForm.agendaItems; track $index) { +
+ {{ $index + 1 }}. + + @if (meetingForm.agendaItems.length > 1) { + + } +
+ } +
+
diff --git a/src/app/election/components/committee-board/committee-board.component.scss b/src/app/election/components/committee-board/committee-board.component.scss index fa4a93173..3dff6d090 100644 --- a/src/app/election/components/committee-board/committee-board.component.scss +++ b/src/app/election/components/committee-board/committee-board.component.scss @@ -1244,6 +1244,99 @@ $font: gap: 12px; } +// ── Agenda Items ────────────────────────────────────────────────── +.agenda-list { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 8px; +} + +.agenda-item-row { + display: flex; + align-items: center; + gap: 8px; + + .agenda-num { + font-size: 12px; + color: #94a3b8; + min-width: 18px; + text-align: right; + flex-shrink: 0; + } + + input { + flex: 1; + } +} + +.btn-remove-agenda { + background: none; + border: none; + cursor: pointer; + color: #ef4444; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + flex-shrink: 0; + transition: background 0.15s; + + mat-icon { + font-size: 20px; + height: 20px; + width: 20px; + } + + &:hover { + background: #fef2f2; + } +} + +.btn-add-agenda { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: 1px dashed #cbd5e1; + border-radius: 8px; + cursor: pointer; + color: #6366f1; + padding: 6px 12px; + font-size: 12px; + font-weight: 600; + transition: + border-color 0.15s, + background 0.15s; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + &:hover { + border-color: #6366f1; + background: #eef2ff; + } +} + +.meeting-agenda-preview { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: #64748b; + margin-top: 2px; + + .meta-icon { + font-size: 14px; + height: 14px; + width: 14px; + } +} + @media (width <= 600px) { .committee-page { padding: 16px; diff --git a/src/app/election/components/committee-board/committee-board.component.ts b/src/app/election/components/committee-board/committee-board.component.ts index c0de360ca..39fadb4af 100644 --- a/src/app/election/components/committee-board/committee-board.component.ts +++ b/src/app/election/components/committee-board/committee-board.component.ts @@ -65,6 +65,8 @@ export class CommitteeBoardComponent implements OnInit { savingMeeting = false; submittedMeeting = false; meetingForm: any = this.emptyMeetingForm(); + meetingErrors: Record = {}; + readonly todayDate: string = new Date().toISOString().split('T')[0]; ngOnInit(): void { this.load(); @@ -314,21 +316,33 @@ export class CommitteeBoardComponent implements OnInit { this.editingMeetingId = meeting.id; this.currentMeetingCommitteeId = committeeId; this.submittedMeeting = false; - this.meetingForm = { - title: meeting.title, - meetingDate: meeting.meetingDate, - meetingTime: meeting.meetingTime ?? '', - mode: meeting.mode ?? 'OFFLINE', - agenda: meeting.agenda ?? '', - platform: meeting.platform ?? '', - meetingLink: meeting.meetingLink ?? '', - meetingIdRef: meeting.meetingIdRef ?? '', - meetingPassword: meeting.meetingPassword ?? '', - venue: meeting.venue ?? '', - address: meeting.address ?? '', - city: meeting.city ?? '' + this.service.getMeeting(meeting.id).subscribe({ + next: (fresh) => { + this.meetingForm = this.buildMeetingFormObj(fresh); + this.showMeetingForm = true; + }, + error: () => { + this.meetingForm = this.buildMeetingFormObj(meeting); + this.showMeetingForm = true; + } + }); + } + + private buildMeetingFormObj(m: any): any { + return { + title: m.title, + meetingDate: m.meetingDate, + meetingTime: m.meetingTime ?? '', + mode: m.mode ?? 'OFFLINE', + agendaItems: m.agendaItems?.length > 0 ? [...m.agendaItems] : [''], + platform: m.platform ?? '', + meetingLink: m.meetingLink ?? '', + meetingIdRef: m.meetingIdRef ?? '', + meetingPassword: m.meetingPassword ?? '', + venue: m.venue ?? '', + address: m.address ?? '', + city: m.city ?? '' }; - this.showMeetingForm = true; } closeMeetingForm(): void { @@ -337,11 +351,47 @@ export class CommitteeBoardComponent implements OnInit { this.currentMeetingCommitteeId = null; this.meetingForm = this.emptyMeetingForm(); this.submittedMeeting = false; + this.meetingErrors = {}; + } + + private validateMeetingForm(): boolean { + const errors: Record = {}; + const title = this.meetingForm.title?.trim() ?? ''; + if (!title) { + errors['title'] = 'Meeting title is required'; + } else if (title.length > 200) { + errors['title'] = 'Meeting title must not exceed 200 characters'; + } + + if (!this.meetingForm.meetingDate) { + errors['meetingDate'] = 'Meeting date is required'; + } else { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const selected = new Date(this.meetingForm.meetingDate); + if (selected < today) { + errors['meetingDate'] = 'Meeting date cannot be in the past'; + } + } + + if (!this.meetingForm.mode) { + errors['mode'] = 'Please select a meeting mode'; + } + + if (this.meetingForm.mode === 'ONLINE' && this.meetingForm.meetingLink?.trim()) { + const link: string = this.meetingForm.meetingLink.trim(); + if (!link.startsWith('http://') && !link.startsWith('https://')) { + errors['meetingLink'] = 'Meeting link must start with http:// or https://'; + } + } + + this.meetingErrors = errors; + return Object.keys(errors).length === 0; } saveMeeting(): void { this.submittedMeeting = true; - if (!this.meetingForm.title?.trim() || !this.meetingForm.meetingDate) return; + if (!this.validateMeetingForm()) return; this.savingMeeting = true; const payload: any = { @@ -349,7 +399,7 @@ export class CommitteeBoardComponent implements OnInit { meetingDate: this.meetingForm.meetingDate, meetingTime: this.meetingForm.meetingTime || null, mode: this.meetingForm.mode, - agenda: this.meetingForm.agenda?.trim() || null + agendaItems: (this.meetingForm.agendaItems as string[]).filter((a) => a?.trim()) }; if (this.meetingForm.mode === 'ONLINE') { @@ -421,17 +471,154 @@ export class CommitteeBoardComponent implements OnInit { return mode === 'ONLINE' ? 'video_call' : 'location_on'; } + copyMeetingDetails(meeting: any, event: MouseEvent): void { + event.stopPropagation(); + const lines: string[] = []; + lines.push(`📅 Committee Meeting: ${meeting.title}`); + lines.push(`Date: ${meeting.meetingDate}`); + if (meeting.meetingTime) lines.push(`Time: ${meeting.meetingTime}`); + lines.push(`Mode: ${meeting.mode}`); + if (meeting.mode === 'ONLINE') { + if (meeting.platform) lines.push(`Platform: ${meeting.platform}`); + if (meeting.meetingLink) lines.push(`Join Link: ${meeting.meetingLink}`); + if (meeting.meetingIdRef) lines.push(`Meeting ID: ${meeting.meetingIdRef}`); + if (meeting.meetingPassword) lines.push(`Password: ${meeting.meetingPassword}`); + } else { + if (meeting.venue) lines.push(`Venue: ${meeting.venue}`); + if (meeting.address) lines.push(`Address: ${meeting.address}`); + if (meeting.city) lines.push(`City: ${meeting.city}`); + } + if (meeting.agendaItems?.length > 0) { + lines.push('Agenda:'); + (meeting.agendaItems as string[]).forEach((item, i) => lines.push(` ${i + 1}. ${item}`)); + } + + navigator.clipboard.writeText(lines.join('\n')).then(() => { + this.snackBar.open('Meeting details copied!', 'Close', { duration: 2500 }); + }); + } + + downloadMeetingPdf(meeting: any, committee: any, event: MouseEvent): void { + event.stopPropagation(); + this.service.getMeeting(meeting.id).subscribe({ + next: (fresh) => this.openPrintWindow(fresh, committee), + error: () => this.openPrintWindow(meeting, committee) + }); + } + + private openPrintWindow(meeting: any, committee: any): void { + const w = window.open('', '_blank', 'width=900,height=700'); + if (!w) { + this.snackBar.open('Popup blocked. Please allow popups.', 'Close', { duration: 3500 }); + return; + } + + const fmt = (d: string) => { + if (!d) return '—'; + const dt = new Date(d); + return isNaN(dt.getTime()) + ? d + : dt.toLocaleDateString('en-IN', { day: '2-digit', month: 'long', year: 'numeric' }); + }; + + const agendaRows = ((meeting.agendaItems as string[]) || []) + .filter((a) => a?.trim()) + .map( + (a, i) => + `${i + 1}${a}` + ) + .join(''); + + const row = (icon: string, label: string, value: string) => + `${icon} ${label}${value || '—'}`; + + const isOnline = meeting.mode === 'ONLINE'; + let locationRows = ''; + if (isOnline) { + if (meeting.platform) locationRows += row('📹', 'Platform', meeting.platform); + if (meeting.meetingLink) + locationRows += row('🔗', 'Join Link', `${meeting.meetingLink}`); + if (meeting.meetingIdRef) locationRows += row('🔑', 'Meeting ID', meeting.meetingIdRef); + if (meeting.meetingPassword) locationRows += row('🔒', 'Password', meeting.meetingPassword); + } else { + if (meeting.venue) locationRows += row('🏛', 'Venue', meeting.venue); + if (meeting.address) locationRows += row('📍', 'Address', meeting.address); + if (meeting.city) locationRows += row('🌍', 'City', meeting.city); + } + + const html = `Meeting - ${meeting.title} + +
+ +
+
+
+
CBSnexora · Committee Meeting
+
📅 ${meeting.title}
+ ${committee?.name ? `
👥 ${committee.name}
` : ''} +
+
+ + + + ${committee?.name ? row('👥', 'Committee', committee.name) : ''} + ${row('📅', 'Date', fmt(meeting.meetingDate))} + ${row('⏰', 'Time', meeting.meetingTime || '—')} + ${row('🔗', 'Mode', isOnline ? '🌐 Online' : '📍 Offline')} + ${row('📊', 'Status', meeting.status || '—')} + ${locationRows} + +
+ ${agendaRows ? `
📋 AGENDA
${agendaRows}
` : ''} +
+ +
+`; + + w.document.write(html); + w.document.close(); + w.focus(); + } + private emptyForm(): any { return { name: '', description: '', image: null, selectedIds: [] }; } + addAgendaItem(): void { + this.meetingForm.agendaItems.push(''); + } + + removeAgendaItem(index: number): void { + if (this.meetingForm.agendaItems.length > 1) { + this.meetingForm.agendaItems.splice(index, 1); + } + } + private emptyMeetingForm(): any { return { title: '', meetingDate: '', meetingTime: '', mode: 'OFFLINE', - agenda: '', + agendaItems: [''], platform: '', meetingLink: '', meetingIdRef: '',