/opt/canhelp/apps/api/src/routes
NameSizeModeActions
admin.ts950570644editdlrm
auto-match.ts74060644editdlrm
auto-response.ts81110644editdlrm
availability.ts72940644editdlrm
categories.ts18670644editdlrm
chat.ts93770644editdlrm
dashboard.ts51420644editdlrm
favorites.ts44410644editdlrm
fomo.ts68440644editdlrm
google.ts48720644editdlrm
health.ts12140644editdlrm
i18n.ts1670860644editdlrm
locations.ts17810644editdlrm
notifications.ts28620644editdlrm
offers.ts100470644editdlrm
payments.ts211670644editdlrm
plans.ts13520644editdlrm
portfolio.ts66580644editdlrm
price-list.ts144170644editdlrm
reports.ts20070644editdlrm
reviews.ts27830644editdlrm
skill-suggestions.ts10350644editdlrm
specialist-cards.ts186410644editdlrm
stats.ts10930644editdlrm
statuses.ts37310644editdlrm
support.ts84520644editdlrm
tasks.ts300180644editdlrm
uploads.ts15260644editdlrm
users.ts371570644editdlrm
Edit: /opt/canhelp/apps/api/src/routes/availability.ts (7294B)
import { Hono } from 'hono' import { eq, and, gte, lte } from 'drizzle-orm' import { db } from '../db.js' import { specialistAvailability, users } from '@canhelp/db' import { requireAuth, type AuthVariables } from '../middleware/auth.js' import { pushAvailabilityChange } from '../lib/google-oauth.js' import { getHelperCapability } from '../lib/helper-capability.js' const app = new Hono<{ Variables: AuthVariables }>() // ─── Helpers ────────────────────────────────────────────────────────────── function monthRange(month: string): { from: string; to: string } { // month = 'YYYY-MM' const [year, mon] = month.split('-').map(Number) const from = `${year}-${String(mon).padStart(2, '0')}-01` const lastDay = new Date(year, mon, 0).getDate() const to = `${year}-${String(mon).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}` return { from, to } } // GET /availability/my?month=YYYY-MM // Returns own availability for the given month (defaults to current) app.get('/my', requireAuth, async (c) => { const user = c.get('user') const month = c.req.query('month') || new Date().toISOString().slice(0, 7) const { from, to } = monthRange(month) const rows = await db .select() .from(specialistAvailability) .where( and( eq(specialistAvailability.specialistId, user.id), gte(specialistAvailability.date, from), lte(specialistAvailability.date, to), ), ) return c.json(rows) }) // POST /availability/set { date: 'YYYY-MM-DD', status: 'available'|'busy'|null } // status=null means "remove" (unset) app.post('/set', requireAuth, async (c) => { const user = c.get('user') const { date, status, note } = await c.req.json<{ date: string status: 'available' | 'busy' | null note?: string }>() if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) { return c.json({ error: 'Invalid date' }, 400) } // Validate status if (status !== null && status !== 'available' && status !== 'busy') { return c.json({ error: 'Invalid status' }, 400) } // Don't allow setting dates in the past const today = new Date().toISOString().slice(0, 10) if (date < today) { return c.json({ error: 'Cannot set past dates' }, 400) } if (status === null) { // Remove the record await db .delete(specialistAvailability) .where( and( eq(specialistAvailability.specialistId, user.id), eq(specialistAvailability.date, date), ), ) // Fire-and-forget delete from Google Calendar pushAvailabilityChange(user.id, date, null).catch(() => {}) return c.json({ ok: true, date, status: null }) } // Upsert — PostgreSQL ON CONFLICT DO UPDATE const existing = await db .select() .from(specialistAvailability) .where( and( eq(specialistAvailability.specialistId, user.id), eq(specialistAvailability.date, date), ), ) .limit(1) if (existing.length > 0) { const [updated] = await db .update(specialistAvailability) .set({ status, note: note ?? null, updatedAt: new Date() }) .where( and( eq(specialistAvailability.specialistId, user.id), eq(specialistAvailability.date, date), ), ) .returning() // Fire-and-forget export to Google Calendar (never block or throw) pushAvailabilityChange(user.id, date, status).catch((err) => console.error('[gcal] push error:', err), ) return c.json(updated) } const [created] = await db .insert(specialistAvailability) .values({ specialistId: user.id, date, status, note: note ?? null, }) .returning() // Fire-and-forget export to Google Calendar (never block or throw) pushAvailabilityChange(user.id, date, status).catch((err) => console.error('[gcal] push error:', err), ) return c.json(created, 201) }) // POST /availability/set-range { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD', status: 'available'|'busy'|null } // Bulk-set a range of dates app.post('/set-range', requireAuth, async (c) => { const user = c.get('user') const { from, to, status, note } = await c.req.json<{ from: string to: string status: 'available' | 'busy' | null note?: string }>() if (!from || !to || !/^\d{4}-\d{2}-\d{2}$/.test(from) || !/^\d{4}-\d{2}-\d{2}$/.test(to)) { return c.json({ error: 'Invalid date range' }, 400) } if (from > to) return c.json({ error: 'from must be <= to' }, 400) const today = new Date().toISOString().slice(0, 10) // Build date list const dates: string[] = [] const cur = new Date(from) const end = new Date(to) while (cur <= end) { const d = cur.toISOString().slice(0, 10) if (d >= today) dates.push(d) cur.setDate(cur.getDate() + 1) } if (dates.length === 0) return c.json({ ok: true, count: 0 }) if (dates.length > 366) return c.json({ error: 'Range too large' }, 400) if (status === null) { // Delete all in range for (const date of dates) { await db .delete(specialistAvailability) .where(and(eq(specialistAvailability.specialistId, user.id), eq(specialistAvailability.date, date))) } // Fire-and-forget calendar sync for (const date of dates) { pushAvailabilityChange(user.id, date, null).catch(() => {}) } return c.json({ ok: true, count: dates.length }) } // Upsert each date for (const date of dates) { const existing = await db .select({ id: specialistAvailability.id }) .from(specialistAvailability) .where(and(eq(specialistAvailability.specialistId, user.id), eq(specialistAvailability.date, date))) .limit(1) if (existing.length > 0) { await db .update(specialistAvailability) .set({ status: status!, note: note ?? null, updatedAt: new Date() }) .where(and(eq(specialistAvailability.specialistId, user.id), eq(specialistAvailability.date, date))) } else { await db.insert(specialistAvailability).values({ specialistId: user.id, date, status: status!, note: note ?? null }) } } // Fire-and-forget export to Google Calendar for (const date of dates) { pushAvailabilityChange(user.id, date, status!).catch(() => {}) } return c.json({ ok: true, count: dates.length }) }) // GET /availability/user/:userId?month=YYYY-MM — public view app.get('/user/:userId', async (c) => { const { userId } = c.req.param() const month = c.req.query('month') || new Date().toISOString().slice(0, 7) const { from, to } = monthRange(month) const [targetUser] = await db .select({ id: users.id }) .from(users) .where(eq(users.id, userId)) .limit(1) if (!targetUser || !(await getHelperCapability(userId))) { return c.json([]) } const rows = await db .select({ date: specialistAvailability.date, status: specialistAvailability.status }) .from(specialistAvailability) .where( and( eq(specialistAvailability.specialistId, userId), gte(specialistAvailability.date, from), lte(specialistAvailability.date, to), ), ) return c.json(rows) }) export default app