feat: Faza A2 -- cautare companii, segmente salvate, research briefs
- pagina Companii: cautare Apollo prin ceo-api, salveaza cautarea ca segment, creeaza research brief per rezultat - pagina Segmente: reruleaza o cautare salvata, sterge - pagina Research: listeaza briefs cu surse (linkuri catre Apollo/altele) - Overview devine un briefing determinist (restante/urmatoarele 7 zile + activitate saptamanala), cu explicatia AI marcata explicit ca neconstruita in loc de ascunsa
This commit is contained in:
parent
883ff370cd
commit
60e5befa31
7 changed files with 524 additions and 20 deletions
89
src/app/dashboard/companies/brief-form.tsx
Normal file
89
src/app/dashboard/companies/brief-form.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
'use client';
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { apiFetch } from '../../../lib/api';
|
||||
|
||||
const briefSchema = z.object({
|
||||
title: z.string().min(1, 'Titlul este obligatoriu').max(250),
|
||||
summary: z.string().min(1, 'Rezumatul este obligatoriu').max(5000),
|
||||
});
|
||||
type BriefForm = z.infer<typeof briefSchema>;
|
||||
|
||||
interface BriefFormProps {
|
||||
tenantId: string;
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function BriefForm({ tenantId, organizationId, organizationName, onClose }: BriefFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<BriefForm>({
|
||||
resolver: zodResolver(briefSchema),
|
||||
defaultValues: { title: `Research brief — ${organizationName}` },
|
||||
});
|
||||
|
||||
const createBrief = useMutation({
|
||||
mutationFn: (values: BriefForm) =>
|
||||
apiFetch('/v1/research-briefs', {
|
||||
method: 'POST',
|
||||
tenantId,
|
||||
body: { organizationId, organizationName, ...values },
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['research-briefs', tenantId] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ink/30 p-4">
|
||||
<div className="card w-full max-w-lg p-6 shadow-overlay">
|
||||
<h2 className="mb-1 font-display text-lg font-semibold text-ink">Research brief</h2>
|
||||
<p className="mb-4 text-sm text-ink-faint">{organizationName}</p>
|
||||
|
||||
<form onSubmit={handleSubmit((values) => createBrief.mutate(values))} noValidate className="space-y-4">
|
||||
<div>
|
||||
<label className="label" htmlFor="brief-title">
|
||||
Titlu
|
||||
</label>
|
||||
<input id="brief-title" type="text" className="field" {...register('title')} />
|
||||
{errors.title && <p className="mt-1 text-xs text-signal-danger">{errors.title.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<label className="label" htmlFor="brief-summary">
|
||||
Rezumat
|
||||
</label>
|
||||
<textarea id="brief-summary" rows={5} className="field" {...register('summary')} />
|
||||
{errors.summary && <p className="mt-1 text-xs text-signal-danger">{errors.summary.message}</p>}
|
||||
<p className="mt-1 text-xs text-ink-faint">
|
||||
Scris de tine — nu există încă un AI Gateway care să genereze acest rezumat automat.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{createBrief.isError && (
|
||||
<p className="text-sm text-signal-danger">
|
||||
{createBrief.error instanceof Error ? createBrief.error.message : 'Eroare'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button type="button" onClick={onClose} className="btn-ghost">
|
||||
Anulează
|
||||
</button>
|
||||
<button type="submit" disabled={isSubmitting || createBrief.isPending} className="btn-primary">
|
||||
Salvează
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
131
src/app/dashboard/companies/page.tsx
Normal file
131
src/app/dashboard/companies/page.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch, type CompanySearchResponse, type CompanySearchResult } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
import { BriefForm } from './brief-form';
|
||||
|
||||
export default function CompaniesPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
const queryClient = useQueryClient();
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<CompanySearchResult[] | null>(null);
|
||||
const [segmentName, setSegmentName] = useState('');
|
||||
const [briefTarget, setBriefTarget] = useState<CompanySearchResult | null>(null);
|
||||
|
||||
const search = useMutation({
|
||||
mutationFn: (q: string) =>
|
||||
apiFetch<CompanySearchResponse>(
|
||||
`/v1/intelligence/companies/search?q=${encodeURIComponent(q)}&limit=20`,
|
||||
{ tenantId },
|
||||
),
|
||||
onSuccess: (data) => setResults(data.results),
|
||||
});
|
||||
|
||||
const saveSegment = useMutation({
|
||||
mutationFn: (name: string) =>
|
||||
apiFetch('/v1/segments', { method: 'POST', tenantId, body: { name, query, resultLimit: 20 } }),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['segments', tenantId] });
|
||||
setSegmentName('');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Companii</h1>
|
||||
<p className="text-sm text-ink-faint">Căutare în datasetul Apollo, prin Intelligence API.</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (query.trim().length >= 2) {
|
||||
search.mutate(query.trim());
|
||||
}
|
||||
}}
|
||||
className="card mb-6 flex gap-3 p-4"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="field"
|
||||
placeholder="Nume companie sau domeniu (ex. acme.com)"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<button type="submit" disabled={search.isPending} className="btn-primary shrink-0">
|
||||
Caută
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{search.isError && (
|
||||
<p className="mb-4 text-sm text-signal-danger">
|
||||
{search.error instanceof Error ? search.error.message : 'Eroare la căutare'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{results && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
className="field"
|
||||
placeholder="Nume pentru segmentul salvat"
|
||||
value={segmentName}
|
||||
onChange={(event) => setSegmentName(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!segmentName.trim() || saveSegment.isPending}
|
||||
onClick={() => saveSegment.mutate(segmentName.trim())}
|
||||
className="btn-ghost shrink-0"
|
||||
>
|
||||
Salvează căutarea
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{results.length === 0 && (
|
||||
<p className="card p-6 text-sm text-ink-faint">Niciun rezultat pentru această căutare.</p>
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{results.map((company) => (
|
||||
<li key={company.organization_id} className="card flex items-center justify-between p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-ink">
|
||||
{company.organization_name || company.normalized_domain || company.organization_id}
|
||||
</p>
|
||||
<p className="text-xs text-ink-faint">
|
||||
{[company.normalized_domain, company.hq_city, company.hq_country]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
{company.num_current_employees ? ` · ${company.num_current_employees} angajați` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBriefTarget(company)}
|
||||
className="btn-ghost shrink-0 text-xs"
|
||||
>
|
||||
Research brief
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{briefTarget && (
|
||||
<BriefForm
|
||||
tenantId={tenantId}
|
||||
organizationId={briefTarget.organization_id}
|
||||
organizationName={briefTarget.organization_name || briefTarget.normalized_domain}
|
||||
onClose={() => setBriefTarget(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,22 +2,16 @@
|
|||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch, type Organization, type Task } from '../../lib/api';
|
||||
import { apiFetch, type Briefing } from '../../lib/api';
|
||||
import { useSession } from '../../components/session-provider';
|
||||
|
||||
export default function OverviewPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
|
||||
const { data: organizations = [] } = useQuery({
|
||||
queryKey: ['organizations', tenantId],
|
||||
queryFn: () => apiFetch<Organization[]>('/v1/organizations', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const { data: openTasks = [] } = useQuery({
|
||||
queryKey: ['tasks', tenantId, 'open'],
|
||||
queryFn: () => apiFetch<Task[]>('/v1/tasks?status=open', { tenantId }),
|
||||
const { data: briefing, isLoading } = useQuery({
|
||||
queryKey: ['briefing', tenantId],
|
||||
queryFn: () => apiFetch<Briefing>('/v1/briefing/today', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
|
|
@ -26,16 +20,83 @@ export default function OverviewPage() {
|
|||
<h1 className="mb-1 font-display text-2xl font-semibold text-ink">
|
||||
Bine ai revenit{activeTenant ? `, ${activeTenant.tenantName}` : ''}
|
||||
</h1>
|
||||
<p className="mb-8 text-sm text-ink-faint">Prezentarea generală a workspace-ului activ.</p>
|
||||
<p className="mb-8 text-sm text-ink-faint">Briefingul zilei — prioritizare deterministă.</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Link href="/dashboard/organizations" className="card block p-5 transition-shadow hover:shadow-overlay">
|
||||
<p className="label">Companii</p>
|
||||
<p className="font-display text-3xl font-semibold text-ink">{organizations.length}</p>
|
||||
{isLoading && <p className="text-sm text-ink-faint">Se încarcă…</p>}
|
||||
|
||||
{briefing && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="card p-5">
|
||||
<p className="label">Companii noi (7 zile)</p>
|
||||
<p className="font-display text-3xl font-semibold text-ink">
|
||||
{briefing.weekInReview.newOrganizations}
|
||||
</p>
|
||||
</div>
|
||||
<div className="card p-5">
|
||||
<p className="label">Segmente noi</p>
|
||||
<p className="font-display text-3xl font-semibold text-ink">
|
||||
{briefing.weekInReview.newSegments}
|
||||
</p>
|
||||
</div>
|
||||
<div className="card p-5">
|
||||
<p className="label">Research briefs noi</p>
|
||||
<p className="font-display text-3xl font-semibold text-ink">
|
||||
{briefing.weekInReview.newResearchBriefs}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{briefing.overdueTasks.length > 0 && (
|
||||
<section>
|
||||
<h2 className="label mb-2">Sarcini restante</h2>
|
||||
<ul className="space-y-2">
|
||||
{briefing.overdueTasks.map((task) => (
|
||||
<li key={task.id} className="card flex items-center justify-between p-4">
|
||||
<span className="text-sm font-medium text-ink">{task.title}</span>
|
||||
<span className="text-xs text-signal-danger">
|
||||
{task.dueAt && new Date(task.dueAt).toLocaleDateString('ro-RO')}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h2 className="label mb-2">Următoarele 7 zile</h2>
|
||||
{briefing.upcomingTasks.length === 0 ? (
|
||||
<p className="card p-6 text-sm text-ink-faint">Nimic programat în următoarea săptămână.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{briefing.upcomingTasks.map((task) => (
|
||||
<li key={task.id} className="card flex items-center justify-between p-4">
|
||||
<span className="text-sm font-medium text-ink">{task.title}</span>
|
||||
<span className="text-xs text-ink-faint">
|
||||
{task.dueAt && new Date(task.dueAt).toLocaleDateString('ro-RO')}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<p className="text-xs text-ink-faint">
|
||||
Prioritizare deterministă din task-uri. Explicația AI nu e încă disponibilă (AI Gateway
|
||||
neconstruit).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 grid grid-cols-3 gap-4">
|
||||
<Link href="/dashboard/companies" className="card block p-4 text-sm font-medium text-ink hover:shadow-overlay">
|
||||
Caută companii →
|
||||
</Link>
|
||||
<Link href="/dashboard/tasks" className="card block p-5 transition-shadow hover:shadow-overlay">
|
||||
<p className="label">Sarcini deschise</p>
|
||||
<p className="font-display text-3xl font-semibold text-ink">{openTasks.length}</p>
|
||||
<Link href="/dashboard/segments" className="card block p-4 text-sm font-medium text-ink hover:shadow-overlay">
|
||||
Segmente salvate →
|
||||
</Link>
|
||||
<Link href="/dashboard/research" className="card block p-4 text-sm font-medium text-ink hover:shadow-overlay">
|
||||
Research briefs →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
75
src/app/dashboard/research/page.tsx
Normal file
75
src/app/dashboard/research/page.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch, type ResearchBrief } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
export default function ResearchPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: briefs = [], isLoading } = useQuery({
|
||||
queryKey: ['research-briefs', tenantId],
|
||||
queryFn: () => apiFetch<ResearchBrief[]>('/v1/research-briefs', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const removeBrief = useMutation({
|
||||
mutationFn: (id: string) => apiFetch(`/v1/research-briefs/${id}`, { method: 'DELETE', tenantId }),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['research-briefs', tenantId] });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Research briefs</h1>
|
||||
<p className="text-sm text-ink-faint">
|
||||
Dovezi curatate despre companii, cu surse. Se creează din pagina Companii.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading && <p className="text-sm text-ink-faint">Se încarcă…</p>}
|
||||
{!isLoading && briefs.length === 0 && (
|
||||
<p className="card p-6 text-sm text-ink-faint">Niciun research brief încă.</p>
|
||||
)}
|
||||
|
||||
<ul className="space-y-3">
|
||||
{briefs.map((brief) => (
|
||||
<li key={brief.id} className="card p-5">
|
||||
<div className="mb-2 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-ink">{brief.title}</p>
|
||||
<p className="text-xs text-ink-faint">{brief.organizationName ?? brief.organizationId}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeBrief.mutate(brief.id)}
|
||||
className="shrink-0 text-xs font-medium text-signal-danger hover:underline"
|
||||
>
|
||||
Șterge
|
||||
</button>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap text-sm text-ink-soft">{brief.summary}</p>
|
||||
<ul className="mt-3 flex flex-wrap gap-2">
|
||||
{brief.sources.map((source) => (
|
||||
<li key={source.url}>
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-full bg-paper-sunken px-2.5 py-1 text-xs text-bronze-deep hover:bg-bronze-wash"
|
||||
>
|
||||
{source.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
94
src/app/dashboard/segments/page.tsx
Normal file
94
src/app/dashboard/segments/page.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch, type CompanySearchResponse, type SavedSegment } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
export default function SegmentsPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
const queryClient = useQueryClient();
|
||||
const [runResults, setRunResults] = useState<{ segmentId: string; data: CompanySearchResponse } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { data: segments = [], isLoading } = useQuery({
|
||||
queryKey: ['segments', tenantId],
|
||||
queryFn: () => apiFetch<SavedSegment[]>('/v1/segments', { tenantId }),
|
||||
enabled: Boolean(tenantId),
|
||||
});
|
||||
|
||||
const runSegment = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
apiFetch<{ segment: SavedSegment } & CompanySearchResponse>(`/v1/segments/${id}/run`, { tenantId }),
|
||||
onSuccess: (data, id) => setRunResults({ segmentId: id, data }),
|
||||
});
|
||||
|
||||
const removeSegment = useMutation({
|
||||
mutationFn: (id: string) => apiFetch(`/v1/segments/${id}`, { method: 'DELETE', tenantId }),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ['segments', tenantId] });
|
||||
setRunResults(null);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mb-6">
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Segmente salvate</h1>
|
||||
<p className="text-sm text-ink-faint">Căutări de companii salvate, reluabile oricând.</p>
|
||||
</div>
|
||||
|
||||
{isLoading && <p className="text-sm text-ink-faint">Se încarcă…</p>}
|
||||
{!isLoading && segments.length === 0 && (
|
||||
<p className="card p-6 text-sm text-ink-faint">
|
||||
Niciun segment salvat încă. Salvează o căutare din pagina Companii.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{segments.map((segment) => (
|
||||
<li key={segment.id} className="card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-ink">{segment.name}</p>
|
||||
<p className="text-xs text-ink-faint">“{segment.query}”</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => runSegment.mutate(segment.id)}
|
||||
disabled={runSegment.isPending}
|
||||
className="btn-ghost text-xs"
|
||||
>
|
||||
Rulează din nou
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSegment.mutate(segment.id)}
|
||||
className="text-xs font-medium text-signal-danger hover:underline"
|
||||
>
|
||||
Șterge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{runResults?.segmentId === segment.id && (
|
||||
<ul className="mt-3 space-y-1.5 border-t border-ink-line pt-3">
|
||||
{runResults.data.results.length === 0 && (
|
||||
<li className="text-xs text-ink-faint">Niciun rezultat.</li>
|
||||
)}
|
||||
{runResults.data.results.map((company) => (
|
||||
<li key={company.organization_id} className="text-xs text-ink-soft">
|
||||
{company.organization_name || company.normalized_domain || company.organization_id}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,9 +7,12 @@ import { useSession } from './session-provider';
|
|||
import { TenantSwitcher } from './tenant-switcher';
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ href: '/dashboard', label: 'Prezentare generală' },
|
||||
{ href: '/dashboard/organizations', label: 'Companii' },
|
||||
{ href: '/dashboard', label: 'Briefing' },
|
||||
{ href: '/dashboard/organizations', label: 'Organizațiile mele' },
|
||||
{ href: '/dashboard/tasks', label: 'Sarcini' },
|
||||
{ href: '/dashboard/companies', label: 'Căutare companii' },
|
||||
{ href: '/dashboard/segments', label: 'Segmente' },
|
||||
{ href: '/dashboard/research', label: 'Research' },
|
||||
{ href: '/dashboard/members', label: 'Echipă' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -103,3 +103,54 @@ export interface TenantMember {
|
|||
role: 'owner' | 'admin' | 'member';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CompanySearchResult {
|
||||
organization_id: string;
|
||||
organization_name: string;
|
||||
normalized_domain: string;
|
||||
hq_city: string;
|
||||
hq_country: string;
|
||||
industries: string[];
|
||||
num_current_employees: number | null;
|
||||
revenue_in_thousands: number | null;
|
||||
}
|
||||
|
||||
export interface CompanySearchResponse {
|
||||
results: CompanySearchResult[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface SavedSegment {
|
||||
id: string;
|
||||
name: string;
|
||||
query: string;
|
||||
resultLimit: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
label: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ResearchBrief {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
organizationName: string | null;
|
||||
title: string;
|
||||
summary: string;
|
||||
sources: Source[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Briefing {
|
||||
generatedAt: string;
|
||||
overdueTasks: Task[];
|
||||
upcomingTasks: Task[];
|
||||
weekInReview: {
|
||||
newOrganizations: number;
|
||||
newSegments: number;
|
||||
newResearchBriefs: number;
|
||||
};
|
||||
aiExplanation: string | null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue