feat(election): relational agenda items, PDF download, email redesign, and nav fixes
- Replace flat agenda text field with relational agenda items (add/remove rows) - Fix edit form pre-populating saved agenda using [value]+(input) binding - Add PDF download button: opens print-ready window with full meeting details - Redesign meeting invite email: table-based layout, compact header, amber agenda box with numbered badges, two-column footer - Fix stale email agenda bug: refresh entity after agenda save to bypass EclipseLink L1 identity map cache - Remove permission wrapper from Election sidenav item; add routerLink to toolbar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e952bc546f
commit
8a1b7d5d6c
@@ -182,22 +182,20 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- Election Management Section -->
|
||||
<ng-container *mifosxHasPermission="'SIDEBAR_ELECTION'">
|
||||
<div class="nav-item-sep mt-1">
|
||||
<mat-divider></mat-divider>
|
||||
<span class="text-muted">Election</span>
|
||||
</div>
|
||||
<mat-list-item
|
||||
[routerLink]="['/election']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="Election Management"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>how_to_vote</mat-icon>
|
||||
<a matLine>Election Management</a>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
<div class="nav-item-sep mt-1">
|
||||
<mat-divider></mat-divider>
|
||||
<span class="text-muted">Election</span>
|
||||
</div>
|
||||
<mat-list-item
|
||||
[routerLink]="['/election']"
|
||||
[matTooltipPosition]="tooltipPosition"
|
||||
matTooltip="Election Management"
|
||||
routerLinkActive="active-menu"
|
||||
[routerLinkActiveOptions]="{ exact: false }"
|
||||
>
|
||||
<mat-icon matListIcon>how_to_vote</mat-icon>
|
||||
<a matLine>Election Management</a>
|
||||
</mat-list-item>
|
||||
|
||||
<!-- RBI Integration Section -->
|
||||
<ng-container *mifosxHasPermission="'SIDEBAR_RBI_INTEGRATION'">
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
<fa-icon icon="users" class="ai-col-h-ic"></fa-icon>
|
||||
<span>Governance</span>
|
||||
</div>
|
||||
<button mat-menu-item class="ai-card" tabindex="0">
|
||||
<button mat-menu-item class="ai-card" routerLink="/election" tabindex="0">
|
||||
<span class="ai-card-ic"><fa-icon icon="users"></fa-icon></span>
|
||||
<span class="ai-card-body">
|
||||
<span class="ai-card-t">Election Management</span>
|
||||
|
||||
@@ -192,9 +192,23 @@
|
||||
<span>{{ m.venue }}{{ m.city ? ', ' + m.city : '' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (m.agendaItems?.length > 0) {
|
||||
<div class="meeting-agenda-preview">
|
||||
<mat-icon class="meta-icon">format_list_bulleted</mat-icon>
|
||||
<span
|
||||
>{{ m.agendaItems.length }} agenda item{{ m.agendaItems.length !== 1 ? 's' : '' }}</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (c.status === 'ACTIVE') {
|
||||
<div class="meeting-actions">
|
||||
<div class="meeting-actions">
|
||||
<button class="btn-icon" title="Copy Details" (click)="copyMeetingDetails(m, $event)">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
</button>
|
||||
<button class="btn-icon" title="Download PDF" (click)="downloadMeetingPdf(m, c, $event)">
|
||||
<mat-icon>picture_as_pdf</mat-icon>
|
||||
</button>
|
||||
@if (c.status === 'ACTIVE') {
|
||||
@if (m.status === 'SCHEDULED') {
|
||||
<button class="btn-icon" title="Edit" (click)="openEditMeeting(m, c.id, $event)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
@@ -221,8 +235,8 @@
|
||||
>
|
||||
<mat-icon>delete_outline</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -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()) {
|
||||
<span class="error-msg">Title is required</span>
|
||||
@if (submittedMeeting && meetingErrors['title']) {
|
||||
<span class="error-msg">{{ meetingErrors['title'] }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -385,10 +400,11 @@
|
||||
<input
|
||||
type="date"
|
||||
[(ngModel)]="meetingForm.meetingDate"
|
||||
[class.input-error]="submittedMeeting && !meetingForm.meetingDate"
|
||||
[min]="todayDate"
|
||||
[class.input-error]="submittedMeeting && meetingErrors['meetingDate']"
|
||||
/>
|
||||
@if (submittedMeeting && !meetingForm.meetingDate) {
|
||||
<span class="error-msg">Date is required</span>
|
||||
@if (submittedMeeting && meetingErrors['meetingDate']) {
|
||||
<span class="error-msg">{{ meetingErrors['meetingDate'] }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -430,7 +446,15 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Meeting Link</label>
|
||||
<input type="url" [(ngModel)]="meetingForm.meetingLink" placeholder="https://..." />
|
||||
<input
|
||||
type="url"
|
||||
[(ngModel)]="meetingForm.meetingLink"
|
||||
placeholder="https://..."
|
||||
[class.input-error]="submittedMeeting && meetingErrors['meetingLink']"
|
||||
/>
|
||||
@if (submittedMeeting && meetingErrors['meetingLink']) {
|
||||
<span class="error-msg">{{ meetingErrors['meetingLink'] }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-row-2">
|
||||
<div class="form-group">
|
||||
@@ -464,10 +488,31 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Agenda -->
|
||||
<!-- Agenda Items -->
|
||||
<div class="form-group">
|
||||
<label>Agenda</label>
|
||||
<textarea [(ngModel)]="meetingForm.agenda" rows="3" placeholder="Meeting agenda..."></textarea>
|
||||
<label>Agenda Items</label>
|
||||
<div class="agenda-list">
|
||||
@for (item of meetingForm.agendaItems; track $index) {
|
||||
<div class="agenda-item-row">
|
||||
<span class="agenda-num">{{ $index + 1 }}.</span>
|
||||
<input
|
||||
type="text"
|
||||
[name]="'agendaItem' + $index"
|
||||
[value]="meetingForm.agendaItems[$index]"
|
||||
(input)="meetingForm.agendaItems[$index] = $any($event.target).value"
|
||||
placeholder="e.g. Review financials, Discuss elections..."
|
||||
/>
|
||||
@if (meetingForm.agendaItems.length > 1) {
|
||||
<button type="button" class="btn-remove-agenda" title="Remove" (click)="removeAgendaItem($index)">
|
||||
<mat-icon>remove_circle_outline</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button type="button" class="btn-add-agenda" (click)="addAgendaItem()">
|
||||
<mat-icon>add_circle_outline</mat-icon> Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -65,6 +65,8 @@ export class CommitteeBoardComponent implements OnInit {
|
||||
savingMeeting = false;
|
||||
submittedMeeting = false;
|
||||
meetingForm: any = this.emptyMeetingForm();
|
||||
meetingErrors: Record<string, string> = {};
|
||||
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<string, string> = {};
|
||||
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) =>
|
||||
`<tr><td style="width:28px;vertical-align:top;padding:4px 0;"><span style="background:#f59e0b;color:#fff;border-radius:50%;width:20px;height:20px;display:inline-block;text-align:center;line-height:20px;font-size:11px;font-weight:700;">${i + 1}</span></td><td style="padding:4px 0 4px 10px;font-size:13px;color:#1c1917;">${a}</td></tr>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const row = (icon: string, label: string, value: string) =>
|
||||
`<tr><td style="padding:9px 12px;font-size:12px;color:#64748b;font-weight:600;letter-spacing:0.3px;text-transform:uppercase;border-bottom:1px solid #e2e8f0;width:130px;">${icon} ${label}</td><td style="padding:9px 12px;font-size:13px;color:#334155;border-bottom:1px solid #e2e8f0;">${value || '—'}</td></tr>`;
|
||||
|
||||
const isOnline = meeting.mode === 'ONLINE';
|
||||
let locationRows = '';
|
||||
if (isOnline) {
|
||||
if (meeting.platform) locationRows += row('📹', 'Platform', meeting.platform);
|
||||
if (meeting.meetingLink)
|
||||
locationRows += row('🔗', 'Join Link', `<a href="${meeting.meetingLink}">${meeting.meetingLink}</a>`);
|
||||
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 = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Meeting - ${meeting.title}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: Arial, Helvetica, sans-serif; background: #f0f4ff; padding: 24px; }
|
||||
.card { max-width: 720px; margin: 0 auto; background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,.10); }
|
||||
.header { background: #1e40af; padding: 22px 28px; }
|
||||
.header-eyebrow { font-size: 11px; font-weight: 700; color: rgba(255,255,255,.6); letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 4px; }
|
||||
.header-title { font-size: 22px; font-weight: 800; color: #fff; }
|
||||
.header-sub { margin-top: 6px; font-size: 13px; color: rgba(255,255,255,.75); }
|
||||
.body { padding: 24px 28px; }
|
||||
.section-label { font-size: 11px; font-weight: 700; color: #94a3b8; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 10px; }
|
||||
.details-table { width: 100%; border-collapse: collapse; background: #f8faff; border-radius: 10px; border: 1px solid #e2e8f0; overflow: hidden; margin-bottom: 20px; }
|
||||
.agenda-box { background: #fefce8; border: 1px solid #fde68a; border-radius: 10px; padding: 16px 20px; margin-bottom: 20px; }
|
||||
.agenda-label { font-size: 13px; font-weight: 700; color: #92400e; margin-bottom: 10px; }
|
||||
.footer { background: #f8fafc; border-top: 1px solid #e2e8f0; padding: 12px 28px; display: flex; justify-content: space-between; font-size: 11px; color: #94a3b8; }
|
||||
@media print { body { background: #fff; padding: 0; } .card { box-shadow: none; border-radius: 0; } .no-print { display: none !important; } }
|
||||
</style></head><body>
|
||||
<div class="no-print" style="max-width:720px;margin:0 auto 12px;text-align:right;">
|
||||
<button onclick="window.print()" style="background:#1e40af;color:#fff;border:none;padding:8px 20px;border-radius:6px;font-size:13px;cursor:pointer;">🖨 Print / Save as PDF</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<div class="header-eyebrow">CBSnexora · Committee Meeting</div>
|
||||
<div class="header-title">📅 ${meeting.title}</div>
|
||||
${committee?.name ? `<div class="header-sub">👥 ${committee.name}</div>` : ''}
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="section-label">Meeting Details</div>
|
||||
<table class="details-table">
|
||||
<tbody>
|
||||
${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}
|
||||
</tbody>
|
||||
</table>
|
||||
${agendaRows ? `<div class="agenda-box"><div class="agenda-label">📋 AGENDA</div><table style="width:100%;border-collapse:collapse;">${agendaRows}</table></div>` : ''}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span>CBSnexora — Committee Meeting Record</span>
|
||||
<span>Generated: ${new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>`;
|
||||
|
||||
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: '',
|
||||
|
||||
Reference in New Issue
Block a user