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>
This commit is contained in:
200
src/components/berths/berth-tabs.tsx
Normal file
200
src/components/berths/berth-tabs.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
'use client';
|
||||
|
||||
import { type DetailTab } from '@/components/shared/detail-layout';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { TagBadge } from '@/components/shared/tag-badge';
|
||||
|
||||
type BerthData = {
|
||||
id: string;
|
||||
mooringNumber: string;
|
||||
area: string | null;
|
||||
status: string;
|
||||
lengthFt: string | null;
|
||||
lengthM: string | null;
|
||||
widthFt: string | null;
|
||||
widthM: string | null;
|
||||
draftFt: string | null;
|
||||
draftM: string | null;
|
||||
widthIsMinimum: boolean | null;
|
||||
nominalBoatSize: string | null;
|
||||
nominalBoatSizeM: string | null;
|
||||
waterDepth: string | null;
|
||||
waterDepthM: string | null;
|
||||
waterDepthIsMinimum: boolean | null;
|
||||
sidePontoon: string | null;
|
||||
powerCapacity: string | null;
|
||||
voltage: string | null;
|
||||
mooringType: string | null;
|
||||
cleatType: string | null;
|
||||
cleatCapacity: string | null;
|
||||
bollardType: string | null;
|
||||
bollardCapacity: string | null;
|
||||
access: string | null;
|
||||
price: string | null;
|
||||
priceCurrency: string;
|
||||
bowFacing: string | null;
|
||||
berthApproved: boolean | null;
|
||||
tenureType: string;
|
||||
tenureYears: number | null;
|
||||
tenureStartDate: string | null;
|
||||
tenureEndDate: string | null;
|
||||
statusLastChangedReason: string | null;
|
||||
statusLastModified: string | null;
|
||||
tags: Array<{ id: string; name: string; color: string }>;
|
||||
};
|
||||
|
||||
function SpecRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
if (!value && value !== 0 && value !== false) return null;
|
||||
return (
|
||||
<div className="flex justify-between py-2 text-sm">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="font-medium text-right max-w-[60%]">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewTab({ berth }: { berth: BerthData }) {
|
||||
const formatDim = (ft: string | null, m: string | null) => {
|
||||
const parts = [];
|
||||
if (ft) parts.push(`${ft} ft`);
|
||||
if (m) parts.push(`${m} m`);
|
||||
return parts.length > 0 ? parts.join(' / ') : null;
|
||||
};
|
||||
|
||||
const price = berth.price
|
||||
? new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: berth.priceCurrency || 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(Number(berth.price))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Specifications */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Specifications</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 divide-y">
|
||||
<SpecRow label="Length" value={formatDim(berth.lengthFt, berth.lengthM)} />
|
||||
<SpecRow
|
||||
label="Width"
|
||||
value={
|
||||
formatDim(berth.widthFt, berth.widthM)
|
||||
? `${formatDim(berth.widthFt, berth.widthM)}${berth.widthIsMinimum ? ' (min)' : ''}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
||||
<SpecRow label="Nominal Boat Size" value={berth.nominalBoatSize || berth.nominalBoatSizeM} />
|
||||
<SpecRow
|
||||
label="Water Depth"
|
||||
value={
|
||||
berth.waterDepth || berth.waterDepthM
|
||||
? `${formatDim(berth.waterDepth, berth.waterDepthM)}${berth.waterDepthIsMinimum ? ' (min)' : ''}`
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<SpecRow label="Mooring Type" value={berth.mooringType} />
|
||||
<SpecRow label="Side Pontoon" value={berth.sidePontoon} />
|
||||
<SpecRow label="Bow Facing" value={berth.bowFacing} />
|
||||
<SpecRow label="Access" value={berth.access} />
|
||||
<SpecRow label="Approved" value={berth.berthApproved ? 'Yes' : null} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Infrastructure & Pricing */}
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Infrastructure</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 divide-y">
|
||||
<SpecRow label="Power Capacity" value={berth.powerCapacity} />
|
||||
<SpecRow label="Voltage" value={berth.voltage} />
|
||||
<SpecRow label="Cleat Type" value={berth.cleatType} />
|
||||
<SpecRow label="Cleat Capacity" value={berth.cleatCapacity} />
|
||||
<SpecRow label="Bollard Type" value={berth.bollardType} />
|
||||
<SpecRow label="Bollard Capacity" value={berth.bollardCapacity} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Tenure & Pricing</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 divide-y">
|
||||
<SpecRow
|
||||
label="Tenure Type"
|
||||
value={berth.tenureType === 'permanent' ? 'Permanent' : 'Fixed Term'}
|
||||
/>
|
||||
{berth.tenureType === 'fixed_term' && (
|
||||
<>
|
||||
<SpecRow label="Years" value={berth.tenureYears} />
|
||||
<SpecRow label="Start Date" value={berth.tenureStartDate} />
|
||||
<SpecRow label="End Date" value={berth.tenureEndDate} />
|
||||
</>
|
||||
)}
|
||||
<SpecRow label="Price" value={price} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{berth.tags.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Tags</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{berth.tags.map((tag) => (
|
||||
<TagBadge key={tag.id} name={tag.name} color={tag.color} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StubTab({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed p-12 text-center">
|
||||
<p className="text-muted-foreground">{label} coming soon</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildBerthTabs(berth: BerthData): DetailTab[] {
|
||||
return [
|
||||
{
|
||||
id: 'overview',
|
||||
label: 'Overview',
|
||||
content: <OverviewTab berth={berth} />,
|
||||
},
|
||||
{
|
||||
id: 'interests',
|
||||
label: 'Interests',
|
||||
content: <StubTab label="Interests" />,
|
||||
},
|
||||
{
|
||||
id: 'waiting-list',
|
||||
label: 'Waiting List',
|
||||
content: <StubTab label="Waiting List" />,
|
||||
},
|
||||
{
|
||||
id: 'maintenance',
|
||||
label: 'Maintenance Log',
|
||||
content: <StubTab label="Maintenance Log" />,
|
||||
},
|
||||
{
|
||||
id: 'activity',
|
||||
label: 'Activity',
|
||||
content: <StubTab label="Activity" />,
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user