168 lines
7.3 KiB
JavaScript
168 lines
7.3 KiB
JavaScript
/**
|
|
* Build a standalone page that renders the REAL dispatch map with mock stops.
|
|
*
|
|
* ── Why this exists ─────────────────────────────────────────────────────────
|
|
*
|
|
* A leaflet map cannot be checked by anything else in this repo. The pure tests
|
|
* never mount it, `renderToString` never runs the effect that builds it, and
|
|
* the jsdom tests can only COUNT what it produced — none of them can tell you
|
|
* whether the thing looks right. The map shipped twice on that basis and came
|
|
* back wrong twice: once invisible behind a crash, once with its routes buried
|
|
* under 966 pins.
|
|
*
|
|
* So this bundles the actual `GroupMap` — not a copy of it, not a sketch —
|
|
* against invented stops, and writes one HTML file to open. What you see is
|
|
* what the console draws.
|
|
*
|
|
* ── Why the mock data lives here and not in `src` ───────────────────────────
|
|
*
|
|
* `npm run verify:live` asserts there is no `src/demo`, and it is right to:
|
|
* a fixture layer inside the app is how a screen ends up quietly rendering
|
|
* invented numbers in production. This is a build tool. It imports from `src`
|
|
* and nothing in `src` imports it, so the app has no path to this data.
|
|
*
|
|
* node scripts/mapPreview.mjs → writes scripts/.preview/map.html
|
|
*/
|
|
import { build } from 'esbuild';
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const out = join(here, '.preview');
|
|
|
|
/* ── The mock day ─────────────────────────────────────────────────────────
|
|
Shaped like the real thing rather than like a neat demo: one shop, two
|
|
riders working outward in a loop, three orders stacked on one address, and
|
|
one rider whose last reported position is nowhere near their last drop. Each
|
|
of those is something the live data does and each has broken this map once. */
|
|
const SHOP = { lat: 11.0168, lng: 76.9558 };
|
|
|
|
const ROUND_A = [
|
|
[11.0245, 76.9601],
|
|
[11.0298, 76.9662],
|
|
[11.0331, 76.9754],
|
|
[11.0288, 76.9823],
|
|
[11.0201, 76.9788],
|
|
// Three orders at one address — the repeat customer that stacked 379 pins.
|
|
[11.0154, 76.9702],
|
|
[11.0154, 76.9702],
|
|
[11.0154, 76.9702],
|
|
];
|
|
|
|
const ROUND_B = [
|
|
[11.0102, 76.9481],
|
|
[11.0044, 76.9412],
|
|
[10.9981, 76.9377],
|
|
[10.9932, 76.9455],
|
|
[11.0011, 76.9521],
|
|
];
|
|
|
|
function stopsFor(rider, userid, points, from) {
|
|
return points.map((point, index) => ({
|
|
kind: 'delivery',
|
|
row: {
|
|
deliveryid: userid * 100 + index,
|
|
orderid: `916-${userid}${String(index + 1).padStart(2, '0')}`,
|
|
userid,
|
|
ridername: rider,
|
|
// A couple left open, so the status colours are visible on the pins.
|
|
orderstatus: index === points.length - 1 ? 'active' : 'delivered',
|
|
assigntime: '2026-08-25 09:00:00',
|
|
deliverytime: `2026-08-25 ${String(from + Math.floor(index / 2)).padStart(2, '0')}:${String((index * 17) % 60).padStart(2, '0')}:00`,
|
|
pickuplat: String(SHOP.lat),
|
|
pickuplon: String(SHOP.lng),
|
|
droplat: String(point[0]),
|
|
droplon: String(point[1]),
|
|
// Only the last stop carries a rider fix, which is the shape the live
|
|
// rows have — a position arrives when a job moves, not per stop.
|
|
...(index === points.length - 1
|
|
? { riderslat: String(point[0] + 0.004), riderslon: String(point[1] - 0.003) }
|
|
: {}),
|
|
deliveryamt: 30 + index * 5,
|
|
deliverycustomer: `${rider}'s customer ${index + 1}`,
|
|
deliveryaddress: `Stop ${index + 1}`,
|
|
},
|
|
}));
|
|
}
|
|
|
|
const STOPS = [
|
|
...stopsFor('Varun', 897, ROUND_A, 10),
|
|
...stopsFor('Murali', 1111, ROUND_B, 11),
|
|
];
|
|
|
|
const entry = join(out, 'entry.jsx');
|
|
mkdirSync(out, { recursive: true });
|
|
|
|
writeFileSync(
|
|
entry,
|
|
`import { createRoot } from 'react-dom/client';
|
|
import { GroupMap } from '../../src/features/store-admin/GroupMap';
|
|
const STOPS = ${JSON.stringify(STOPS)};
|
|
createRoot(document.getElementById('root')).render(
|
|
<div style={{ padding: 16, maxWidth: 1100, margin: '0 auto' }}>
|
|
<h1 style={{ font: '600 18px system-ui', margin: '0 0 4px' }}>Dispatch map — mock day</h1>
|
|
<p style={{ font: '13px system-ui', color: '#5b6472', margin: '0 0 16px' }}>
|
|
Two riders, one shop, ${STOPS.length} stops. Three of Varun's orders are at one address.
|
|
This is the real GroupMap component with invented stops.
|
|
</p>
|
|
<GroupMap stops={STOPS} groupName="the mock day" />
|
|
</div>,
|
|
);
|
|
`,
|
|
);
|
|
|
|
await build({
|
|
entryPoints: [entry],
|
|
bundle: true,
|
|
outfile: join(out, 'map.js'),
|
|
jsx: 'automatic',
|
|
format: 'iife',
|
|
platform: 'browser',
|
|
// Leaflet's stylesheet references sprite PNGs for controls we do not use;
|
|
// inlined as data URIs so the page is a single self-contained file.
|
|
loader: { '.css': 'css', '.png': 'dataurl', '.svg': 'dataurl' },
|
|
// The real thing, minus the parts a static page has no business having.
|
|
// `import.meta.env` is Vite's and does not exist here; the modules that read
|
|
// it already optional-chain, so an empty object is enough.
|
|
define: { 'import.meta.env': 'undefined', 'process.env.NODE_ENV': '"production"' },
|
|
logLevel: 'warning',
|
|
});
|
|
|
|
writeFileSync(
|
|
join(out, 'map.html'),
|
|
`<!doctype html>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Dispatch map preview</title>
|
|
<link rel="stylesheet" href="./map.css">
|
|
<style>
|
|
/* The console's own tokens, so the map is styled as it is in the app. */
|
|
:root {
|
|
--color-brand: #662582; --color-brand-strong: #531b6e; --color-brand-tint: #f4eef8;
|
|
--color-ink-1: #1e2530; --color-ink-2: #414a58; --color-ink-3: #5b6472; --color-ink-4: #97a1b0;
|
|
--color-surface: #fff; --color-surface-subtle: #fafbfc; --color-surface-sunken: #f2f4f7;
|
|
--color-border: #e3e7ec;
|
|
}
|
|
body { margin: 0; background: #fff; font-family: system-ui, sans-serif; }
|
|
.pva-note { display:flex; gap:7px; align-items:flex-start; padding:8px 10px; border-radius:7px;
|
|
background: var(--color-surface-subtle); font-size:11.5px; line-height:1.5; color: var(--color-ink-3); }
|
|
.map-key { display:inline-flex; gap:5px; align-items:center; font-size:11px; color:var(--color-ink-4); }
|
|
.map-key i { width:9px; height:9px; margin-left:6px; border-radius:50%; background:var(--color-ink-4); }
|
|
.map-key i:first-child { margin-left:0 }
|
|
.map-key i[data-key='shop'] { border-radius:2px; background:var(--color-brand) }
|
|
.map-key i[data-key='drop'] { background:#10b981 }
|
|
.map-key i[data-key='rider'] { background:transparent; border:2px solid var(--color-ink-3) }
|
|
.rider-chip { display:inline-flex; gap:6px; align-items:center; padding:4px 10px; border:1px solid var(--color-border);
|
|
border-radius:999px; background:#fff; font:inherit; font-size:12px; color:var(--color-ink-2); cursor:pointer }
|
|
.rider-chip[data-active='true'] { border-color:var(--color-brand); background:var(--color-brand-tint); color:var(--color-ink-1) }
|
|
.rider-chip i { width:8px; height:8px; border-radius:50% }
|
|
</style>
|
|
<div id="root"></div>
|
|
<script src="./map.js"></script>
|
|
`,
|
|
);
|
|
|
|
console.log('preview written to', join(out, 'map.html'));
|
|
console.log('open it in a browser to see the real map with mock stops');
|