mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 16:28:20 +00:00
update logic fix
This commit is contained in:
parent
3e897fbd26
commit
df2039908f
7 changed files with 272 additions and 54 deletions
|
|
@ -1,5 +1,3 @@
|
|||
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86on
|
||||
|
||||
# CLAUDE.md
|
||||
|
||||
## Architecture
|
||||
|
|
|
|||
66
backend/dist/index.js
vendored
66
backend/dist/index.js
vendored
|
|
@ -34517,6 +34517,26 @@ admin.get("/users/:id/bonuses", async ({ params, headers }) => {
|
|||
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 }) => {
|
||||
try {
|
||||
const user2 = await requireReviewer(headers);
|
||||
|
|
@ -34727,7 +34747,6 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
}
|
||||
let scrapsAwarded = 0;
|
||||
if (action === "approved") {
|
||||
const hours = hoursOverride ?? project[0].hours ?? 0;
|
||||
const tier = tierOverride ?? project[0].tier ?? 1;
|
||||
const { effectiveHours } = await computeEffectiveHoursForProject({
|
||||
...project[0],
|
||||
|
|
@ -35243,10 +35262,9 @@ admin.post("/shop/items", async ({ headers, body, status: status2 }) => {
|
|||
category,
|
||||
count,
|
||||
baseProbability,
|
||||
baseUpgradeCost,
|
||||
costMultiplier,
|
||||
boostAmount,
|
||||
rollCostOverride
|
||||
rollCostOverride,
|
||||
perRollMultiplier: bodyPerRollMultiplier,
|
||||
upgradeBudgetMultiplier: bodyUpgradeBudgetMultiplier
|
||||
} = body;
|
||||
if (!name?.trim() || !image?.trim() || !description?.trim() || !category?.trim()) {
|
||||
return status2(400, { error: "All fields are required" });
|
||||
|
|
@ -35262,8 +35280,8 @@ admin.post("/shop/items", async ({ headers, body, status: status2 }) => {
|
|||
try {
|
||||
const dollarCost = typeof price === "number" ? price / SCRAPS_PER_DOLLAR : 0;
|
||||
const pricing = computeItemPricing(dollarCost, baseProbability, count ?? 1);
|
||||
const perRollMultiplierVal = body?.perRollMultiplier ?? 0.05;
|
||||
const upgradeBudgetMultiplierVal = body?.upgradeBudgetMultiplier ?? 3;
|
||||
const perRollMultiplierVal = bodyPerRollMultiplier ?? 0.05;
|
||||
const upgradeBudgetMultiplierVal = bodyUpgradeBudgetMultiplier ?? 3;
|
||||
await db.insert(shopItemsTable).values({
|
||||
name: name.trim(),
|
||||
image: image.trim(),
|
||||
|
|
@ -35744,10 +35762,12 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
|
|||
let alreadyRow = null;
|
||||
try {
|
||||
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) {
|
||||
const msg = err?.message ?? "";
|
||||
const code = err?.code ?? null;
|
||||
const dbErr = err;
|
||||
const msg = dbErr?.message ?? "";
|
||||
const code = dbErr?.code ?? null;
|
||||
if (msg.includes("does not exist") || code === "42P01") {
|
||||
await db.execute(sql`
|
||||
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);
|
||||
`);
|
||||
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 {
|
||||
throw err;
|
||||
}
|
||||
|
|
@ -35824,12 +35845,13 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
|
|||
return { success: true };
|
||||
} catch (err) {
|
||||
try {
|
||||
console.error("Admin delete order error (stack):", err?.stack ?? err);
|
||||
console.error("Admin delete order error (name):", err?.name ?? null);
|
||||
console.error("Admin delete order error (message):", err?.message ?? String(err));
|
||||
console.error("Admin delete order error (cause):", err?.cause ?? null);
|
||||
console.error("Admin delete order error (query):", err?.query ?? null);
|
||||
console.error("Admin delete order error (params):", err?.params ?? null);
|
||||
const e = err;
|
||||
console.error("Admin delete order error (stack):", e?.stack ?? err);
|
||||
console.error("Admin delete order error (name):", e?.name ?? null);
|
||||
console.error("Admin delete order error (message):", e?.message ?? String(err));
|
||||
console.error("Admin delete order error (cause):", e?.cause ?? null);
|
||||
console.error("Admin delete order error (query):", e?.query ?? null);
|
||||
console.error("Admin delete order error (params):", e?.params ?? null);
|
||||
try {
|
||||
console.error("Admin delete order error (full):", JSON.stringify(err, Object.getOwnPropertyNames(err), 2));
|
||||
} catch {
|
||||
|
|
@ -35840,7 +35862,7 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
|
|||
console.error("Original error:", err);
|
||||
}
|
||||
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)
|
||||
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 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)
|
||||
return status2(404, {
|
||||
error: "Archived order not found or already restored"
|
||||
|
|
@ -36045,7 +36068,8 @@ admin.get("/users/:id/timeline", async ({ params, headers, status: status2 }) =>
|
|||
type: "bonus",
|
||||
amount: b.amount,
|
||||
description: b.reason,
|
||||
date: b.createdAt.toISOString()
|
||||
date: b.createdAt.toISOString(),
|
||||
bonusId: b.id
|
||||
});
|
||||
}
|
||||
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" });
|
||||
}
|
||||
});
|
||||
admin.post("/sync-ysws", async ({ headers, set }) => {
|
||||
admin.post("/sync-ysws", async ({ headers }) => {
|
||||
const user2 = await requireAdmin(headers);
|
||||
if (!user2)
|
||||
return { error: "Unauthorized" };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
admin.get("/reviews", async ({ headers, query }) => {
|
||||
try {
|
||||
|
|
@ -3000,6 +3032,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status }) => {
|
|||
itemName?: string;
|
||||
paid?: boolean;
|
||||
orderId?: number;
|
||||
bonusId?: number;
|
||||
};
|
||||
|
||||
const timeline: TimelineEvent[] = [];
|
||||
|
|
@ -3022,6 +3055,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status }) => {
|
|||
amount: b.amount,
|
||||
description: b.reason,
|
||||
date: b.createdAt.toISOString(),
|
||||
bonusId: b.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@
|
|||
let lastDeletedError = $state<string | null>(null);
|
||||
let filterItem = $state('');
|
||||
let filterUser = $state('');
|
||||
let filterRegion = $state<'' | 'us' | 'intl'>('');
|
||||
|
||||
let uniqueItems = $derived(
|
||||
[...new Map(orders.map((o) => [o.itemName, o.itemName])).values()].sort()
|
||||
|
|
@ -113,6 +114,15 @@
|
|||
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()) {
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
result = result.filter((o) => {
|
||||
|
|
@ -435,6 +445,18 @@
|
|||
{/each}
|
||||
</select>
|
||||
</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 flex-col">
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
{#if dateFrom || dateTo || filterItem || filterUser}
|
||||
{#if dateFrom || dateTo || filterItem || filterUser || filterRegion}
|
||||
<button
|
||||
onclick={() => {
|
||||
dateFrom = '';
|
||||
dateTo = '';
|
||||
filterItem = '';
|
||||
filterUser = '';
|
||||
filterRegion = '';
|
||||
}}
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
updateDescription: string | null;
|
||||
aiDescription: string | null;
|
||||
reviewerNotes: string | null;
|
||||
scrapsAwarded: number;
|
||||
}
|
||||
|
||||
interface User {
|
||||
|
|
@ -114,6 +115,19 @@
|
|||
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(
|
||||
hoursOverride !== undefined && project && hoursOverride > project.hours
|
||||
? `Hours override cannot exceed project hours (${formatHours(project.hours)}h)`
|
||||
|
|
@ -522,6 +536,29 @@
|
|||
</div>
|
||||
{/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">
|
||||
{#if project.githubUrl}
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@
|
|||
updateDescription: string | null;
|
||||
aiDescription: string | null;
|
||||
reviewerNotes: string | null;
|
||||
scrapsAwarded: number;
|
||||
}
|
||||
|
||||
interface User {
|
||||
|
|
@ -102,6 +103,19 @@
|
|||
let effectiveHours = $derived(
|
||||
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(
|
||||
hoursOverride !== undefined && project && hoursOverride > project.hours
|
||||
? `hours override cannot exceed project hours (${formatHours(project.hours)}h)`
|
||||
|
|
@ -408,6 +422,29 @@
|
|||
</div>
|
||||
{/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">
|
||||
{#if project.githubUrl}
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
TrendingUp,
|
||||
TrendingDown,
|
||||
Undo2,
|
||||
Trash2,
|
||||
ShoppingCart,
|
||||
Dices,
|
||||
FileText,
|
||||
|
|
@ -102,6 +103,7 @@
|
|||
itemName?: string;
|
||||
paid?: boolean;
|
||||
orderId?: number;
|
||||
bonusId?: number;
|
||||
}
|
||||
|
||||
let timeline = $state<TimelineEvent[]>([]);
|
||||
|
|
@ -114,7 +116,8 @@
|
|||
let timelineLoading = $state(false);
|
||||
let showTimeline = $state(false);
|
||||
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 unshipReason = $state('');
|
||||
let unshipping = $state(false);
|
||||
|
|
@ -284,15 +287,13 @@
|
|||
|
||||
async function undoOrder(orderId: number) {
|
||||
undoingOrder = orderId;
|
||||
showUndoConfirm = null;
|
||||
showDeleteConfirm = null;
|
||||
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}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ reason: 'reverted via admin UI' })
|
||||
body: JSON.stringify({ reason: 'reverted via admin timeline' })
|
||||
});
|
||||
const refundResult = await refundRes.json();
|
||||
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) {
|
||||
unshipping = true;
|
||||
try {
|
||||
|
|
@ -835,31 +874,19 @@
|
|||
<p class="truncate text-sm text-gray-700">{event.description}</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
{#if event.type.startsWith('shop_') && event.orderId}
|
||||
{#if showUndoConfirm === event.orderId}
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
onclick={() => undoOrder(event.orderId ?? 0)}
|
||||
disabled={undoingOrder === event.orderId}
|
||||
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"
|
||||
>
|
||||
{undoingOrder === event.orderId ? '...' : 'confirm'}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (showUndoConfirm = null)}
|
||||
class="cursor-pointer rounded-full border-2 border-black px-2 py-1 text-xs font-bold transition-all duration-200 hover:border-dashed"
|
||||
>
|
||||
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 (event.type.startsWith('shop_') && event.orderId) || (event.type === 'bonus' && event.bonusId)}
|
||||
<button
|
||||
onclick={() => confirmTimelineDelete(event)}
|
||||
disabled={undoingOrder === event.orderId || deletingBonus === event.bonusId}
|
||||
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"
|
||||
title="delete"
|
||||
>
|
||||
{#if undoingOrder === event.orderId || deletingBonus === event.bonusId}
|
||||
...
|
||||
{:else}
|
||||
<Trash2 size={12} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<div class="text-right">
|
||||
<p class="font-bold {getTimelineColor(event.type, event.amount)}">
|
||||
|
|
@ -999,3 +1026,41 @@
|
|||
</div>
|
||||
</div>
|
||||
{/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}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue