Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
|
|
2026-05-04 22:57:01 +02:00
|
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
import { useFeatureFlag } from '@/hooks/use-feature-flag';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
import type { InterestScore } from '@/lib/services/interest-scoring.service';
|
|
|
|
|
|
|
|
|
|
// ─── Score tier helpers ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function getScoreTier(score: number): { label: string; className: string } {
|
2026-05-04 22:57:01 +02:00
|
|
|
if (score >= 80)
|
|
|
|
|
return { label: 'Hot', className: 'bg-green-100 text-green-800 border-green-200' };
|
|
|
|
|
if (score >= 60)
|
|
|
|
|
return { label: 'Warm', className: 'bg-yellow-100 text-yellow-800 border-yellow-200' };
|
|
|
|
|
if (score >= 40)
|
|
|
|
|
return { label: 'Cool', className: 'bg-orange-100 text-orange-800 border-orange-200' };
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
return { label: 'Cold', className: 'bg-gray-100 text-gray-700 border-gray-200' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
interface InterestScoreBadgeProps {
|
|
|
|
|
interestId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function InterestScoreBadge({ interestId }: InterestScoreBadgeProps) {
|
|
|
|
|
const featureEnabled = useFeatureFlag('ai_interest_scoring');
|
|
|
|
|
|
|
|
|
|
const { data, isLoading } = useQuery<{ data: InterestScore }>({
|
|
|
|
|
queryKey: ['interest-score', interestId],
|
|
|
|
|
queryFn: () => apiFetch(`/api/v1/ai/interest-score?interestId=${interestId}`),
|
|
|
|
|
enabled: featureEnabled,
|
2026-05-04 22:57:01 +02:00
|
|
|
staleTime: 60 * 60 * 1000, // 1 hour - mirrors server-side cache TTL
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!featureEnabled) return null;
|
|
|
|
|
if (isLoading || !data) return null;
|
|
|
|
|
|
|
|
|
|
const score = data.data;
|
|
|
|
|
const { label, className } = getScoreTier(score.totalScore);
|
|
|
|
|
const { breakdown } = score;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<TooltipProvider>
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<span
|
|
|
|
|
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-semibold cursor-default select-none ${className}`}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
<span className="opacity-70">{score.totalScore}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent className="p-3 space-y-1 text-left min-w-[180px]">
|
|
|
|
|
<p className="font-semibold text-sm mb-2">Interest Score Breakdown</p>
|
|
|
|
|
<ScoreRow label="Pipeline Age" value={breakdown.pipelineAge} max={100} />
|
|
|
|
|
<ScoreRow label="Stage Speed" value={breakdown.stageSpeed} max={100} />
|
|
|
|
|
<ScoreRow label="Documents" value={breakdown.documentCompleteness} max={100} />
|
|
|
|
|
<ScoreRow label="Engagement" value={breakdown.engagement} max={100} />
|
|
|
|
|
<ScoreRow label="Berth Linked" value={breakdown.berthLinked} max={25} />
|
|
|
|
|
<p className="text-xs opacity-60 pt-1 border-t border-primary-foreground/20">
|
|
|
|
|
Total: {score.totalScore}/100
|
|
|
|
|
</p>
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</TooltipProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ScoreRow({ label, value, max }: { label: string; value: number; max: number }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-between gap-4 text-xs">
|
|
|
|
|
<span>{label}</span>
|
|
|
|
|
<span className="font-medium">
|
|
|
|
|
{value}/{max}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|