Remove Programs tab and all supporting code
The Programs feature is superseded by Shows, which provides a more flexible model with schedule slots. This commit removes all Programs code across frontend and backend: Frontend: - Delete admin-program-form component (ts/html/scss) - Delete program.service.ts and program.ts interface - Remove Programs tab, form modal, and all CRUD logic from admin component (ts + html) Backend: - Delete programs API router - Remove Program ORM model and ProgramCreate/Update/Response schemas - Remove programs router registration from main.py - Remove Program from seed data, seed helpers, and truncate/reset
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
<div class="form-overlay" (click)="onClose()">
|
||||
<div class="form-dialog" (click)="$event.stopPropagation()">
|
||||
<div class="form-header">
|
||||
<h2>{{ id ? 'Edit Program' : 'Add Program' }}</h2>
|
||||
<button type="button" class="btn-close" (click)="onClose()">×</button>
|
||||
</div>
|
||||
|
||||
@if (error) {
|
||||
<div class="form-error" role="alert">{{ error }}</div>
|
||||
}
|
||||
|
||||
<form (ngSubmit)="onSubmit()" class="admin-form" #form="ngForm">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="day">Day</label>
|
||||
<select id="day" [(ngModel)]="day_of_week" name="day" (change)="onDayChange()" required>
|
||||
@for (day of days; track day.value) {
|
||||
<option [value]="day.value">{{ day.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="time">Time</label>
|
||||
<input id="time" type="text" [(ngModel)]="time" name="time" placeholder="6:00 AM" required [class.is-invalid]="submitted && !time">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="title">Title</label>
|
||||
<input id="title" type="text" [(ngModel)]="title" name="title" placeholder="Program title" required [class.is-invalid]="submitted && !title">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="host">Host</label>
|
||||
<input id="host" type="text" [(ngModel)]="host" name="host" placeholder="Host name" required [class.is-invalid]="submitted && !host">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="genre">Genre</label>
|
||||
<input id="genre" type="text" [(ngModel)]="genre" name="genre" placeholder="Genre" required [class.is-invalid]="submitted && !genre">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-cancel" (click)="onClose()">Cancel</button>
|
||||
<button type="submit" class="btn btn-save" [disabled]="loading">
|
||||
{{ loading ? 'Saving…' : (id ? 'Update' : 'Create') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,129 +0,0 @@
|
||||
@use '../../styles/variables' as *;
|
||||
@use '../../styles/mixins' as *;
|
||||
|
||||
.form-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
@include flex-center;
|
||||
z-index: $z-modal;
|
||||
@include fade-in;
|
||||
}
|
||||
|
||||
.form-dialog {
|
||||
background: $neutral-white;
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-xl;
|
||||
width: 90%;
|
||||
max-width: 560px;
|
||||
box-shadow: $shadow-xl;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
@include flex-between;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
h2 {
|
||||
font-family: $font-heading;
|
||||
color: $primary-blue;
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: $neutral-medium;
|
||||
line-height: 1;
|
||||
padding: $spacing-xs;
|
||||
|
||||
&:hover {
|
||||
color: $neutral-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
background: rgba($danger-red, 0.1);
|
||||
color: $danger-red;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border-radius: $radius-md;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: $spacing-md;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.admin-form {
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: $neutral-dark;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid $neutral-light;
|
||||
border-radius: $radius-md;
|
||||
font-size: 0.95rem;
|
||||
transition: border-color $transition-fast;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary-blue;
|
||||
box-shadow: 0 0 0 3px rgba($primary-blue, 0.15);
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
border-color: $danger-red;
|
||||
box-shadow: 0 0 0 3px rgba($danger-red, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $spacing-sm;
|
||||
margin-top: $spacing-md;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
@include button-style($primary-blue, $neutral-white, $primary-blue-light);
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: $neutral-light;
|
||||
color: $neutral-dark;
|
||||
border: none;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: background $transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: $neutral-medium;
|
||||
color: $neutral-white;
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Component, EventEmitter, inject, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { Program } from '../interfaces/program';
|
||||
import { ProgramService } from '../services/program.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-program-form',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
templateUrl: './admin-program-form.component.html',
|
||||
styleUrl: './admin-program-form.component.scss',
|
||||
})
|
||||
export class AdminProgramFormComponent implements OnChanges {
|
||||
private programService = inject(ProgramService);
|
||||
|
||||
@Input() editItem: Program | null = null;
|
||||
@Output() saved = new EventEmitter<void>();
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
|
||||
readonly days = [
|
||||
{ value: 1, label: 'Monday' },
|
||||
{ value: 2, label: 'Tuesday' },
|
||||
{ value: 3, label: 'Wednesday' },
|
||||
{ value: 4, label: 'Thursday' },
|
||||
{ value: 5, label: 'Friday' },
|
||||
{ value: 6, label: 'Saturday' },
|
||||
{ value: 7, label: 'Sunday' },
|
||||
];
|
||||
|
||||
id: number | null = null;
|
||||
day_of_week = 1;
|
||||
day_label = 'Monday';
|
||||
time = '';
|
||||
title = '';
|
||||
host = '';
|
||||
genre = '';
|
||||
|
||||
loading = false;
|
||||
error = '';
|
||||
submitted = false;
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['editItem'] && this.editItem) {
|
||||
this.edit(this.editItem);
|
||||
}
|
||||
}
|
||||
|
||||
/** Populate form for editing an existing program. */
|
||||
edit(program: Program): void {
|
||||
this.id = program.id;
|
||||
this.day_of_week = program.day_of_week;
|
||||
this.day_label = program.day_label;
|
||||
this.time = program.time;
|
||||
this.title = program.title;
|
||||
this.host = program.host;
|
||||
this.genre = program.genre;
|
||||
}
|
||||
|
||||
/** Reset the form for a new entry. */
|
||||
formatError(err: any): string {
|
||||
if (err?.error?.detail) return err.error.detail;
|
||||
if (err?.error?.message) return err.error.message;
|
||||
if (err?.message) return err.message;
|
||||
return 'Failed to save. Please try again.';
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.id = null;
|
||||
this.day_of_week = 1;
|
||||
this.day_label = 'Monday';
|
||||
this.time = '';
|
||||
this.title = '';
|
||||
this.host = '';
|
||||
this.genre = '';
|
||||
this.error = '';
|
||||
this.submitted = false;
|
||||
}
|
||||
|
||||
/** Close the dialog without saving. */
|
||||
onClose(): void {
|
||||
this.closed.emit();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
async onSubmit(): Promise<void> {
|
||||
this.submitted = true;
|
||||
if (!this.title || !this.time || !this.host || !this.genre) {
|
||||
this.error = 'Please fill in all required fields.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
|
||||
const payload = {
|
||||
day_of_week: this.day_of_week,
|
||||
day_label: this.day_label,
|
||||
time: this.time,
|
||||
title: this.title,
|
||||
host: this.host,
|
||||
genre: this.genre,
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.id) {
|
||||
await firstValueFrom(this.programService.updateProgram(this.id, payload));
|
||||
} else {
|
||||
await firstValueFrom(this.programService.createProgram(payload));
|
||||
}
|
||||
this.saved.emit();
|
||||
this.reset();
|
||||
} catch (err: any) {
|
||||
this.error = this.formatError(err);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDayChange(): void {
|
||||
const day = this.days.find((d) => d.value === this.day_of_week);
|
||||
if (day) this.day_label = day.label;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="container">
|
||||
<div class="admin-header">
|
||||
<h1>Admin Dashboard</h1>
|
||||
<p>Manage shows, programs, events, tiers, history, team, and community content</p>
|
||||
<p>Manage shows, events, tiers, history, team, and community content</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
@@ -12,11 +12,6 @@
|
||||
[class.active]="activeTab() === 'shows'"
|
||||
(click)="activeTab.set('shows')"
|
||||
>Shows</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
[class.active]="activeTab() === 'programs'"
|
||||
(click)="activeTab.set('programs')"
|
||||
>Programs</button>
|
||||
<button
|
||||
class="tab-btn"
|
||||
[class.active]="activeTab() === 'events'"
|
||||
@@ -95,48 +90,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Programs Tab -->
|
||||
@if (activeTab() === 'programs') {
|
||||
@if (programs$ | async; as programs) {
|
||||
<div class="tab-content">
|
||||
<div class="tab-toolbar">
|
||||
<h2>Programs ({{ programs.length }})</h2>
|
||||
<button class="btn btn-add" (click)="openProgramForm()">+ Add Program</button>
|
||||
</div>
|
||||
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Day</th>
|
||||
<th>Time</th>
|
||||
<th>Title</th>
|
||||
<th>Host</th>
|
||||
<th>Genre</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (program of programs; track program.id) {
|
||||
<tr>
|
||||
<td>{{ program.day_label }}</td>
|
||||
<td>{{ program.time }}</td>
|
||||
<td>{{ program.title }}</td>
|
||||
<td>{{ program.host }}</td>
|
||||
<td>{{ program.genre }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn-icon btn-edit" (click)="editProgram(program)">Edit</button>
|
||||
<button class="btn-icon btn-delete" (click)="deleteProgram(program.id)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr><td colspan="6" class="empty-row">No programs yet. Click "Add Program" to create one.</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Events Tab -->
|
||||
@if (activeTab() === 'events') {
|
||||
@if (events$ | async; as events) {
|
||||
@@ -393,9 +346,6 @@
|
||||
@if (showShowForm) {
|
||||
<app-admin-show-form [editItem]="editingShow" (saved)="onShowSaved()" (closed)="onShowClosed()" />
|
||||
}
|
||||
@if (showProgramForm) {
|
||||
<app-admin-program-form [editItem]="editingProgram" (saved)="onProgramSaved()" (closed)="onProgramClosed()" />
|
||||
}
|
||||
@if (showEventForm) {
|
||||
<app-admin-event-form [editItem]="editingEvent" (saved)="onEventSaved()" (closed)="onEventClosed()" />
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CommonModule, AsyncPipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||
|
||||
import { Program } from '../interfaces/program';
|
||||
import { Show } from '../interfaces/show';
|
||||
import { Event } from '../interfaces/event';
|
||||
import { Tier } from '../interfaces/tier';
|
||||
@@ -11,7 +10,6 @@ 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 { ProgramService } from '../services/program.service';
|
||||
import { ShowService } from '../services/show.service';
|
||||
import { EventService } from '../services/event.service';
|
||||
import { TierService } from '../services/tier.service';
|
||||
@@ -19,7 +17,6 @@ import { StationConfigService } from '../services/station-config.service';
|
||||
import { HistoryEntryService } from '../services/history-entry.service';
|
||||
import { TeamMemberService } from '../services/team-member.service';
|
||||
import { CommunityHighlightService } from '../services/community-highlight.service';
|
||||
import { AdminProgramFormComponent } from './admin-program-form.component';
|
||||
import { AdminShowFormComponent } from './admin-show-form.component';
|
||||
import { AdminEventFormComponent } from './admin-event-form.component';
|
||||
import { AdminTierFormComponent } from './admin-tier-form.component';
|
||||
@@ -28,7 +25,7 @@ import { AdminHistoryFormComponent } from './admin-history-form.component';
|
||||
import { AdminTeamFormComponent } from './admin-team-form.component';
|
||||
import { AdminCommunityFormComponent } from './admin-community-form.component';
|
||||
|
||||
type TabKey = 'shows' | 'programs' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'community';
|
||||
type TabKey = 'shows' | 'events' | 'tiers' | 'station' | 'history' | 'team' | 'community';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
@@ -38,7 +35,6 @@ type TabKey = 'shows' | 'programs' | 'events' | 'tiers' | 'station' | 'history'
|
||||
AsyncPipe,
|
||||
FormsModule,
|
||||
AdminShowFormComponent,
|
||||
AdminProgramFormComponent,
|
||||
AdminEventFormComponent,
|
||||
AdminTierFormComponent,
|
||||
AdminStationFormComponent,
|
||||
@@ -50,7 +46,6 @@ type TabKey = 'shows' | 'programs' | 'events' | 'tiers' | 'station' | 'history'
|
||||
styleUrl: './admin.component.scss',
|
||||
})
|
||||
export class AdminComponent implements OnInit {
|
||||
private programService = inject(ProgramService);
|
||||
private showService = inject(ShowService);
|
||||
private eventService = inject(EventService);
|
||||
private tierService = inject(TierService);
|
||||
@@ -63,7 +58,6 @@ export class AdminComponent implements OnInit {
|
||||
|
||||
/** Item currently being edited (set by editXxx, cleared on save/close). */
|
||||
editingShow: Show | null = null;
|
||||
editingProgram: Program | null = null;
|
||||
editingEvent: Event | null = null;
|
||||
editingTier: Tier | null = null;
|
||||
editingHistory: HistoryEntry | null = null;
|
||||
@@ -72,7 +66,6 @@ export class AdminComponent implements OnInit {
|
||||
|
||||
/** BehaviorSubjects guarantee change detection via the async pipe — start empty, populated on load. */
|
||||
readonly shows$ = new BehaviorSubject<Show[]>([]);
|
||||
readonly programs$ = new BehaviorSubject<Program[]>([]);
|
||||
readonly events$ = new BehaviorSubject<Event[]>([]);
|
||||
readonly tiers$ = new BehaviorSubject<Tier[]>([]);
|
||||
readonly stationConfig$ = new BehaviorSubject<StationConfig | null>(null);
|
||||
@@ -81,7 +74,6 @@ export class AdminComponent implements OnInit {
|
||||
readonly communityHighlights$ = new BehaviorSubject<CommunityHighlight[]>([]);
|
||||
|
||||
showShowForm = false;
|
||||
showProgramForm = false;
|
||||
showEventForm = false;
|
||||
showTierForm = false;
|
||||
showStationForm = false;
|
||||
@@ -98,11 +90,6 @@ export class AdminComponent implements OnInit {
|
||||
this.shows$.next(data);
|
||||
}
|
||||
|
||||
async reloadPrograms(): Promise<void> {
|
||||
const data = await firstValueFrom(this.programService.getPrograms());
|
||||
this.programs$.next(data);
|
||||
}
|
||||
|
||||
async reloadEvents(): Promise<void> {
|
||||
const data = await firstValueFrom(this.eventService.getEvents());
|
||||
this.events$.next(data);
|
||||
@@ -136,7 +123,6 @@ export class AdminComponent implements OnInit {
|
||||
async loadAll(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.reloadShows(),
|
||||
this.reloadPrograms(),
|
||||
this.reloadEvents(),
|
||||
this.reloadTiers(),
|
||||
this.reloadStationConfig(),
|
||||
@@ -175,35 +161,6 @@ export class AdminComponent implements OnInit {
|
||||
await this.reloadShows();
|
||||
}
|
||||
|
||||
// ── Program CRUD ────────────────────────────────────────
|
||||
|
||||
openProgramForm(): void {
|
||||
this.editingProgram = null;
|
||||
this.showProgramForm = true;
|
||||
}
|
||||
|
||||
editProgram(program: Program): void {
|
||||
this.editingProgram = program;
|
||||
this.showProgramForm = true;
|
||||
}
|
||||
|
||||
onProgramSaved(): void {
|
||||
this.reloadPrograms();
|
||||
this.showProgramForm = false;
|
||||
this.editingProgram = null;
|
||||
}
|
||||
|
||||
onProgramClosed(): void {
|
||||
this.showProgramForm = false;
|
||||
this.editingProgram = null;
|
||||
}
|
||||
|
||||
async deleteProgram(id: number): Promise<void> {
|
||||
if (!confirm('Delete this program?')) return;
|
||||
await firstValueFrom(this.programService.deleteProgram(id));
|
||||
await this.reloadPrograms();
|
||||
}
|
||||
|
||||
// ── Event CRUD ──────────────────────────────────────────
|
||||
|
||||
openEventForm(): void {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface Program {
|
||||
id: number;
|
||||
day_of_week: number;
|
||||
day_label: string;
|
||||
time: string;
|
||||
title: string;
|
||||
host: string;
|
||||
genre: string;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { Program } from '../interfaces/program';
|
||||
import { getAppConfig } from './app-config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProgramService {
|
||||
private http = inject(HttpClient);
|
||||
private baseUrl = `${getAppConfig().apiBaseUrl}/api/programs`;
|
||||
|
||||
/** Fetch all programs, optionally filtered by day_of_week (1=Mon … 7=Sun). */
|
||||
getPrograms(day?: number): Observable<Program[]> {
|
||||
let params = new HttpParams();
|
||||
if (day !== undefined) params = params.set('day', String(day));
|
||||
return this.http.get<Program[]>(this.baseUrl, { params });
|
||||
}
|
||||
|
||||
getProgram(id: number): Observable<Program> {
|
||||
return this.http.get<Program>(`${this.baseUrl}/${id}`);
|
||||
}
|
||||
|
||||
createProgram(payload: Omit<Program, 'id'>): Observable<Program> {
|
||||
return this.http.post<Program>(this.baseUrl, payload);
|
||||
}
|
||||
|
||||
updateProgram(id: number, payload: Partial<Program>): Observable<Program> {
|
||||
return this.http.put<Program>(`${this.baseUrl}/${id}`, payload);
|
||||
}
|
||||
|
||||
deleteProgram(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}/${id}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user