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:
@@ -2,7 +2,7 @@ from fastapi import APIRouter
|
||||
|
||||
from app.config import settings
|
||||
from app.database import get_session
|
||||
from app.models import DonationTier, Event, Program, StationConfig, Show, ShowSchedule
|
||||
from app.models import DonationTier, Event, StationConfig, Show, ShowSchedule
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -19,7 +19,6 @@ async def reset_database():
|
||||
async with async_session() as session:
|
||||
await session.execute(ShowSchedule.__table__.delete())
|
||||
await session.execute(Show.__table__.delete())
|
||||
await session.execute(Program.__table__.delete())
|
||||
await session.execute(Event.__table__.delete())
|
||||
await session.execute(DonationTier.__table__.delete())
|
||||
await session.execute(StationConfig.__table__.delete())
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import get_current_admin_user
|
||||
from app.database import get_session
|
||||
from app.models import Program
|
||||
from app.schemas import ProgramCreate, ProgramUpdate, ProgramResponse
|
||||
from app.user_models import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _day_filter(day: int):
|
||||
"""Return the SQLAlchemy filter for a given day_of_week."""
|
||||
return Program.day_of_week == day
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ProgramResponse])
|
||||
async def list_programs(
|
||||
day: Optional[int] = Query(None, description="Filter by day_of_week (1=Mon … 7=Sun)"),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
query = select(Program).order_by(Program.day_of_week, Program.time)
|
||||
if day is not None:
|
||||
query = query.filter(_day_filter(day))
|
||||
result = await session.execute(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@router.get("/{program_id}", response_model=ProgramResponse)
|
||||
async def get_program(program_id: int, session: AsyncSession = Depends(get_session)):
|
||||
result = await session.execute(select(Program).where(Program.id == program_id))
|
||||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return {"error": "Program not found"}
|
||||
return program
|
||||
|
||||
|
||||
@router.post("/", response_model=ProgramResponse, status_code=201)
|
||||
async def create_program(
|
||||
payload: ProgramCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
program = Program(**payload.model_dump())
|
||||
session.add(program)
|
||||
await session.commit()
|
||||
await session.refresh(program)
|
||||
return program
|
||||
|
||||
|
||||
@router.put("/{program_id}", response_model=ProgramResponse)
|
||||
async def update_program(
|
||||
program_id: int,
|
||||
payload: ProgramUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(select(Program).where(Program.id == program_id))
|
||||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return {"error": "Program not found"}
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
setattr(program, key, value)
|
||||
await session.commit()
|
||||
await session.refresh(program)
|
||||
return program
|
||||
|
||||
|
||||
@router.delete("/{program_id}", status_code=204)
|
||||
async def delete_program(
|
||||
program_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
_: User = Depends(get_current_admin_user),
|
||||
):
|
||||
result = await session.execute(select(Program).where(Program.id == program_id))
|
||||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return {"error": "Program not found"}
|
||||
await session.delete(program)
|
||||
await session.commit()
|
||||
+2
-3
@@ -8,9 +8,9 @@ from sqlalchemy import func, select
|
||||
|
||||
from app.config import settings
|
||||
from app.database import async_session, engine
|
||||
from app.models import Base, Program, Show, StationConfig, HistoryEntry, TeamMember, CommunityHighlight
|
||||
from app.models import Base, Show, StationConfig, HistoryEntry, TeamMember, CommunityHighlight
|
||||
from app.user_models import User
|
||||
from app.api import programs, events, tiers, auth, station_config, history, team, community, shows, upload
|
||||
from app.api import events, tiers, auth, station_config, history, team, community, shows, upload
|
||||
|
||||
|
||||
async def _ensure_station_config(session):
|
||||
@@ -92,7 +92,6 @@ app.add_middleware(
|
||||
|
||||
# Register routers
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||
app.include_router(programs.router, prefix="/api/programs", tags=["programs"])
|
||||
app.include_router(events.router, prefix="/api/events", tags=["events"])
|
||||
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
|
||||
app.include_router(station_config.router, prefix="/api/station-config", tags=["station-config"])
|
||||
|
||||
@@ -17,18 +17,6 @@ class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Program(Base):
|
||||
__tablename__ = "programs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
day_of_week = Column(Integer, nullable=False) # 1=Mon … 7=Sun
|
||||
day_label = Column(String(10), nullable=False) # "Monday", …
|
||||
time = Column(String(10), nullable=False) # "6:00 AM"
|
||||
title = Column(String(200), nullable=False)
|
||||
host = Column(String(100), nullable=False)
|
||||
genre = Column(String(80), nullable=False)
|
||||
|
||||
|
||||
class Show(Base):
|
||||
__tablename__ = "shows"
|
||||
|
||||
|
||||
+1
-35
@@ -4,7 +4,7 @@ from typing import Optional
|
||||
from pydantic import BaseModel, computed_field, field_validator
|
||||
|
||||
|
||||
# ── Program ──────────────────────────────────────────────
|
||||
# ── Shared ───────────────────────────────────────────────
|
||||
|
||||
DAY_NAMES = {
|
||||
1: "Monday",
|
||||
@@ -17,40 +17,6 @@ DAY_NAMES = {
|
||||
}
|
||||
|
||||
|
||||
class ProgramCreate(BaseModel):
|
||||
day_of_week: int
|
||||
day_label: str
|
||||
time: str
|
||||
title: str
|
||||
host: str
|
||||
genre: str
|
||||
|
||||
|
||||
class ProgramUpdate(BaseModel):
|
||||
day_of_week: Optional[int] = None
|
||||
day_label: Optional[str] = None
|
||||
time: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
genre: Optional[str] = None
|
||||
|
||||
|
||||
class ProgramResponse(BaseModel):
|
||||
id: int
|
||||
day_of_week: int
|
||||
time: str
|
||||
title: str
|
||||
host: str
|
||||
genre: str
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def day_label(self) -> str:
|
||||
return DAY_NAMES.get(self.day_of_week, "Unknown")
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ── Event ────────────────────────────────────────────────
|
||||
|
||||
class EventCreate(BaseModel):
|
||||
|
||||
+1
-36
@@ -14,24 +14,11 @@ from datetime import date
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.database import async_session
|
||||
from app.models import Program, Show, ShowSchedule, Event, DonationTier, StationConfig
|
||||
from app.models import Show, ShowSchedule, Event, DonationTier, StationConfig
|
||||
from app.models import HistoryEntry, TeamMember, CommunityHighlight
|
||||
|
||||
# ── Seed data ──────────────────────────────────────────────
|
||||
|
||||
PROGRAMS: list[dict] = [
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "6:00 AM", "title": "Morning Mountain Mist", "host": "Diana Walsh", "genre": "Ambient / Nature"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "8:00 AM", "title": "Community Roundup", "host": "Tom Breen", "genre": "News / Talk"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "10:00 AM", "title": "Bluegrass Trails", "host": "Sarah Lynn", "genre": "Bluegrass"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "12:00 PM", "title": "Lunchtime Jazz", "host": "Mike Darrow", "genre": "Jazz"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "2:00 PM", "title": "Folk Roots Hour", "host": "Nadia Cole", "genre": "Folk"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "4:00 PM", "title": "Afternoon Acoustics", "host": "Jen Reeves", "genre": "Acoustic"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "6:00 PM", "title": "Evening Echoes", "host": "Carlos Mendez", "genre": "Indie / Alternative"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "8:00 PM", "title": "Classical Mountains", "host": "Elena Cross", "genre": "Classical"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "10:00 PM", "title": "Night Owl Session", "host": "DJ Kofi", "genre": "Electronic"},
|
||||
{"day_of_week": 1, "day_label": "Monday", "time": "12:00 AM", "title": "Late Night Jazz", "host": "Mike Darrow", "genre": "Jazz"},
|
||||
]
|
||||
|
||||
SHOWS: list[dict] = [
|
||||
{
|
||||
"title": "Morning Mountain Mist",
|
||||
@@ -345,22 +332,6 @@ COMMUNITY_HIGHLIGHTS: list[dict] = [
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────
|
||||
|
||||
async def _upsert_program(session, data: dict) -> None:
|
||||
"""Get-or-create a program matched on (day_of_week, time)."""
|
||||
existing = await session.execute(
|
||||
select(Program).where(
|
||||
Program.day_of_week == data["day_of_week"],
|
||||
Program.time == data["time"],
|
||||
)
|
||||
)
|
||||
program = existing.scalar_one_or_none()
|
||||
if program:
|
||||
for key, value in data.items():
|
||||
setattr(program, key, value)
|
||||
else:
|
||||
session.add(Program(**data))
|
||||
|
||||
|
||||
async def _upsert_event(session, data: dict) -> None:
|
||||
"""Get-or-create an event matched on (date, title)."""
|
||||
existing = await session.execute(
|
||||
@@ -476,11 +447,6 @@ async def _upsert_community_highlight(session, data: dict) -> None:
|
||||
async def seed() -> None:
|
||||
"""Populate the database with station data (idempotent)."""
|
||||
async with async_session() as session:
|
||||
for data in PROGRAMS:
|
||||
await _upsert_program(session, data)
|
||||
await session.commit()
|
||||
print(f" ✓ Seeded {len(PROGRAMS)} programs")
|
||||
|
||||
for data in EVENTS:
|
||||
await _upsert_event(session, data)
|
||||
await session.commit()
|
||||
@@ -524,7 +490,6 @@ async def truncate_all() -> None:
|
||||
# Delete child tables first to respect foreign keys
|
||||
await session.execute(ShowSchedule.__table__.delete())
|
||||
await session.execute(Show.__table__.delete())
|
||||
await session.execute(Program.__table__.delete())
|
||||
await session.execute(Event.__table__.delete())
|
||||
await session.execute(DonationTier.__table__.delete())
|
||||
await session.execute(StationConfig.__table__.delete())
|
||||
|
||||
@@ -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