Election Management
diff --git a/src/app/election/components/committee-board/committee-board.component.html b/src/app/election/components/committee-board/committee-board.component.html
index 9654ecb5c..8c367cbeb 100644
--- a/src/app/election/components/committee-board/committee-board.component.html
+++ b/src/app/election/components/committee-board/committee-board.component.html
@@ -192,9 +192,23 @@
{{ m.venue }}{{ m.city ? ', ' + m.city : '' }}
}
+ @if (m.agendaItems?.length > 0) {
+
+ format_list_bulleted
+ {{ m.agendaItems.length }} agenda item{{ m.agendaItems.length !== 1 ? 's' : '' }}
+
+ }
- @if (c.status === 'ACTIVE') {
-
+
+
+
+ @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'] }}
+ }
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}
+
+
+
+
+
+
+
+
Meeting Details
+
+
+ ${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 ? `
` : ''}
+
+
+
+`;
+
+ 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: '',