Files
CIAgent/apps/web/app/(app)/companies/page.tsx
T
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

163 lines
7.3 KiB
TypeScript

"use client";
import Link from "next/link";
import { useState } from "react";
import { Building2, Pause, Play, Plus, RefreshCw, Trash2 } from "lucide-react";
import { CompanyStatusBadge } from "@/components/ui/badge";
import { useCompanies, useDeleteCompany, useSetCompanyPaused } from "@/hooks/use-companies";
import { useRunAnyCompanyNow } from "@/hooks/use-monitoring-runs";
import { FREQUENCY_LABELS } from "@/lib/types";
import { formatDateTime } from "@/lib/format";
export default function CompaniesPage() {
const { data: companies, isLoading } = useCompanies();
const setPaused = useSetCompanyPaused();
const deleteCompany = useDeleteCompany();
const runNow = useRunAnyCompanyNow();
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
const companyList = companies ?? [];
return (
<div>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-slate-900">Companies</h1>
<p className="mt-1 text-sm text-slate-600">
Everything you&apos;re currently monitoring.
</p>
</div>
<Link
href="/companies/new"
className="focus-ring inline-flex items-center gap-2 rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700"
>
<Plus className="h-4 w-4" aria-hidden /> Add company
</Link>
</div>
{isLoading ? (
<p className="mt-8 text-sm text-slate-500">Loading</p>
) : companyList.length === 0 ? (
<div className="mt-8 rounded-lg border border-dashed border-slate-300 bg-white p-10 text-center">
<Building2 className="mx-auto h-8 w-8 text-slate-400" aria-hidden />
<p className="mt-3 text-sm text-slate-600">No companies yet.</p>
<Link
href="/companies/new"
className="focus-ring mt-4 inline-block rounded-md bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700"
>
Add your first company
</Link>
</div>
) : (
<div className="mt-6 overflow-x-auto rounded-lg border border-slate-200 bg-white">
<table className="w-full min-w-[720px] text-left text-sm">
<thead className="border-b border-slate-200 bg-slate-50 text-xs uppercase tracking-wide text-slate-500">
<tr>
<th className="px-4 py-3">Company</th>
<th className="px-4 py-3">Status</th>
<th className="px-4 py-3">Frequency</th>
<th className="px-4 py-3">Last checked</th>
<th className="px-4 py-3">Next check</th>
<th className="px-4 py-3">Alerts</th>
<th className="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{companyList.map((company) => (
<tr key={company.id}>
<td className="px-4 py-3">
<Link
href={`/companies/${company.id}`}
className="font-medium text-slate-900 hover:text-brand-600"
>
{company.name}
</Link>
{company.official_website && (
<div className="text-xs text-slate-500">{company.official_website}</div>
)}
</td>
<td className="px-4 py-3">
<CompanyStatusBadge status={company.status} />
</td>
<td className="px-4 py-3 text-slate-600">
{company.monitor_configuration
? FREQUENCY_LABELS[company.monitor_configuration.frequency_type]
: "—"}
</td>
<td className="px-4 py-3 text-slate-600">
{formatDateTime(company.monitor_configuration?.last_run)}
</td>
<td className="px-4 py-3 text-slate-600">
{formatDateTime(company.monitor_configuration?.next_run)}
</td>
<td className="px-4 py-3 text-slate-600">{company.unresolved_alert_count}</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-1">
<button
title={
company.status === "active" ? "Pause monitoring" : "Resume monitoring"
}
onClick={() =>
setPaused.mutate({
companyId: company.id,
paused: company.status === "active",
})
}
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-slate-100"
>
{company.status === "active" ? (
<Pause className="h-4 w-4" aria-hidden />
) : (
<Play className="h-4 w-4" aria-hidden />
)}
<span className="sr-only">
{company.status === "active" ? "Pause" : "Resume"} {company.name}
</span>
</button>
<button
title="Run now"
onClick={() => runNow.mutate(company.id)}
disabled={runNow.isPending}
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-slate-100 disabled:opacity-60"
>
<RefreshCw className="h-4 w-4" aria-hidden />
<span className="sr-only">Run now for {company.name}</span>
</button>
{pendingDeleteId === company.id ? (
<div className="flex items-center gap-1 text-xs">
<span className="text-slate-600">Delete?</span>
<button
onClick={() => deleteCompany.mutate(company.id)}
className="focus-ring rounded-md bg-red-600 px-2 py-1 font-semibold text-white hover:bg-red-700"
>
Yes
</button>
<button
onClick={() => setPendingDeleteId(null)}
className="focus-ring rounded-md px-2 py-1 text-slate-600 hover:bg-slate-100"
>
No
</button>
</div>
) : (
<button
title="Delete company"
onClick={() => setPendingDeleteId(company.id)}
className="focus-ring rounded-md p-2 text-slate-500 hover:bg-red-50 hover:text-red-600"
>
<Trash2 className="h-4 w-4" aria-hidden />
<span className="sr-only">Delete {company.name}</span>
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}