diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 3a5f725d..b7e82abe 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -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))); diff --git a/AgentCompany-New/src/main/database/migrations/042-approval-notes-meetings-sprints.ts b/AgentCompany-New/src/main/database/migrations/042-approval-notes-meetings-sprints.ts new file mode 100644 index 00000000..5d9122bc --- /dev/null +++ b/AgentCompany-New/src/main/database/migrations/042-approval-notes-meetings-sprints.ts @@ -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); +`; diff --git a/AgentCompany-New/src/main/database/schema.ts b/AgentCompany-New/src/main/database/schema.ts index 9aea993d..4a9c724a 100644 --- a/AgentCompany-New/src/main/database/schema.ts +++ b/AgentCompany-New/src/main/database/schema.ts @@ -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; diff --git a/AgentCompany-New/src/main/services/approval-service.ts b/AgentCompany-New/src/main/services/approval-service.ts index 89bad10a..743939d5 100644 --- a/AgentCompany-New/src/main/services/approval-service.ts +++ b/AgentCompany-New/src/main/services/approval-service.ts @@ -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, diff --git a/AgentCompany-New/src/main/services/meeting-service.ts b/AgentCompany-New/src/main/services/meeting-service.ts new file mode 100644 index 00000000..4cdd9c43 --- /dev/null +++ b/AgentCompany-New/src/main/services/meeting-service.ts @@ -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."); + } +} diff --git a/AgentCompany-New/src/main/services/snapshot-service.ts b/AgentCompany-New/src/main/services/snapshot-service.ts index ae1deec4..3f848413 100644 --- a/AgentCompany-New/src/main/services/snapshot-service.ts +++ b/AgentCompany-New/src/main/services/snapshot-service.ts @@ -160,6 +160,8 @@ export class SnapshotService { knowledgeEntries: list(this.db, "knowledge_entries", "updated_at desc"), automationRules: list(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"), }; } diff --git a/AgentCompany-New/src/main/services/sprint-service.ts b/AgentCompany-New/src/main/services/sprint-service.ts new file mode 100644 index 00000000..ff15919e --- /dev/null +++ b/AgentCompany-New/src/main/services/sprint-service.ts @@ -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."); + } +} diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 1cbe9055..3f2dcdae 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -30,6 +30,10 @@ const api: DesktopApi = { createAutomationRule: (input) => invoke(IPC_CHANNELS.createAutomationRule, input), updateAutomationRule: (input) => invoke(IPC_CHANNELS.updateAutomationRule, input), deleteAutomationRule: (input) => invoke(IPC_CHANNELS.deleteAutomationRule, input), + createMeeting: (input) => invoke(IPC_CHANNELS.createMeeting, input), + deleteMeeting: (input) => invoke(IPC_CHANNELS.deleteMeeting, input), + createSprint: (input) => invoke(IPC_CHANNELS.createSprint, input), + deleteSprint: (input) => invoke(IPC_CHANNELS.deleteSprint, input), createApproval: (input) => invoke(IPC_CHANNELS.createApproval, input), decideApproval: (input) => invoke(IPC_CHANNELS.decideApproval, input), syncEmployeeIdentity: (input) => invoke(IPC_CHANNELS.syncEmployeeIdentity, input), diff --git a/AgentCompany-New/src/renderer/src/app/App.tsx b/AgentCompany-New/src/renderer/src/app/App.tsx index 46298264..794ba220 100644 --- a/AgentCompany-New/src/renderer/src/app/App.tsx +++ b/AgentCompany-New/src/renderer/src/app/App.tsx @@ -12,6 +12,8 @@ import { SocialPage } from "../pages/SocialPage"; import { DocumentsPage } from "../pages/DocumentsPage"; import { KnowledgePage } from "../pages/KnowledgePage"; import { AutomationPage } from "../pages/AutomationPage"; +import { MeetingsPage } from "../pages/MeetingsPage"; +import { SprintsPage } from "../pages/SprintsPage"; import { SettingsPage } from "../pages/SettingsPage"; export function App() { @@ -43,6 +45,8 @@ export function App() { docs: , knowledge: , automation: , + meetings: , + sprints: , approvals: , social: , settings: , diff --git a/AgentCompany-New/src/renderer/src/pages/ApprovalsPage.tsx b/AgentCompany-New/src/renderer/src/pages/ApprovalsPage.tsx index e6926112..6fcaac5a 100644 --- a/AgentCompany-New/src/renderer/src/pages/ApprovalsPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/ApprovalsPage.tsx @@ -46,13 +46,14 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) { const editing = editingId ? approvals.find((approval) => approval.id === editingId) ?? null : null; - const decide = (approval: ApprovalRecord, state: "approved" | "declined") => { + const decide = (approval: ApprovalRecord, state: "approved" | "declined", notes?: string) => { setEditingId(null); void actions.run(() => window.agentCompany.decideApproval({ orgId, approvalId: approval.id, state, decidedByEmployeeId: employee.id, + notes, })); }; @@ -113,8 +114,7 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) { approval={editing} requester={editing ? resolveRequester(editing, employees, agents) : ""} onClose={() => setEditingId(null)} - onApprove={() => editing && decide(editing, "approved")} - onDecline={() => editing && decide(editing, "declined")} + onDecide={(state, notes) => editing && decide(editing, state, notes)} /> ); @@ -124,15 +124,14 @@ function ApprovalDetailDialog({ approval, requester, onClose, - onApprove, - onDecline, + onDecide, }: { approval: ApprovalRecord | null; requester: string; onClose: () => void; - onApprove: () => void; - onDecline: () => void; + onDecide: (state: "approved" | "declined", notes?: string) => void; }) { + const [notes, setNotes] = useState(""); if (!approval) return null; const tone = RISK_TONES[approval.risk]; if (!tone) throw new Error("Approval risk tone is missing."); @@ -158,9 +157,17 @@ function ApprovalDetailDialog({ Requested {formatRelative(approval.createdAt)} +