Upgrade Companies page: stats, delete, status badge, dropdown menu
Server: - companyService.stats() returns per-company agent/issue counts in one query pair - companyService.remove() cascades deletes across all child tables in dependency order - GET /companies/stats endpoint (board-accessible) - DELETE /companies/:companyId endpoint (board-only) UI: - Companies page shows agent count, issue count, spend/budget, and created-at per card - Company status shown as a colored badge (active/paused/archived) - Three-dot dropdown menu with Rename and Delete Company actions - Inline delete confirmation to prevent accidental data loss - 'New Company' button opens onboarding wizard instead of inline form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,11 @@ export function companyRoutes(db: Db) {
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.get("/stats", async (_req, res) => {
|
||||
const stats = await svc.stats();
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
router.get("/:companyId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
const company = await svc.getById(companyId);
|
||||
@@ -78,5 +83,16 @@ export function companyRoutes(db: Db) {
|
||||
res.json(company);
|
||||
});
|
||||
|
||||
router.delete("/:companyId", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
const company = await svc.remove(companyId);
|
||||
if (!company) {
|
||||
res.status(404).json({ error: "Company not found" });
|
||||
return;
|
||||
}
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, sql, count } from "drizzle-orm";
|
||||
import type { Db } from "@paperclip/db";
|
||||
import { companies } from "@paperclip/db";
|
||||
import {
|
||||
companies,
|
||||
agents,
|
||||
agentApiKeys,
|
||||
agentRuntimeState,
|
||||
agentWakeupRequests,
|
||||
issues,
|
||||
issueComments,
|
||||
projects,
|
||||
goals,
|
||||
heartbeatRuns,
|
||||
heartbeatRunEvents,
|
||||
costEvents,
|
||||
approvals,
|
||||
activityLog,
|
||||
} from "@paperclip/db";
|
||||
|
||||
export function companyService(db: Db) {
|
||||
return {
|
||||
@@ -35,5 +50,53 @@ export function companyService(db: Db) {
|
||||
.where(eq(companies.id, id))
|
||||
.returning()
|
||||
.then((rows) => rows[0] ?? null),
|
||||
|
||||
remove: (id: string) =>
|
||||
db.transaction(async (tx) => {
|
||||
// Delete from child tables in dependency order
|
||||
await tx.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.companyId, id));
|
||||
await tx.delete(heartbeatRuns).where(eq(heartbeatRuns.companyId, id));
|
||||
await tx.delete(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, id));
|
||||
await tx.delete(agentApiKeys).where(eq(agentApiKeys.companyId, id));
|
||||
await tx.delete(agentRuntimeState).where(eq(agentRuntimeState.companyId, id));
|
||||
await tx.delete(issueComments).where(eq(issueComments.companyId, id));
|
||||
await tx.delete(costEvents).where(eq(costEvents.companyId, id));
|
||||
await tx.delete(approvals).where(eq(approvals.companyId, id));
|
||||
await tx.delete(issues).where(eq(issues.companyId, id));
|
||||
await tx.delete(goals).where(eq(goals.companyId, id));
|
||||
await tx.delete(projects).where(eq(projects.companyId, id));
|
||||
await tx.delete(agents).where(eq(agents.companyId, id));
|
||||
await tx.delete(activityLog).where(eq(activityLog.companyId, id));
|
||||
const rows = await tx
|
||||
.delete(companies)
|
||||
.where(eq(companies.id, id))
|
||||
.returning();
|
||||
return rows[0] ?? null;
|
||||
}),
|
||||
|
||||
stats: () =>
|
||||
Promise.all([
|
||||
db
|
||||
.select({ companyId: agents.companyId, count: count() })
|
||||
.from(agents)
|
||||
.groupBy(agents.companyId),
|
||||
db
|
||||
.select({ companyId: issues.companyId, count: count() })
|
||||
.from(issues)
|
||||
.groupBy(issues.companyId),
|
||||
]).then(([agentRows, issueRows]) => {
|
||||
const result: Record<string, { agentCount: number; issueCount: number }> = {};
|
||||
for (const row of agentRows) {
|
||||
result[row.companyId] = { agentCount: row.count, issueCount: 0 };
|
||||
}
|
||||
for (const row of issueRows) {
|
||||
if (result[row.companyId]) {
|
||||
result[row.companyId].issueCount = row.count;
|
||||
} else {
|
||||
result[row.companyId] = { agentCount: 0, issueCount: row.count };
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user