feat(web): add routine sync operations to platform-sync client

- SyncRoutineDTO + SyncRoutineStepDTO types
- pullRoutineDelta, pushRoutine, updateRemoteRoutine, deleteRemoteRoutine, batchUpsertRoutines
- routineToDTO / dtoToRoutinePatch conversion functions
- OfflineQueueItem now supports entityType (timer|routine)
This commit is contained in:
saravanakumardb1 2026-02-28 13:54:43 -08:00
parent 6e339c6cf0
commit 09ded150f4

View File

@ -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<BatchResult>('/timers/batch', 'POST', { timers });
}
// ── Routine Sync Operations ──────────────────────────────────
export async function pullRoutineDelta(since?: string): Promise<SyncRoutineDTO[]> {
const params = since ? `?since=${encodeURIComponent(since)}` : '';
return apiRequest<SyncRoutineDTO[]>(`/routines/sync${params}`, 'GET');
}
export async function pushRoutine(dto: SyncRoutineDTO): Promise<SyncRoutineDTO> {
return apiRequest<SyncRoutineDTO>('/routines', 'POST', dto);
}
export async function updateRemoteRoutine(
id: string,
updates: Partial<SyncRoutineDTO>
): Promise<SyncRoutineDTO> {
return apiRequest<SyncRoutineDTO>(`/routines/${id}`, 'PUT', updates);
}
export async function deleteRemoteRoutine(id: string): Promise<void> {
return apiRequest<void>(`/routines/${id}`, 'DELETE');
}
export async function batchUpsertRoutines(
routines: SyncRoutineDTO[]
): Promise<BatchResult> {
return apiRequest<BatchResult>('/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<Routine> {
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<Timer> {
return {
id: dto.id,