Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled

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>
This commit is contained in:
2026-03-26 11:52:51 +01:00
commit 67d7e6e3d5
572 changed files with 86496 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import type { Template } from '@pdfme/common';
import type { OccupancyData } from '@/lib/services/report-generators';
export const occupancyReportTemplate: Template = {
basePdf: 'BLANK_PDF' as any,
schemas: [
[
{
name: 'reportTitle',
type: 'text',
position: { x: 20, y: 15 },
width: 170,
height: 12,
fontSize: 20,
},
{
name: 'portName',
type: 'text',
position: { x: 20, y: 30 },
width: 130,
height: 8,
fontSize: 11,
},
{
name: 'generatedAt',
type: 'text',
position: { x: 140, y: 30 },
width: 50,
height: 8,
fontSize: 9,
},
{
name: 'occupancyRate',
type: 'text',
position: { x: 20, y: 50 },
width: 170,
height: 20,
fontSize: 16,
},
{
name: 'statusBreakdown',
type: 'text',
position: { x: 20, y: 80 },
width: 170,
height: 80,
fontSize: 10,
},
],
],
};
export function buildOccupancyInputs(
data: OccupancyData,
portName?: string,
): Record<string, string>[] {
const statusLabels: Record<string, string> = {
available: 'Available',
under_offer: 'Under Offer',
sold: 'Sold / Occupied',
};
const breakdownLines = ['Berth Status Breakdown', '─────────────────────'];
const allStatuses = ['available', 'under_offer', 'sold'];
const unknownStatuses = Object.keys(data.statusCounts).filter(
(s) => !allStatuses.includes(s),
);
for (const status of [...allStatuses, ...unknownStatuses]) {
const cnt = data.statusCounts[status] ?? 0;
const label = statusLabels[status] ?? status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
const pct = data.totalBerths > 0 ? ((cnt / data.totalBerths) * 100).toFixed(1) : '0.0';
breakdownLines.push(`${label}: ${cnt} berth(s) (${pct}%)`);
}
breakdownLines.push('─────────────────────');
breakdownLines.push(`Total Berths: ${data.totalBerths}`);
return [
{
reportTitle: 'Berth Occupancy Report',
portName: portName ?? 'Port Nimara',
generatedAt: `Generated: ${new Date(data.generatedAt).toLocaleString('en-GB')}`,
occupancyRate: `Occupancy Rate: ${data.occupancyRate}%`,
statusBreakdown: breakdownLines.join('\n'),
},
];
}