ソースを参照

Lazy load sharp (#248)

* hover

* sharp
Bernt Christian Egeland 1 ヶ月 前
親
コミット
70190c9502

+ 13 - 1
Dockerfile

@@ -71,15 +71,27 @@ RUN rm -rf \
       /app/node_modules/prisma \
       /app/node_modules/@prisma \
       /app/node_modules/.prisma \
-    && npm install prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx
+    && npm install prisma@7.6.0 @prisma/client@7.6.0 @prisma/adapter-pg@7.6.0 pg dotenv tsx sharp
 
 # The npm install above re-resolves the dependency tree and replaces the
 # standalone build's PATCHED next package with a fresh unpatched copy from the
 # registry, which silently breaks all WebSocket routes (live updates, work
 # board sync). Re-apply the next-ws patch so the runtime server can accept
 # WebSocket upgrades. next-ws itself is already in the standalone bundle.
+#
+# sharp is named in that install for the same reason. It is the only native
+# module in the tree, its binary lives in a platform-specific optional
+# dependency (@img/sharp-linuxmusl-x64 on this image), and re-resolving the
+# tree can leave the traced copy without one. Installing it by name makes npm
+# resolve the binary against the platform the image will actually run on.
 RUN npx next-ws patch --yes
 
+# Fail the build here rather than at the first certificate download: a native
+# module that cannot be loaded throws while the route module is being
+# evaluated, which reaches the browser as an empty HTTP 500 with nothing in it
+# to explain itself.
+RUN node -e "require('sharp'); console.log('sharp loads')"
+
 # Copy init script
 COPY --chown=nextjs:nodejs init-db.sh ./init-db.sh
 RUN chmod +x ./init-db.sh

+ 1 - 0
messages/de/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Fahrzeug",
     "template": "Vorlage",
     "progress": "Fortschritt",
+    "progressTooltip": "{graded} von {total} Prüfpunkten bewertet",
     "status": "Status",
     "date": "Datum",
     "statusInProgress": "Läuft",

+ 1 - 0
messages/en/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Vehicle",
     "template": "Template",
     "progress": "Progress",
+    "progressTooltip": "{graded} of {total} checks graded",
     "status": "Status",
     "date": "Date",
     "statusInProgress": "In Progress",

+ 1 - 0
messages/es/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Vehículo",
     "template": "Plantilla",
     "progress": "Avance",
+    "progressTooltip": "{graded} de {total} puntos calificados",
     "status": "Estado",
     "date": "Fecha",
     "statusInProgress": "En curso",

+ 1 - 0
messages/fr/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Véhicule",
     "template": "Modèle",
     "progress": "Avancement",
+    "progressTooltip": "{graded} points sur {total} évalués",
     "status": "Statut",
     "date": "Date",
     "statusInProgress": "En cours",

+ 1 - 0
messages/it/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Veicolo",
     "template": "Modello",
     "progress": "Avanzamento",
+    "progressTooltip": "{graded} punti su {total} valutati",
     "status": "Stato",
     "date": "Data",
     "statusInProgress": "In corso",

+ 1 - 0
messages/lt/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Transporto priemonė",
     "template": "Šablonas",
     "progress": "Eiga",
+    "progressTooltip": "Įvertinta {graded} iš {total} punktų",
     "status": "Būsena",
     "date": "Data",
     "statusInProgress": "Vyksta",

+ 1 - 0
messages/nb/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Kjøretøy",
     "template": "Mal",
     "progress": "Fremdrift",
+    "progressTooltip": "{graded} av {total} punkter vurdert",
     "status": "Status",
     "date": "Dato",
     "statusInProgress": "Pågår",

+ 1 - 0
messages/nl/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Voertuig",
     "template": "Sjabloon",
     "progress": "Voortgang",
+    "progressTooltip": "{graded} van {total} punten beoordeeld",
     "status": "Status",
     "date": "Datum",
     "statusInProgress": "Loopt",

+ 1 - 0
messages/pl/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Pojazd",
     "template": "Szablon",
     "progress": "Postęp",
+    "progressTooltip": "Oceniono {graded} z {total} pozycji",
     "status": "Status",
     "date": "Data",
     "statusInProgress": "W toku",

+ 1 - 0
messages/pt-BR/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Veículo",
     "template": "Modelo",
     "progress": "Progresso",
+    "progressTooltip": "{graded} de {total} itens classificados",
     "status": "Status",
     "date": "Data",
     "statusInProgress": "Em andamento",

+ 1 - 0
messages/ru/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Транспортное средство",
     "template": "Шаблон",
     "progress": "Ход выполнения",
+    "progressTooltip": "Оценено {graded} из {total} пунктов",
     "status": "Статус",
     "date": "Дата",
     "statusInProgress": "В работе",

+ 1 - 0
messages/tr/inspections.json

@@ -366,6 +366,7 @@
     "vehicle": "Araç",
     "template": "Şablon",
     "progress": "İlerleme",
+    "progressTooltip": "{total} maddeden {graded} tanesi değerlendirildi",
     "status": "Durum",
     "date": "Tarih",
     "statusInProgress": "Sürüyor",

+ 80 - 19
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -23,7 +23,14 @@ import { TableCellLink } from "@/components/table-cell-link";
 import { ArrowDown, ArrowUp, ArrowUpDown, Car, ExternalLink, Loader2, Plus, Search } from "lucide-react";
 import { useTranslations } from "next-intl";
 import { NewInspectionDialog } from "@/features/inspections/Components/NewInspectionDialog";
-import { CONDITION_TOKENS, countConditions } from "@/features/inspections/Lib/conditions";
+import {
+  CONDITION_TOKENS,
+  countConditions,
+  type Condition,
+  type SeverityScale,
+} from "@/features/inspections/Lib/conditions";
+import { useConditionLabels } from "@/features/inspections/Lib/useConditionLabels";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
 
 interface InspectionRecord {
   id: string;
@@ -31,8 +38,9 @@ interface InspectionRecord {
   mileage: number | null;
   createdAt: Date;
   completedAt: Date | null;
+  severityScale: string | null;
   vehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null };
-  template: { id: string; name: string };
+  template: { id: string; name: string; severityScale: string | null };
   items: { id: string; condition: string }[];
 }
 
@@ -62,24 +70,70 @@ const statusColors: Record<string, string> = {
   completed: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20",
 };
 
-function InspectionProgress({ items }: { items: { condition: string }[] }) {
-  if (items.length === 0) return null;
+/**
+ * The rail is a breakdown of the grades given, not a completion meter: a full
+ * rail with an amber run means every check was graded and some came back as
+ * minor defects. Read cold that is easy to mistake for "half done", so the
+ * hover spells out what each colour stands for and how many checks it covers.
+ */
+function InspectionProgress({
+  items,
+  scale,
+}: {
+  items: { condition: string }[];
+  scale: SeverityScale;
+}) {
+  const t = useTranslations("inspections.list");
+  const { label: gradeLabel } = useConditionLabels(scale);
   const counts = countConditions(items);
+  if (items.length === 0) return null;
+
+  const summary = t("progressTooltip", { graded: counts.inspected, total: counts.total });
+  const legend = ([
+    ["pass", counts.pass],
+    ["attention", counts.attention],
+    ["fail", counts.fail],
+    ["dangerous", counts.dangerous],
+    ["not_inspected", counts.notInspected],
+  ] as const).filter(([, value]) => value > 0);
+
   return (
-    <div className="flex items-center gap-1.5">
-      <div className="flex h-2 w-20 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
-        {(["pass", "attention", "fail", "dangerous"] as const).map((c) => {
-          const pct = (counts[c] / items.length) * 100;
-          if (pct === 0) return null;
-          return (
-            <div key={c} className={CONDITION_TOKENS[c].bar} style={{ width: `${pct}%` }} />
-          );
-        })}
-      </div>
-      <span className="text-xs text-muted-foreground">
-        {counts.inspected}/{counts.total}
-      </span>
-    </div>
+    <Tooltip>
+      <TooltipTrigger
+        className="group flex cursor-help items-center gap-1.5 rounded-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
+        aria-label={summary}
+      >
+        <div className="ring-offset-background group-hover:ring-foreground/25 flex h-2 w-20 overflow-hidden rounded-full bg-gray-200 ring-offset-1 transition-shadow group-hover:ring-2 dark:bg-gray-700">
+          {(["pass", "attention", "fail", "dangerous"] as const).map((c) => {
+            const pct = (counts[c] / items.length) * 100;
+            if (pct === 0) return null;
+            return (
+              <div key={c} className={CONDITION_TOKENS[c].bar} style={{ width: `${pct}%` }} />
+            );
+          })}
+        </div>
+        <span className="text-muted-foreground group-hover:text-foreground text-xs">
+          {counts.inspected}/{counts.total}
+        </span>
+      </TooltipTrigger>
+      <TooltipContent side="top" className="px-3 py-2">
+        <p className="font-medium">{summary}</p>
+        <ul className="mt-1.5 space-y-1">
+          {legend.map(([condition, value]) => (
+            <li key={condition} className="flex items-center gap-2">
+              <span
+                className={`h-2 w-2 shrink-0 rounded-full ${
+                  condition === "not_inspected" ? "bg-background/40" : CONDITION_TOKENS[condition].bar
+                }`}
+                aria-hidden="true"
+              />
+              <span className="flex-1">{gradeLabel(condition as Condition)}</span>
+              <span className="tabular-nums">{value}</span>
+            </li>
+          ))}
+        </ul>
+      </TooltipContent>
+    </Tooltip>
   );
 }
 
@@ -250,7 +304,14 @@ export function InspectionsClient({
                     {insp.template.name}
                   </TableCell>
                   <TableCell>
-                    <InspectionProgress items={insp.items} />
+                    <InspectionProgress
+                      items={insp.items}
+                      scale={
+                        (insp.severityScale ?? insp.template.severityScale) === "basic"
+                          ? "basic"
+                          : "eu"
+                      }
+                    />
                   </TableCell>
                   <TableCell>
                     <Badge variant="outline" className={`text-xs ${statusColors[insp.status] || ""}`}>

+ 1 - 1
src/features/inspections/Actions/inspectionActions.ts

@@ -47,7 +47,7 @@ export async function getInspectionsPaginated(params: {
         where,
         include: {
           vehicle: { select: { id: true, make: true, model: true, year: true, licensePlate: true } },
-          template: { select: { id: true, name: true } },
+          template: { select: { id: true, name: true, severityScale: true } },
           items: { select: { id: true, condition: true } },
         },
         orderBy: (() => {