diff --git a/web/src/lib/platform-sync.ts b/web/src/lib/platform-sync.ts index 7af9687..feee330 100644 --- a/web/src/lib/platform-sync.ts +++ b/web/src/lib/platform-sync.ts @@ -3,6 +3,7 @@ // Consumed by useSyncHook for React integration import type { Timer } from './timer-engine'; +import type { Routine, RoutineStep } from './routines'; import { getAuthClient, PRODUCT_ID as _PRODUCT_ID } from './auth-api'; // ── DTOs ────────────────────────────────────────────────────── @@ -47,10 +48,37 @@ export interface BatchResult { errors: string[]; } +export interface SyncRoutineStepDTO { + id: string; + label: string; + durationMinutes: number; + transition: string; + customTransitionMinutes?: number; + notes?: string; + status: string; +} + +export interface SyncRoutineDTO { + id: string; + name: string; + description?: string; + steps: SyncRoutineStepDTO[]; + totalDurationMinutes: number; + status: string; + currentStepIndex: number; + isTemplate: boolean; + createdAt: string; + startedAt?: string; + completedAt?: string; + syncVersion: number; +} + export interface OfflineQueueItem { id: string; action: 'create' | 'update' | 'delete'; timer?: SyncTimerDTO; + routine?: SyncRoutineDTO; + entityType: 'timer' | 'routine'; enqueuedAt: number; } @@ -182,6 +210,34 @@ export async function batchUpsert( return apiRequest('/timers/batch', 'POST', { timers }); } +// ── Routine Sync Operations ────────────────────────────────── + +export async function pullRoutineDelta(since?: string): Promise { + const params = since ? `?since=${encodeURIComponent(since)}` : ''; + return apiRequest(`/routines/sync${params}`, 'GET'); +} + +export async function pushRoutine(dto: SyncRoutineDTO): Promise { + return apiRequest('/routines', 'POST', dto); +} + +export async function updateRemoteRoutine( + id: string, + updates: Partial +): Promise { + return apiRequest(`/routines/${id}`, 'PUT', updates); +} + +export async function deleteRemoteRoutine(id: string): Promise { + return apiRequest(`/routines/${id}`, 'DELETE'); +} + +export async function batchUpsertRoutines( + routines: SyncRoutineDTO[] +): Promise { + return apiRequest('/routines/batch', 'POST', { routines }); +} + // ── Offline Queue ───────────────────────────────────────────── export function loadOfflineQueue(): OfflineQueueItem[] { @@ -208,6 +264,7 @@ export function enqueueChange( id: timer.id, action, timer: action !== 'delete' ? timerToDTO(timer) : undefined, + entityType: 'timer', enqueuedAt: Date.now(), }); saveOfflineQueue(queue); @@ -215,7 +272,7 @@ export function enqueueChange( export function enqueueDeleteChange(timerId: string): void { const queue = loadOfflineQueue().filter((item) => item.id !== timerId); - queue.push({ id: timerId, action: 'delete', enqueuedAt: Date.now() }); + queue.push({ id: timerId, action: 'delete', entityType: 'timer', enqueuedAt: Date.now() }); saveOfflineQueue(queue); } @@ -316,6 +373,59 @@ export function timerToDTO(timer: Timer): SyncTimerDTO { }; } +// ── Routine DTO Conversion ─────────────────────────────────── + +export function routineToDTO(routine: Routine): SyncRoutineDTO { + return { + id: routine.id, + name: routine.name, + description: routine.description, + steps: routine.steps.map((s) => ({ + id: s.id, + label: s.label, + durationMinutes: s.durationMinutes, + transition: s.transition, + customTransitionMinutes: s.customTransitionMinutes, + notes: s.notes, + status: s.status, + })), + totalDurationMinutes: routine.totalDurationMinutes, + status: routine.status, + currentStepIndex: routine.currentStepIndex, + isTemplate: routine.isTemplate, + createdAt: new Date(routine.createdAt).toISOString(), + startedAt: routine.startedAt ? new Date(routine.startedAt).toISOString() : undefined, + completedAt: routine.completedAt ? new Date(routine.completedAt).toISOString() : undefined, + syncVersion: 1, + }; +} + +export function dtoToRoutinePatch(dto: SyncRoutineDTO): Partial { + return { + id: dto.id, + name: dto.name, + description: dto.description, + steps: dto.steps.map((s) => ({ + id: s.id, + label: s.label, + durationMinutes: s.durationMinutes, + transition: s.transition as RoutineStep['transition'], + customTransitionMinutes: s.customTransitionMinutes ?? 0, + notes: s.notes, + status: s.status as RoutineStep['status'], + startedAt: null, + completedAt: null, + })), + totalDurationMinutes: dto.totalDurationMinutes, + status: dto.status as Routine['status'], + currentStepIndex: dto.currentStepIndex, + isTemplate: dto.isTemplate, + createdAt: new Date(dto.createdAt).getTime(), + startedAt: dto.startedAt ? new Date(dto.startedAt).getTime() : null, + completedAt: dto.completedAt ? new Date(dto.completedAt).getTime() : null, + }; +} + export function dtoToTimerPatch(dto: SyncTimerDTO): Partial { return { id: dto.id,