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 { NextResponse } from 'next/server';
|
|
|
|
|
import { and, eq, desc, inArray } from 'drizzle-orm';
|
|
|
|
|
|
|
|
|
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
|
|
|
|
import { errorResponse, NotFoundError } from '@/lib/errors';
|
|
|
|
|
import { db } from '@/lib/db';
|
|
|
|
|
import { interests } from '@/lib/db/schema/interests';
|
|
|
|
|
import { auditLogs } from '@/lib/db/schema/system';
|
|
|
|
|
import { documents, documentEvents } from '@/lib/db/schema/documents';
|
2026-05-02 00:19:55 +02:00
|
|
|
import { stageLabel } from '@/lib/constants';
|
|
|
|
|
|
|
|
|
|
const OUTCOME_LABELS: Record<string, string> = {
|
|
|
|
|
won: 'Won',
|
|
|
|
|
lost_other_marina: 'Lost — went to another marina',
|
|
|
|
|
lost_unqualified: 'Lost — unqualified',
|
|
|
|
|
lost_no_response: 'Lost — no response',
|
|
|
|
|
cancelled: 'Cancelled',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DOC_EVENT_LABELS: Record<string, string> = {
|
|
|
|
|
sent: 'sent for signing',
|
|
|
|
|
completed: 'fully signed',
|
|
|
|
|
signed: 'signed by recipient',
|
|
|
|
|
rejected: 'rejected',
|
|
|
|
|
expired: 'expired',
|
|
|
|
|
cancelled: 'cancelled',
|
|
|
|
|
reminder_sent: 'reminder sent',
|
|
|
|
|
};
|
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
|
|
|
|
|
|
|
|
interface TimelineEvent {
|
|
|
|
|
id: string;
|
|
|
|
|
type: 'audit' | 'document_event';
|
|
|
|
|
action: string;
|
|
|
|
|
description: string;
|
|
|
|
|
userId: string | null;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
metadata: Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/v1/interests/[id]/timeline
|
|
|
|
|
export const GET = withAuth(
|
|
|
|
|
withPermission('interests', 'view', async (req, ctx, params) => {
|
|
|
|
|
try {
|
|
|
|
|
const interestId = params.id!;
|
|
|
|
|
|
|
|
|
|
const interest = await db.query.interests.findFirst({
|
|
|
|
|
where: and(eq(interests.id, interestId), eq(interests.portId, ctx.portId)),
|
|
|
|
|
});
|
|
|
|
|
if (!interest) throw new NotFoundError('Interest');
|
|
|
|
|
|
|
|
|
|
// Fetch audit logs for this interest
|
|
|
|
|
const auditRows = await db
|
|
|
|
|
.select()
|
|
|
|
|
.from(auditLogs)
|
2026-05-02 00:19:55 +02:00
|
|
|
.where(and(eq(auditLogs.entityType, 'interest'), eq(auditLogs.entityId, interestId)))
|
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
|
|
|
.orderBy(desc(auditLogs.createdAt))
|
|
|
|
|
.limit(50);
|
|
|
|
|
|
|
|
|
|
// Fetch document events for documents linked to this interest
|
|
|
|
|
const interestDocs = await db
|
|
|
|
|
.select({ id: documents.id, title: documents.title })
|
|
|
|
|
.from(documents)
|
|
|
|
|
.where(eq(documents.interestId, interestId));
|
|
|
|
|
|
|
|
|
|
const docIds = interestDocs.map((d) => d.id);
|
|
|
|
|
const docEventRows =
|
|
|
|
|
docIds.length > 0
|
|
|
|
|
? await db
|
|
|
|
|
.select({
|
|
|
|
|
id: documentEvents.id,
|
|
|
|
|
documentId: documentEvents.documentId,
|
|
|
|
|
eventType: documentEvents.eventType,
|
|
|
|
|
eventData: documentEvents.eventData,
|
|
|
|
|
createdAt: documentEvents.createdAt,
|
|
|
|
|
})
|
|
|
|
|
.from(documentEvents)
|
|
|
|
|
.where(inArray(documentEvents.documentId, docIds))
|
|
|
|
|
.orderBy(desc(documentEvents.createdAt))
|
|
|
|
|
.limit(50)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const docTitles = Object.fromEntries(interestDocs.map((d) => [d.id, d.title]));
|
|
|
|
|
|
|
|
|
|
// Union and sort
|
|
|
|
|
const auditEvents: TimelineEvent[] = auditRows.map((row) => ({
|
|
|
|
|
id: row.id,
|
|
|
|
|
type: 'audit',
|
|
|
|
|
action: row.action,
|
2026-05-02 00:19:55 +02:00
|
|
|
description: buildAuditDescription(
|
|
|
|
|
row.action,
|
|
|
|
|
row.newValue as Record<string, unknown> | null,
|
|
|
|
|
(row.metadata as Record<string, unknown>) ?? {},
|
|
|
|
|
row.userId,
|
|
|
|
|
),
|
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
|
|
|
userId: row.userId,
|
|
|
|
|
createdAt: row.createdAt,
|
|
|
|
|
metadata: (row.metadata as Record<string, unknown>) ?? {},
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-02 00:19:55 +02:00
|
|
|
const docEvents: TimelineEvent[] = docEventRows.map((row) => {
|
|
|
|
|
const title = docTitles[row.documentId] ?? row.documentId;
|
|
|
|
|
const action = DOC_EVENT_LABELS[row.eventType] ?? row.eventType;
|
|
|
|
|
return {
|
|
|
|
|
id: row.id,
|
|
|
|
|
type: 'document_event',
|
|
|
|
|
action: row.eventType,
|
|
|
|
|
description: `Document "${title}" ${action}`,
|
|
|
|
|
userId: null,
|
|
|
|
|
createdAt: row.createdAt,
|
|
|
|
|
metadata: (row.eventData as Record<string, unknown>) ?? {},
|
|
|
|
|
};
|
|
|
|
|
});
|
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 allEvents = [...auditEvents, ...docEvents];
|
|
|
|
|
allEvents.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
|
|
|
|
|
|
|
|
return NextResponse.json({ data: allEvents.slice(0, 50) });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return errorResponse(error);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function buildAuditDescription(
|
|
|
|
|
action: string,
|
|
|
|
|
newValue: Record<string, unknown> | null,
|
2026-05-02 00:19:55 +02:00
|
|
|
metadata: Record<string, unknown>,
|
|
|
|
|
userId: string | 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
|
|
|
): string {
|
|
|
|
|
if (action === 'create') return 'Interest created';
|
|
|
|
|
if (action === 'archive') return 'Interest archived';
|
|
|
|
|
if (action === 'restore') return 'Interest restored';
|
2026-05-02 00:19:55 +02:00
|
|
|
|
|
|
|
|
const type = metadata.type;
|
|
|
|
|
|
|
|
|
|
if (type === 'outcome_set') {
|
|
|
|
|
const outcomeKey = (newValue?.outcome as string | undefined) ?? '';
|
|
|
|
|
const label = OUTCOME_LABELS[outcomeKey] ?? outcomeKey ?? 'Closed';
|
|
|
|
|
const reason = (newValue?.reason as string | undefined) ?? '';
|
|
|
|
|
return reason ? `Marked as ${label} — ${reason}` : `Marked as ${label}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === 'outcome_cleared') {
|
|
|
|
|
const stage = (newValue?.pipelineStage as string | undefined) ?? '';
|
|
|
|
|
return stage ? `Reopened to ${stageLabel(stage)}` : 'Reopened';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === 'stage_change' && newValue?.pipelineStage) {
|
|
|
|
|
const stage = stageLabel(newValue.pipelineStage as string);
|
|
|
|
|
const reason = (newValue.reason as string | undefined) ?? '';
|
|
|
|
|
const auto = userId === 'system';
|
|
|
|
|
if (auto) {
|
|
|
|
|
return reason ? `${stage} (auto-advanced — ${reason})` : `Stage advanced to ${stage}`;
|
|
|
|
|
}
|
|
|
|
|
return reason ? `Stage changed to ${stage} — ${reason}` : `Stage changed to ${stage}`;
|
|
|
|
|
}
|
|
|
|
|
|
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 (action === 'update' && newValue?.pipelineStage) {
|
2026-05-02 00:19:55 +02:00
|
|
|
return `Stage changed to ${stageLabel(newValue.pipelineStage as string)}`;
|
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 (action === 'update') return 'Interest updated';
|
|
|
|
|
return action;
|
|
|
|
|
}
|