feat(simple): add setup timeline and id copy
This commit is contained in:
parent
beb75c1d89
commit
d7516c440a
@ -350,6 +350,41 @@ function statusToneClasses(tone: SimpleRuntimeSnapshot['tone']): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SIMPLE_TIMELINE_STEPS: Array<SimpleRuntimeSnapshot['stage']> = [
|
||||||
|
'armed',
|
||||||
|
'entry_submitted',
|
||||||
|
'filled',
|
||||||
|
'exit_submitted',
|
||||||
|
'closed',
|
||||||
|
];
|
||||||
|
|
||||||
|
function isTimelineStepComplete(
|
||||||
|
current: SimpleRuntimeSnapshot['stage'] | undefined,
|
||||||
|
step: SimpleRuntimeSnapshot['stage'],
|
||||||
|
): boolean {
|
||||||
|
if (!current) return false;
|
||||||
|
const currentIndex = SIMPLE_TIMELINE_STEPS.indexOf(current);
|
||||||
|
const stepIndex = SIMPLE_TIMELINE_STEPS.indexOf(step);
|
||||||
|
return currentIndex >= 0 && stepIndex >= 0 && stepIndex <= currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimelineStepLabel(step: SimpleRuntimeSnapshot['stage']) {
|
||||||
|
switch (step) {
|
||||||
|
case 'armed':
|
||||||
|
return 'Armed';
|
||||||
|
case 'entry_submitted':
|
||||||
|
return 'Entry sent';
|
||||||
|
case 'filled':
|
||||||
|
return 'Filled';
|
||||||
|
case 'exit_submitted':
|
||||||
|
return 'Exit sent';
|
||||||
|
case 'closed':
|
||||||
|
return 'Closed';
|
||||||
|
default:
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatSetupUpdatedAt(entry: ManualEntryPayload): string | null {
|
function formatSetupUpdatedAt(entry: ManualEntryPayload): string | null {
|
||||||
const raw = String(entry.sell_time || entry.buy_time || '').trim();
|
const raw = String(entry.sell_time || entry.buy_time || '').trim();
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
@ -449,6 +484,7 @@ export function SimpleView() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [loadingPrice, setLoadingPrice] = useState(false);
|
const [loadingPrice, setLoadingPrice] = useState(false);
|
||||||
const [marketPriceSource, setMarketPriceSource] = useState<MarketPriceSource>(null);
|
const [marketPriceSource, setMarketPriceSource] = useState<MarketPriceSource>(null);
|
||||||
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||||
const [message, setMessage] = useState<string | null>(null);
|
const [message, setMessage] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const marketPriceRequestSymbolRef = useRef<string>('');
|
const marketPriceRequestSymbolRef = useRef<string>('');
|
||||||
@ -572,6 +608,20 @@ export function SimpleView() {
|
|||||||
setDraft((prev) => ({ ...prev, [key]: value }));
|
setDraft((prev) => ({ ...prev, [key]: value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyIdentifier(kind: 'trade' | 'order', value: string | null | undefined) {
|
||||||
|
if (!value) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
const key = `${kind}:${value}`;
|
||||||
|
setCopiedKey(key);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setCopiedKey((prev) => (prev === key ? null : prev));
|
||||||
|
}, 1200);
|
||||||
|
} catch {
|
||||||
|
setError(`Failed to copy ${kind} ID`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshSetupList() {
|
async function refreshSetupList() {
|
||||||
const [profileRows, entryRows] = await Promise.all([
|
const [profileRows, entryRows] = await Promise.all([
|
||||||
fetchTradeProfiles(),
|
fetchTradeProfiles(),
|
||||||
@ -1077,14 +1127,26 @@ export function SimpleView() {
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{(runtimeSnapshot?.tradeId || entry.linked_trade_id) ? (
|
{(runtimeSnapshot?.tradeId || entry.linked_trade_id) ? (
|
||||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
<button
|
||||||
Trade {String(runtimeSnapshot?.tradeId || entry.linked_trade_id).slice(0, 18)}…
|
type="button"
|
||||||
</span>
|
onClick={() => void copyIdentifier('trade', String(runtimeSnapshot?.tradeId || entry.linked_trade_id))}
|
||||||
|
className="rounded-full border border-[var(--border)] px-3 py-1 transition hover:border-[var(--primary)] hover:text-[var(--foreground)]"
|
||||||
|
>
|
||||||
|
{copiedKey === `trade:${String(runtimeSnapshot?.tradeId || entry.linked_trade_id)}`
|
||||||
|
? 'Trade copied'
|
||||||
|
: `Trade ${String(runtimeSnapshot?.tradeId || entry.linked_trade_id).slice(0, 18)}…`}
|
||||||
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{runtimeSnapshot?.orderId ? (
|
{runtimeSnapshot?.orderId ? (
|
||||||
<span className="rounded-full border border-[var(--border)] px-3 py-1">
|
<button
|
||||||
Order {runtimeSnapshot.orderId.slice(0, 12)}…
|
type="button"
|
||||||
</span>
|
onClick={() => void copyIdentifier('order', runtimeSnapshot.orderId)}
|
||||||
|
className="rounded-full border border-[var(--border)] px-3 py-1 transition hover:border-[var(--primary)] hover:text-[var(--foreground)]"
|
||||||
|
>
|
||||||
|
{copiedKey === `order:${runtimeSnapshot.orderId}`
|
||||||
|
? 'Order copied'
|
||||||
|
: `Order ${runtimeSnapshot.orderId.slice(0, 12)}…`}
|
||||||
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{updatedAt ? (
|
{updatedAt ? (
|
||||||
<span className="rounded-full border border-[var(--border)] px-3 py-1 normal-case tracking-normal">
|
<span className="rounded-full border border-[var(--border)] px-3 py-1 normal-case tracking-normal">
|
||||||
@ -1092,6 +1154,27 @@ export function SimpleView() {
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 grid gap-2 md:grid-cols-5">
|
||||||
|
{SIMPLE_TIMELINE_STEPS.map((step) => {
|
||||||
|
const complete = isTimelineStepComplete(runtimeSnapshot?.stage, step);
|
||||||
|
const isCurrent = runtimeSnapshot?.stage === step;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={step}
|
||||||
|
className={`rounded-2xl border px-3 py-2 text-[11px] font-semibold ${
|
||||||
|
isCurrent
|
||||||
|
? 'border-[var(--primary)] bg-[var(--accent-soft)] text-[var(--primary)]'
|
||||||
|
: complete
|
||||||
|
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
||||||
|
: 'border-[var(--border)] bg-[var(--background)] text-[var(--muted-foreground)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatTimelineStepLabel(step)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user