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';
|
|
|
|
|
|
|
2026-05-12 15:48:51 +02:00
|
|
|
|
import { useEffect, useState } from 'react';
|
2026-05-11 17:58:42 +02:00
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
|
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 { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets';
|
2026-05-11 17:58:42 +02:00
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
2026-04-28 12:09:59 +02:00
|
|
|
|
import { PageHeader } from '@/components/shared/page-header';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
import { CustomizeWidgetsMenu } from './customize-widgets-menu';
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
import { DateRangePicker } from './date-range-picker';
|
2026-05-12 15:48:51 +02:00
|
|
|
|
import { TimezoneDriftBanner } from './timezone-drift-banner';
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
import { WidgetErrorBoundary } from './widget-error-boundary';
|
2026-05-12 14:50:58 +02:00
|
|
|
|
import type { DashboardWidget } from './widget-registry';
|
2026-05-04 22:54:55 +02:00
|
|
|
|
import { isCustomRange, type DateRange } from '@/lib/analytics/range';
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
|
2026-05-04 22:54:55 +02:00
|
|
|
|
const PRESET_LABELS: Record<'today' | '7d' | '30d' | '90d', string> = {
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
today: 'Today',
|
|
|
|
|
|
'7d': 'Last 7 days',
|
|
|
|
|
|
'30d': 'Last 30 days',
|
|
|
|
|
|
'90d': 'Last 90 days',
|
|
|
|
|
|
};
|
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
|
|
|
|
|
2026-05-04 22:54:55 +02:00
|
|
|
|
function rangeLabel(range: DateRange): string {
|
|
|
|
|
|
if (isCustomRange(range)) {
|
|
|
|
|
|
const fmt: Intl.DateTimeFormatOptions = {
|
|
|
|
|
|
month: 'short',
|
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
|
timeZone: 'UTC',
|
|
|
|
|
|
};
|
|
|
|
|
|
const from = new Date(`${range.from}T00:00:00.000Z`).toLocaleDateString('en-US', fmt);
|
|
|
|
|
|
const to = new Date(`${range.to}T00:00:00.000Z`).toLocaleDateString('en-US', fmt);
|
|
|
|
|
|
return `${from} – ${to}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return PRESET_LABELS[range];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 17:58:42 +02:00
|
|
|
|
interface MeData {
|
|
|
|
|
|
data?: {
|
2026-05-12 14:50:58 +02:00
|
|
|
|
profile?: {
|
|
|
|
|
|
firstName?: string | null;
|
|
|
|
|
|
displayName?: string | null;
|
|
|
|
|
|
};
|
2026-05-11 17:58:42 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function timeOfDayGreeting(): string {
|
2026-05-12 15:48:51 +02:00
|
|
|
|
// new Date().getHours() uses the browser's local timezone, which the OS
|
|
|
|
|
|
// updates automatically on most modern devices when the user travels (laptop
|
|
|
|
|
|
// "set timezone automatically" on macOS, phones via cell-tower / GPS). So
|
|
|
|
|
|
// this reflects the user's physical location as long as their machine clock
|
|
|
|
|
|
// does. We still defer the compute to a useEffect on mount so the rendered
|
|
|
|
|
|
// HTML can never disagree with the server's clock during hydration.
|
2026-05-11 17:58:42 +02:00
|
|
|
|
const hour = new Date().getHours();
|
|
|
|
|
|
if (hour < 5) return 'Up late';
|
|
|
|
|
|
if (hour < 12) return 'Good morning';
|
|
|
|
|
|
if (hour < 18) return 'Good afternoon';
|
|
|
|
|
|
return 'Good evening';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 16:14:12 +02:00
|
|
|
|
interface DashboardShellProps {
|
|
|
|
|
|
/** SSR-prefetched first name. When provided, the greeting renders with it
|
|
|
|
|
|
* on first paint instead of flickering "Welcome back" → "Hello, Matt". */
|
|
|
|
|
|
initialFirstName?: string | null;
|
|
|
|
|
|
/** SSR-prefetched widget visibility map. Seeds the preferences cache so the
|
|
|
|
|
|
* layout doesn't reflow once the client-side fetch resolves. */
|
|
|
|
|
|
initialWidgetVisibility?: Record<string, boolean> | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function DashboardShell({
|
|
|
|
|
|
initialFirstName,
|
|
|
|
|
|
initialWidgetVisibility,
|
|
|
|
|
|
}: DashboardShellProps = {}) {
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
const [range, setRange] = useState<DateRange>('30d');
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
2026-05-12 16:14:12 +02:00
|
|
|
|
const { visibleWidgets } = useDashboardWidgets({
|
|
|
|
|
|
initialVisibility: initialWidgetVisibility ?? null,
|
|
|
|
|
|
});
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
|
|
// Bucket once so the JSX stays readable. Registry order is preserved
|
|
|
|
|
|
// inside each bucket, so reordering the registry reorders the render.
|
|
|
|
|
|
const charts = visibleWidgets.filter((w) => w.group === 'chart');
|
|
|
|
|
|
const rails = visibleWidgets.filter((w) => w.group === 'rail');
|
|
|
|
|
|
const feed = visibleWidgets.filter((w) => w.group === 'feed');
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
|
2026-05-11 17:58:42 +02:00
|
|
|
|
// Reuses the existing ['me'] cache (5-minute staleTime) populated by
|
|
|
|
|
|
// useTablePreferences elsewhere — usually a cache hit, so no extra
|
2026-05-12 16:14:12 +02:00
|
|
|
|
// request. When the page server-prefetches the first name we seed it
|
|
|
|
|
|
// here via `initialData` so the cache is warm before the post-mount
|
|
|
|
|
|
// fetch resolves, eliminating the "Welcome back → Hello, Matt" flash.
|
2026-05-11 17:58:42 +02:00
|
|
|
|
const me = useQuery<MeData>({
|
|
|
|
|
|
queryKey: ['me'],
|
|
|
|
|
|
queryFn: ({ signal }) => apiFetch<MeData>('/api/v1/me', { signal }),
|
|
|
|
|
|
staleTime: 5 * 60_000,
|
2026-05-12 16:14:12 +02:00
|
|
|
|
initialData:
|
|
|
|
|
|
initialFirstName !== undefined
|
|
|
|
|
|
? ({ data: { profile: { firstName: initialFirstName } } } as MeData)
|
|
|
|
|
|
: undefined,
|
2026-05-11 17:58:42 +02:00
|
|
|
|
});
|
2026-05-12 14:50:58 +02:00
|
|
|
|
const firstName = me.data?.data?.profile?.firstName?.trim();
|
2026-05-12 15:48:51 +02:00
|
|
|
|
|
|
|
|
|
|
// Greeting word is computed in a useEffect so the rendered HTML can't lock
|
|
|
|
|
|
// to the server's clock during hydration. Until the effect fires, the
|
|
|
|
|
|
// header reads "Welcome" — a neutral phrase that's correct at every hour
|
|
|
|
|
|
// and never produces a hydration warning. `clientGreeting` flips to the
|
|
|
|
|
|
// local-time-aware phrasing once the component has mounted.
|
|
|
|
|
|
const [clientGreeting, setClientGreeting] = useState<string | null>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setClientGreeting(timeOfDayGreeting());
|
|
|
|
|
|
// Re-evaluate hourly so a rep who leaves the dashboard open through a
|
|
|
|
|
|
// boundary (5am, noon, 6pm) doesn't keep stale text on screen.
|
2026-05-12 16:14:12 +02:00
|
|
|
|
const interval = window.setInterval(() => setClientGreeting(timeOfDayGreeting()), 60 * 60_000);
|
2026-05-12 15:48:51 +02:00
|
|
|
|
return () => window.clearInterval(interval);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const greeting = firstName
|
|
|
|
|
|
? `${clientGreeting ?? 'Welcome'}, ${firstName}`
|
|
|
|
|
|
: (clientGreeting ?? 'Welcome back');
|
2026-05-11 17:58:42 +02:00
|
|
|
|
|
2026-05-04 22:54:55 +02:00
|
|
|
|
// Use a partial query-key prefix (no range segment) for invalidations.
|
|
|
|
|
|
// Reading: "any cached analytics result, regardless of range, please
|
|
|
|
|
|
// refetch on this event." This avoids any chance that a custom-range
|
|
|
|
|
|
// object literal hashes differently than the one stored in the cache,
|
|
|
|
|
|
// and keeps the invalidation surface broad enough to refresh whichever
|
|
|
|
|
|
// range the user is currently looking at.
|
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
|
|
|
|
useRealtimeInvalidation({
|
2026-04-28 12:09:59 +02:00
|
|
|
|
'interest:stageChanged': [
|
2026-05-04 22:54:55 +02:00
|
|
|
|
['analytics', 'pipeline_funnel'],
|
|
|
|
|
|
['analytics', 'lead_source_attribution'],
|
feat(phase-b): ship analytics dashboard, alerts, scanner PWA, dedup, audit view
Phase B (Insights & Alerts) PR4-11 in one drop. Builds on the schema +
service skeletons committed in PRs 1-3.
PR4 Analytics dashboard — 4 chart types (funnel/timeline/breakdown/source),
date-range picker (today/7d/30d/90d), CSV+PNG export per card.
PR5 Alert rail UI + /alerts page — topbar bell w/ live count, dashboard
right-rail, three-tab page (active/dismissed/resolved), socket-driven
invalidation. Bell lazy-loads list on popover open to keep cold pages
fast in non-dashboard routes.
PR6 EOI queue tab on documents hub — filters to in-flight EOIs, count
surfaces in tab label.
PR7 Interests-by-berth tab on berth detail — replaces the stub.
PR8 Expense duplicate detection — BullMQ job runs scan on create, yellow
banner on detail w/ Merge / Not-a-duplicate, transactional merge
consolidates receipts and archives the source.
PR9 Receipt scanner PWA + multi-provider AI — port-scoped /scan route in
its own (scanner) group with no dashboard chrome, dynamic per-port
manifest, OpenAI + Claude provider abstraction, admin OCR settings
page (port-level + super-admin global default w/ opt-in fallback),
test-connection endpoint, manual-entry fallback when no key is
configured. Verify form always shown before save — no ghost rows.
PR10 Audit log read view — swap to tsvector full-text search on the
existing GIN index, cursor pagination, filters for entity/action/user
/date range, batched actor-email resolution.
PR11 Real-API tests — opt-in receipt-ocr.spec (admin save+test, optional
real-receipt parse via REALAPI_RECEIPT_FIXTURE) and alert-engine
socket-fanout spec gated behind RUN_ALERT_ENGINE_REALAPI. Both skip
cleanly without their gate envs so CI stays green.
Test totals: vitest 690 -> 713, smoke 130 -> 138, realapi +2 opt-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:21:55 +02:00
|
|
|
|
['dashboard', 'kpis'],
|
2026-04-28 12:09:59 +02:00
|
|
|
|
],
|
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
|
|
|
|
'client:created': [['dashboard', 'kpis']],
|
2026-04-28 12:09:59 +02:00
|
|
|
|
'berth:statusChanged': [
|
2026-05-04 22:54:55 +02:00
|
|
|
|
['analytics', 'occupancy_timeline'],
|
2026-04-28 12:09:59 +02:00
|
|
|
|
['dashboard', 'kpis'],
|
|
|
|
|
|
],
|
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 (
|
|
|
|
|
|
<div className="space-y-6">
|
2026-05-12 14:50:58 +02:00
|
|
|
|
{/* Mobile-only greeting strip. The shared PageHeader is hidden
|
|
|
|
|
|
below `sm` (its title is normally duplicated by the topbar),
|
|
|
|
|
|
so we render the welcome message inline here for mobile —
|
|
|
|
|
|
keeps the personalized touch from desktop without polluting
|
|
|
|
|
|
the topbar (which stays "Dashboard" for wayfinding). */}
|
|
|
|
|
|
<div className="sm:hidden">
|
|
|
|
|
|
<p className="text-xs font-semibold uppercase tracking-wide text-brand">Dashboard</p>
|
|
|
|
|
|
<h1 className="mt-1 text-xl font-bold tracking-tight text-foreground">{greeting}</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-05-12 15:48:51 +02:00
|
|
|
|
<TimezoneDriftBanner />
|
|
|
|
|
|
|
2026-04-28 12:09:59 +02:00
|
|
|
|
<PageHeader
|
2026-05-11 17:58:42 +02:00
|
|
|
|
title={greeting}
|
|
|
|
|
|
eyebrow="Dashboard"
|
2026-05-12 14:50:58 +02:00
|
|
|
|
// The date-range subtitle only means something when at least
|
|
|
|
|
|
// one widget is on the page to consume the range; if everything
|
|
|
|
|
|
// is hidden it just reads as an orphaned line.
|
|
|
|
|
|
kpiLine={visibleWidgets.length > 0 ? <span>{rangeLabel(range)}</span> : undefined}
|
2026-04-28 12:09:59 +02:00
|
|
|
|
variant="gradient"
|
2026-05-12 14:50:58 +02:00
|
|
|
|
actions={
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<DateRangePicker value={range} onChange={setRange} />
|
|
|
|
|
|
<CustomizeWidgetsMenu />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2026-04-28 12:09:59 +02:00
|
|
|
|
/>
|
|
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
|
{/* Charts + rails sit side-by-side at xl+. Each side is an auto-fit
|
|
|
|
|
|
grid, so hiding a card causes the remaining ones to widen.
|
|
|
|
|
|
`items-start` is critical: without it, the right-column aside is
|
2026-05-04 22:54:55 +02:00
|
|
|
|
stretched to match the chart column's row height, which forces
|
|
|
|
|
|
MyRemindersRail (or any other child with `h-full`) to push later
|
2026-05-12 14:50:58 +02:00
|
|
|
|
children out of the aside's box. */}
|
|
|
|
|
|
{/* Charts + rails. Layout adapts to which regions have content so
|
|
|
|
|
|
we never leave a 320px stripe of dead space when only one side
|
|
|
|
|
|
is populated:
|
|
|
|
|
|
both → main 1fr column + 320px rail (the original layout)
|
|
|
|
|
|
charts only → single full-width auto-fit chart grid
|
|
|
|
|
|
rails only → rails widen into an auto-fit grid (no fixed 320)
|
|
|
|
|
|
neither → nothing renders
|
|
|
|
|
|
The chart grid uses `minmax(360px, 1fr)` so a lone chart fills
|
|
|
|
|
|
the row; the rails-only grid uses a slightly tighter `280px`
|
|
|
|
|
|
minimum so KPI tiles + rails fit 3-4 across on a wide viewport
|
|
|
|
|
|
instead of stretching to 600px+ each. */}
|
|
|
|
|
|
{charts.length > 0 && rails.length > 0 ? (
|
|
|
|
|
|
<div className="grid gap-4 grid-cols-1 items-start xl:grid-cols-[minmax(0,1fr)_320px]">
|
|
|
|
|
|
<div className="grid gap-4 grid-cols-1 lg:grid-cols-[repeat(auto-fit,minmax(360px,1fr))]">
|
|
|
|
|
|
{charts.map((w) => (
|
|
|
|
|
|
<WidgetCell key={w.id} widget={w} range={range} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<aside className="min-w-0 space-y-4">
|
|
|
|
|
|
{rails.map((w) => (
|
|
|
|
|
|
<WidgetCell key={w.id} widget={w} range={range} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</aside>
|
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
|
|
|
|
</div>
|
2026-05-12 14:50:58 +02:00
|
|
|
|
) : charts.length > 0 ? (
|
|
|
|
|
|
<div className="grid gap-4 grid-cols-1 lg:grid-cols-[repeat(auto-fit,minmax(360px,1fr))]">
|
|
|
|
|
|
{charts.map((w) => (
|
|
|
|
|
|
<WidgetCell key={w.id} widget={w} range={range} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : rails.length > 0 ? (
|
|
|
|
|
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(280px,1fr))]">
|
|
|
|
|
|
{rails.map((w) => (
|
|
|
|
|
|
<WidgetCell key={w.id} widget={w} range={range} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
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
|
|
|
|
|
2026-05-12 14:50:58 +02:00
|
|
|
|
{feed.map((w) => (
|
|
|
|
|
|
<WidgetCell key={w.id} widget={w} range={range} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{visibleWidgets.length === 0 ? <EmptyDashboardHint /> : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Placeholder shown when the rep has hidden every widget. Without this,
|
|
|
|
|
|
* the dashboard collapses to just the gradient header strip and looks
|
|
|
|
|
|
* like a broken page — this hints at the "Customize" button to bring
|
|
|
|
|
|
* widgets back.
|
|
|
|
|
|
*/
|
|
|
|
|
|
function EmptyDashboardHint() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-card/40 px-6 py-16 text-center">
|
|
|
|
|
|
<p className="text-sm font-medium text-foreground">No widgets on your dashboard yet</p>
|
|
|
|
|
|
<p className="max-w-sm text-sm text-muted-foreground">
|
|
|
|
|
|
Click <span className="font-medium text-foreground">Customize</span> above to pick which
|
|
|
|
|
|
analytics cards appear here.
|
|
|
|
|
|
</p>
|
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
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-05-12 14:50:58 +02:00
|
|
|
|
|
|
|
|
|
|
function WidgetCell({ widget, range }: { widget: DashboardWidget; range: DateRange }) {
|
|
|
|
|
|
return <WidgetErrorBoundary>{widget.render(range)}</WidgetErrorBoundary>;
|
|
|
|
|
|
}
|