import type { Database } from "bun:sqlite"; interface TaskFilters { orgId?: string | null; projectId?: string | null; status?: string | null; priority?: string | null; assigneeUserId?: string | null; } function toCamelCaseTask(row: Record) { return { id: row.id, orgId: row.org_id, projectId: row.project_id, assigneeUserId: row.assignee_user_id, title: row.title, description: row.description, status: row.status, priority: row.priority, storyPoints: row.story_points, dueDate: row.due_date, createdAt: row.created_at, updatedAt: row.updated_at, projectName: row.project_name, projectKey: row.project_key, assigneeName: row.assignee_name, }; } function getUser(db: Database, userId: string) { return db .query(` SELECT id, org_id AS orgId, full_name AS fullName, email, role, timezone FROM users WHERE id = ? `) .get(userId); } function getProject(db: Database, projectId: string) { return db .query(` SELECT p.id, p.org_id AS orgId, p.key, p.name, p.status, p.owner_user_id AS ownerUserId, p.due_date AS dueDate, u.full_name AS ownerName, COUNT(t.id) AS taskCount FROM projects p LEFT JOIN users u ON u.id = p.owner_user_id LEFT JOIN tasks t ON t.project_id = p.id WHERE p.id = ? GROUP BY p.id `) .get(projectId); } export function listOrganizations(db: Database) { return db .query(` SELECT o.id, o.slug, o.name, o.plan, o.industry, o.created_at AS createdAt, COUNT(DISTINCT p.id) AS projectCount, COUNT(DISTINCT t.id) AS taskCount, COUNT(DISTINCT CASE WHEN t.status != 'done' THEN t.id END) AS openTaskCount FROM organizations o LEFT JOIN projects p ON p.org_id = o.id LEFT JOIN tasks t ON t.org_id = o.id GROUP BY o.id ORDER BY o.name `) .all(); } export function getOrganizationSummary(db: Database, orgId: string) { const org = db .query(` SELECT o.id, o.slug, o.name, o.plan, o.industry, o.created_at AS createdAt, COUNT(DISTINCT u.id) AS userCount, COUNT(DISTINCT p.id) AS projectCount, COUNT(DISTINCT t.id) AS taskCount FROM organizations o LEFT JOIN users u ON u.org_id = o.id LEFT JOIN projects p ON p.org_id = o.id LEFT JOIN tasks t ON t.org_id = o.id WHERE o.id = ? GROUP BY o.id `) .get(orgId); if (!org) { return null; } const statusBreakdown = db .query(` SELECT status, COUNT(*) AS count FROM tasks WHERE org_id = ? GROUP BY status ORDER BY count DESC, status ASC `) .all(orgId); return { ...org, statusBreakdown, }; } export interface CreateOrganizationInput { slug: string; name: string; plan: string; industry: string; } export function createOrganization(db: Database, input: CreateOrganizationInput) { const id = `org_${Math.random().toString(36).slice(2, 10)}`; const now = new Date().toISOString(); db.query(` INSERT INTO organizations (id, slug, name, plan, industry, created_at) VALUES (?, ?, ?, ?, ?, ?) `).run(id, input.slug, input.name, input.plan, input.industry, now); return getOrganizationSummary(db, id); } export function listUsers(db: Database, orgId?: string | null) { if (orgId) { return db .query(` SELECT id, org_id AS orgId, full_name AS fullName, email, role, timezone FROM users WHERE org_id = ? ORDER BY full_name `) .all(orgId); } return db .query(` SELECT id, org_id AS orgId, full_name AS fullName, email, role, timezone FROM users ORDER BY org_id, full_name `) .all(); } export interface CreateUserInput { orgId: string; fullName: string; email: string; role: string; timezone: string; } export function createUser(db: Database, input: CreateUserInput) { const id = `user_${Math.random().toString(36).slice(2, 10)}`; db.query(` INSERT INTO users (id, org_id, full_name, email, role, timezone) VALUES (?, ?, ?, ?, ?, ?) `).run(id, input.orgId, input.fullName, input.email, input.role, input.timezone); return getUser(db, id); } export function listProjects(db: Database, orgId?: string | null) { const sql = ` SELECT p.id, p.org_id AS orgId, p.key, p.name, p.status, p.owner_user_id AS ownerUserId, p.due_date AS dueDate, u.full_name AS ownerName, COUNT(t.id) AS taskCount FROM projects p LEFT JOIN users u ON u.id = p.owner_user_id LEFT JOIN tasks t ON t.project_id = p.id ${orgId ? "WHERE p.org_id = ?" : ""} GROUP BY p.id ORDER BY p.name `; return orgId ? db.query(sql).all(orgId) : db.query(sql).all(); } export interface CreateProjectInput { orgId: string; key: string; name: string; status: string; ownerUserId: string; dueDate?: string | null; } export function createProject(db: Database, input: CreateProjectInput) { const id = `proj_${Math.random().toString(36).slice(2, 10)}`; db.query(` INSERT INTO projects (id, org_id, key, name, status, owner_user_id, due_date) VALUES (?, ?, ?, ?, ?, ?, ?) `).run( id, input.orgId, input.key, input.name, input.status, input.ownerUserId, input.dueDate ?? null, ); return getProject(db, id); } export function listTasks(db: Database, filters: TaskFilters = {}) { const conditions: string[] = []; const values: string[] = []; if (filters.orgId) { conditions.push("t.org_id = ?"); values.push(filters.orgId); } if (filters.projectId) { conditions.push("t.project_id = ?"); values.push(filters.projectId); } if (filters.status) { conditions.push("t.status = ?"); values.push(filters.status); } if (filters.priority) { conditions.push("t.priority = ?"); values.push(filters.priority); } if (filters.assigneeUserId) { conditions.push("t.assignee_user_id = ?"); values.push(filters.assigneeUserId); } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const rows = db .query(` SELECT t.*, p.name AS project_name, p.key AS project_key, u.full_name AS assignee_name FROM tasks t JOIN projects p ON p.id = t.project_id LEFT JOIN users u ON u.id = t.assignee_user_id ${whereClause} ORDER BY CASE t.priority WHEN 'urgent' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, t.due_date ASC, t.updated_at DESC `) .all(...values) as Record[]; return rows.map(toCamelCaseTask); } export function getTaskDetail(db: Database, taskId: string) { const row = db .query(` SELECT t.*, p.name AS project_name, p.key AS project_key, u.full_name AS assignee_name FROM tasks t JOIN projects p ON p.id = t.project_id LEFT JOIN users u ON u.id = t.assignee_user_id WHERE t.id = ? `) .get(taskId) as Record | null; if (!row) { return null; } const labels = db .query(` SELECT l.id, l.name, l.color FROM task_labels tl JOIN labels l ON l.id = tl.label_id WHERE tl.task_id = ? ORDER BY l.name `) .all(taskId); const comments = db .query(` SELECT c.id, c.body, c.created_at AS createdAt, c.author_user_id AS authorUserId, u.full_name AS authorName FROM comments c JOIN users u ON u.id = c.author_user_id WHERE c.task_id = ? ORDER BY c.created_at ASC `) .all(taskId); return { ...toCamelCaseTask(row), labels, comments, }; } export interface CreateTaskInput { orgId: string; projectId: string; assigneeUserId?: string | null; title: string; description?: string; status?: string; priority?: string; storyPoints?: number | null; dueDate?: string | null; } export function createTask(db: Database, input: CreateTaskInput) { const now = new Date().toISOString(); const id = `task_${Math.random().toString(36).slice(2, 10)}`; db.query(` INSERT INTO tasks ( id, org_id, project_id, assignee_user_id, title, description, status, priority, story_points, due_date, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( id, input.orgId, input.projectId, input.assigneeUserId ?? null, input.title, input.description ?? "", input.status ?? "todo", input.priority ?? "medium", input.storyPoints ?? null, input.dueDate ?? null, now, now, ); return getTaskDetail(db, id); } export interface UpdateTaskInput { title?: string; description?: string; status?: string; priority?: string; assigneeUserId?: string | null; storyPoints?: number | null; dueDate?: string | null; } export function updateTask(db: Database, taskId: string, input: UpdateTaskInput) { const existing = db.query("SELECT id FROM tasks WHERE id = ?").get(taskId); if (!existing) { return null; } const fields: string[] = []; const values: Array = []; if (input.title !== undefined) { fields.push("title = ?"); values.push(input.title); } if (input.description !== undefined) { fields.push("description = ?"); values.push(input.description); } if (input.status !== undefined) { fields.push("status = ?"); values.push(input.status); } if (input.priority !== undefined) { fields.push("priority = ?"); values.push(input.priority); } if (input.assigneeUserId !== undefined) { fields.push("assignee_user_id = ?"); values.push(input.assigneeUserId); } if (input.storyPoints !== undefined) { fields.push("story_points = ?"); values.push(input.storyPoints); } if (input.dueDate !== undefined) { fields.push("due_date = ?"); values.push(input.dueDate); } fields.push("updated_at = ?"); values.push(new Date().toISOString()); values.push(taskId); db.query(`UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`).run(...values); return getTaskDetail(db, taskId); }