feat(slot-definitions): add materialization policy selector (SAFE/CLEANUP)
continuous-integration/drone/push Build is passing Details

- Add MaterialisationPolicyProfile type and MATERIALISATION_POLICIES constants
- Add policy selector UI with radio buttons for SAFE and CLEANUP modes
- SAFE mode (default): skips overlapping slots to protect existing schedule
- CLEANUP mode: removes conflicting slots to fix overlaps (keeps bookings safe)
- Update MaterialisationTriggerRequest to include policy_profile field
- Pass selected policy to triggerMaterialisation API call
- Fixes issue where partial overlaps blocked slot materialization
- Implements Backend Brooke's BUILD 352 policy preset feature
master
Guillermo Pages 1 month ago
parent 52b3d38088
commit 2881721b18

@ -3,12 +3,13 @@
import { useState, useEffect } from 'react';
import { Calendar, Loader2, AlertCircle, CheckCircle, Clock, AlertTriangle } from 'lucide-react';
import { getMaterialisationStatus, triggerMaterialisation } from '@/src/lib/api/materialisation';
import type { MaterialisationStatus } from '@/src/types/materialisation';
import type { MaterialisationStatus, MaterialisationPolicyProfile } from '@/src/types/materialisation';
import {
generateIdempotencyKey,
calculateRemainingCooldown,
formatCountdown,
formatTimestamp,
MATERIALISATION_POLICIES,
} from '@/src/types/materialisation';
interface MaterialisationStatusPanelProps {
@ -21,6 +22,7 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
const [triggering, setTriggering] = useState(false);
const [error, setError] = useState<string | null>(null);
const [countdown, setCountdown] = useState(0);
const [selectedPolicy, setSelectedPolicy] = useState<MaterialisationPolicyProfile>('SAFE');
// Poll status on mount and when triggering completes
useEffect(() => {
@ -85,7 +87,10 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
const idempotencyKey = generateIdempotencyKey();
const result = await triggerMaterialisation(clubId, { idempotency_key: idempotencyKey });
const result = await triggerMaterialisation(clubId, {
idempotency_key: idempotencyKey,
policy_profile: selectedPolicy,
});
if (result.success) {
// Start polling immediately
@ -275,6 +280,51 @@ export default function MaterialisationStatusPanel({ clubId }: MaterialisationSt
</div>
)}
{/* Policy Selector */}
<div className="mt-6 space-y-3">
<label className="block text-sm font-semibold text-slate-700">
Materialization Policy
</label>
<div className="space-y-2">
{MATERIALISATION_POLICIES.map((policy) => (
<label
key={policy.name}
className={`flex items-start p-4 border-2 rounded-lg cursor-pointer transition-all ${
selectedPolicy === policy.name
? policy.color === 'blue'
? 'border-blue-600 bg-blue-50'
: 'border-orange-600 bg-orange-50'
: 'border-slate-200 bg-white hover:border-slate-300'
}`}
>
<input
type="radio"
name="policy"
value={policy.name}
checked={selectedPolicy === policy.name}
onChange={(e) => setSelectedPolicy(e.target.value as MaterialisationPolicyProfile)}
className="mt-1 w-4 h-4"
disabled={!canTrigger}
/>
<div className="ml-3 flex-1">
<div className="flex items-center space-x-2">
<span className="text-lg">{policy.icon}</span>
<span className="font-semibold text-slate-900">
{policy.name === 'SAFE' ? 'Safe Mode (Default)' : 'Cleanup Mode'}
</span>
{policy.badge && (
<span className="text-xs px-2 py-0.5 bg-orange-600 text-white rounded-full font-medium">
{policy.badge}
</span>
)}
</div>
<p className="text-sm text-slate-700 mt-1">{policy.description}</p>
</div>
</label>
))}
</div>
</div>
{/* Trigger button */}
<div className="mt-6">
<button

@ -7,6 +7,32 @@
export type MaterialisationJobStatus = 'idle' | 'running' | 'completed' | 'failed';
export type MaterialisationPolicyProfile = 'SAFE' | 'CLEANUP';
export interface MaterialisationPolicy {
name: MaterialisationPolicyProfile;
description: string;
icon: string;
color: string;
badge?: string;
}
export const MATERIALISATION_POLICIES: MaterialisationPolicy[] = [
{
name: 'SAFE',
description: 'Normal mode - skips overlapping slots to protect existing schedule',
icon: '🛡️',
color: 'blue',
},
{
name: 'CLEANUP',
description: 'Cleanup mode - removes conflicting slots (keeps bookings safe)',
icon: '🧹',
color: 'orange',
badge: 'Deletes old slots',
},
];
export interface MaterialisationRateLimit {
can_trigger: boolean;
next_available_at: string | null; // ISO 8601 timestamp
@ -24,6 +50,8 @@ export interface MaterialisationStatus {
export interface MaterialisationTriggerRequest {
idempotency_key: string; // UUID v4
horizon_days?: number; // 1-28, default 7
policy_profile?: MaterialisationPolicyProfile; // default 'SAFE'
}
export interface MaterialisationTriggerResponse {

Loading…
Cancel
Save