Add meetings, sprints, and approval decision notes

Migration 042 creates meetings and sprints tables, adds decision_notes
to approvals. Full-stack: MeetingService, SprintService, IPC channels,
preload bridge, domain types. UI: MeetingsPage with scheduling and
type filtering, SprintsPage with progress tracking, ApprovalDetailDialog
with decision notes textarea.
This commit is contained in:
ZacharyZhang-NY committed 2026-06-07 18:01:58 -04:00
1 parent 75e5d17a86
commit ed9b3a3dd1
18 files changed
+430 -12

No files matched your search

@@ -79,11 +79,12 @@ export class ApprovalService {
"Deciding employee was not found.",
);
const now = new Date().toISOString();
const notes = input.notes?.trim() || null;
const result = this.db.prepare(`
update approvals
set state = ?, decided_by_employee_id = ?, decided_at = ?
set state = ?, decided_by_employee_id = ?, decided_at = ?, decision_notes = ?
where id = ? and org_id = ? and state = ?
`).run(input.state, input.decidedByEmployeeId, now, input.approvalId, input.orgId, APPROVAL_STATE.PENDING);
`).run(input.state, input.decidedByEmployeeId, now, notes, input.approvalId, input.orgId, APPROVAL_STATE.PENDING);
if (result.changes === 0) throw new Error("Pending approval was not found.");
this.audit.record({
orgId: input.orgId,
@@ -0,0 +1,36 @@
import type { DatabaseSync } from "node:sqlite";
import type { CreateMeetingInput, DeleteMeetingInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
export class MeetingService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
create(input: CreateMeetingInput): string {
const id = createId("mtg");
const now = new Date().toISOString();
this.db.prepare(`
insert into meetings(id, org_id, title, meeting_type, status, scheduled_at, duration_minutes, agenda, notes, decisions, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(id, input.orgId, input.title.trim(), input.meetingType, "scheduled", input.scheduledAt, input.durationMinutes, input.agenda.trim(), "", "", now, now);
this.audit.record({
orgId: input.orgId,
entityType: "meeting",
entityId: id,
actorType: "system",
actorId: null,
action: "created",
payload: { title: input.title.trim(), meetingType: input.meetingType },
});
return id;
}
delete(input: DeleteMeetingInput): void {
const result = this.db.prepare("delete from meetings where id = ? and org_id = ?").run(input.meetingId, input.orgId);
if (result.changes === 0) throw new Error("Meeting was not found.");
}
}
@@ -160,6 +160,8 @@ export class SnapshotService {
knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"),
automationRules: list<AppSnapshot["automationRules"][number]>(this.db, "automation_rules", "priority asc")
.map((rule) => ({ ...rule, enabled: toBool(rule.enabled) })),
meetings: list(this.db, "meetings", "scheduled_at desc"),
sprints: list(this.db, "sprints", "start_date desc"),
};
}
@@ -0,0 +1,36 @@
import type { DatabaseSync } from "node:sqlite";
import type { CreateSprintInput, DeleteSprintInput } from "../../shared/commands";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
export class SprintService {
private readonly audit: AuditService;
constructor(private readonly db: DatabaseSync) {
this.audit = new AuditService(db);
}
create(input: CreateSprintInput): string {
const id = createId("spr");
const now = new Date().toISOString();
this.db.prepare(`
insert into sprints(id, org_id, name, goal, status, start_date, end_date, velocity_points, completed_points, retrospective, created_at, updated_at)
values (?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?)
`).run(id, input.orgId, input.name.trim(), input.goal.trim(), "planned", input.startDate, input.endDate, "", now, now);
this.audit.record({
orgId: input.orgId,
entityType: "sprint",
entityId: id,
actorType: "system",
actorId: null,
action: "created",
payload: { name: input.name.trim() },
});
return id;
}
delete(input: DeleteSprintInput): void {
const result = this.db.prepare("delete from sprints where id = ? and org_id = ?").run(input.sprintId, input.orgId);
if (result.changes === 0) throw new Error("Sprint was not found.");
}
}