import { Component, inject, signal, OnInit } from '@angular/core'; import { CommonModule, AsyncPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { Show } from '../interfaces/show'; import { Event } from '../interfaces/event'; import { StationConfig } from '../interfaces/station-config'; import { HistoryEntry } from '../interfaces/history-entry'; import { TeamMember } from '../interfaces/team-member'; import { CommunityHighlight } from '../interfaces/community-highlight'; import { Underwriter } from '../interfaces/underwriter'; import { ShowService } from '../services/show.service'; import { EventService } from '../services/event.service'; import { StationConfigService } from '../services/station-config.service'; import { ThemeService } from '../services/theme.service'; import { HistoryEntryService } from '../services/history-entry.service'; import { TeamMemberService } from '../services/team-member.service'; import { CommunityHighlightService } from '../services/community-highlight.service'; import { UnderwriterService } from '../services/underwriter.service'; import { AdminShowFormComponent } from './admin-show-form.component'; import { AdminEventFormComponent } from './admin-event-form.component'; import { AdminStationFormComponent } from './admin-station-form.component'; import { AdminHistoryFormComponent } from './admin-history-form.component'; import { AdminTeamFormComponent } from './admin-team-form.component'; import { AdminCommunityFormComponent } from './admin-community-form.component'; import { AdminUnderwriterFormComponent } from './admin-underwriter-form.component'; import { AdminStatsDashboardComponent } from './admin-stats-dashboard.component'; import { COLOR_ROLES, HARMONY_RULE_LABELS, detectHarmonyRule, validatePalette, autoFixPalette, generateLight, generateDark, generateMuted, HarmonyRule, ColorValidation, } from '../utils/color-harmony'; type TabKey = 'shows' | 'events' | 'station' | 'theme' | 'history' | 'team' | 'community' | 'underwriters' | 'stats'; interface ThemeColorState { color_primary: string; color_primary_light: string; color_primary_dark: string; color_primary_muted: string; color_accent: string; color_accent_light: string; color_accent_dark: string; color_accent_muted: string; color_background: string; color_text: string; color_success: string; color_danger: string; color_info: string; [key: string]: string; } @Component({ selector: 'app-admin', standalone: true, imports: [ CommonModule, AsyncPipe, FormsModule, AdminShowFormComponent, AdminEventFormComponent, AdminStationFormComponent, AdminHistoryFormComponent, AdminTeamFormComponent, AdminCommunityFormComponent, AdminUnderwriterFormComponent, AdminStatsDashboardComponent, ], templateUrl: './admin.component.html', styleUrl: './admin.component.scss', }) export class AdminComponent implements OnInit { private showService = inject(ShowService); private eventService = inject(EventService); private stationConfigService = inject(StationConfigService); private themeService = inject(ThemeService); private historyEntryService = inject(HistoryEntryService); private teamMemberService = inject(TeamMemberService); private communityHighlightService = inject(CommunityHighlightService); private underwriterService = inject(UnderwriterService); activeTab = signal('shows'); /** Item currently being edited (set by editXxx, cleared on save/close). */ editingShow: Show | null = null; editingEvent: Event | null = null; editingHistory: HistoryEntry | null = null; editingTeam: TeamMember | null = null; editingCommunity: CommunityHighlight | null = null; editingUnderwriter: Underwriter | null = null; /** BehaviorSubjects guarantee change detection via the async pipe — start empty, populated on load. */ readonly shows$ = new BehaviorSubject([]); readonly events$ = new BehaviorSubject([]); readonly stationConfig$ = new BehaviorSubject(null); readonly historyEntries$ = new BehaviorSubject([]); readonly teamMembers$ = new BehaviorSubject([]); readonly communityHighlights$ = new BehaviorSubject([]); readonly underwriters$ = new BehaviorSubject([]); // ── Theme tab state ────────────────────────────────────────── /** Local editable color palette (copied from StationConfig on tab open). */ themeColors: ThemeColorState = {} as ThemeColorState; /** Current harmony rule detected from the palette. */ themeHarmonyRule: HarmonyRule = 'none'; /** Validation results per color field. */ themeValidation: { [key: string]: ColorValidation } = {}; /** Whether auto-generate variants is enabled. */ themeAutoVariants = true; /** Saving state for theme tab. */ themeSaving = false; themeSaveSuccess = ''; themeSaveError = ''; /** Color role definitions for the template. */ readonly colorRoles = COLOR_ROLES; /** Default palette for reset. */ readonly themeDefaults: ThemeColorState = { color_primary: '#1a3a5c', color_primary_light: '#2a5a8c', color_primary_dark: '#0e2440', color_primary_muted: '#3a6a9c', color_accent: '#e87a2e', color_accent_light: '#f09a4e', color_accent_dark: '#c85a1e', color_accent_muted: '#f0a86a', color_background: '#faf6f0', color_text: '#3a3632', color_success: '#4a9e4f', color_danger: '#c83030', color_info: '#3a8abf', }; showShowForm = false; showEventForm = false; showStationForm = false; showHistoryForm = false; showTeamForm = false; showCommunityForm = false; showUnderwriterForm = false; ngOnInit(): void { this.loadAll(); } async reloadShows(): Promise { const data = await firstValueFrom(this.showService.getShows()); this.shows$.next(data); } async reloadEvents(): Promise { const data = await firstValueFrom(this.eventService.getEvents()); this.events$.next(data); } async reloadStationConfig(): Promise { await this.stationConfigService.load(); this.stationConfig$.next(this.stationConfigService.config()); } async reloadHistoryEntries(): Promise { const data = await firstValueFrom(this.historyEntryService.getHistoryEntries()); this.historyEntries$.next(data); } async reloadTeamMembers(): Promise { const data = await firstValueFrom(this.teamMemberService.getTeamMembers()); this.teamMembers$.next(data); } async reloadCommunityHighlights(): Promise { const data = await firstValueFrom(this.communityHighlightService.getCommunityHighlights()); this.communityHighlights$.next(data); } async reloadUnderwriters(): Promise { const data = await firstValueFrom(this.underwriterService.getUnderwriters()); this.underwriters$.next(data); } async loadAll(): Promise { await Promise.all([ this.reloadShows(), this.reloadEvents(), this.reloadStationConfig(), this.reloadHistoryEntries(), this.reloadTeamMembers(), this.reloadCommunityHighlights(), this.reloadUnderwriters(), ]); } // ── Show CRUD ─────────────────────────────────────────── openShowForm(): void { this.editingShow = null; this.showShowForm = true; } editShow(show: Show): void { this.editingShow = show; this.showShowForm = true; } onShowSaved(): void { this.reloadShows(); this.showShowForm = false; this.editingShow = null; } onShowClosed(): void { this.showShowForm = false; this.editingShow = null; } async deleteShow(id: number): Promise { if (!confirm('Delete this show?')) return; await firstValueFrom(this.showService.deleteShow(id)); await this.reloadShows(); } // ── Event CRUD ────────────────────────────────────────── openEventForm(): void { this.editingEvent = null; this.showEventForm = true; } editEvent(event: Event): void { this.editingEvent = event; this.showEventForm = true; } onEventSaved(): void { this.reloadEvents(); this.showEventForm = false; this.editingEvent = null; } onEventClosed(): void { this.showEventForm = false; this.editingEvent = null; } async deleteEvent(id: number): Promise { if (!confirm('Delete this event?')) return; await firstValueFrom(this.eventService.deleteEvent(id)); await this.reloadEvents(); } // ── Station Config ────────────────────────────────────── openStationForm(): void { this.showStationForm = true; } onStationSaved(): void { this.reloadStationConfig(); this.showStationForm = false; } onStationClosed(): void { this.showStationForm = false; } // ── History CRUD ──────────────────────────────────────── openHistoryForm(): void { this.editingHistory = null; this.showHistoryForm = true; } editHistory(entry: HistoryEntry): void { this.editingHistory = entry; this.showHistoryForm = true; } onHistorySaved(): void { this.reloadHistoryEntries(); this.showHistoryForm = false; this.editingHistory = null; } onHistoryClosed(): void { this.showHistoryForm = false; this.editingHistory = null; } async deleteHistory(id: number): Promise { if (!confirm('Delete this history entry?')) return; await firstValueFrom(this.historyEntryService.deleteHistoryEntry(id)); await this.reloadHistoryEntries(); } // ── Team CRUD ─────────────────────────────────────────── openTeamForm(): void { this.editingTeam = null; this.showTeamForm = true; } editTeam(member: TeamMember): void { this.editingTeam = member; this.showTeamForm = true; } onTeamSaved(): void { this.reloadTeamMembers(); this.showTeamForm = false; this.editingTeam = null; } onTeamClosed(): void { this.showTeamForm = false; this.editingTeam = null; } async deleteTeam(id: number): Promise { if (!confirm('Delete this team member?')) return; await firstValueFrom(this.teamMemberService.deleteTeamMember(id)); await this.reloadTeamMembers(); } // ── Community CRUD ────────────────────────────────────── openCommunityForm(): void { this.editingCommunity = null; this.showCommunityForm = true; } editCommunity(highlight: CommunityHighlight): void { this.editingCommunity = highlight; this.showCommunityForm = true; } onCommunitySaved(): void { this.reloadCommunityHighlights(); this.showCommunityForm = false; this.editingCommunity = null; } onCommunityClosed(): void { this.showCommunityForm = false; this.editingCommunity = null; } async deleteCommunity(id: number): Promise { if (!confirm('Delete this community highlight?')) return; await firstValueFrom(this.communityHighlightService.deleteCommunityHighlight(id)); await this.reloadCommunityHighlights(); } // ── Underwriter CRUD ──────────────────────────────────── openUnderwriterForm(): void { this.editingUnderwriter = null; this.showUnderwriterForm = true; } editUnderwriter(underwriter: Underwriter): void { this.editingUnderwriter = underwriter; this.showUnderwriterForm = true; } onUnderwriterSaved(): void { this.reloadUnderwriters(); this.showUnderwriterForm = false; this.editingUnderwriter = null; } onUnderwriterClosed(): void { this.showUnderwriterForm = false; this.editingUnderwriter = null; } async deleteUnderwriter(id: number): Promise { if (!confirm('Delete this underwriter?')) return; await firstValueFrom(this.underwriterService.deleteUnderwriter(id)); await this.reloadUnderwriters(); } // ── Theme tab ──────────────────────────────────────────── onTabChanged(): void { if (this.activeTab() === 'theme') { this.loadThemeColors(); } } loadThemeColors(): void { const config = this.stationConfigService.config(); this.themeColors = { color_primary: config.color_primary || this.themeDefaults.color_primary, color_primary_light: config.color_primary_light || this.themeDefaults.color_primary_light, color_primary_dark: config.color_primary_dark || this.themeDefaults.color_primary_dark, color_primary_muted: config.color_primary_muted || this.themeDefaults.color_primary_muted, color_accent: config.color_accent || this.themeDefaults.color_accent, color_accent_light: config.color_accent_light || this.themeDefaults.color_accent_light, color_accent_dark: config.color_accent_dark || this.themeDefaults.color_accent_dark, color_accent_muted: config.color_accent_muted || this.themeDefaults.color_accent_muted, color_background: config.color_background || this.themeDefaults.color_background, color_text: config.color_text || this.themeDefaults.color_text, color_success: config.color_success || this.themeDefaults.color_success, color_danger: config.color_danger || this.themeDefaults.color_danger, color_info: config.color_info || this.themeDefaults.color_info, }; this.themeSaveSuccess = ''; this.themeSaveError = ''; this.revalidateTheme(); } /** Re-run harmony detection + validation on the current palette. */ revalidateTheme(): void { const primary = this.themeColors.color_primary; const accent = this.themeColors.color_accent; this.themeHarmonyRule = detectHarmonyRule(primary, accent); this.themeValidation = validatePalette(this.themeColors, this.themeHarmonyRule); } /** Handle color change from the picker. */ onThemeColorChange(field: string, value: string): void { this.themeColors[field] = value; // Auto-generate variants for primary/accent bases if (this.themeAutoVariants) { if (field === 'color_primary') { this.themeColors.color_primary_light = generateLight(value); this.themeColors.color_primary_dark = generateDark(value); this.themeColors.color_primary_muted = generateMuted(value); } else if (field === 'color_accent') { this.themeColors.color_accent_light = generateLight(value); this.themeColors.color_accent_dark = generateDark(value); this.themeColors.color_accent_muted = generateMuted(value); } } // Live-apply to DOM for instant preview this.themeService.apply(this.themeColors as any); this.revalidateTheme(); } /** Auto-fix all non-harmonious colors. */ autoFixTheme(): void { const fixed = autoFixPalette(this.themeColors, this.themeHarmonyRule); for (const key of Object.keys(fixed) as (keyof ThemeColorState)[]) { (this.themeColors as any)[key] = fixed[key]; } this.themeService.apply(this.themeColors as any); this.revalidateTheme(); } /** Reset to default palette. */ resetTheme(): void { for (const key of Object.keys(this.themeDefaults) as (keyof ThemeColorState)[]) { (this.themeColors as any)[key] = this.themeDefaults[key]; } this.themeService.apply(this.themeColors as any); this.revalidateTheme(); } /** Save theme colors to the database. */ async saveTheme(): Promise { this.themeSaving = true; this.themeSaveError = ''; this.themeSaveSuccess = ''; try { await this.stationConfigService.updateConfig(this.themeColors); this.themeSaveSuccess = 'Theme saved successfully!'; } catch (err: any) { this.themeSaveError = err?.error?.detail || 'Failed to save theme.'; } finally { this.themeSaving = false; } } /** Get harmony rule label for display. */ getHarmonyLabel(): string { return HARMONY_RULE_LABELS[this.themeHarmonyRule]; } /** Check if a color field has a harmony issue. */ hasHarmonyIssue(field: string): boolean { const v = this.themeValidation[field]; return v ? !v.isHarmonious : false; } }