diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index 20df06e..fd6aa89 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -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()) diff --git a/backend/app/api/programs.py b/backend/app/api/programs.py deleted file mode 100644 index ab66c2b..0000000 --- a/backend/app/api/programs.py +++ /dev/null @@ -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() diff --git a/backend/app/main.py b/backend/app/main.py index c042cf3..fae6adc 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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"]) diff --git a/backend/app/models.py b/backend/app/models.py index 92130ae..c3b839d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -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" diff --git a/backend/app/schemas.py b/backend/app/schemas.py index ef25776..234d3e5 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -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): diff --git a/backend/seed.py b/backend/seed.py index 305d5ab..dd9dd99 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -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()) diff --git a/src/app/admin/admin-program-form.component.html b/src/app/admin/admin-program-form.component.html deleted file mode 100644 index bdc0ae4..0000000 --- a/src/app/admin/admin-program-form.component.html +++ /dev/null @@ -1,54 +0,0 @@ -
-
-
-

{{ id ? 'Edit Program' : 'Add Program' }}

- -
- - @if (error) { - - } - -
-
-
- - -
- -
- - -
-
- -
- - -
- -
-
- - -
- -
- - -
-
- -
- - -
-
-
-
diff --git a/src/app/admin/admin-program-form.component.scss b/src/app/admin/admin-program-form.component.scss deleted file mode 100644 index 854d9bc..0000000 --- a/src/app/admin/admin-program-form.component.scss +++ /dev/null @@ -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; - } -} diff --git a/src/app/admin/admin-program-form.component.ts b/src/app/admin/admin-program-form.component.ts deleted file mode 100644 index 0f19c6d..0000000 --- a/src/app/admin/admin-program-form.component.ts +++ /dev/null @@ -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(); - @Output() closed = new EventEmitter(); - - 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 { - 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; - } -} diff --git a/src/app/admin/admin.component.html b/src/app/admin/admin.component.html index 2e33127..1dfe7e3 100644 --- a/src/app/admin/admin.component.html +++ b/src/app/admin/admin.component.html @@ -2,7 +2,7 @@

Admin Dashboard

-

Manage shows, programs, events, tiers, history, team, and community content

+

Manage shows, events, tiers, history, team, and community content

@@ -12,11 +12,6 @@ [class.active]="activeTab() === 'shows'" (click)="activeTab.set('shows')" >Shows - -
- - - - - - - - - - - - - - @for (program of programs; track program.id) { - - - - - - - - - } @empty { - - } - -
DayTimeTitleHostGenreActions
{{ program.day_label }}{{ program.time }}{{ program.title }}{{ program.host }}{{ program.genre }} - - -
No programs yet. Click "Add Program" to create one.
- - } - } - @if (activeTab() === 'events') { @if (events$ | async; as events) { @@ -393,9 +346,6 @@ @if (showShowForm) { } - @if (showProgramForm) { - - } @if (showEventForm) { } diff --git a/src/app/admin/admin.component.ts b/src/app/admin/admin.component.ts index d8a0878..deca079 100644 --- a/src/app/admin/admin.component.ts +++ b/src/app/admin/admin.component.ts @@ -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([]); - readonly programs$ = new BehaviorSubject([]); readonly events$ = new BehaviorSubject([]); readonly tiers$ = new BehaviorSubject([]); readonly stationConfig$ = new BehaviorSubject(null); @@ -81,7 +74,6 @@ export class AdminComponent implements OnInit { readonly communityHighlights$ = new BehaviorSubject([]); 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 { - const data = await firstValueFrom(this.programService.getPrograms()); - this.programs$.next(data); - } - async reloadEvents(): Promise { const data = await firstValueFrom(this.eventService.getEvents()); this.events$.next(data); @@ -136,7 +123,6 @@ export class AdminComponent implements OnInit { async loadAll(): Promise { 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 { - if (!confirm('Delete this program?')) return; - await firstValueFrom(this.programService.deleteProgram(id)); - await this.reloadPrograms(); - } - // ── Event CRUD ────────────────────────────────────────── openEventForm(): void { diff --git a/src/app/interfaces/program.ts b/src/app/interfaces/program.ts deleted file mode 100644 index edd7809..0000000 --- a/src/app/interfaces/program.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface Program { - id: number; - day_of_week: number; - day_label: string; - time: string; - title: string; - host: string; - genre: string; -} diff --git a/src/app/services/program.service.ts b/src/app/services/program.service.ts deleted file mode 100644 index be416d7..0000000 --- a/src/app/services/program.service.ts +++ /dev/null @@ -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 { - let params = new HttpParams(); - if (day !== undefined) params = params.set('day', String(day)); - return this.http.get(this.baseUrl, { params }); - } - - getProgram(id: number): Observable { - return this.http.get(`${this.baseUrl}/${id}`); - } - - createProgram(payload: Omit): Observable { - return this.http.post(this.baseUrl, payload); - } - - updateProgram(id: number, payload: Partial): Observable { - return this.http.put(`${this.baseUrl}/${id}`, payload); - } - - deleteProgram(id: number): Observable { - return this.http.delete(`${this.baseUrl}/${id}`); - } -}