diff --git a/src/lib/api/habits.triplit.ts b/src/lib/api/habits.triplit.ts new file mode 100644 index 0000000..dfaf1a5 --- /dev/null +++ b/src/lib/api/habits.triplit.ts @@ -0,0 +1,175 @@ +import { triplit, Query } from '../triplit/client'; +import type { Habit, HabitCompletion } from '../triplit/schema'; + +export type CreateHabitData = Omit & { + endDate?: Date; +}; + +export type UpdateHabitData = Partial>; + +export type CreateCompletionData = Omit & { + completedAt?: Date; +}; + +export async function createHabit(data: CreateHabitData & { active?: boolean; startDate?: Date }) { + return await triplit.insert('habits', { + userId: data.userId, + name: data.name, + duration: data.duration, + target: data.target, + increment: data.increment, + unit: data.unit || null, + active: data.active ?? true, + startDate: data.startDate || new Date(), + endDate: data.endDate || null, + createdAt: new Date(), + updatedAt: new Date(), + }); +} + +export async function updateHabit(id: string, data: UpdateHabitData) { + return await triplit.update('habits', id, { + ...data, + updatedAt: new Date(), + }); +} + +export async function deleteHabit(id: string) { + // Delete all associated completions + const completions = await triplit.fetch( + Query('habit_completions').Where('habitId', '=', id) + ); + + for (const completion of completions) { + await triplit.delete('habit_completions', completion.id); + } + + // Then delete the habit + return await triplit.delete('habits', id); +} + +export function getHabits(userId: string) { + return Query('habits') + .Where('userId', '=', userId) + .Order('createdAt', 'ASC'); +} + +export function getHabit(id: string) { + return Query('habits') + .Where('id', '=', id); +} + +export async function createCompletion(data: CreateCompletionData) { + return await triplit.insert('habit_completions', { + habitId: data.habitId, + value: data.value, + failed: data.failed || false, + completedAt: data.completedAt || new Date(), + }); +} + +export async function deleteCompletion(id: string) { + return await triplit.delete('habit_completions', id); +} + +export function getCompletions(habitId: string, startDate?: Date, endDate?: Date) { + let query = Query('habit_completions') + .Where('habitId', '=', habitId); + + if (startDate) { + query = query.Where('completedAt', '>=', startDate); + } + + if (endDate) { + query = query.Where('completedAt', '<=', endDate); + } + + return query.Order('completedAt', 'DESC'); +} + +// Get habits with completions for a date range +export function getHabitsWithCompletions(userId: string, startDate: Date, endDate: Date) { + return Query('habits') + .Where('userId', '=', userId) + .Where([ + { + exists: Query('habit_completions').Where([ + ['habitId', '=', '$1.id'], + ['completedAt', '>=', startDate], + ['completedAt', '<=', endDate] + ]) + } + ]) + .Include('completions') + .Order('createdAt', 'ASC'); +} + +export function getHabitCompletions(habitId: string, startDate: Date, endDate: Date) { + return Query('habit_completions') + .Where('habitId', '=', habitId) + .Where('completedAt', '>=', startDate) + .Where('completedAt', '<=', endDate) + .Where('failed', '=', false); +} + +export async function archiveHabit(id: string) { + return await updateHabit(id, { active: false }); +} + +export async function updateCompletion(id: string, data: Partial>) { + return await triplit.update('habit_completions', id, data); +} + +export async function getCompletionByDate(habitId: string, date: Date) { + // Create date range for the specific day (start of day to end of day) + const startOfDay = new Date(date); + startOfDay.setHours(0, 0, 0, 0); + + const endOfDay = new Date(date); + endOfDay.setHours(23, 59, 59, 999); + + const completions = await triplit.fetch( + Query('habit_completions') + .Where('habitId', '=', habitId) + .Where('completedAt', '>=', startOfDay) + .Where('completedAt', '<=', endOfDay) + ); + + return completions[0] || null; +} + +export async function upsertCompletion(habitId: string, date: Date, value: number, failed: boolean = false) { + const existing = await getCompletionByDate(habitId, date); + + if (existing) { + return await updateCompletion(existing.id, { value, failed }); + } else { + return await createCompletion({ + habitId, + value, + failed, + completedAt: date + }); + } +} + +// Reset completions in date range to value 0 +export async function resetCompletions(habitId: string, startDate: Date, endDate: Date) { + return await triplit.transact(async (tx) => { + const completions = await tx.fetch( + Query('habit_completions') + .Where('habitId', '=', habitId) + .Where('completedAt', '>=', startDate) + .Where('completedAt', '<=', endDate) + ); + + for (const completion of completions) { + await tx.update('habit_completions', completion.id, { + value: 0, + failed: false + }); + } + + return completions.length; + }); +} \ No newline at end of file