diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 1b2de88..4d2044e 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -1,11 +1,22 @@ -from datetime import date +from datetime import date as _date from typing import Optional -from pydantic import BaseModel, field_validator +from pydantic import BaseModel, computed_field, field_validator # ── Program ────────────────────────────────────────────── +DAY_NAMES = { + 1: "Monday", + 2: "Tuesday", + 3: "Wednesday", + 4: "Thursday", + 5: "Friday", + 6: "Saturday", + 7: "Sunday", +} + + class ProgramCreate(BaseModel): day_of_week: int day_label: str @@ -27,19 +38,23 @@ class ProgramUpdate(BaseModel): class ProgramResponse(BaseModel): id: int day_of_week: int - day_label: str 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): - date: date + date: _date title: str description: str location: str @@ -48,7 +63,7 @@ class EventCreate(BaseModel): class EventUpdate(BaseModel): - date: Optional[date] = None + date: Optional[_date] = None title: Optional[str] = None description: Optional[str] = None location: Optional[str] = None @@ -59,7 +74,7 @@ class EventUpdate(BaseModel): class EventResponse(BaseModel): id: int - date: date + date: _date title: str description: str location: str diff --git a/src/app/admin/admin-event-form.component.ts b/src/app/admin/admin-event-form.component.ts new file mode 100644 index 0000000..9311edb --- /dev/null +++ b/src/app/admin/admin-event-form.component.ts @@ -0,0 +1,105 @@ +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 { Event } from '../interfaces/event'; +import { EventService } from '../services/event.service'; + +@Component({ + selector: 'app-admin-event-form', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './admin-event-form.component.html', + styleUrl: './admin-event-form.component.scss', +}) +export class AdminEventFormComponent implements OnChanges { + private eventService = inject(EventService); + + @Input() editItem: Event | null = null; + @Output() saved = new EventEmitter(); + @Output() closed = new EventEmitter(); + + id: number | null = null; + date = ''; + title = ''; + description = ''; + location = ''; + icon = '🎵'; + rsvp_url = ''; + active = true; + + loading = false; + error = ''; + submitted = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['editItem'] && this.editItem) { + this.edit(this.editItem); + } + } + + edit(event: Event): void { + this.id = event.id; + this.date = event.date; + this.title = event.title; + this.description = event.description; + this.location = event.location; + this.icon = event.icon; + this.rsvp_url = event.rsvp_url ?? ''; + this.active = event.active; + } + + reset(): void { + this.id = null; + this.date = ''; + this.title = ''; + this.description = ''; + this.location = ''; + this.icon = '🎵'; + this.rsvp_url = ''; + this.active = true; + this.error = ''; + this.submitted = false; + } + + onClose(): void { + this.closed.emit(); + this.reset(); + } + + async onSubmit(): Promise { + this.submitted = true; + if (!this.title || !this.date || !this.description || !this.location) { + this.error = 'Please fill in all required fields.'; + return; + } + + this.loading = true; + this.error = ''; + + const payload = { + date: this.date, + title: this.title, + description: this.description, + location: this.location, + icon: this.icon, + rsvp_url: this.rsvp_url || null, + active: this.active, + }; + + try { + if (this.id) { + await firstValueFrom(this.eventService.updateEvent(this.id, payload)); + } else { + await firstValueFrom(this.eventService.createEvent(payload)); + } + this.saved.emit(); + this.reset(); + } catch (err: any) { + this.error = this.formatError(err); + } finally { + this.loading = false; + } + } +} diff --git a/src/app/admin/admin-program-form.component.ts b/src/app/admin/admin-program-form.component.ts new file mode 100644 index 0000000..0595f30 --- /dev/null +++ b/src/app/admin/admin-program-form.component.ts @@ -0,0 +1,119 @@ +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. */ + 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-tier-form.component.ts b/src/app/admin/admin-tier-form.component.ts new file mode 100644 index 0000000..a3f0066 --- /dev/null +++ b/src/app/admin/admin-tier-form.component.ts @@ -0,0 +1,102 @@ +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 { Tier } from '../interfaces/tier'; +import { TierService } from '../services/tier.service'; + +@Component({ + selector: 'app-admin-tier-form', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './admin-tier-form.component.html', + styleUrl: './admin-tier-form.component.scss', +}) +export class AdminTierFormComponent implements OnChanges { + private tierService = inject(TierService); + + @Input() editItem: Tier | null = null; + @Output() saved = new EventEmitter(); + @Output() closed = new EventEmitter(); + + id: number | null = null; + name = ''; + amount = 0; + icon = '⭐'; + benefits_text = ''; + display_order = 1; + + loading = false; + error = ''; + submitted = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['editItem'] && this.editItem) { + this.edit(this.editItem); + } + } + + edit(tier: Tier): void { + this.id = tier.id; + this.name = tier.name; + this.amount = tier.amount; + this.icon = tier.icon; + this.benefits_text = tier.benefits.join(', '); + this.display_order = tier.display_order; + } + + reset(): void { + this.id = null; + this.name = ''; + this.amount = 0; + this.icon = '⭐'; + this.benefits_text = ''; + this.display_order = 1; + this.error = ''; + this.submitted = false; + } + + onClose(): void { + this.closed.emit(); + this.reset(); + } + + async onSubmit(): Promise { + this.submitted = true; + if (!this.name || this.amount <= 0) { + this.error = 'Please fill in all required fields.'; + return; + } + + this.loading = true; + this.error = ''; + + const benefits = this.benefits_text + .split(',') + .map((b) => b.trim()) + .filter((b) => b.length > 0); + + const payload = { + name: this.name, + amount: this.amount, + icon: this.icon, + benefits: benefits, + display_order: this.display_order, + }; + + try { + if (this.id) { + await firstValueFrom(this.tierService.updateTier(this.id, payload)); + } else { + await firstValueFrom(this.tierService.createTier(payload)); + } + this.saved.emit(); + this.reset(); + } catch (err: any) { + this.error = this.formatError(err); + } finally { + this.loading = false; + } + } +}