/opt/canhelp/apps/api/src/routes
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