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:
1 parent
75e5d17a86
commit
ed9b3a3dd1
18 files changed
+430
-12
No files matched your search
@@ -25,6 +25,8 @@ import { TaskExecutionService } from "../services/task-execution-service";
|
||||
import { DocumentService } from "../services/document-service";
|
||||
import { KnowledgeService } from "../services/knowledge-service";
|
||||
import { AutomationService } from "../services/automation-service";
|
||||
import { MeetingService } from "../services/meeting-service";
|
||||
import { SprintService } from "../services/sprint-service";
|
||||
|
||||
export interface IpcDeps {
|
||||
connection: DatabaseConnection;
|
||||
@@ -54,6 +56,8 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
const documents = new DocumentService(deps.connection.db);
|
||||
const knowledge = new KnowledgeService(deps.connection.db);
|
||||
const automation = new AutomationService(deps.connection.db);
|
||||
const meetingService = new MeetingService(deps.connection.db);
|
||||
const sprintService = new SprintService(deps.connection.db);
|
||||
const onboarding = new OnboardingService({
|
||||
db: deps.connection.db,
|
||||
saveProviderCredential: (input) => providers.saveCredential(input),
|
||||
@@ -120,6 +124,10 @@ export function registerIpcHandlers(deps: IpcDeps): void {
|
||||
ipcMain.handle(IPC_CHANNELS.createAutomationRule, (_event, input) => mutate(() => automation.create(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.updateAutomationRule, (_event, input) => mutate(() => automation.update(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.deleteAutomationRule, (_event, input) => mutate(() => automation.delete(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createMeeting, (_event, input) => mutate(() => meetingService.create(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.deleteMeeting, (_event, input) => mutate(() => meetingService.delete(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createSprint, (_event, input) => mutate(() => sprintService.create(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.deleteSprint, (_event, input) => mutate(() => sprintService.delete(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.createApproval, (_event, input) => mutate(() => approvals.create(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.decideApproval, (_event, input) => mutate(() => approvals.decide(input)));
|
||||
ipcMain.handle(IPC_CHANNELS.syncEmployeeIdentity, (_event, input) => mutate(() => identityAccess.syncEmployeeIdentity(input)));
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export const approvalNotesMeetingsSprintsSchema = `
|
||||
alter table approvals add column decision_notes text;
|
||||
|
||||
create table if not exists meetings (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
title text not null,
|
||||
meeting_type text not null,
|
||||
status text not null,
|
||||
scheduled_at text,
|
||||
duration_minutes integer not null,
|
||||
agenda text not null,
|
||||
notes text not null,
|
||||
decisions text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_meetings_org on meetings(org_id, scheduled_at);
|
||||
|
||||
create table if not exists sprints (
|
||||
id text primary key,
|
||||
org_id text not null references organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
goal text not null,
|
||||
status text not null,
|
||||
start_date text not null,
|
||||
end_date text not null,
|
||||
velocity_points integer not null default 0,
|
||||
completed_points integer not null default 0,
|
||||
retrospective text not null,
|
||||
created_at text not null,
|
||||
updated_at text not null
|
||||
);
|
||||
|
||||
create index if not exists idx_sprints_org on sprints(org_id, status);
|
||||
`;
|
||||
@@ -40,6 +40,7 @@ import { providerAuthProfileCascadeSchema } from "./migrations/038-provider-auth
|
||||
import { auditImmutabilityOrgTeardownSchema } from "./migrations/039-audit-immutability-org-teardown";
|
||||
import { documentsKnowledgeAutomationSchema } from "./migrations/040-documents-knowledge-automation";
|
||||
import { taskSubtasksDeleteSchema } from "./migrations/041-task-subtasks-delete";
|
||||
import { approvalNotesMeetingsSprintsSchema } from "./migrations/042-approval-notes-meetings-sprints";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -89,6 +90,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 39, name: "audit_immutability_org_teardown", sql: auditImmutabilityOrgTeardownSchema },
|
||||
{ version: 40, name: "documents_knowledge_automation", sql: documentsKnowledgeAutomationSchema },
|
||||
{ version: 41, name: "task_subtasks_delete", sql: taskSubtasksDeleteSchema },
|
||||
{ version: 42, name: "approval_notes_meetings_sprints", sql: approvalNotesMeetingsSprintsSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user