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

@@ -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.");
}
}
+4
View File
@@ -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),
@@ -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: <DocumentsPage {...pageProps} />,
knowledge: <KnowledgePage {...pageProps} />,
automation: <AutomationPage {...pageProps} />,
meetings: <MeetingsPage {...pageProps} />,
sprints: <SprintsPage {...pageProps} />,
approvals: <ApprovalsPage {...pageProps} />,
social: <SocialPage {...pageProps} />,
settings: <SettingsPage {...pageProps} />,
@@ -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)}
/>
</div>
);
@@ -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({
<span style={{ color: "var(--text-3)" }}>Requested</span>
<span>{formatRelative(approval.createdAt)}</span>
</div>
<textarea
className="input"
placeholder="Decision notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={2}
style={{ marginTop: 14 }}
/>
<div className="modal-actions">
<Button onClick={onDecline}>Decline</Button>
<Button tone="primary" onClick={onApprove}>Approve</Button>
<Button onClick={() => onDecide("declined", notes || undefined)}>Decline</Button>
<Button tone="primary" onClick={() => onDecide("approved", notes || undefined)}>Approve</Button>
</div>
</div>
</div>
@@ -0,0 +1,97 @@
import { useMemo, useState, type FormEvent } from "react";
import { Button, Field, Icon, IconButton, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import { Dialog } from "../shared/Dialog";
import type { PageProps } from "./types";
import { activeOrgId, formatRelative } from "./types";
const MEETING_TYPES = ["standup", "all_hands", "sprint_review", "retrospective", "planning", "one_on_one", "ad_hoc"] as const;
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function MeetingsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const meetings = useMemo(() => snapshot.meetings.filter((m) => m.orgId === orgId), [snapshot.meetings, orgId]);
const [createOpen, setCreateOpen] = useState(false);
const create = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const scheduledAt = text(form, "scheduledAt");
void actions.run(() => window.agentCompany.createMeeting({
orgId,
title: text(form, "title"),
meetingType: text(form, "meetingType"),
scheduledAt: scheduledAt || null,
durationMinutes: Number(text(form, "durationMinutes")) || 30,
agenda: text(form, "agenda"),
})).then((r) => { if (r) setCreateOpen(false); });
};
const remove = (meetingId: string) => {
void actions.run(() => window.agentCompany.deleteMeeting({ orgId, meetingId }));
};
const scheduled = meetings.filter((m) => m.status === "scheduled");
const completed = meetings.filter((m) => m.status !== "scheduled");
return (
<div className="page">
<TopbarActions>
<Button tone="primary" onClick={() => setCreateOpen(true)}><Icon name="plus" size={13} /> Schedule</Button>
</TopbarActions>
<h1 className="page-h1">Meetings</h1>
<p className="page-sub">{scheduled.length} upcoming · {completed.length} past</p>
{scheduled.length > 0 && <div className="section-label" style={{ marginBottom: 10 }}>Upcoming</div>}
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 24 }}>
{scheduled.map((m) => (
<MeetingCard key={m.id} meeting={m} onDelete={() => remove(m.id)} />
))}
</div>
{completed.length > 0 && (
<>
<div className="section-label" style={{ marginBottom: 10 }}>Past</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{completed.map((m) => (
<MeetingCard key={m.id} meeting={m} onDelete={() => remove(m.id)} />
))}
</div>
</>
)}
<Dialog title="Schedule meeting" open={createOpen} submitLabel="Schedule" onClose={() => setCreateOpen(false)} onSubmit={create}>
<Field label="Title"><input className="input" name="title" required /></Field>
<Field label="Type">
<select className="input" name="meetingType" defaultValue="standup">
{MEETING_TYPES.map((t) => <option key={t} value={t}>{t.replace(/_/g, " ")}</option>)}
</select>
</Field>
<Field label="Scheduled at"><input className="input" name="scheduledAt" type="datetime-local" /></Field>
<Field label="Duration (min)"><input className="input" name="durationMinutes" type="number" defaultValue="30" min="5" /></Field>
<Field label="Agenda"><textarea className="input" name="agenda" rows={3} /></Field>
</Dialog>
</div>
);
}
function MeetingCard({ meeting, onDelete }: { meeting: { id: string; title: string; meetingType: string; status: string; scheduledAt: string | null; durationMinutes: number; agenda: string; createdAt: string }; onDelete: () => void }) {
return (
<div className="card" style={{ padding: 14 }}>
<div className="row" style={{ marginBottom: 6 }}>
<span style={{ fontWeight: 540, flex: 1 }}>{meeting.title}</span>
<IconButton icon="x" label="Delete" onClick={onDelete} />
</div>
<div className="row" style={{ gap: 6, marginBottom: 8 }}>
<Pill>{meeting.meetingType.replace(/_/g, " ")}</Pill>
<Pill tone={meeting.status === "scheduled" ? "amber" : undefined}>{meeting.status}</Pill>
<Pill>{meeting.durationMinutes}m</Pill>
</div>
{meeting.scheduledAt && <div style={{ fontSize: 12, color: "var(--text-3)" }}>{new Date(meeting.scheduledAt).toLocaleString()}</div>}
{meeting.agenda && <div style={{ fontSize: 13, color: "var(--text-2)", marginTop: 6, whiteSpace: "pre-wrap", lineHeight: 1.5 }}>{meeting.agenda}</div>}
</div>
);
}
@@ -0,0 +1,107 @@
import { useMemo, useState, type FormEvent } from "react";
import { Button, Field, Icon, IconButton, Pill } from "../shared/ui";
import { TopbarActions } from "../shared/TopbarActions";
import { Dialog } from "../shared/Dialog";
import type { SprintRecord, TaskRecord } from "../../../shared/domain";
import { TASK_STATUS } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function SprintsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const sprints = useMemo(() => snapshot.sprints.filter((s) => s.orgId === orgId), [snapshot.sprints, orgId]);
const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]);
const [createOpen, setCreateOpen] = useState(false);
const create = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createSprint({
orgId,
name: text(form, "name"),
goal: text(form, "goal"),
startDate: text(form, "startDate"),
endDate: text(form, "endDate"),
})).then((r) => { if (r) setCreateOpen(false); });
};
const remove = (sprintId: string) => {
void actions.run(() => window.agentCompany.deleteSprint({ orgId, sprintId }));
};
return (
<div className="page">
<TopbarActions>
<Button tone="primary" onClick={() => setCreateOpen(true)}><Icon name="plus" size={13} /> New sprint</Button>
</TopbarActions>
<h1 className="page-h1">Sprints</h1>
<p className="page-sub">{sprints.length} {sprints.length === 1 ? "sprint" : "sprints"}</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 12 }}>
{sprints.map((sprint) => (
<SprintCard key={sprint.id} sprint={sprint} tasks={tasks} onDelete={() => remove(sprint.id)} />
))}
</div>
<Dialog title="New sprint" open={createOpen} submitLabel="Create" onClose={() => setCreateOpen(false)} onSubmit={create}>
<Field label="Name"><input className="input" name="name" required /></Field>
<Field label="Goal"><textarea className="input" name="goal" rows={2} required /></Field>
<Field label="Start date"><input className="input" name="startDate" type="date" required /></Field>
<Field label="End date"><input className="input" name="endDate" type="date" required /></Field>
</Dialog>
</div>
);
}
function SprintCard({ sprint, tasks, onDelete }: { sprint: SprintRecord; tasks: TaskRecord[]; onDelete: () => void }) {
const start = new Date(sprint.startDate);
const end = new Date(sprint.endDate);
const now = new Date();
const totalDays = Math.max(1, Math.ceil((end.getTime() - start.getTime()) / 86400000));
const elapsed = Math.max(0, Math.ceil((now.getTime() - start.getTime()) / 86400000));
const progressPct = Math.min(100, Math.round((elapsed / totalDays) * 100));
const done = tasks.filter((t) => t.status === TASK_STATUS.DONE).length;
const total = tasks.length;
const taskPct = total > 0 ? Math.round((done / total) * 100) : 0;
return (
<article className="card" style={{ padding: 16 }}>
<div className="row" style={{ marginBottom: 8 }}>
<span style={{ fontWeight: 560, flex: 1 }}>{sprint.name}</span>
<IconButton icon="x" label="Delete" onClick={onDelete} />
</div>
<div className="row" style={{ gap: 6, marginBottom: 10 }}>
<Pill tone={sprintStatusTone(sprint.status)}>{sprint.status}</Pill>
<Pill>{sprint.startDate} to {sprint.endDate}</Pill>
</div>
<div style={{ fontSize: 13, color: "var(--text-2)", lineHeight: 1.5, marginBottom: 12 }}>{sprint.goal}</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<ProgressBar label="Time" pct={progressPct} />
<ProgressBar label="Tasks" pct={taskPct} />
</div>
</article>
);
}
function ProgressBar({ label, pct }: { label: string; pct: number }) {
return (
<div className="row" style={{ gap: 8, fontSize: 12 }}>
<span style={{ width: 40, color: "var(--text-3)" }}>{label}</span>
<div style={{ flex: 1, height: 3, background: "var(--panel-3)", borderRadius: 2, overflow: "hidden" }}>
<div style={{ width: `${pct}%`, height: "100%", background: "var(--text-1)" }} />
</div>
<span className="num" style={{ color: "var(--text-3)", width: 30, textAlign: "right" }}>{pct}%</span>
</div>
);
}
function sprintStatusTone(status: string): "amber" | "green" | "violet" | undefined {
if (status === "planned") return undefined;
if (status === "active") return "amber";
if (status === "completed") return "green";
return "violet";
}
@@ -15,6 +15,8 @@ export type RouteId =
| "docs"
| "knowledge"
| "automation"
| "meetings"
| "sprints"
| "approvals"
| "social"
| "settings";
@@ -38,6 +40,8 @@ const primaryNav: NavSpec[] = [
{ id: "docs", label: "Documents", icon: "file" },
{ id: "knowledge", label: "Knowledge", icon: "sparkle" },
{ id: "automation", label: "Automation", icon: "bot" },
{ id: "meetings", label: "Meetings", icon: "team" },
{ id: "sprints", label: "Sprints", icon: "work" },
{ id: "approvals", label: "Approvals", icon: "approve" },
{ id: "social", label: "Social", icon: "social" },
];
+28
View File
@@ -143,6 +143,7 @@ export interface DecideApprovalInput {
approvalId: string;
state: Extract<ApprovalState, "approved" | "declined">;
decidedByEmployeeId: string;
notes?: string;
}
export interface SyncEmployeeIdentityInput {
@@ -554,6 +555,33 @@ export interface DeleteAutomationRuleInput {
ruleId: string;
}
export interface CreateMeetingInput {
orgId: string;
title: string;
meetingType: string;
scheduledAt: string | null;
durationMinutes: number;
agenda: string;
}
export interface DeleteMeetingInput {
orgId: string;
meetingId: string;
}
export interface CreateSprintInput {
orgId: string;
name: string;
goal: string;
startDate: string;
endDate: string;
}
export interface DeleteSprintInput {
orgId: string;
sprintId: string;
}
export interface NativeDialogResult {
canceled: boolean;
filePaths: string[];
@@ -12,6 +12,10 @@ import type {
CreateAutomationRuleInput,
CreateDocumentInput,
CreateGoalInput,
CreateMeetingInput,
CreateSprintInput,
DeleteMeetingInput,
DeleteSprintInput,
CreateKnowledgeEntryInput,
CreateProjectInput,
CreateWorkspaceInput,
@@ -116,6 +120,10 @@ export interface DesktopApi {
createAutomationRule(input: CreateAutomationRuleInput): Promise<DesktopResult<AppSnapshot>>;
updateAutomationRule(input: UpdateAutomationRuleInput): Promise<DesktopResult<AppSnapshot>>;
deleteAutomationRule(input: DeleteAutomationRuleInput): Promise<DesktopResult<AppSnapshot>>;
createMeeting(input: CreateMeetingInput): Promise<DesktopResult<AppSnapshot>>;
deleteMeeting(input: DeleteMeetingInput): Promise<DesktopResult<AppSnapshot>>;
createSprint(input: CreateSprintInput): Promise<DesktopResult<AppSnapshot>>;
deleteSprint(input: DeleteSprintInput): Promise<DesktopResult<AppSnapshot>>;
createApproval(input: CreateApprovalInput): Promise<DesktopResult<AppSnapshot>>;
decideApproval(input: DecideApprovalInput): Promise<DesktopResult<AppSnapshot>>;
syncEmployeeIdentity(input: SyncEmployeeIdentityInput): Promise<DesktopResult<AppSnapshot>>;
+33
View File
@@ -178,11 +178,42 @@ export interface ApprovalRecord {
sourceEntityType: string | null;
sourceEntityId: string | null;
state: ApprovalState;
decisionNotes: string | null;
decidedByEmployeeId: string | null;
createdAt: string;
decidedAt: string | null;
}
export interface MeetingRecord {
id: string;
orgId: string;
title: string;
meetingType: string;
status: string;
scheduledAt: string | null;
durationMinutes: number;
agenda: string;
notes: string;
decisions: string;
createdAt: string;
updatedAt: string;
}
export interface SprintRecord {
id: string;
orgId: string;
name: string;
goal: string;
status: string;
startDate: string;
endDate: string;
velocityPoints: number;
completedPoints: number;
retrospective: string;
createdAt: string;
updatedAt: string;
}
export type {
ChatSpaceRecord,
ChatMessageRecord,
@@ -488,4 +519,6 @@ export interface AppSnapshot {
documents: DocumentRecord[];
knowledgeEntries: KnowledgeEntryRecord[];
automationRules: AutomationRuleRecord[];
meetings: MeetingRecord[];
sprints: SprintRecord[];
}
+4
View File
@@ -70,6 +70,10 @@ export const IPC_CHANNELS = {
createAutomationRule: "agentcompany:create-automation-rule",
updateAutomationRule: "agentcompany:update-automation-rule",
deleteAutomationRule: "agentcompany:delete-automation-rule",
createMeeting: "agentcompany:create-meeting",
deleteMeeting: "agentcompany:delete-meeting",
createSprint: "agentcompany:create-sprint",
deleteSprint: "agentcompany:delete-sprint",
search: "agentcompany:search",
rendererEvent: "agentcompany:renderer-event",
windowMinimize: "agentcompany:window-minimize",
@@ -24,7 +24,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42]);
connection.close();
});