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-03-26 12:06:18 +01:00
|
|
|
import { useState, useCallback, useMemo } from 'react';
|
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 { useQuery, useQueryClient, type QueryKey } from '@tanstack/react-query';
|
|
|
|
|
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
|
|
|
|
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
import type { PaginatedResponse } from '@/types/api';
|
|
|
|
|
import {
|
|
|
|
|
serializeFiltersToParams,
|
|
|
|
|
deserializeFiltersFromParams,
|
|
|
|
|
type FilterDefinition,
|
|
|
|
|
type FilterValues,
|
|
|
|
|
} from '@/components/shared/filter-bar';
|
|
|
|
|
|
2026-03-26 12:06:18 +01:00
|
|
|
interface UsePaginatedQueryOptions {
|
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
|
|
|
queryKey: QueryKey;
|
|
|
|
|
endpoint: string;
|
|
|
|
|
initialPage?: number;
|
|
|
|
|
initialPageSize?: number;
|
|
|
|
|
filterDefinitions?: FilterDefinition[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function usePaginatedQuery<T>({
|
|
|
|
|
queryKey,
|
|
|
|
|
endpoint,
|
|
|
|
|
initialPage = 1,
|
|
|
|
|
initialPageSize = 25,
|
|
|
|
|
filterDefinitions = [],
|
2026-03-26 12:06:18 +01:00
|
|
|
}: UsePaginatedQueryOptions) {
|
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
|
|
|
const searchParams = useSearchParams();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const pathname = usePathname();
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
// Read initial state from URL
|
|
|
|
|
const pageFromUrl = Number(searchParams.get('page')) || initialPage;
|
|
|
|
|
const pageSizeFromUrl = Number(searchParams.get('limit')) || initialPageSize;
|
|
|
|
|
const sortFieldFromUrl = searchParams.get('sort') ?? undefined;
|
|
|
|
|
const sortOrderFromUrl = (searchParams.get('order') as 'asc' | 'desc') ?? 'desc';
|
|
|
|
|
|
|
|
|
|
const [page, setPageState] = useState(pageFromUrl);
|
|
|
|
|
const [pageSize, setPageSizeState] = useState(pageSizeFromUrl);
|
|
|
|
|
const [sort, setSortState] = useState<{ field: string; direction: 'asc' | 'desc' } | undefined>(
|
|
|
|
|
sortFieldFromUrl ? { field: sortFieldFromUrl, direction: sortOrderFromUrl } : undefined,
|
|
|
|
|
);
|
|
|
|
|
const [filters, setFiltersState] = useState<FilterValues>(() =>
|
|
|
|
|
deserializeFiltersFromParams(searchParams, filterDefinitions),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Sync state to URL
|
|
|
|
|
const syncUrl = useCallback(
|
|
|
|
|
(p: number, ps: number, s?: typeof sort, f?: FilterValues) => {
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
if (p !== 1) params.set('page', String(p));
|
|
|
|
|
if (ps !== initialPageSize) params.set('limit', String(ps));
|
|
|
|
|
if (s) {
|
|
|
|
|
params.set('sort', s.field);
|
|
|
|
|
params.set('order', s.direction);
|
|
|
|
|
}
|
|
|
|
|
if (f) {
|
|
|
|
|
const filterParams = serializeFiltersToParams(f);
|
|
|
|
|
filterParams.forEach((value, key) => params.set(key, value));
|
|
|
|
|
}
|
|
|
|
|
// Keep existing tab param
|
|
|
|
|
const tab = searchParams.get('tab');
|
|
|
|
|
if (tab) params.set('tab', tab);
|
|
|
|
|
|
|
|
|
|
const qs = params.toString();
|
2026-03-26 12:29:55 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
router.replace(`${pathname}${qs ? `?${qs}` : ''}` as any, { scroll: false });
|
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
|
|
|
},
|
|
|
|
|
[pathname, router, searchParams, initialPageSize],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function setPage(p: number) {
|
|
|
|
|
setPageState(p);
|
|
|
|
|
syncUrl(p, pageSize, sort, filters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setPageSize(ps: number) {
|
|
|
|
|
setPageSizeState(ps);
|
|
|
|
|
setPageState(1);
|
|
|
|
|
syncUrl(1, ps, sort, filters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setSort(field: string, direction: 'asc' | 'desc') {
|
|
|
|
|
const newSort = { field, direction };
|
|
|
|
|
setSortState(newSort);
|
|
|
|
|
setPageState(1);
|
|
|
|
|
syncUrl(1, pageSize, newSort, filters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setFilter(key: string, value: unknown) {
|
|
|
|
|
const newFilters = { ...filters };
|
|
|
|
|
if (value === undefined || value === null) {
|
|
|
|
|
delete newFilters[key];
|
|
|
|
|
} else {
|
|
|
|
|
newFilters[key] = value;
|
|
|
|
|
}
|
|
|
|
|
setFiltersState(newFilters);
|
|
|
|
|
setPageState(1);
|
|
|
|
|
syncUrl(1, pageSize, sort, newFilters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearFilters() {
|
|
|
|
|
setFiltersState({});
|
|
|
|
|
setPageState(1);
|
|
|
|
|
syncUrl(1, pageSize, sort, {});
|
|
|
|
|
}
|
|
|
|
|
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
/**
|
|
|
|
|
* Atomically replace the entire filter set. Used by the saved-views
|
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
2026-05-23 00:52:59 +02:00
|
|
|
* apply path - calling `clearFilters()` + N x `setFilter()` in a row
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
* lost all but the last setFilter because each one reads the stale
|
|
|
|
|
* `filters` closure and overwrites with `{...filters, key: val}`.
|
|
|
|
|
* setAllFilters writes the whole object in one setState so the view
|
|
|
|
|
* lands intact.
|
|
|
|
|
*/
|
|
|
|
|
function setAllFilters(next: FilterValues) {
|
|
|
|
|
setFiltersState(next);
|
|
|
|
|
setPageState(1);
|
|
|
|
|
syncUrl(1, pageSize, sort, next);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Build query string for API
|
|
|
|
|
const apiParams = useMemo(() => {
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
params.set('page', String(page));
|
|
|
|
|
params.set('limit', String(pageSize));
|
|
|
|
|
if (sort) {
|
|
|
|
|
params.set('sort', sort.field);
|
|
|
|
|
params.set('order', sort.direction);
|
|
|
|
|
}
|
|
|
|
|
const filterParams = serializeFiltersToParams(filters);
|
|
|
|
|
filterParams.forEach((value, key) => params.set(key, value));
|
|
|
|
|
return params.toString();
|
|
|
|
|
}, [page, pageSize, sort, filters]);
|
|
|
|
|
|
|
|
|
|
const fullQueryKey = [...queryKey, apiParams];
|
|
|
|
|
|
2026-05-02 00:30:27 +02:00
|
|
|
// Endpoints that already carry a query string (e.g. `/api/v1/documents?tab=eoi_queue`)
|
|
|
|
|
// need our pagination params merged with `&`, not a second `?`. Without this guard
|
|
|
|
|
// the URL becomes `…?tab=foo?page=1` and the API rejects it as 400.
|
|
|
|
|
const separator = endpoint.includes('?') ? '&' : '?';
|
|
|
|
|
|
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
|
|
|
const { data, isLoading, isFetching } = useQuery<PaginatedResponse<T>>({
|
|
|
|
|
queryKey: fullQueryKey,
|
2026-05-02 00:30:27 +02:00
|
|
|
queryFn: () => apiFetch<PaginatedResponse<T>>(`${endpoint}${separator}${apiParams}`),
|
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
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const pagination = data?.pagination
|
|
|
|
|
? {
|
|
|
|
|
page: data.pagination.page,
|
|
|
|
|
pageSize: data.pagination.pageSize,
|
|
|
|
|
total: data.pagination.total,
|
|
|
|
|
totalPages: data.pagination.totalPages,
|
|
|
|
|
}
|
|
|
|
|
: { page, pageSize, total: 0, totalPages: 0 };
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Optimistically removes an item from the cached list (e.g., after archive).
|
|
|
|
|
*/
|
|
|
|
|
function optimisticRemove(id: string) {
|
|
|
|
|
queryClient.setQueryData<PaginatedResponse<T>>(fullQueryKey, (old) => {
|
|
|
|
|
if (!old) return old;
|
|
|
|
|
return {
|
|
|
|
|
...old,
|
2026-03-26 12:29:55 +01:00
|
|
|
data: old.data.filter((item) => (item as Record<string, unknown>).id !== id),
|
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
|
|
|
pagination: {
|
|
|
|
|
...old.pagination,
|
|
|
|
|
total: old.pagination.total - 1,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: data?.data ?? [],
|
|
|
|
|
pagination,
|
|
|
|
|
isLoading,
|
|
|
|
|
isFetching,
|
|
|
|
|
sort,
|
|
|
|
|
setSort,
|
|
|
|
|
page,
|
|
|
|
|
setPage,
|
|
|
|
|
pageSize,
|
|
|
|
|
setPageSize,
|
|
|
|
|
filters,
|
|
|
|
|
setFilter,
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
setAllFilters,
|
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
|
|
|
clearFilters,
|
|
|
|
|
optimisticRemove,
|
|
|
|
|
};
|
|
|
|
|
}
|