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:
parent
6e339c6cf0
commit
09ded150f4
@ -3,6 +3,7 @@
|
|||||||
// Consumed by useSyncHook for React integration
|
// Consumed by useSyncHook for React integration
|
||||||
|
|
||||||
import type { Timer } from './timer-engine';
|
import type { Timer } from './timer-engine';
|
||||||
|
import type { Routine, RoutineStep } from './routines';
|
||||||
import { getAuthClient, PRODUCT_ID as _PRODUCT_ID } from './auth-api';
|
import { getAuthClient, PRODUCT_ID as _PRODUCT_ID } from './auth-api';
|
||||||
|
|
||||||
// ── DTOs ──────────────────────────────────────────────────────
|
// ── DTOs ──────────────────────────────────────────────────────
|
||||||
@ -47,10 +48,37 @@ export interface BatchResult {
|
|||||||
errors: string[];
|
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 {
|
export interface OfflineQueueItem {
|
||||||
id: string;
|
id: string;
|
||||||
action: 'create' | 'update' | 'delete';
|
action: 'create' | 'update' | 'delete';
|
||||||
timer?: SyncTimerDTO;
|
timer?: SyncTimerDTO;
|
||||||
|
routine?: SyncRoutineDTO;
|
||||||
|
entityType: 'timer' | 'routine';
|
||||||
enqueuedAt: number;
|
enqueuedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +210,34 @@ export async function batchUpsert(
|
|||||||
return apiRequest<BatchResult>('/timers/batch', 'POST', { timers });
|
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 ─────────────────────────────────────────────
|
// ── Offline Queue ─────────────────────────────────────────────
|
||||||
|
|
||||||
export function loadOfflineQueue(): OfflineQueueItem[] {
|
export function loadOfflineQueue(): OfflineQueueItem[] {
|
||||||
@ -208,6 +264,7 @@ export function enqueueChange(
|
|||||||
id: timer.id,
|
id: timer.id,
|
||||||
action,
|
action,
|
||||||
timer: action !== 'delete' ? timerToDTO(timer) : undefined,
|
timer: action !== 'delete' ? timerToDTO(timer) : undefined,
|
||||||
|
entityType: 'timer',
|
||||||
enqueuedAt: Date.now(),
|
enqueuedAt: Date.now(),
|
||||||
});
|
});
|
||||||
saveOfflineQueue(queue);
|
saveOfflineQueue(queue);
|
||||||
@ -215,7 +272,7 @@ export function enqueueChange(
|
|||||||
|
|
||||||
export function enqueueDeleteChange(timerId: string): void {
|
export function enqueueDeleteChange(timerId: string): void {
|
||||||
const queue = loadOfflineQueue().filter((item) => item.id !== timerId);
|
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);
|
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> {
|
export function dtoToTimerPatch(dto: SyncTimerDTO): Partial<Timer> {
|
||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user