recent changes

This commit is contained in:
Marcelo
2026-04-29 05:05:00 +00:00
parent 7e0fe5c2e1
commit 62169b163c
25 changed files with 6698 additions and 1013 deletions

View File

@@ -7,6 +7,7 @@ import {
formatTime,
LABEL_MIN_WIDTH_PCT,
normalizeTimelineSegments,
SEGMENT_MIN_WIDTH_PCT,
TIMELINE_COLORS,
} from "@/components/recap/timelineRender";
import { useI18n } from "@/lib/i18n/useI18n";
@@ -17,28 +18,47 @@ type Props = {
segments: RecapTimelineSegment[];
locale: string;
hasData?: boolean;
loading?: boolean;
};
const MIN_SEGMENT_PCT = 1.5;
export default function RecapFullTimeline({ rangeStart, rangeEnd, segments, locale, hasData = true }: Props) {
export default function RecapFullTimeline({
rangeStart,
rangeEnd,
segments,
locale,
hasData = false,
loading = false,
}: Props) {
const { t } = useI18n();
const startMs = new Date(rangeStart).getTime();
const endMs = new Date(rangeEnd).getTime();
const totalMs = Math.max(1, endMs - startMs);
const normalized = normalizeTimelineSegments(segments, startMs, endMs);
const widths = computeWidths(normalized, totalMs, MIN_SEGMENT_PCT);
const normalized = hasData ? normalizeTimelineSegments(segments, startMs, endMs) : [];
const widths = computeWidths(normalized, totalMs, SEGMENT_MIN_WIDTH_PCT);
return (
<div className="rounded-2xl border border-white/10 bg-black/40 p-4">
<div className="mb-3 text-sm font-semibold text-white">{t("recap.timeline.title")}</div>
{!hasData ? (
{loading ? (
<div className="overflow-x-auto">
<div className="min-w-[560px]">
<div className="flex h-14 w-full animate-pulse overflow-hidden rounded-xl bg-white/5">
<div className="h-full w-[12%] bg-zinc-700/70" />
<div className="h-full w-[8%] bg-orange-500/60" />
<div className="h-full w-[14%] bg-zinc-700/70" />
<div className="h-full w-[7%] bg-red-500/60" />
<div className="h-full w-[59%] bg-zinc-700/70" />
</div>
</div>
</div>
) : null}
{!loading && !hasData ? (
<div className="rounded-xl border border-dashed border-white/10 bg-black/20 p-4 text-sm text-zinc-400">
{t("recap.timeline.noData")}
</div>
) : null}
{hasData ? (
{!loading && hasData ? (
<div className="overflow-x-auto">
<div className="min-w-[560px]">
<div className="flex h-14 w-full overflow-hidden rounded-xl">

View File

@@ -3,7 +3,6 @@
import Link from "next/link";
import { useEffect, useState } from "react";
import { useI18n } from "@/lib/i18n/useI18n";
import { RECAP_HEARTBEAT_STALE_MS } from "@/lib/recap/recapUiConstants";
import type { RecapSummaryMachine, RecapTimelineResponse } from "@/lib/recap/types";
import RecapMiniTimeline from "@/components/recap/RecapMiniTimeline";
@@ -35,7 +34,6 @@ function toInt(value: number | null | undefined) {
export default function RecapMachineCard({ machine, rangeStart, rangeEnd }: Props) {
const { t, locale } = useI18n();
const [timeline, setTimeline] = useState<RecapTimelineResponse | null>(null);
const [nowMs, setNowMs] = useState(() => Date.now());
const zeroActivity = machine.goodParts === 0 && machine.scrap === 0 && machine.stopsCount === 0;
const primaryMetric = machine.oee == null ? "—" : `${machine.oee.toFixed(1)}%`;
@@ -43,8 +41,6 @@ export default function RecapMachineCard({ machine, rangeStart, rangeEnd }: Prop
const timelineStart = timeline?.range.start ?? rangeStart;
const timelineEnd = timeline?.range.end ?? rangeEnd;
const hasTimelineData = timeline?.hasData ?? timelineSegments.length > 0;
const staleHeartbeat =
machine.lastSeenMs == null ? true : nowMs - machine.lastSeenMs > RECAP_HEARTBEAT_STALE_MS;
const lastSeenLabel =
machine.lastActivityMin == null
@@ -57,11 +53,6 @@ export default function RecapMachineCard({ machine, rangeStart, rangeEnd }: Prop
const moldMinutes = machine.moldChange?.active ? machine.moldChange.elapsedMin : null;
useEffect(() => {
const timer = window.setInterval(() => setNowMs(Date.now()), 60000);
return () => window.clearInterval(timer);
}, []);
useEffect(() => {
let alive = true;
@@ -144,11 +135,6 @@ export default function RecapMachineCard({ machine, rangeStart, rangeEnd }: Prop
{t("recap.banner.offline", { min: toInt(machine.offlineForMin) })}
</div>
) : null}
{staleHeartbeat ? (
<div className="mt-2 rounded-lg border border-amber-400/40 bg-amber-400/10 px-2 py-1.5 text-xs text-amber-200">
{t("recap.card.desynced")}
</div>
) : null}
<div className="mt-3 text-xs text-zinc-400">{footerText}</div>
</Link>

View File

@@ -10,6 +10,7 @@ export const TIMELINE_COLORS: Record<RecapTimelineSegment["type"], string> = {
};
export const LABEL_MIN_WIDTH_PCT = 5;
export const SEGMENT_MIN_WIDTH_PCT = 1.5;
export function formatTime(valueMs: number, locale: string) {
return new Date(valueMs).toLocaleTimeString(locale, {