feat(timetable): types + zod schemas + idempotency-key helper (Session 4 step 1)
- Extract generateIdempotencyKey() from useFormDraft into reusable lib/ - New types/timetable.ts mirrors PerformanceResource, ArtistEngagementResource, StageResource, GenreResource and the four enums verbatim - New schemas/timetable.ts adds zod parsers for runtime validation of API responses + form payloads (createPerformance, createStage, moveTimetable) RFC v0.2 §10 contract surface for the upcoming timetable canvas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
229
apps/app/src/schemas/timetable.ts
Normal file
229
apps/app/src/schemas/timetable.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
ArtistEngagementStatus,
|
||||
BumaHandledBy,
|
||||
FeeType,
|
||||
PaymentStatus,
|
||||
} from '@/types/timetable'
|
||||
|
||||
const artistEngagementStatusSchema = z.enum([
|
||||
ArtistEngagementStatus.DRAFT,
|
||||
ArtistEngagementStatus.REQUESTED,
|
||||
ArtistEngagementStatus.OPTION,
|
||||
ArtistEngagementStatus.OFFERED,
|
||||
ArtistEngagementStatus.CONFIRMED,
|
||||
ArtistEngagementStatus.CONTRACTED,
|
||||
ArtistEngagementStatus.CANCELLED,
|
||||
ArtistEngagementStatus.REJECTED,
|
||||
ArtistEngagementStatus.DECLINED,
|
||||
])
|
||||
|
||||
const bumaHandledBySchema = z.enum([
|
||||
BumaHandledBy.ORGANISATION,
|
||||
BumaHandledBy.BOOKING_AGENCY,
|
||||
BumaHandledBy.NOT_APPLICABLE,
|
||||
])
|
||||
|
||||
const feeTypeSchema = z.enum([
|
||||
FeeType.FLAT,
|
||||
FeeType.DOOR_SPLIT,
|
||||
FeeType.GUARANTEE_PLUS_SPLIT,
|
||||
])
|
||||
|
||||
const paymentStatusSchema = z.enum([
|
||||
PaymentStatus.NONE,
|
||||
PaymentStatus.DEPOSIT_PAID,
|
||||
PaymentStatus.PAID_IN_FULL,
|
||||
])
|
||||
|
||||
function enumLabel<T extends z.ZodTypeAny>(value: T) {
|
||||
return z.object({
|
||||
value: value.nullable(),
|
||||
label: z.string().nullable(),
|
||||
})
|
||||
}
|
||||
|
||||
export const genreSchema = z.object({
|
||||
id: z.string(),
|
||||
organisation_id: z.string(),
|
||||
name: z.string(),
|
||||
color: z.string().nullable(),
|
||||
sort_order: z.number(),
|
||||
is_active: z.boolean(),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const artistContactSchema = z.object({
|
||||
id: z.string(),
|
||||
artist_id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().nullable(),
|
||||
phone: z.string().nullable(),
|
||||
role: z.string(),
|
||||
is_primary: z.boolean(),
|
||||
receives_briefing: z.boolean(),
|
||||
receives_infosheet: z.boolean(),
|
||||
})
|
||||
|
||||
export const artistSchema = z.object({
|
||||
id: z.string(),
|
||||
organisation_id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
default_genre_id: z.string().nullable(),
|
||||
default_genre: genreSchema.nullable().optional(),
|
||||
default_draw: z.number().nullable(),
|
||||
star_rating: z.number().nullable(),
|
||||
home_base_country: z.string().nullable(),
|
||||
agent_company_id: z.string().nullable(),
|
||||
agent_company: z
|
||||
.object({
|
||||
id: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
handles_buma: z.boolean(),
|
||||
})
|
||||
.optional(),
|
||||
notes: z.string().nullable(),
|
||||
contacts: z.array(artistContactSchema).optional(),
|
||||
engagements_summary: z.object({
|
||||
lifetime_count: z.number(),
|
||||
upcoming_count: z.number(),
|
||||
}),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
deleted_at: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const artistEngagementSchema = z.object({
|
||||
id: z.string(),
|
||||
organisation_id: z.string(),
|
||||
artist_id: z.string(),
|
||||
event_id: z.string(),
|
||||
artist: artistSchema.optional(),
|
||||
project_leader_id: z.string().nullable(),
|
||||
project_leader: z
|
||||
.object({
|
||||
id: z.string().nullable(),
|
||||
name: z.string(),
|
||||
email: z.string().nullable(),
|
||||
})
|
||||
.optional(),
|
||||
booking_status: enumLabel(artistEngagementStatusSchema),
|
||||
fee_amount: z.number().nullable(),
|
||||
fee_currency: z.string(),
|
||||
fee_type: enumLabel(feeTypeSchema),
|
||||
buma_applicable: z.boolean(),
|
||||
buma_percentage: z.number().nullable(),
|
||||
buma_handled_by: enumLabel(bumaHandledBySchema),
|
||||
vat_applicable: z.boolean(),
|
||||
vat_percentage: z.number().nullable(),
|
||||
deal_breakdown: z
|
||||
.array(z.object({ label: z.string().optional(), amount: z.number() }))
|
||||
.nullable(),
|
||||
deposit_percentage: z.number().nullable(),
|
||||
deposit_due_date: z.string().nullable(),
|
||||
balance_due_date: z.string().nullable(),
|
||||
payment_status: enumLabel(paymentStatusSchema),
|
||||
crew_count: z.number(),
|
||||
guests_count: z.number(),
|
||||
requested_at: z.string().nullable(),
|
||||
option_expires_at: z.string().nullable(),
|
||||
advance_open_from: z.string().nullable(),
|
||||
advance_open_to: z.string().nullable(),
|
||||
advancing_completed_count: z.number(),
|
||||
advancing_total_count: z.number(),
|
||||
notes: z.string().nullable(),
|
||||
computed: z.object({
|
||||
buma_amount: z.number(),
|
||||
vat_grondslag: z.number(),
|
||||
vat_amount: z.number(),
|
||||
breakdown_total: z.number(),
|
||||
total_cost: z.number(),
|
||||
}),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
deleted_at: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const stageSchema = z.object({
|
||||
id: z.string(),
|
||||
event_id: z.string(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
capacity: z.number().nullable(),
|
||||
sort_order: z.number(),
|
||||
stage_days: z.array(z.string()).optional(),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const performanceWarningSchema = z.enum([
|
||||
'overlap',
|
||||
'b2b',
|
||||
'capacity',
|
||||
'b2b_left',
|
||||
'b2b_right',
|
||||
])
|
||||
|
||||
export const performanceSchema = z.object({
|
||||
id: z.string(),
|
||||
engagement_id: z.string(),
|
||||
event_id: z.string(),
|
||||
stage_id: z.string().nullable(),
|
||||
lane: z.number(),
|
||||
lane_resolved: z.number(),
|
||||
start_at: z.string().nullable(),
|
||||
end_at: z.string().nullable(),
|
||||
version: z.number(),
|
||||
notes: z.string().nullable(),
|
||||
warnings: z.array(performanceWarningSchema),
|
||||
engagement: artistEngagementSchema.optional(),
|
||||
stage: stageSchema.nullable().optional(),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
deleted_at: z.string().nullable(),
|
||||
})
|
||||
|
||||
export const moveTimetableSuccessSchema = z.object({
|
||||
moved: performanceSchema,
|
||||
cascaded: z.array(performanceSchema),
|
||||
})
|
||||
|
||||
export const moveTimetableConflictSchema = z.object({
|
||||
conflict: z.literal('version_mismatch'),
|
||||
current_version: z.number(),
|
||||
client_version: z.number(),
|
||||
server_data: performanceSchema,
|
||||
})
|
||||
|
||||
// ─── Form-payload schemas (validated client-side before POST) ─────────
|
||||
|
||||
export const createPerformancePayloadSchema = z.object({
|
||||
engagement_id: z.string().min(1, 'Engagement is verplicht.'),
|
||||
event_id: z.string().min(1, 'Sub-event is verplicht.'),
|
||||
stage_id: z.string().nullable(),
|
||||
start_at: z.string().min(1, 'Starttijd is verplicht.'),
|
||||
end_at: z.string().min(1, 'Eindtijd is verplicht.'),
|
||||
lane: z.number().int().min(0).max(9).optional().nullable(),
|
||||
notes: z.string().max(1000).optional().nullable(),
|
||||
}).refine(p => p.start_at < p.end_at, {
|
||||
message: 'Eindtijd moet na de starttijd liggen.',
|
||||
path: ['end_at'],
|
||||
})
|
||||
|
||||
export const createStagePayloadSchema = z.object({
|
||||
name: z.string().min(1, 'Naam is verplicht.').max(120),
|
||||
color: z.string().regex(/^#[0-9A-F]{6}$/i, 'Kleur moet een #RRGGBB hex zijn.'),
|
||||
capacity: z.number().int().min(0).optional().nullable(),
|
||||
sort_order: z.number().int().min(0).optional().nullable(),
|
||||
})
|
||||
|
||||
export const moveTimetablePayloadSchema = z.object({
|
||||
performance_id: z.string(),
|
||||
target_stage_id: z.string().nullable(),
|
||||
target_start_at: z.string().nullable(),
|
||||
target_end_at: z.string().nullable(),
|
||||
target_lane: z.number().int().min(0).max(9).nullable(),
|
||||
version: z.number().int().min(0),
|
||||
})
|
||||
Reference in New Issue
Block a user