This commit is contained in:
Your Name committed 2026-09-13 18:52:42 +07:00
1 parent 8e35cb48d1
commit d5ea45a536
1 file changed
+80 -30
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, Fragment } from 'react';
import { import {
Clock, Clock,
RefreshCw, RefreshCw,
@@ -429,42 +429,92 @@ export default function RepeatCustomersTimelinePage() {
</span> </span>
</div> </div>
{/* Horizontal Bar with Nodes */} {/* Horizontal Bar with Nodes & Inter-order Days */}
<div className="relative w-full py-4 px-3 bg-slate-950/50 rounded-xl border border-white/5 overflow-x-auto"> <div className="relative w-full pt-6 pb-4 px-4 bg-slate-950/50 rounded-xl border border-white/5 overflow-x-auto">
<div className="relative flex items-center justify-between min-w-[500px]"> <div
{/* Connecting line */} className="relative flex items-start justify-between"
<div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-0.5 bg-gradient-to-r from-indigo-500 via-purple-500 to-amber-500" /> style={{ minWidth: `${Math.max(560, (cust.orders.length + 1) * 70)}px` }}
>
{/* Order Nodes */}
{cust.orders.map((o: any, oIdx: number) => { {cust.orders.map((o: any, oIdx: number) => {
const isLast = oIdx === cust.orders.length - 1; const isLast = oIdx === cust.orders.length - 1;
const nextOrder = cust.orders[oIdx + 1];
// Calculate interval days between this order and next order
let diffDays: number | null = null;
if (nextOrder?.timestamp && o?.timestamp) {
diffDays = Math.max(0, Math.round((nextOrder.timestamp - o.timestamp) / 86400000));
}
return ( return (
<div <Fragment key={o.orderId || oIdx}>
key={o.orderId || oIdx} {/* Order Node */}
className="relative z-10 flex flex-col items-center group cursor-pointer"
title={`ออเดอร์ #${o.orderId} — ${o.date} (${fmtBaht(o.amount)})`}
>
{/* Node Circle */}
<div <div
className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] shadow-lg transition-transform group-hover:scale-125 ${ className="relative z-10 flex flex-col items-center group cursor-pointer"
isLast onClick={() => setSelectedCustomer(cust)}
? 'bg-indigo-500 text-white ring-4 ring-indigo-500/20' title={`ออเดอร์ #${o.orderId} — ${o.date} (${fmtBaht(o.amount)})`}
: 'bg-slate-800 text-slate-300 border border-indigo-400'
}`}
> >
{oIdx + 1} {/* Node Circle */}
<div
className={`w-5 h-5 rounded-full flex items-center justify-center font-bold text-[10px] shadow-lg transition-transform group-hover:scale-125 ${
isLast
? 'bg-indigo-500 text-white ring-4 ring-indigo-500/20'
: 'bg-slate-800 text-slate-300 border border-indigo-400'
}`}
>
{oIdx + 1}
</div>
{/* Node Label Below */}
<div className="mt-1.5 text-center whitespace-nowrap">
<span className="text-[10px] text-slate-400 block font-mono">
{o.date.slice(5, 10)}
</span>
<span className="text-[10px] font-bold text-slate-200 font-mono block">
{fmtBaht(o.amount)}
</span>
</div>
</div> </div>
{/* Node Label Below */} {/* Connector Segment & Gap Days Floating Above Line */}
<div className="mt-1.5 text-center whitespace-nowrap"> {nextOrder && (
<span className="text-[10px] text-slate-400 block font-mono"> <div
{o.date.slice(5, 10)} className="flex-1 flex items-center justify-center relative min-w-[28px] mx-1 h-5 group/gap cursor-default"
</span> title={`รอบ #${oIdx + 1} ➔ รอบ #${oIdx + 2}: เว้นช่วง ${diffDays} วัน (${o.date.slice(0, 10)} ถึง ${nextOrder.date.slice(0, 10)})`}
<span className="text-[10px] font-bold text-slate-200 font-mono block"> >
{fmtBaht(o.amount)} {/* Connector Line */}
</span> <div className="w-full h-0.5 bg-gradient-to-r from-indigo-500/80 via-purple-500/80 to-indigo-500/80 group-hover/gap:h-1 group-hover/gap:from-indigo-400 group-hover/gap:to-purple-400 transition-all" />
</div>
</div> {/* Days Number Floating Above the Line */}
{diffDays !== null && (
<span
className={`absolute -top-3.5 left-1/2 -translate-x-1/2 text-[10px] font-mono font-semibold transition-all select-none ${
diffDays > 90
? 'text-amber-400 font-bold'
: 'text-slate-400 group-hover/gap:text-white'
}`}
>
{diffDays}
</span>
)}
</div>
)}
{/* Gap to Predicted Order */}
{isLast && (
<div
className="flex-1 flex items-center justify-center relative min-w-[28px] mx-1 h-5 group/gap cursor-default"
title={`รอบถัดไป (คาดการณ์): ~${cust.avgIntervalDays} วัน (ซื้อล่าสุด ${cust.lastOrderDate.slice(0, 10)})`}
>
{/* Dashed Connector Line */}
<div className="w-full border-t-2 border-dashed border-amber-400/50 group-hover/gap:border-amber-400 transition-all" />
{/* Predicted Days Floating Above Line */}
<span className="absolute -top-3.5 left-1/2 -translate-x-1/2 text-[10px] font-mono font-semibold text-amber-400/90 select-none">
{Math.round(cust.avgIntervalDays)}
</span>
</div>
)}
</Fragment>
); );
})} })}