update logic fix

This commit is contained in:
NotARoomba 2026-02-23 15:35:59 -05:00
parent 3e897fbd26
commit df2039908f
7 changed files with 272 additions and 54 deletions

View file

@ -1,5 +1,3 @@
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86on
# CLAUDE.md # CLAUDE.md
## Architecture ## Architecture

66
backend/dist/index.js vendored
View file

@ -34517,6 +34517,26 @@ admin.get("/users/:id/bonuses", async ({ params, headers }) => {
return { error: "Failed to fetch user bonuses" }; return { error: "Failed to fetch user bonuses" };
} }
}); });
admin.delete("/bonuses/:id", async ({ params, headers, status: status2 }) => {
try {
const user2 = await requireAdmin(headers);
if (!user2)
return status2(401, { error: "Unauthorized" });
const bonusId = parseInt(params.id);
if (!Number.isInteger(bonusId) || bonusId <= 0) {
return status2(400, { error: "Invalid bonus id" });
}
const bonus = await db.select({ id: userBonusesTable.id }).from(userBonusesTable).where(eq(userBonusesTable.id, bonusId)).limit(1);
if (!bonus[0]) {
return status2(404, { error: "Bonus not found" });
}
await db.delete(userBonusesTable).where(eq(userBonusesTable.id, bonusId));
return { success: true };
} catch (err) {
console.error(err);
return status2(500, { error: "Failed to delete bonus" });
}
});
admin.get("/reviews", async ({ headers, query }) => { admin.get("/reviews", async ({ headers, query }) => {
try { try {
const user2 = await requireReviewer(headers); const user2 = await requireReviewer(headers);
@ -34727,7 +34747,6 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
} }
let scrapsAwarded = 0; let scrapsAwarded = 0;
if (action === "approved") { if (action === "approved") {
const hours = hoursOverride ?? project[0].hours ?? 0;
const tier = tierOverride ?? project[0].tier ?? 1; const tier = tierOverride ?? project[0].tier ?? 1;
const { effectiveHours } = await computeEffectiveHoursForProject({ const { effectiveHours } = await computeEffectiveHoursForProject({
...project[0], ...project[0],
@ -35243,10 +35262,9 @@ admin.post("/shop/items", async ({ headers, body, status: status2 }) => {
category, category,
count, count,
baseProbability, baseProbability,
baseUpgradeCost, rollCostOverride,
costMultiplier, perRollMultiplier: bodyPerRollMultiplier,
boostAmount, upgradeBudgetMultiplier: bodyUpgradeBudgetMultiplier
rollCostOverride
} = body; } = body;
if (!name?.trim() || !image?.trim() || !description?.trim() || !category?.trim()) { if (!name?.trim() || !image?.trim() || !description?.trim() || !category?.trim()) {
return status2(400, { error: "All fields are required" }); return status2(400, { error: "All fields are required" });
@ -35262,8 +35280,8 @@ admin.post("/shop/items", async ({ headers, body, status: status2 }) => {
try { try {
const dollarCost = typeof price === "number" ? price / SCRAPS_PER_DOLLAR : 0; const dollarCost = typeof price === "number" ? price / SCRAPS_PER_DOLLAR : 0;
const pricing = computeItemPricing(dollarCost, baseProbability, count ?? 1); const pricing = computeItemPricing(dollarCost, baseProbability, count ?? 1);
const perRollMultiplierVal = body?.perRollMultiplier ?? 0.05; const perRollMultiplierVal = bodyPerRollMultiplier ?? 0.05;
const upgradeBudgetMultiplierVal = body?.upgradeBudgetMultiplier ?? 3; const upgradeBudgetMultiplierVal = bodyUpgradeBudgetMultiplier ?? 3;
await db.insert(shopItemsTable).values({ await db.insert(shopItemsTable).values({
name: name.trim(), name: name.trim(),
image: image.trim(), image: image.trim(),
@ -35744,10 +35762,12 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
let alreadyRow = null; let alreadyRow = null;
try { try {
const already = await db.execute(sql`SELECT 1 FROM admin_deleted_orders WHERE original_order_id = ${orderId} AND restored = false LIMIT 1`); const already = await db.execute(sql`SELECT 1 FROM admin_deleted_orders WHERE original_order_id = ${orderId} AND restored = false LIMIT 1`);
alreadyRow = already.rows?.[0] ?? (Array.isArray(already) ? already[0] : null); const alreadyResult = already;
alreadyRow = alreadyResult.rows?.[0] ?? (Array.isArray(already) ? already[0] : null);
} catch (err) { } catch (err) {
const msg = err?.message ?? ""; const dbErr = err;
const code = err?.code ?? null; const msg = dbErr?.message ?? "";
const code = dbErr?.code ?? null;
if (msg.includes("does not exist") || code === "42P01") { if (msg.includes("does not exist") || code === "42P01") {
await db.execute(sql` await db.execute(sql`
CREATE TABLE IF NOT EXISTS admin_deleted_orders ( CREATE TABLE IF NOT EXISTS admin_deleted_orders (
@ -35777,7 +35797,8 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
CREATE INDEX IF NOT EXISTS idx_admin_deleted_orders_original_order_id ON admin_deleted_orders (original_order_id); CREATE INDEX IF NOT EXISTS idx_admin_deleted_orders_original_order_id ON admin_deleted_orders (original_order_id);
`); `);
const already2 = await db.execute(sql`SELECT 1 FROM admin_deleted_orders WHERE original_order_id = ${orderId} AND restored = false LIMIT 1`); const already2 = await db.execute(sql`SELECT 1 FROM admin_deleted_orders WHERE original_order_id = ${orderId} AND restored = false LIMIT 1`);
alreadyRow = already2.rows?.[0] ?? (Array.isArray(already2) ? already2[0] : null); const already2Result = already2;
alreadyRow = already2Result.rows?.[0] ?? (Array.isArray(already2) ? already2[0] : null);
} else { } else {
throw err; throw err;
} }
@ -35824,12 +35845,13 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
return { success: true }; return { success: true };
} catch (err) { } catch (err) {
try { try {
console.error("Admin delete order error (stack):", err?.stack ?? err); const e = err;
console.error("Admin delete order error (name):", err?.name ?? null); console.error("Admin delete order error (stack):", e?.stack ?? err);
console.error("Admin delete order error (message):", err?.message ?? String(err)); console.error("Admin delete order error (name):", e?.name ?? null);
console.error("Admin delete order error (cause):", err?.cause ?? null); console.error("Admin delete order error (message):", e?.message ?? String(err));
console.error("Admin delete order error (query):", err?.query ?? null); console.error("Admin delete order error (cause):", e?.cause ?? null);
console.error("Admin delete order error (params):", err?.params ?? null); console.error("Admin delete order error (query):", e?.query ?? null);
console.error("Admin delete order error (params):", e?.params ?? null);
try { try {
console.error("Admin delete order error (full):", JSON.stringify(err, Object.getOwnPropertyNames(err), 2)); console.error("Admin delete order error (full):", JSON.stringify(err, Object.getOwnPropertyNames(err), 2));
} catch { } catch {
@ -35840,7 +35862,7 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
console.error("Original error:", err); console.error("Original error:", err);
} }
return status2(500, { return status2(500, {
error: "Failed to delete order: " + (err?.message || String(err)) error: "Failed to delete order: " + (err instanceof Error ? err.message : String(err))
}); });
} }
}); });
@ -35853,7 +35875,8 @@ admin.post("/orders/:id/restore", async ({ params, headers, status: status2 }) =
if (!Number.isInteger(originalOrderId) || originalOrderId <= 0) if (!Number.isInteger(originalOrderId) || originalOrderId <= 0)
return status2(400, { error: "Invalid order id" }); return status2(400, { error: "Invalid order id" });
const archivedRes = await db.execute(sql`SELECT * FROM admin_deleted_orders WHERE original_order_id = ${originalOrderId} AND restored = false LIMIT 1`); const archivedRes = await db.execute(sql`SELECT * FROM admin_deleted_orders WHERE original_order_id = ${originalOrderId} AND restored = false LIMIT 1`);
const archived = archivedRes.rows?.[0] ?? (Array.isArray(archivedRes) ? archivedRes[0] : null); const archivedResult = archivedRes;
const archived = archivedResult.rows?.[0] ?? (Array.isArray(archivedRes) ? archivedRes[0] : null);
if (!archived) if (!archived)
return status2(404, { return status2(404, {
error: "Archived order not found or already restored" error: "Archived order not found or already restored"
@ -36045,7 +36068,8 @@ admin.get("/users/:id/timeline", async ({ params, headers, status: status2 }) =>
type: "bonus", type: "bonus",
amount: b.amount, amount: b.amount,
description: b.reason, description: b.reason,
date: b.createdAt.toISOString() date: b.createdAt.toISOString(),
bonusId: b.id
}); });
} }
for (const o of shopOrders) { for (const o of shopOrders) {
@ -36103,7 +36127,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status: status2 }) =>
return status2(500, { error: "Failed to fetch user timeline" }); return status2(500, { error: "Failed to fetch user timeline" });
} }
}); });
admin.post("/sync-ysws", async ({ headers, set }) => { admin.post("/sync-ysws", async ({ headers }) => {
const user2 = await requireAdmin(headers); const user2 = await requireAdmin(headers);
if (!user2) if (!user2)
return { error: "Unauthorized" }; return { error: "Unauthorized" };

View file

@ -595,6 +595,38 @@ admin.get("/users/:id/bonuses", async ({ params, headers }) => {
} }
}); });
// Delete a bonus (admin only)
admin.delete("/bonuses/:id", async ({ params, headers, status }) => {
try {
const user = await requireAdmin(headers as Record<string, string>);
if (!user) return status(401, { error: "Unauthorized" });
const bonusId = parseInt(params.id);
if (!Number.isInteger(bonusId) || bonusId <= 0) {
return status(400, { error: "Invalid bonus id" });
}
const bonus = await db
.select({ id: userBonusesTable.id })
.from(userBonusesTable)
.where(eq(userBonusesTable.id, bonusId))
.limit(1);
if (!bonus[0]) {
return status(404, { error: "Bonus not found" });
}
await db
.delete(userBonusesTable)
.where(eq(userBonusesTable.id, bonusId));
return { success: true };
} catch (err) {
console.error(err);
return status(500, { error: "Failed to delete bonus" });
}
});
// Get projects waiting for review // Get projects waiting for review
admin.get("/reviews", async ({ headers, query }) => { admin.get("/reviews", async ({ headers, query }) => {
try { try {
@ -3000,6 +3032,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status }) => {
itemName?: string; itemName?: string;
paid?: boolean; paid?: boolean;
orderId?: number; orderId?: number;
bonusId?: number;
}; };
const timeline: TimelineEvent[] = []; const timeline: TimelineEvent[] = [];
@ -3022,6 +3055,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status }) => {
amount: b.amount, amount: b.amount,
description: b.reason, description: b.reason,
date: b.createdAt.toISOString(), date: b.createdAt.toISOString(),
bonusId: b.id,
}); });
} }

View file

@ -89,6 +89,7 @@
let lastDeletedError = $state<string | null>(null); let lastDeletedError = $state<string | null>(null);
let filterItem = $state(''); let filterItem = $state('');
let filterUser = $state(''); let filterUser = $state('');
let filterRegion = $state<'' | 'us' | 'intl'>('');
let uniqueItems = $derived( let uniqueItems = $derived(
[...new Map(orders.map((o) => [o.itemName, o.itemName])).values()].sort() [...new Map(orders.map((o) => [o.itemName, o.itemName])).values()].sort()
@ -113,6 +114,15 @@
result = result.filter((o) => o.username === filterUser); result = result.filter((o) => o.username === filterUser);
} }
if (filterRegion) {
result = result.filter((o) => {
const addr = parseShippingAddress(o.shippingAddress);
const country = addr?.country?.toLowerCase().trim() ?? '';
const isUS = country === 'us' || country === 'usa' || country === 'united states' || country === 'united states of america';
return filterRegion === 'us' ? isUS : !isUS;
});
}
if (searchQuery.trim()) { if (searchQuery.trim()) {
const q = searchQuery.trim().toLowerCase(); const q = searchQuery.trim().toLowerCase();
result = result.filter((o) => { result = result.filter((o) => {
@ -435,6 +445,18 @@
{/each} {/each}
</select> </select>
</div> </div>
<div class="flex flex-col">
<label for="filter-region" class="mb-1 text-xs font-bold text-gray-500 uppercase">region</label>
<select
id="filter-region"
bind:value={filterRegion}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none {filterRegion ? 'bg-black text-white' : ''}"
>
<option value="">all regions</option>
<option value="us">US only</option>
<option value="intl">non-US</option>
</select>
</div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<div class="flex flex-col"> <div class="flex flex-col">
<label for="date-from" class="mb-1 text-xs font-bold text-gray-500 uppercase">from</label> <label for="date-from" class="mb-1 text-xs font-bold text-gray-500 uppercase">from</label>
@ -454,13 +476,14 @@
class="rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none" class="rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none"
/> />
</div> </div>
{#if dateFrom || dateTo || filterItem || filterUser} {#if dateFrom || dateTo || filterItem || filterUser || filterRegion}
<button <button
onclick={() => { onclick={() => {
dateFrom = ''; dateFrom = '';
dateTo = ''; dateTo = '';
filterItem = ''; filterItem = '';
filterUser = ''; filterUser = '';
filterRegion = '';
}} }}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 hover:border-dashed" class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 hover:border-dashed"
title="clear filters" title="clear filters"

View file

@ -74,6 +74,7 @@
updateDescription: string | null; updateDescription: string | null;
aiDescription: string | null; aiDescription: string | null;
reviewerNotes: string | null; reviewerNotes: string | null;
scrapsAwarded: number;
} }
interface User { interface User {
@ -114,6 +115,19 @@
project ? Math.max(0, (hoursOverride ?? project.hours) - deductedHours) : 0 project ? Math.max(0, (hoursOverride ?? project.hours) - deductedHours) : 0
); );
const PHI = (1 + Math.sqrt(5)) / 2;
const MULTIPLIER = 10;
const TIER_MULTIPLIERS: Record<number, number> = { 1: 0.8, 2: 1.0, 3: 1.25, 4: 1.5 };
let isUpdate = $derived(project ? project.scrapsAwarded > 0 : false);
let previewTier = $derived(project ? (tierOverride ?? project.tier ?? 1) : 1);
let previewScraps = $derived(
Math.floor(effectiveHours * PHI * MULTIPLIER * (TIER_MULTIPLIERS[previewTier] ?? 1.0))
);
let additionalScraps = $derived(
project ? Math.max(0, previewScraps - project.scrapsAwarded) : previewScraps
);
let hoursOverrideError = $derived( let hoursOverrideError = $derived(
hoursOverride !== undefined && project && hoursOverride > project.hours hoursOverride !== undefined && project && hoursOverride > project.hours
? `Hours override cannot exceed project hours (${formatHours(project.hours)}h)` ? `Hours override cannot exceed project hours (${formatHours(project.hours)}h)`
@ -522,6 +536,29 @@
</div> </div>
{/if} {/if}
{#if isUpdate}
<div class="mt-4 rounded-lg border-2 border-dashed border-blue-500 bg-blue-50 p-4">
<p class="mb-2 flex items-center gap-1.5 text-sm font-bold text-blue-700">
<RefreshCw size={14} />
update — scraps preview
</p>
<p class="mb-2 text-sm text-blue-700">
this is an updated project. previously awarded scraps will be subtracted from the new total.
</p>
<div class="flex flex-wrap gap-3 text-sm font-bold">
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
previously awarded: {project.scrapsAwarded} scraps
</span>
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
new total: {previewScraps} scraps ({formatHours(effectiveHours)}h × tier {previewTier})
</span>
<span class="rounded-full border-2 border-black bg-blue-200 px-3 py-1 text-black">
additional: +{additionalScraps} scraps
</span>
</div>
</div>
{/if}
<div class="mt-4 flex flex-wrap gap-3"> <div class="mt-4 flex flex-wrap gap-3">
{#if project.githubUrl} {#if project.githubUrl}
<a <a

View file

@ -68,6 +68,7 @@
updateDescription: string | null; updateDescription: string | null;
aiDescription: string | null; aiDescription: string | null;
reviewerNotes: string | null; reviewerNotes: string | null;
scrapsAwarded: number;
} }
interface User { interface User {
@ -102,6 +103,19 @@
let effectiveHours = $derived( let effectiveHours = $derived(
project ? Math.max(0, (hoursOverride ?? project.hoursOverride ?? project.hours) - deductedHours) : 0 project ? Math.max(0, (hoursOverride ?? project.hoursOverride ?? project.hours) - deductedHours) : 0
); );
const PHI = (1 + Math.sqrt(5)) / 2;
const MULTIPLIER = 10;
const TIER_MULTIPLIERS: Record<number, number> = { 1: 0.8, 2: 1.0, 3: 1.25, 4: 1.5 };
let isUpdate = $derived(project ? project.scrapsAwarded > 0 : false);
let previewTier = $derived(project ? (project.tierOverride ?? project.tier ?? 1) : 1);
let previewScraps = $derived(
Math.floor(effectiveHours * PHI * MULTIPLIER * (TIER_MULTIPLIERS[previewTier] ?? 1.0))
);
let additionalScraps = $derived(
project ? Math.max(0, previewScraps - project.scrapsAwarded) : previewScraps
);
let hoursOverrideError = $derived( let hoursOverrideError = $derived(
hoursOverride !== undefined && project && hoursOverride > project.hours hoursOverride !== undefined && project && hoursOverride > project.hours
? `hours override cannot exceed project hours (${formatHours(project.hours)}h)` ? `hours override cannot exceed project hours (${formatHours(project.hours)}h)`
@ -408,6 +422,29 @@
</div> </div>
{/if} {/if}
{#if isUpdate}
<div class="mt-4 rounded-lg border-2 border-dashed border-blue-500 bg-blue-50 p-4">
<p class="mb-2 flex items-center gap-1.5 text-sm font-bold text-blue-700">
<RefreshCw size={14} />
update — scraps preview
</p>
<p class="mb-2 text-sm text-blue-700">
this is an updated project. previously awarded scraps will be subtracted from the new total.
</p>
<div class="flex flex-wrap gap-3 text-sm font-bold">
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
previously awarded: {project.scrapsAwarded} scraps
</span>
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
new total: {previewScraps} scraps ({formatHours(effectiveHours)}h × tier {previewTier})
</span>
<span class="rounded-full border-2 border-black bg-blue-200 px-3 py-1 text-black">
additional: +{additionalScraps} scraps
</span>
</div>
</div>
{/if}
<div class="mt-4 flex flex-wrap gap-3"> <div class="mt-4 flex flex-wrap gap-3">
{#if project.githubUrl} {#if project.githubUrl}
<a <a

View file

@ -17,6 +17,7 @@
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
Undo2, Undo2,
Trash2,
ShoppingCart, ShoppingCart,
Dices, Dices,
FileText, FileText,
@ -102,6 +103,7 @@
itemName?: string; itemName?: string;
paid?: boolean; paid?: boolean;
orderId?: number; orderId?: number;
bonusId?: number;
} }
let timeline = $state<TimelineEvent[]>([]); let timeline = $state<TimelineEvent[]>([]);
@ -114,7 +116,8 @@
let timelineLoading = $state(false); let timelineLoading = $state(false);
let showTimeline = $state(false); let showTimeline = $state(false);
let undoingOrder = $state<number | null>(null); let undoingOrder = $state<number | null>(null);
let showUndoConfirm = $state<number | null>(null); let deletingBonus = $state<number | null>(null);
let showDeleteConfirm = $state<{ type: 'order'; id: number } | { type: 'bonus'; id: number } | null>(null);
let showUnshipConfirm = $state<number | null>(null); let showUnshipConfirm = $state<number | null>(null);
let unshipReason = $state(''); let unshipReason = $state('');
let unshipping = $state(false); let unshipping = $state(false);
@ -284,15 +287,13 @@
async function undoOrder(orderId: number) { async function undoOrder(orderId: number) {
undoingOrder = orderId; undoingOrder = orderId;
showUndoConfirm = null; showDeleteConfirm = null;
try { try {
// Directly call DELETE with a required reason payload.
// The server expects a short reason when deleting/reverting an order.
const refundRes = await fetch(`${API_URL}/admin/orders/${orderId}`, { const refundRes = await fetch(`${API_URL}/admin/orders/${orderId}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ reason: 'reverted via admin UI' }) body: JSON.stringify({ reason: 'reverted via admin timeline' })
}); });
const refundResult = await refundRes.json(); const refundResult = await refundRes.json();
if (refundResult.error) { if (refundResult.error) {
@ -307,6 +308,44 @@
} }
} }
async function deleteBonus(bonusId: number) {
deletingBonus = bonusId;
showDeleteConfirm = null;
try {
const res = await fetch(`${API_URL}/admin/bonuses/${bonusId}`, {
method: 'DELETE',
credentials: 'include'
});
const result = await res.json();
if (result.error) {
console.error(result.error);
return;
}
await fetchTimeline();
} catch (e) {
console.error('Failed to delete bonus:', e);
} finally {
deletingBonus = null;
}
}
function confirmTimelineDelete(event: TimelineEvent) {
if (event.type === 'bonus' && event.bonusId) {
showDeleteConfirm = { type: 'bonus', id: event.bonusId };
} else if (event.type.startsWith('shop_') && event.orderId) {
showDeleteConfirm = { type: 'order', id: event.orderId };
}
}
function executeTimelineDelete() {
if (!showDeleteConfirm) return;
if (showDeleteConfirm.type === 'bonus') {
deleteBonus(showDeleteConfirm.id);
} else {
undoOrder(showDeleteConfirm.id);
}
}
async function unshipProject(projectId: number) { async function unshipProject(projectId: number) {
unshipping = true; unshipping = true;
try { try {
@ -835,31 +874,19 @@
<p class="truncate text-sm text-gray-700">{event.description}</p> <p class="truncate text-sm text-gray-700">{event.description}</p>
</div> </div>
<div class="flex shrink-0 items-center gap-2"> <div class="flex shrink-0 items-center gap-2">
{#if event.type.startsWith('shop_') && event.orderId} {#if (event.type.startsWith('shop_') && event.orderId) || (event.type === 'bonus' && event.bonusId)}
{#if showUndoConfirm === event.orderId} <button
<div class="flex items-center gap-1"> onclick={() => confirmTimelineDelete(event)}
<button disabled={undoingOrder === event.orderId || deletingBonus === event.bonusId}
onclick={() => undoOrder(event.orderId ?? 0)} class="cursor-pointer rounded-full border-2 border-red-600 px-2 py-1 text-xs font-bold text-red-600 transition-all duration-200 hover:border-dashed disabled:opacity-50"
disabled={undoingOrder === event.orderId} title="delete"
class="cursor-pointer rounded-full bg-red-600 px-2 py-1 text-xs font-bold text-white transition-all duration-200 hover:bg-red-700 disabled:opacity-50" >
> {#if undoingOrder === event.orderId || deletingBonus === event.bonusId}
{undoingOrder === event.orderId ? '...' : 'confirm'} ...
</button> {:else}
<button <Trash2 size={12} />
onclick={() => (showUndoConfirm = null)} {/if}
class="cursor-pointer rounded-full border-2 border-black px-2 py-1 text-xs font-bold transition-all duration-200 hover:border-dashed" </button>
>
cancel
</button>
</div>
{:else}
<button
onclick={() => (showUndoConfirm = event.orderId ?? null)}
class="cursor-pointer rounded-full border-2 border-red-600 px-2 py-1 text-xs font-bold text-red-600 transition-all duration-200 hover:border-dashed disabled:opacity-50"
>
<Undo2 size={12} />
</button>
{/if}
{/if} {/if}
<div class="text-right"> <div class="text-right">
<p class="font-bold {getTimelineColor(event.type, event.amount)}"> <p class="font-bold {getTimelineColor(event.type, event.amount)}">
@ -999,3 +1026,41 @@
</div> </div>
</div> </div>
{/if} {/if}
<!-- Timeline Delete Confirmation Modal -->
{#if showDeleteConfirm}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={(e) => e.target === e.currentTarget && (showDeleteConfirm = null)}
onkeydown={(e) => e.key === 'Escape' && (showDeleteConfirm = null)}
role="dialog"
tabindex="-1"
>
<div class="w-full max-w-md rounded-2xl border-4 border-black bg-white p-6">
<h2 class="mb-4 text-2xl font-bold">
delete {showDeleteConfirm.type === 'bonus' ? 'bonus' : 'order'}
</h2>
<p class="mb-6 text-gray-600">
{#if showDeleteConfirm.type === 'bonus'}
this will permanently delete this bonus entry from the database. the user's balance will be recalculated.
{:else}
this will permanently delete this order and all associated records (refinery upgrades, rolls, penalties). item stock will be restored.
{/if}
</p>
<div class="flex gap-3">
<button
onclick={() => (showDeleteConfirm = null)}
class="flex-1 cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
cancel
</button>
<button
onclick={executeTimelineDelete}
class="flex-1 cursor-pointer rounded-full border-4 border-red-600 bg-red-600 px-4 py-2 font-bold text-white transition-all duration-200 hover:border-dashed disabled:cursor-not-allowed disabled:opacity-50"
>
delete
</button>
</div>
</div>
</div>
{/if}