POL-127: Add organizations table and championship ownership

- Create organizations table with Alembic migration (3-phase: create table, migrate data, drop old column)
- Add org_id FK on championships linking to organizations
- Refactor all schemas into one-class-per-file packages (auth, championship, organization, participant, registration, user)
- Update CRUD layer with selectinload for organization relationships
- Update frontend types and components to use nested organization object
- Remove phantom Championship fields (subtitle, venue, accent_color) from frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dianaka123
2026-03-01 22:09:10 +03:00
parent 96e02bf64a
commit d4f0a05707
44 changed files with 450 additions and 183 deletions

View File

@@ -8,7 +8,7 @@ import { RegistrationTimeline } from "@/components/registrations/RegistrationTim
import { StatusBadge } from "@/components/shared/StatusBadge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { MapPin, Building2, Calendar, CreditCard, Film, ExternalLink } from "lucide-react";
import { MapPin, Calendar, CreditCard, Film, ExternalLink } from "lucide-react";
export default function ChampionshipDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
@@ -54,7 +54,6 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="font-display text-3xl font-bold tracking-wide">{championship.title}</h1>
{championship.subtitle && <p className="text-muted-foreground mt-1">{championship.subtitle}</p>}
</div>
<StatusBadge status={championship.status} />
</div>
@@ -67,12 +66,6 @@ export default function ChampionshipDetailPage({ params }: { params: Promise<{ i
<span className="text-muted-foreground">{championship.location}</span>
</div>
)}
{championship.venue && (
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
<Building2 size={16} className="text-purple-accent shrink-0" />
<span className="text-muted-foreground">{championship.venue}</span>
</div>
)}
{eventDate && (
<div className="flex items-center gap-2.5 rounded-xl bg-surface-elevated border border-border/30 px-4 py-3 text-sm">
<Calendar size={16} className="text-gold-accent shrink-0" />

View File

@@ -64,13 +64,13 @@ export default function ProfilePage() {
<span className="text-sm text-foreground">{user.phone}</span>
</div>
)}
{user.organization_name && (
{user.organization?.name && (
<div className="flex items-center justify-between py-2">
<span className="flex items-center gap-2 text-sm text-dim">
<Building2 size={14} />
Organization
</span>
<span className="text-sm text-foreground">{user.organization_name}</span>
<span className="text-sm text-foreground">{user.organization.name}</span>
</div>
)}
{user.instagram_handle && (

View File

@@ -35,10 +35,10 @@ export function UserCard({ user, onApprove, onReject, isActing }: Props) {
<span className={`h-2 w-2 rounded-full shrink-0 ${STATUS_DOT[user.status] ?? "bg-dim"}`} />
</div>
<p className="text-sm text-muted-foreground">{user.email}</p>
{user.organization_name && (
{user.organization?.name && (
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
<Building2 size={12} className="text-dim" />
{user.organization_name}
{user.organization.name}
</p>
)}
{user.phone && (

View File

@@ -27,6 +27,6 @@ export const authApi = {
me: () => apiClient.get<UserOut>("/auth/me").then((r) => r.data),
updateMe: (data: Partial<Pick<UserOut, "full_name" | "phone" | "organization_name" | "instagram_handle">>) =>
updateMe: (data: Partial<Pick<UserOut, "full_name" | "phone" | "instagram_handle">> & { organization_name?: string }) =>
apiClient.patch<UserOut>("/auth/me", data).then((r) => r.data),
};

View File

@@ -1,10 +1,16 @@
export interface OrganizationBrief {
id: string;
name: string;
instagram: string | null;
logo_url: string | null;
}
export interface Championship {
id: string;
org_id: string | null;
title: string;
subtitle: string | null;
description: string | null;
location: string | null;
venue: string | null;
event_date: string | null;
registration_open_at: string | null;
registration_close_at: string | null;
@@ -14,7 +20,7 @@ export interface Championship {
status: "draft" | "open" | "closed" | "completed";
source: string;
image_url: string | null;
accent_color: string | null;
organization: OrganizationBrief | null;
created_at: string;
updated_at: string;
}

View File

@@ -1,3 +1,14 @@
export interface OrganizationOut {
id: string;
name: string;
instagram: string | null;
email: string | null;
city: string | null;
logo_url: string | null;
verified: boolean;
status: string;
}
export interface UserOut {
id: string;
email: string;
@@ -5,7 +16,7 @@ export interface UserOut {
phone: string | null;
role: "member" | "organizer" | "admin";
status: "pending" | "approved" | "rejected";
organization_name: string | null;
instagram_handle: string | null;
organization: OrganizationOut | null;
created_at: string;
}