import { z } from 'zod'; export function canCompleteTask(dueDate: number): boolean { if (dueDate === 0) return true; const now = new Date(); const endOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime(); return dueDate <= endOfToday; } export const categoryCreateSchema = z.object({ name: z.string().min(1).max(50), color: z.string().regex(/^#[0-9A-Fa-f]{6}$/), order: z.number().int().min(0).optional(), }); export const categoryUpdateSchema = z.object({ name: z.string().min(1).max(50).optional(), color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), order: z.number().int().min(0).optional(), }); export const repeatSchema = z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']); // The client stores '' / 0 for unset repeat values; normalize them so stale or // legacy rows never fail validation on push. export const repeatFieldSchema = repeatSchema .or(z.literal('')) .transform((v) => (v === '' ? 'none' : v)) .optional(); export const repeatIntervalFieldSchema = z .number() .int() .min(0) .max(30) .transform((v) => Math.max(1, v)) .optional(); export const taskCreateSchema = z.object({ title: z.string().min(1).max(100), description: z.string().max(1000).optional(), categoryId: z.string().max(100).transform((v) => (v === '' ? null : v)).nullable(), tags: z.string().max(500).optional(), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), dueDate: z.number().int().min(0).optional(), dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''), endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')), allDay: z.boolean().optional(), repeat: repeatFieldSchema, repeatInterval: repeatIntervalFieldSchema, repeatDays: z.string().max(20).optional(), seriesId: z.string().max(50).optional(), reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(), reminders: z.string().max(100).optional(), assigneeId: z.string().nullable().optional(), completedAt: z.number().int().min(0).nullable().optional(), subtasks: z.array(z.object({ title: z.string().min(1).max(100), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)) })).optional(), }); export const taskUpdateSchema = z.object({ title: z.string().min(1).max(100).optional(), description: z.string().max(1000).optional(), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)), tags: z.string().max(500).optional(), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), completed: z.boolean().optional(), dueDate: z.number().int().min(0).optional(), dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v === '' ? undefined : v), endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(), reminders: z.string().max(100).optional(), assigneeId: z.string().nullable().optional(), completedAt: z.number().int().min(0).nullable().optional(), }); export const subtaskCreateSchema = z.object({ title: z.string().min(1).max(100), description: z.string().max(1000).optional(), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), dueDate: z.number().int().min(0).optional(), dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''), endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')), allDay: z.boolean().optional(), repeat: repeatFieldSchema, repeatInterval: repeatIntervalFieldSchema, repeatDays: z.string().max(20).optional(), seriesId: z.string().max(50).optional(), reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(), reminders: z.string().max(100).optional(), assigneeId: z.string().nullable().optional(), order: z.number().int().min(0).optional(), parentSubtaskId: z.string().nullable().optional(), }); export const subtaskUpdateSchema = z.object({ title: z.string().min(1).max(100).optional(), description: z.string().max(1000).optional(), categoryId: z.string().max(100).nullable().optional().transform((v) => (v === '' ? null : v)), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), completed: z.boolean().optional(), dueDate: z.number().int().min(0).optional(), dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v === '' ? undefined : v), endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(), allDay: z.boolean().optional(), repeat: repeatFieldSchema, repeatInterval: repeatIntervalFieldSchema, repeatDays: z.string().max(20).optional(), seriesId: z.string().max(50).optional(), reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(), reminders: z.string().max(100).optional(), assigneeId: z.string().nullable().optional(), order: z.number().int().min(0).optional(), parentSubtaskId: z.string().nullable().optional(), }); export const userSettingsSchema = z.object({ darkMode: z.boolean().optional(), notifications: z.boolean().optional(), reminderTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(), defaultCategory: z.string().optional().nullable(), sortBy: z.enum(['dueDate', 'priority', 'title', 'createdAt']).optional(), sortOrder: z.enum(['asc', 'desc']).optional(), }); export const repeatProfileSchema = z.object({ name: z.string().min(1).max(50), repeat: repeatSchema, repeatInterval: z.number().int().min(1).max(30), repeatDays: z.string().max(20), }); export const repeatProfileUpdateSchema = z.object({ name: z.string().min(1).max(50).optional(), repeat: repeatSchema.optional(), repeatInterval: z.number().int().min(1).max(30).optional(), repeatDays: z.string().max(20).optional(), }); export const searchQuerySchema = z.object({ q: z.string().min(1).max(50), }); export const friendRequestSchema = z.object({ username: z.string().min(1).max(50), }); export const friendshipSchema = z.object({ id: z.string(), userId: z.string(), friendId: z.string(), status: z.enum(['pending', 'accepted']), createdAt: z.number(), updatedAt: z.number(), }); export const tombstoneSchema = z.object({ entity: z.enum(['categories', 'tasks', 'subtasks', 'repeatProfiles', 'friendships']), id: z.string(), updatedAt: z.number().int().min(0), }); export const pushChangesSchema = z.object({ changes: z.object({ categories: z.array(categoryCreateSchema.extend({ id: z.string(), createdAt: z.number(), updatedAt: z.number() })).optional(), tasks: z.array(taskCreateSchema.extend({ id: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number(), completedAt: z.number().int().min(0).nullable().optional() })).optional(), subtasks: z.array(subtaskCreateSchema.extend({ id: z.string(), taskId: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number() })).optional(), repeatProfiles: z.array(repeatProfileSchema.extend({ id: z.string(), createdAt: z.number(), updatedAt: z.number() })).optional(), friendships: z.array(friendshipSchema).optional(), }), deleted: z.array(tombstoneSchema).optional(), lastPulledAt: z.number().int().min(0), }); export const syncQuerySchema = z.object({ since: z.string().transform(Number).pipe(z.number().int().min(0)), }); export const taskQuerySchema = z.object({ categoryId: z.string().optional(), completed: z.string().transform(v => v === 'true').optional(), dueBefore: z.string().transform(Number).pipe(z.number().int().min(0)).optional(), dueAfter: z.string().transform(Number).pipe(z.number().int().min(0)).optional(), priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(), sortBy: z.enum(['dueDate', 'priority', 'createdAt', 'title']).optional(), sortOrder: z.enum(['asc', 'desc']).optional(), limit: z.string().transform(Number).pipe(z.number().int().min(1).max(100)).optional(), offset: z.string().transform(Number).pipe(z.number().int().min(0)).optional(), });