feat(berths,seed): polish detail display + prune ports to Port Nimara + Amador
Berth detail (src/components/berths/berth-tabs.tsx):
- Numeric display polish, exposed by the new NocoDB-sourced seed:
- Power capacity now renders with kW unit (e.g. "330 kW")
- Voltage now renders with V unit (e.g. "480 V")
- All metric/imperial values rounded to <= 2 decimals
(was: "62.999112 m" -> now: "62.99 m")
- Nominal Boat Size shows full ft + m pair (was: ft only)
Seed ports (src/lib/db/seed.ts):
- Drop Marina Azzurra and Harbor Royale; install now seeds only:
- Port Nimara (the real install)
- Port Amador (secondary, for multi-tenant isolation tests / Panama
scaffolding)
- Existing dev DBs are not touched; this only affects fresh `pnpm db:seed`
runs. Users wanting to migrate should drop existing rows in the obsolete
ports manually before re-seeding.
Verification:
- lint clean, tsc unchanged from baseline (36 pre-existing errors), 858/858
vitest passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,13 +57,45 @@ function SpecRow({ label, value }: { label: string; value: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function OverviewTab({ berth }: { berth: BerthData }) {
|
function OverviewTab({ berth }: { berth: BerthData }) {
|
||||||
|
// Round to at most 2 decimals; trim trailing zeros so "5.00" -> "5".
|
||||||
|
const fmt = (v: string | null, fractionDigits = 2): string | null => {
|
||||||
|
if (v == null || v === '') return null;
|
||||||
|
const n = Number(v);
|
||||||
|
if (Number.isNaN(n)) return v;
|
||||||
|
return n.toLocaleString('en-US', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: fractionDigits,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formatDim = (ft: string | null, m: string | null) => {
|
const formatDim = (ft: string | null, m: string | null) => {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (ft) parts.push(`${ft} ft`);
|
const ftFmt = fmt(ft);
|
||||||
if (m) parts.push(`${m} m`);
|
const mFmt = fmt(m);
|
||||||
|
if (ftFmt) parts.push(`${ftFmt} ft`);
|
||||||
|
if (mFmt) parts.push(`${mFmt} m`);
|
||||||
return parts.length > 0 ? parts.join(' / ') : null;
|
return parts.length > 0 ? parts.join(' / ') : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatNominalBoatSize = (ft: string | null, m: string | null): string | null => {
|
||||||
|
const ftFmt = fmt(ft, 0);
|
||||||
|
const mFmt = fmt(m);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (ftFmt) parts.push(`${ftFmt} ft`);
|
||||||
|
if (mFmt) parts.push(`${mFmt} m`);
|
||||||
|
return parts.length > 0 ? parts.join(' / ') : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPower = (kw: string | null) => {
|
||||||
|
const v = fmt(kw, 0);
|
||||||
|
return v ? `${v} kW` : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatVoltage = (v: string | null) => {
|
||||||
|
const fv = fmt(v, 0);
|
||||||
|
return fv ? `${fv} V` : null;
|
||||||
|
};
|
||||||
|
|
||||||
const price = berth.price
|
const price = berth.price
|
||||||
? new Intl.NumberFormat('en-US', {
|
? new Intl.NumberFormat('en-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -97,7 +129,7 @@ function OverviewTab({ berth }: { berth: BerthData }) {
|
|||||||
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
<SpecRow label="Draft" value={formatDim(berth.draftFt, berth.draftM)} />
|
||||||
<SpecRow
|
<SpecRow
|
||||||
label="Nominal Boat Size"
|
label="Nominal Boat Size"
|
||||||
value={berth.nominalBoatSize || berth.nominalBoatSizeM}
|
value={formatNominalBoatSize(berth.nominalBoatSize, berth.nominalBoatSizeM)}
|
||||||
/>
|
/>
|
||||||
<SpecRow
|
<SpecRow
|
||||||
label="Water Depth"
|
label="Water Depth"
|
||||||
@@ -122,8 +154,8 @@ function OverviewTab({ berth }: { berth: BerthData }) {
|
|||||||
<CardTitle className="text-sm font-medium">Infrastructure</CardTitle>
|
<CardTitle className="text-sm font-medium">Infrastructure</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0 divide-y">
|
<CardContent className="pt-0 divide-y">
|
||||||
<SpecRow label="Power Capacity" value={berth.powerCapacity} />
|
<SpecRow label="Power Capacity" value={formatPower(berth.powerCapacity)} />
|
||||||
<SpecRow label="Voltage" value={berth.voltage} />
|
<SpecRow label="Voltage" value={formatVoltage(berth.voltage)} />
|
||||||
<SpecRow label="Cleat Type" value={berth.cleatType} />
|
<SpecRow label="Cleat Type" value={berth.cleatType} />
|
||||||
<SpecRow label="Cleat Capacity" value={berth.cleatCapacity} />
|
<SpecRow label="Cleat Capacity" value={berth.cleatCapacity} />
|
||||||
<SpecRow label="Bollard Type" value={berth.bollardType} />
|
<SpecRow label="Bollard Type" value={berth.bollardType} />
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
* Seed script for Port Nimara CRM.
|
* Seed script for Port Nimara CRM.
|
||||||
*
|
*
|
||||||
* Top-level orchestrator:
|
* Top-level orchestrator:
|
||||||
* 1. Create 3 ports (idempotent):
|
* 1. Create the operational ports (idempotent):
|
||||||
* - Port Nimara
|
* - Port Nimara (primary install — the real marina)
|
||||||
* - Marina Azzurra
|
* - Port Amador (secondary, kept for multi-tenant isolation tests
|
||||||
* - Harbor Royale
|
* and as scaffolding for a future Panama install)
|
||||||
* 2. Create 5 system roles with full permission maps
|
* 2. Create 5 system roles with full permission maps
|
||||||
* 3. Create the super admin user profile placeholder (matt@portnimara.com)
|
* 3. Create the super admin user profile placeholder (matt@portnimara.com)
|
||||||
* 4. For each port, call `seedPortData(portId, portSlug)` from seed-data.ts
|
* 4. For each port, call `seedPortData(portId, portSlug)` from seed-data.ts
|
||||||
* to produce the realistic multi-cardinality fixture
|
* to produce the realistic multi-cardinality fixture
|
||||||
* (berths, clients, companies, yachts, memberships, interests,
|
* (117 berths from the NocoDB snapshot, plus clients, companies, yachts,
|
||||||
* reservations, ownership-transfer history).
|
* memberships, interests, reservations, ownership-transfer history).
|
||||||
* 5. Print a summary.
|
* 5. Print a summary.
|
||||||
*
|
*
|
||||||
* Run with: pnpm db:seed
|
* Run with: pnpm db:seed
|
||||||
@@ -413,19 +413,15 @@ const PORT_DEFINITIONS: Array<{
|
|||||||
defaultCurrency: 'USD',
|
defaultCurrency: 'USD',
|
||||||
timezone: 'America/Anguilla',
|
timezone: 'America/Anguilla',
|
||||||
},
|
},
|
||||||
|
// Second port kept for multi-tenant isolation tests (cross-port scoping,
|
||||||
|
// permission boundaries). Drop or rename if the production install is
|
||||||
|
// single-port.
|
||||||
{
|
{
|
||||||
name: 'Marina Azzurra',
|
name: 'Port Amador',
|
||||||
slug: 'marina-azzurra',
|
slug: 'port-amador',
|
||||||
primaryColor: '#2E86AB',
|
primaryColor: '#D97706',
|
||||||
defaultCurrency: 'EUR',
|
defaultCurrency: 'USD',
|
||||||
timezone: 'Europe/Rome',
|
timezone: 'America/Panama',
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Harbor Royale',
|
|
||||||
slug: 'harbor-royale',
|
|
||||||
primaryColor: '#8B1E3F',
|
|
||||||
defaultCurrency: 'GBP',
|
|
||||||
timezone: 'Europe/London',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user