recent changes
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user