/plant and /plant/map import Fuel Logs CSVs by exact QR and show each vehicle's current and previous distinct site with the dates they were observed. - plant_site_observations is durable, QR-keyed history. It is never wiped and a previous site is never manufactured. - An older or repeated snapshot must not move a vehicle backwards; untimed conflicting sites on one day are held for review. - A manual site change shares the same history and the same lock. - Current/previous metadata and the sticker QR are captured BEFORE the asset rows are deleted in the seed, so a re-seed cannot wipe them. Explicit empty values are preserved too: clearing a site is a deliberate manual observation, not permission to restore an old sheet value. - Imports change location metadata only. They never touch income, cost or fuel measurements. - A new named site is created only for a matched Plant vehicle. Missing coordinates mean "awaiting a pin", never a headquarters fallback. Owner-confirmed 2026-11-04: TLB06's QR is 1089, not the old meter reading 4704. lib/plant-site-policy.json holds the confirmed aliases (Equestria Bastion Job -> Equestria 215, Cosmo City / Lion Park -> Cosmo City); no other identity is guessed.
36 lines
2.2 KiB
TypeScript
36 lines
2.2 KiB
TypeScript
import type { Asset, Site } from "./data";
|
|
import headOffice from "./plant-location-policy.json";
|
|
|
|
export const HEAD_OFFICE = headOffice;
|
|
|
|
export type MapAsset = Pick<Asset, "id" | "code" | "name" | "category" | "type" | "is_active" | "site" | "latitude" | "longitude" | "qr_code" | "site_label" | "previous_site" | "previous_site_label" | "site_observed_on" | "previous_site_observed_on" | "site_source">;
|
|
export type MapSite = Pick<Site, "id" | "name" | "latitude" | "longitude">;
|
|
type Coordinates = { latitude: number | null; longitude: number | null };
|
|
export type MapLocation = Coordinates & {
|
|
source: "site" | "missing-site" | "head-office" | "manual" | "unlocated";
|
|
siteName: string | null;
|
|
};
|
|
|
|
export function hasCoords(point: Coordinates): boolean {
|
|
const { latitude, longitude } = point;
|
|
return latitude != null && longitude != null && Number.isFinite(latitude) && Number.isFinite(longitude)
|
|
&& Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180 && (latitude !== 0 || longitude !== 0);
|
|
}
|
|
|
|
// Current assignment controls placement, independently of historic invoice hires.
|
|
// Resolve at read time so existing assignments and site-coordinate edits work
|
|
// immediately, without overwriting a machine's separately saved manual pin.
|
|
export function resolveAssetLocation(asset: Coordinates & { site?: string; site_label?: string }, sites: MapSite[]): MapLocation {
|
|
if (asset.site) {
|
|
const site = sites.find(s => s.id === asset.site);
|
|
return site && hasCoords(site)
|
|
? { latitude: site.latitude, longitude: site.longitude, source: "site", siteName: site.name }
|
|
: { latitude: null, longitude: null, source: "missing-site", siteName: site?.name ?? "Unknown site" };
|
|
}
|
|
// A recorded site without a resolved/pinned yard must not leave a phantom HQ pin.
|
|
if (asset.site_label) return { latitude: null, longitude: null, source: "missing-site", siteName: asset.site_label };
|
|
if (!hasCoords(asset)) return { latitude: null, longitude: null, source: "unlocated", siteName: null };
|
|
const atOffice = Math.abs(asset.latitude! - HEAD_OFFICE.latitude) < 0.000001
|
|
&& Math.abs(asset.longitude! - HEAD_OFFICE.longitude) < 0.000001;
|
|
return { latitude: asset.latitude, longitude: asset.longitude, source: atOffice ? "head-office" : "manual", siteName: null };
|
|
}
|