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.config import settings
|
||||||
from app.database import get_session
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ async def reset_database():
|
|||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
await session.execute(ShowSchedule.__table__.delete())
|
await session.execute(ShowSchedule.__table__.delete())
|
||||||
await session.execute(Show.__table__.delete())
|
await session.execute(Show.__table__.delete())
|
||||||
await session.execute(Program.__table__.delete())
|
|
||||||
await session.execute(Event.__table__.delete())
|
await session.execute(Event.__table__.delete())
|
||||||
await session.execute(DonationTier.__table__.delete())
|
await session.execute(DonationTier.__table__.delete())
|
||||||
await session.execute(StationConfig.__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.config import settings
|
||||||
from app.database import async_session, engine
|
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.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):
|
async def _ensure_station_config(session):
|
||||||
@@ -92,7 +92,6 @@ app.add_middleware(
|
|||||||
|
|
||||||
# Register routers
|
# Register routers
|
||||||
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
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(events.router, prefix="/api/events", tags=["events"])
|
||||||
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
|
app.include_router(tiers.router, prefix="/api/tiers", tags=["tiers"])
|
||||||
app.include_router(station_config.router, prefix="/api/station-config", tags=["station-config"])
|
app.include_router(station_config.router, prefix="/api/station-config", tags=["station-config"])
|
||||||
|
|||||||
@@ -17,18 +17,6 @@ class Base(DeclarativeBase):
|
|||||||
pass
|
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):
|
class Show(Base):
|
||||||
__tablename__ = "shows"
|
__tablename__ = "shows"
|
||||||
|
|
||||||
|
|||||||
+1
-35
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from pydantic import BaseModel, computed_field, field_validator
|
from pydantic import BaseModel, computed_field, field_validator
|
||||||
|
|
||||||
|
|
||||||
# ── Program ──────────────────────────────────────────────
|
# ── Shared ───────────────────────────────────────────────
|
||||||
|
|
||||||
DAY_NAMES = {
|
DAY_NAMES = {
|
||||||
1: "Monday",
|
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 ────────────────────────────────────────────────
|
# ── Event ────────────────────────────────────────────────
|
||||||
|
|
||||||
class EventCreate(BaseModel):
|
class EventCreate(BaseModel):
|
||||||
|
|||||||
+1
-36
@@ -14,24 +14,11 @@ from datetime import date
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.database import async_session
|
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
|
from app.models import HistoryEntry, TeamMember, CommunityHighlight
|
||||||
|
|
||||||
# ── Seed data ──────────────────────────────────────────────
|
# ── 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] = [
|
SHOWS: list[dict] = [
|
||||||
{
|
{
|
||||||
"title": "Morning Mountain Mist",
|
"title": "Morning Mountain Mist",
|
||||||
@@ -345,22 +332,6 @@ COMMUNITY_HIGHLIGHTS: list[dict] = [
|
|||||||
|
|
||||||
# ── Helpers ────────────────────────────────────────────────
|
# ── 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:
|
async def _upsert_event(session, data: dict) -> None:
|
||||||
"""Get-or-create an event matched on (date, title)."""
|
"""Get-or-create an event matched on (date, title)."""
|
||||||
existing = await session.execute(
|
existing = await session.execute(
|
||||||
@@ -476,11 +447,6 @@ async def _upsert_community_highlight(session, data: dict) -> None:
|
|||||||
async def seed() -> None:
|
async def seed() -> None:
|
||||||
"""Populate the database with station data (idempotent)."""
|
"""Populate the database with station data (idempotent)."""
|
||||||
async with async_session() as session:
|
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:
|
for data in EVENTS:
|
||||||
await _upsert_event(session, data)
|
await _upsert_event(session, data)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
@@ -524,7 +490,6 @@ async def truncate_all() -> None:
|
|||||||
# Delete child tables first to respect foreign keys
|
# Delete child tables first to respect foreign keys
|
||||||
await session.execute(ShowSchedule.__table__.delete())
|
await session.execute(ShowSchedule.__table__.delete())
|
||||||
await session.execute(Show.__table__.delete())
|
await session.execute(Show.__table__.delete())
|
||||||
await session.execute(Program.__table__.delete())
|
|
||||||
await session.execute(Event.__table__.delete())
|
await session.execute(Event.__table__.delete())
|
||||||
await session.execute(DonationTier.__table__.delete())
|
await session.execute(DonationTier.__table__.delete())
|
||||||
await session.execute(StationConfig.__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="container">
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h1>Admin Dashboard</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
@@ -12,11 +12,6 @@
|
|||||||
[class.active]="activeTab() === 'shows'"
|
[class.active]="activeTab() === 'shows'"
|
||||||
(click)="activeTab.set('shows')"
|
(click)="activeTab.set('shows')"
|
||||||
>Shows</button>
|
>Shows</button>
|
||||||
<button
|
|
||||||
class="tab-btn"
|
|
||||||
[class.active]="activeTab() === 'programs'"
|
|
||||||
(click)="activeTab.set('programs')"
|
|
||||||
>Programs</button>
|
|
||||||
<button
|
<button
|
||||||
class="tab-btn"
|
class="tab-btn"
|
||||||
[class.active]="activeTab() === 'events'"
|
[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 -->
|
<!-- Events Tab -->
|
||||||
@if (activeTab() === 'events') {
|
@if (activeTab() === 'events') {
|
||||||
@if (events$ | async; as events) {
|
@if (events$ | async; as events) {
|
||||||
@@ -393,9 +346,6 @@
|
|||||||
@if (showShowForm) {
|
@if (showShowForm) {
|
||||||
<app-admin-show-form [editItem]="editingShow" (saved)="onShowSaved()" (closed)="onShowClosed()" />
|
<app-admin-show-form [editItem]="editingShow" (saved)="onShowSaved()" (closed)="onShowClosed()" />
|
||||||
}
|
}
|
||||||
@if (showProgramForm) {
|
|
||||||
<app-admin-program-form [editItem]="editingProgram" (saved)="onProgramSaved()" (closed)="onProgramClosed()" />
|
|
||||||
}
|
|
||||||
@if (showEventForm) {
|
@if (showEventForm) {
|
||||||
<app-admin-event-form [editItem]="editingEvent" (saved)="onEventSaved()" (closed)="onEventClosed()" />
|
<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 { FormsModule } from '@angular/forms';
|
||||||
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { Program } from '../interfaces/program';
|
|
||||||
import { Show } from '../interfaces/show';
|
import { Show } from '../interfaces/show';
|
||||||
import { Event } from '../interfaces/event';
|
import { Event } from '../interfaces/event';
|
||||||
import { Tier } from '../interfaces/tier';
|
import { Tier } from '../interfaces/tier';
|
||||||
@@ -11,7 +10,6 @@ import { StationConfig } from '../interfaces/station-config';
|
|||||||
import { HistoryEntry } from '../interfaces/history-entry';
|
import { HistoryEntry } from '../interfaces/history-entry';
|
||||||
import { TeamMember } from '../interfaces/team-member';
|
import { TeamMember } from '../interfaces/team-member';
|
||||||
import { CommunityHighlight } from '../interfaces/community-highlight';
|
import { CommunityHighlight } from '../interfaces/community-highlight';
|
||||||
import { ProgramService } from '../services/program.service';
|
|
||||||
import { ShowService } from '../services/show.service';
|
import { ShowService } from '../services/show.service';
|
||||||
import { EventService } from '../services/event.service';
|
import { EventService } from '../services/event.service';
|
||||||
import { TierService } from '../services/tier.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 { HistoryEntryService } from '../services/history-entry.service';
|
||||||
import { TeamMemberService } from '../services/team-member.service';
|
import { TeamMemberService } from '../services/team-member.service';
|
||||||
import { CommunityHighlightService } from '../services/community-highlight.service';
|
import { CommunityHighlightService } from '../services/community-highlight.service';
|
||||||
import { AdminProgramFormComponent } from './admin-program-form.component';
|
|
||||||
import { AdminShowFormComponent } from './admin-show-form.component';
|
import { AdminShowFormComponent } from './admin-show-form.component';
|
||||||
import { AdminEventFormComponent } from './admin-event-form.component';
|
import { AdminEventFormComponent } from './admin-event-form.component';
|
||||||
import { AdminTierFormComponent } from './admin-tier-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 { AdminTeamFormComponent } from './admin-team-form.component';
|
||||||
import { AdminCommunityFormComponent } from './admin-community-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({
|
@Component({
|
||||||
selector: 'app-admin',
|
selector: 'app-admin',
|
||||||
@@ -38,7 +35,6 @@ type TabKey = 'shows' | 'programs' | 'events' | 'tiers' | 'station' | 'history'
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
AdminShowFormComponent,
|
AdminShowFormComponent,
|
||||||
AdminProgramFormComponent,
|
|
||||||
AdminEventFormComponent,
|
AdminEventFormComponent,
|
||||||
AdminTierFormComponent,
|
AdminTierFormComponent,
|
||||||
AdminStationFormComponent,
|
AdminStationFormComponent,
|
||||||
@@ -50,7 +46,6 @@ type TabKey = 'shows' | 'programs' | 'events' | 'tiers' | 'station' | 'history'
|
|||||||
styleUrl: './admin.component.scss',
|
styleUrl: './admin.component.scss',
|
||||||
})
|
})
|
||||||
export class AdminComponent implements OnInit {
|
export class AdminComponent implements OnInit {
|
||||||
private programService = inject(ProgramService);
|
|
||||||
private showService = inject(ShowService);
|
private showService = inject(ShowService);
|
||||||
private eventService = inject(EventService);
|
private eventService = inject(EventService);
|
||||||
private tierService = inject(TierService);
|
private tierService = inject(TierService);
|
||||||
@@ -63,7 +58,6 @@ export class AdminComponent implements OnInit {
|
|||||||
|
|
||||||
/** Item currently being edited (set by editXxx, cleared on save/close). */
|
/** Item currently being edited (set by editXxx, cleared on save/close). */
|
||||||
editingShow: Show | null = null;
|
editingShow: Show | null = null;
|
||||||
editingProgram: Program | null = null;
|
|
||||||
editingEvent: Event | null = null;
|
editingEvent: Event | null = null;
|
||||||
editingTier: Tier | null = null;
|
editingTier: Tier | null = null;
|
||||||
editingHistory: HistoryEntry | 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. */
|
/** BehaviorSubjects guarantee change detection via the async pipe — start empty, populated on load. */
|
||||||
readonly shows$ = new BehaviorSubject<Show[]>([]);
|
readonly shows$ = new BehaviorSubject<Show[]>([]);
|
||||||
readonly programs$ = new BehaviorSubject<Program[]>([]);
|
|
||||||
readonly events$ = new BehaviorSubject<Event[]>([]);
|
readonly events$ = new BehaviorSubject<Event[]>([]);
|
||||||
readonly tiers$ = new BehaviorSubject<Tier[]>([]);
|
readonly tiers$ = new BehaviorSubject<Tier[]>([]);
|
||||||
readonly stationConfig$ = new BehaviorSubject<StationConfig | null>(null);
|
readonly stationConfig$ = new BehaviorSubject<StationConfig | null>(null);
|
||||||
@@ -81,7 +74,6 @@ export class AdminComponent implements OnInit {
|
|||||||
readonly communityHighlights$ = new BehaviorSubject<CommunityHighlight[]>([]);
|
readonly communityHighlights$ = new BehaviorSubject<CommunityHighlight[]>([]);
|
||||||
|
|
||||||
showShowForm = false;
|
showShowForm = false;
|
||||||
showProgramForm = false;
|
|
||||||
showEventForm = false;
|
showEventForm = false;
|
||||||
showTierForm = false;
|
showTierForm = false;
|
||||||
showStationForm = false;
|
showStationForm = false;
|
||||||
@@ -98,11 +90,6 @@ export class AdminComponent implements OnInit {
|
|||||||
this.shows$.next(data);
|
this.shows$.next(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadPrograms(): Promise<void> {
|
|
||||||
const data = await firstValueFrom(this.programService.getPrograms());
|
|
||||||
this.programs$.next(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async reloadEvents(): Promise<void> {
|
async reloadEvents(): Promise<void> {
|
||||||
const data = await firstValueFrom(this.eventService.getEvents());
|
const data = await firstValueFrom(this.eventService.getEvents());
|
||||||
this.events$.next(data);
|
this.events$.next(data);
|
||||||
@@ -136,7 +123,6 @@ export class AdminComponent implements OnInit {
|
|||||||
async loadAll(): Promise<void> {
|
async loadAll(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.reloadShows(),
|
this.reloadShows(),
|
||||||
this.reloadPrograms(),
|
|
||||||
this.reloadEvents(),
|
this.reloadEvents(),
|
||||||
this.reloadTiers(),
|
this.reloadTiers(),
|
||||||
this.reloadStationConfig(),
|
this.reloadStationConfig(),
|
||||||
@@ -175,35 +161,6 @@ export class AdminComponent implements OnInit {
|
|||||||
await this.reloadShows();
|
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 ──────────────────────────────────────────
|
// ── Event CRUD ──────────────────────────────────────────
|
||||||
|
|
||||||
openEventForm(): void {
|
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