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:
@@ -7,6 +7,7 @@ import {
|
|||||||
useSaveFormDraft,
|
useSaveFormDraft,
|
||||||
useSubmitForm,
|
useSubmitForm,
|
||||||
} from '@/composables/api/usePublicForm'
|
} from '@/composables/api/usePublicForm'
|
||||||
|
import { generateIdempotencyKey } from '@/lib/idempotencyKey'
|
||||||
import type { FormValues, PublicFormSubmission, SaveDraftBody } from '@/types/forms/formBuilder'
|
import type { FormValues, PublicFormSubmission, SaveDraftBody } from '@/types/forms/formBuilder'
|
||||||
|
|
||||||
/** sessionStorage key for reusing an idempotency key across reloads. */
|
/** sessionStorage key for reusing an idempotency key across reloads. */
|
||||||
@@ -19,25 +20,6 @@ export function draftSubmitterStorageKey(token: string): string {
|
|||||||
return `draft_submitter:${token}`
|
return `draft_submitter:${token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateIdempotencyKey(): string {
|
|
||||||
const c = (globalThis as { crypto?: { randomUUID?: () => string; getRandomValues?: (arr: Uint8Array) => Uint8Array } }).crypto
|
|
||||||
if (c?.randomUUID) {
|
|
||||||
// UUID v4 (36 chars) exceeds backend max:30. Backend expects 6..30
|
|
||||||
// chars so compress to 24 hex chars (still collision-resistant).
|
|
||||||
return c.randomUUID().replace(/-/g, '').slice(0, 24)
|
|
||||||
}
|
|
||||||
if (c?.getRandomValues) {
|
|
||||||
const buf = new Uint8Array(12)
|
|
||||||
|
|
||||||
c.getRandomValues(buf)
|
|
||||||
|
|
||||||
return Array.from(buf, b => b.toString(16).padStart(2, '0')).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last-resort fallback — still within 6..30.
|
|
||||||
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`.slice(0, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseFormDraftOptions {
|
interface UseFormDraftOptions {
|
||||||
|
|
||||||
/** Preferred locale string for `submitted_in_locale` (e.g. `"nl"`). */
|
/** Preferred locale string for `submitted_in_locale` (e.g. `"nl"`). */
|
||||||
|
|||||||
26
apps/app/src/lib/idempotencyKey.ts
Normal file
26
apps/app/src/lib/idempotencyKey.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Generate a 24-hex idempotency key (UUID v4 with dashes stripped, sliced to 24).
|
||||||
|
*
|
||||||
|
* Backend constraint: Crewli's idempotency middleware accepts 6..30 chars
|
||||||
|
* (FORM-07). UUID v4's 36-char form exceeds the cap, so it is normalised
|
||||||
|
* to 24 hex chars — still collision-resistant for per-mutation keys.
|
||||||
|
*
|
||||||
|
* Safe fallbacks: `getRandomValues` (12 random bytes) when randomUUID is
|
||||||
|
* unavailable; finally a non-cryptographic time+random suffix so calling
|
||||||
|
* code never throws.
|
||||||
|
*/
|
||||||
|
export function generateIdempotencyKey(): string {
|
||||||
|
const c = (globalThis as { crypto?: { randomUUID?: () => string; getRandomValues?: (arr: Uint8Array) => Uint8Array } }).crypto
|
||||||
|
if (c?.randomUUID)
|
||||||
|
return c.randomUUID().replace(/-/g, '').slice(0, 24)
|
||||||
|
|
||||||
|
if (c?.getRandomValues) {
|
||||||
|
const buf = new Uint8Array(12)
|
||||||
|
|
||||||
|
c.getRandomValues(buf)
|
||||||
|
|
||||||
|
return Array.from(buf, b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`.slice(0, 30)
|
||||||
|
}
|
||||||
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),
|
||||||
|
})
|
||||||
273
apps/app/src/types/timetable.ts
Normal file
273
apps/app/src/types/timetable.ts
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
/**
|
||||||
|
* RFC-TIMETABLE v0.2 — frontend type mirrors of the backend resources.
|
||||||
|
*
|
||||||
|
* Source of truth: api/app/Http/Resources/Api/V1/Artist/*.php and the
|
||||||
|
* matching enums under api/app/Enums/Artist/*.php. Keep this file in
|
||||||
|
* lockstep with backend changes; runtime parsers live in
|
||||||
|
* src/schemas/timetable.ts and provide the safety net for drift.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ─── Enums (mirror backend `enum: string` types verbatim) ────────────
|
||||||
|
|
||||||
|
export const ArtistEngagementStatus = {
|
||||||
|
DRAFT: 'draft',
|
||||||
|
REQUESTED: 'requested',
|
||||||
|
OPTION: 'option',
|
||||||
|
OFFERED: 'offered',
|
||||||
|
CONFIRMED: 'confirmed',
|
||||||
|
CONTRACTED: 'contracted',
|
||||||
|
CANCELLED: 'cancelled',
|
||||||
|
REJECTED: 'rejected',
|
||||||
|
DECLINED: 'declined',
|
||||||
|
} as const
|
||||||
|
export type ArtistEngagementStatus = typeof ArtistEngagementStatus[keyof typeof ArtistEngagementStatus]
|
||||||
|
|
||||||
|
export const BumaHandledBy = {
|
||||||
|
ORGANISATION: 'organisation',
|
||||||
|
BOOKING_AGENCY: 'booking_agency',
|
||||||
|
NOT_APPLICABLE: 'not_applicable',
|
||||||
|
} as const
|
||||||
|
export type BumaHandledBy = typeof BumaHandledBy[keyof typeof BumaHandledBy]
|
||||||
|
|
||||||
|
export const FeeType = {
|
||||||
|
FLAT: 'flat',
|
||||||
|
DOOR_SPLIT: 'door_split',
|
||||||
|
GUARANTEE_PLUS_SPLIT: 'guarantee_plus_split',
|
||||||
|
} as const
|
||||||
|
export type FeeType = typeof FeeType[keyof typeof FeeType]
|
||||||
|
|
||||||
|
export const PaymentStatus = {
|
||||||
|
NONE: 'none',
|
||||||
|
DEPOSIT_PAID: 'deposit_paid',
|
||||||
|
PAID_IN_FULL: 'paid_in_full',
|
||||||
|
} as const
|
||||||
|
export type PaymentStatus = typeof PaymentStatus[keyof typeof PaymentStatus]
|
||||||
|
|
||||||
|
// Backend wraps each enum in `{ value, label }` for i18n display.
|
||||||
|
export interface EnumLabel<T extends string> {
|
||||||
|
value: T | null
|
||||||
|
label: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Genre / Artist / Engagement ─────────────────────────────────────
|
||||||
|
|
||||||
|
export interface Genre {
|
||||||
|
id: string
|
||||||
|
organisation_id: string
|
||||||
|
name: string
|
||||||
|
color: string | null
|
||||||
|
sort_order: number
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistContact {
|
||||||
|
id: string
|
||||||
|
artist_id: string
|
||||||
|
name: string
|
||||||
|
email: string | null
|
||||||
|
phone: string | null
|
||||||
|
role: string
|
||||||
|
is_primary: boolean
|
||||||
|
receives_briefing: boolean
|
||||||
|
receives_infosheet: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistAgentCompany {
|
||||||
|
id: string | null
|
||||||
|
name: string | null
|
||||||
|
handles_buma: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistEngagementsSummary {
|
||||||
|
lifetime_count: number
|
||||||
|
upcoming_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Artist {
|
||||||
|
id: string
|
||||||
|
organisation_id: string
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
default_genre_id: string | null
|
||||||
|
default_genre?: Genre | null
|
||||||
|
default_draw: number | null
|
||||||
|
star_rating: number | null
|
||||||
|
home_base_country: string | null
|
||||||
|
agent_company_id: string | null
|
||||||
|
agent_company?: ArtistAgentCompany
|
||||||
|
notes: string | null
|
||||||
|
contacts?: ArtistContact[]
|
||||||
|
engagements_summary: ArtistEngagementsSummary
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
deleted_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistEngagementProjectLeader {
|
||||||
|
id: string | null
|
||||||
|
name: string
|
||||||
|
email: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DealBreakdownLine {
|
||||||
|
label?: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistEngagementComputed {
|
||||||
|
buma_amount: number
|
||||||
|
vat_grondslag: number
|
||||||
|
vat_amount: number
|
||||||
|
breakdown_total: number
|
||||||
|
total_cost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtistEngagement {
|
||||||
|
id: string
|
||||||
|
organisation_id: string
|
||||||
|
artist_id: string
|
||||||
|
event_id: string
|
||||||
|
artist?: Artist
|
||||||
|
project_leader_id: string | null
|
||||||
|
project_leader?: ArtistEngagementProjectLeader
|
||||||
|
booking_status: EnumLabel<ArtistEngagementStatus>
|
||||||
|
fee_amount: number | null
|
||||||
|
fee_currency: string
|
||||||
|
fee_type: EnumLabel<FeeType>
|
||||||
|
buma_applicable: boolean
|
||||||
|
buma_percentage: number | null
|
||||||
|
buma_handled_by: EnumLabel<BumaHandledBy>
|
||||||
|
vat_applicable: boolean
|
||||||
|
vat_percentage: number | null
|
||||||
|
deal_breakdown: DealBreakdownLine[] | null
|
||||||
|
deposit_percentage: number | null
|
||||||
|
deposit_due_date: string | null
|
||||||
|
balance_due_date: string | null
|
||||||
|
payment_status: EnumLabel<PaymentStatus>
|
||||||
|
crew_count: number
|
||||||
|
guests_count: number
|
||||||
|
requested_at: string | null
|
||||||
|
option_expires_at: string | null
|
||||||
|
advance_open_from: string | null
|
||||||
|
advance_open_to: string | null
|
||||||
|
advancing_completed_count: number
|
||||||
|
advancing_total_count: number
|
||||||
|
notes: string | null
|
||||||
|
computed: ArtistEngagementComputed
|
||||||
|
performances?: Performance[]
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
deleted_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Stage / StageDay ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface Stage {
|
||||||
|
id: string
|
||||||
|
event_id: string
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
capacity: number | null
|
||||||
|
sort_order: number
|
||||||
|
|
||||||
|
/** Sub-event IDs the stage is active on. Present only when loaded. */
|
||||||
|
stage_days?: string[]
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Performance ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type PerformanceWarning = 'overlap' | 'b2b' | 'capacity' | 'b2b_left' | 'b2b_right'
|
||||||
|
|
||||||
|
export interface Performance {
|
||||||
|
id: string
|
||||||
|
engagement_id: string
|
||||||
|
event_id: string
|
||||||
|
stage_id: string | null
|
||||||
|
|
||||||
|
/** 0-indexed lane (raw, persisted). Use `lane_resolved` for rendering. */
|
||||||
|
lane: number
|
||||||
|
|
||||||
|
/** 0-indexed lane after server-side Pass 1 + Pass 2 resolution (RFC D19). */
|
||||||
|
lane_resolved: number
|
||||||
|
start_at: string | null
|
||||||
|
end_at: string | null
|
||||||
|
|
||||||
|
/** Optimistic-lock cursor (RFC D14). */
|
||||||
|
version: number
|
||||||
|
notes: string | null
|
||||||
|
warnings: PerformanceWarning[]
|
||||||
|
engagement?: ArtistEngagement
|
||||||
|
stage?: Stage | null
|
||||||
|
created_at: string | null
|
||||||
|
updated_at: string | null
|
||||||
|
deleted_at: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Move endpoint contract (POST /timetable/move, RFC D18) ───────────
|
||||||
|
|
||||||
|
export interface MoveTimetablePayload {
|
||||||
|
performance_id: string
|
||||||
|
target_stage_id: string | null
|
||||||
|
target_start_at: string | null
|
||||||
|
target_end_at: string | null
|
||||||
|
target_lane: number | null
|
||||||
|
version: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoveTimetableSuccess {
|
||||||
|
moved: Performance
|
||||||
|
cascaded: Performance[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoveTimetableConflict {
|
||||||
|
conflict: 'version_mismatch'
|
||||||
|
current_version: number
|
||||||
|
client_version: number
|
||||||
|
server_data: Performance
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Performance create payload ───────────────────────────────────────
|
||||||
|
|
||||||
|
export interface CreatePerformancePayload {
|
||||||
|
engagement_id: string
|
||||||
|
event_id: string
|
||||||
|
stage_id: string | null
|
||||||
|
start_at: string
|
||||||
|
end_at: string
|
||||||
|
lane?: number | null
|
||||||
|
notes?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdatePerformancePayload {
|
||||||
|
notes?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Stage create / update / reorder ──────────────────────────────────
|
||||||
|
|
||||||
|
export interface CreateStagePayload {
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
capacity?: number | null
|
||||||
|
sort_order?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateStagePayload extends Partial<CreateStagePayload> {}
|
||||||
|
|
||||||
|
export interface ReorderStagesPayload {
|
||||||
|
stage_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplaceStageDaysPayload {
|
||||||
|
event_ids: string[]
|
||||||
|
force_orphan?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplaceStageDaysResponse {
|
||||||
|
stage: Stage
|
||||||
|
added_event_ids: string[]
|
||||||
|
removed_event_ids: string[]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user