// server/src/routes/org.ts — full file
import { Hono } from 'hono';
import { eq, and, sql, count, sum, isNull, inArray } from 'drizzle-orm';
import { requireOrgContext } from '../middleware/org-context';
import { getReadDb, getWriteDb } from '../lib/db';
import { handleError } from '../lib/errors';
import { formatDatePKT } from '../lib/pkt';
import * as schema from '../schema';
const org = new Hono();
org.use('*', requireOrgContext);
/** Returns today's date in PKT as YYYY-MM-DD */
function todayPKT(): string {
return formatDatePKT(new Date());
}
// GET /api/v1/org/overview
// Returns today's network-wide stats across all branches in the org.
org.get('/overview', async (c) => {
try {
const orgCtx = c.get('orgContext');
const db = getReadDb();
const today = todayPKT();
// Fetch all clinic IDs in this org
const branchRows = await db
.select({ clinicId: schema.organizationClinics.clinicId, branchDisplayName: schema.organizationClinics.branchDisplayName })
.from(schema.organizationClinics)
.where(eq(schema.organizationClinics.organizationId, orgCtx.organizationId));
const clinicIds = branchRows.map((r) => r.clinicId);
if (!clinicIds.length) {
return c.json({ totalAppointments: 0, totalRevenue: 0, activeBranches: 0, noShowRate: 0, branches: [] });
}
// Today's appointment count
const [apptRow] = await db
.select({ total: count() })
.from(schema.appointments)
.where(
and(
inArray(schema.appointments.clinicId, clinicIds),
eq(schema.appointments.appointmentDate, today),
isNull(schema.appointments.deletedAt),
)
);
// Today's completed payments revenue (payments → invoices for clinic scoping)
const [revenueRow] = await db
.select({ total: sum(schema.payments.amount) })
.from(schema.payments)
.innerJoin(schema.invoices, eq(schema.invoices.id, schema.payments.invoiceId))
.where(
and(
inArray(schema.invoices.clinicId, clinicIds),
sql`DATE(${schema.payments.createdAt} AT TIME ZONE 'Asia/Karachi') = ${today}`,
)
);
// No-show count today
const [noShowRow] = await db
.select({ total: count() })
.from(schema.appointments)
.where(
and(
inArray(schema.appointments.clinicId, clinicIds),
eq(schema.appointments.appointmentDate, today),
eq(schema.appointments.status, 'no_show'),
isNull(schema.appointments.deletedAt),
)
);
const totalAppts = Number(apptRow?.total ?? 0);
const noShows = Number(noShowRow?.total ?? 0);
return c.json({
totalAppointments: totalAppts,
totalRevenue: Number(revenueRow?.total ?? 0),
activeBranches: branchRows.length,
noShowRate: totalAppts > 0 ? Math.round((noShows / totalAppts) * 100) : 0,
branches: branchRows,
});
} catch (err) {
return handleError(c, err);
}
});
// GET /api/v1/org/branches
org.get('/branches', async (c) => {
try {
const orgCtx = c.get('orgContext');
const db = getReadDb();
const rows = await db
.select({
clinicId: schema.organizationClinics.clinicId,
branchDisplayName: schema.organizationClinics.branchDisplayName,
city: schema.organizationClinics.city,
sortOrder: schema.organizationClinics.sortOrder,
clinicName: schema.clinics.name,
isActive: schema.clinics.isActive,
})
.from(schema.organizationClinics)
.innerJoin(schema.clinics, eq(schema.clinics.id, schema.organizationClinics.clinicId))
.where(eq(schema.organizationClinics.organizationId, orgCtx.organizationId))
.orderBy(schema.organizationClinics.sortOrder);
return c.json(rows);
} catch (err) {
return handleError(c, err);
}
});
// GET /api/v1/org/staff
org.get('/staff', async (c) => {
try {
const orgCtx = c.get('orgContext');
const db = getReadDb();
const branchRows = await db
.select({ clinicId: schema.organizationClinics.clinicId })
.from(schema.organizationClinics)
.where(eq(schema.organizationClinics.organizationId, orgCtx.organizationId));
const clinicIds = branchRows.map((r) => r.clinicId);
if (!clinicIds.length) return c.json([]);
const staff = await db
.select({
userId: schema.userClinicAssignments.userId,
clinicId: schema.userClinicAssignments.clinicId,
role: schema.userClinicAssignments.role,
firstName: schema.users.firstName,
lastName: schema.users.lastName,
email: schema.users.email,
status: schema.userClinicAssignments.status,
})
.from(schema.userClinicAssignments)
.innerJoin(schema.users, eq(schema.users.id, schema.userClinicAssignments.userId))
.where(
and(
inArray(schema.userClinicAssignments.clinicId, clinicIds),
eq(schema.userClinicAssignments.status, 'active'),
)
);
return c.json(staff);
} catch (err) {
return handleError(c, err);
}
});
// GET /api/v1/org/templates
org.get('/templates', async (c) => {
try {
const orgCtx = c.get('orgContext');
const db = getReadDb();
const rows = await db
.select()
.from(schema.organizationTemplates)
.where(eq(schema.organizationTemplates.organizationId, orgCtx.organizationId));
return c.json(rows);
} catch (err) {
return handleError(c, err);
}
});
// POST /api/v1/org/templates
org.post('/templates', async (c) => {
try {
const orgCtx = c.get('orgContext');
const user = c.get('user');
const body = await c.req.json<{ type: string; name: string; contentJson: object }>();
const db = getWriteDb();
const [row] = await db
.insert(schema.organizationTemplates)
.values({
organizationId: orgCtx.organizationId,
type: body.type,
name: body.name,
contentJson: body.contentJson,
createdBy: user.id,
})
.returning();
return c.json(row, 201);
} catch (err) {
return handleError(c, err);
}
});
// GET /api/v1/org/info
org.get('/info', async (c) => {
try {
const orgCtx = c.get('orgContext');
const db = getReadDb();
const [org] = await db
.select()
.from(schema.organizations)
.where(eq(schema.organizations.id, orgCtx.organizationId));
return c.json(org);
} catch (err) {
return handleError(c, err);
}
});
export default org;