mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 22:05:09 +00:00
add refinery undo, fix displaying balances at admin & leaderboard due to bonuses
This commit is contained in:
parent
03131c6eb9
commit
ceec8c9c2b
6 changed files with 163 additions and 13 deletions
|
|
@ -100,7 +100,7 @@ admin.get('/users', async ({ headers, query, status }) => {
|
|||
)
|
||||
: undefined
|
||||
|
||||
const [users, countResult] = await Promise.all([
|
||||
const [userIds, countResult] = await Promise.all([
|
||||
db.select({
|
||||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
|
|
@ -109,23 +109,32 @@ admin.get('/users', async ({ headers, query, status }) => {
|
|||
email: usersTable.email,
|
||||
role: usersTable.role,
|
||||
internalNotes: usersTable.internalNotes,
|
||||
createdAt: usersTable.createdAt,
|
||||
scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id}), 0)`.as('scraps_earned'),
|
||||
scrapsSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_spent')
|
||||
createdAt: usersTable.createdAt
|
||||
}).from(usersTable).where(searchCondition).orderBy(desc(usersTable.createdAt)).limit(limit).offset(offset),
|
||||
db.select({ count: sql<number>`count(*)` }).from(usersTable).where(searchCondition)
|
||||
])
|
||||
|
||||
const total = Number(countResult[0]?.count || 0)
|
||||
|
||||
// Get scraps balance for each user
|
||||
const usersWithScraps = await Promise.all(
|
||||
userIds.map(async (u) => {
|
||||
const balance = await getUserScrapsBalance(u.id)
|
||||
return {
|
||||
...u,
|
||||
scraps: balance.balance
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
data: users.map(u => ({
|
||||
data: usersWithScraps.map(u => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
slackId: u.slackId,
|
||||
email: user.role === 'admin' ? u.email : undefined,
|
||||
scraps: Number(u.scrapsEarned) - Number(u.scrapsSpent),
|
||||
scraps: u.scraps,
|
||||
role: u.role,
|
||||
internalNotes: u.internalNotes,
|
||||
createdAt: u.createdAt
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ leaderboard.get('/', async ({ query }) => {
|
|||
username: usersTable.username,
|
||||
avatar: usersTable.avatar,
|
||||
scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND status != 'permanently_rejected'), 0)`.as('scraps_earned'),
|
||||
scrapsSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_spent'),
|
||||
scrapsBonus: sql<number>`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as('scraps_bonus'),
|
||||
scrapsShopSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_shop_spent'),
|
||||
scrapsRefinerySpent: sql<number>`COALESCE((SELECT SUM(cost) FROM refinery_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_refinery_spent'),
|
||||
hours: sql<number>`COALESCE(SUM(${projectsTable.hours}), 0)`.as('total_hours'),
|
||||
projectCount: sql<number>`COUNT(${projectsTable.id})`.as('project_count')
|
||||
})
|
||||
|
|
@ -37,7 +39,7 @@ leaderboard.get('/', async ({ query }) => {
|
|||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
hours: Number(user.hours),
|
||||
scraps: Number(user.scrapsEarned) - Number(user.scrapsSpent),
|
||||
scraps: Number(user.scrapsEarned) + Number(user.scrapsBonus) - Number(user.scrapsShopSpent) - Number(user.scrapsRefinerySpent),
|
||||
scrapsEarned: Number(user.scrapsEarned),
|
||||
projectCount: Number(user.projectCount)
|
||||
}))
|
||||
|
|
@ -49,7 +51,9 @@ leaderboard.get('/', async ({ query }) => {
|
|||
username: usersTable.username,
|
||||
avatar: usersTable.avatar,
|
||||
scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND status != 'permanently_rejected'), 0)`.as('scraps_earned'),
|
||||
scrapsSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_spent'),
|
||||
scrapsBonus: sql<number>`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as('scraps_bonus'),
|
||||
scrapsShopSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_shop_spent'),
|
||||
scrapsRefinerySpent: sql<number>`COALESCE((SELECT SUM(cost) FROM refinery_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_refinery_spent'),
|
||||
hours: sql<number>`COALESCE(SUM(${projectsTable.hours}), 0)`.as('total_hours'),
|
||||
projectCount: sql<number>`COUNT(${projectsTable.id})`.as('project_count')
|
||||
})
|
||||
|
|
@ -60,7 +64,7 @@ leaderboard.get('/', async ({ query }) => {
|
|||
sql`${projectsTable.status} != 'permanently_rejected'`
|
||||
))
|
||||
.groupBy(usersTable.id)
|
||||
.orderBy(desc(sql`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND status != 'permanently_rejected'), 0) - COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`))
|
||||
.orderBy(desc(sql`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND status != 'permanently_rejected'), 0) + COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0) - COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0) - COALESCE((SELECT SUM(cost) FROM refinery_orders WHERE user_id = ${usersTable.id}), 0)`))
|
||||
.limit(10)
|
||||
|
||||
return results.map((user, index) => ({
|
||||
|
|
@ -69,7 +73,7 @@ leaderboard.get('/', async ({ query }) => {
|
|||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
hours: Number(user.hours),
|
||||
scraps: Number(user.scrapsEarned) - Number(user.scrapsSpent),
|
||||
scraps: Number(user.scrapsEarned) + Number(user.scrapsBonus) - Number(user.scrapsShopSpent) - Number(user.scrapsRefinerySpent),
|
||||
scrapsEarned: Number(user.scrapsEarned),
|
||||
projectCount: Number(user.projectCount)
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -882,4 +882,89 @@ shop.post('/orders/:id/address', async ({ params, body, headers }) => {
|
|||
return { success: true }
|
||||
})
|
||||
|
||||
shop.post('/items/:id/refinery/undo', async ({ params, headers }) => {
|
||||
const user = await getUserFromSession(headers as Record<string, string>)
|
||||
if (!user) {
|
||||
return { error: 'Unauthorized' }
|
||||
}
|
||||
|
||||
const itemId = parseInt(params.id)
|
||||
if (!Number.isInteger(itemId)) {
|
||||
return { error: 'Invalid item id' }
|
||||
}
|
||||
|
||||
const item = await db
|
||||
.select()
|
||||
.from(shopItemsTable)
|
||||
.where(eq(shopItemsTable.id, itemId))
|
||||
.limit(1)
|
||||
|
||||
if (item.length === 0) {
|
||||
return { error: 'Item not found' }
|
||||
}
|
||||
|
||||
// Get the most recent refinery order for this user and item
|
||||
const orders = await db
|
||||
.select()
|
||||
.from(refineryOrdersTable)
|
||||
.where(and(
|
||||
eq(refineryOrdersTable.userId, user.id),
|
||||
eq(refineryOrdersTable.shopItemId, itemId)
|
||||
))
|
||||
.orderBy(desc(refineryOrdersTable.createdAt))
|
||||
.limit(1)
|
||||
|
||||
if (orders.length === 0) {
|
||||
return { error: 'No refinery upgrades to undo' }
|
||||
}
|
||||
|
||||
const order = orders[0]
|
||||
|
||||
// Delete the order (balance is calculated dynamically, so deleting this removes the cost from the spent total)
|
||||
await db
|
||||
.delete(refineryOrdersTable)
|
||||
.where(eq(refineryOrdersTable.id, order.id))
|
||||
|
||||
// Get updated boost and upgrade count
|
||||
const boost = await db
|
||||
.select({
|
||||
boostPercent: sql<number>`COALESCE(SUM(${refineryOrdersTable.boostAmount}), 0)`,
|
||||
upgradeCount: sql<number>`COUNT(*)`
|
||||
})
|
||||
.from(refineryOrdersTable)
|
||||
.where(and(
|
||||
eq(refineryOrdersTable.userId, user.id),
|
||||
eq(refineryOrdersTable.shopItemId, itemId)
|
||||
))
|
||||
|
||||
const newBoostPercent = boost.length > 0 ? Number(boost[0].boostPercent) : 0
|
||||
const newUpgradeCount = boost.length > 0 ? Number(boost[0].upgradeCount) : 0
|
||||
const refundedCost = order.cost
|
||||
|
||||
// Get penalty for effective probability calculation
|
||||
const penalty = await db
|
||||
.select({ probabilityMultiplier: shopPenaltiesTable.probabilityMultiplier })
|
||||
.from(shopPenaltiesTable)
|
||||
.where(and(
|
||||
eq(shopPenaltiesTable.userId, user.id),
|
||||
eq(shopPenaltiesTable.shopItemId, itemId)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
const penaltyMultiplier = penalty.length > 0 ? penalty[0].probabilityMultiplier : 100
|
||||
const adjustedBaseProbability = Math.floor(item[0].baseProbability * penaltyMultiplier / 100)
|
||||
const maxBoost = 100 - adjustedBaseProbability
|
||||
const nextCost = newBoostPercent >= maxBoost
|
||||
? null
|
||||
: Math.floor(item[0].baseUpgradeCost * Math.pow(item[0].costMultiplier / 100, newUpgradeCount))
|
||||
|
||||
return {
|
||||
boostPercent: newBoostPercent,
|
||||
upgradeCount: newUpgradeCount,
|
||||
refundedCost,
|
||||
effectiveProbability: Math.min(adjustedBaseProbability + newBoostPercent, 100),
|
||||
nextCost
|
||||
}
|
||||
})
|
||||
|
||||
export default shop
|
||||
|
|
|
|||
|
|
@ -167,6 +167,8 @@ export default {
|
|||
fromPreviousBuy: 'from previous buy',
|
||||
noItemsAvailable: 'no items available for upgrades',
|
||||
failedToUpgrade: 'Failed to upgrade probability',
|
||||
failedToUndo: 'Failed to undo upgrade',
|
||||
undo: 'undo',
|
||||
ok: 'ok'
|
||||
},
|
||||
leaderboard: {
|
||||
|
|
|
|||
|
|
@ -167,6 +167,8 @@ export default {
|
|||
fromPreviousBuy: 'de compra anterior',
|
||||
noItemsAvailable: 'no hay artículos disponibles para mejorar',
|
||||
failedToUpgrade: 'Error al mejorar probabilidad',
|
||||
failedToUndo: 'Error al deshacer mejora',
|
||||
undo: 'deshacer',
|
||||
ok: 'ok'
|
||||
},
|
||||
leaderboard: {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { Undo2 } from '@lucide/svelte';
|
||||
import { getUser, refreshUserScraps, userScrapsStore } from '$lib/auth-client';
|
||||
import { shopItemsStore, shopLoading, fetchShopItems, type ShopItem } from '$lib/stores';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
let probabilityItems = $derived($shopItemsStore.filter((item) => item.baseProbability > 0));
|
||||
let upgrading = $state<number | null>(null);
|
||||
let undoing = $state<number | null>(null);
|
||||
let alertMessage = $state<string | null>(null);
|
||||
|
||||
function getProbabilityColor(probability: number): string {
|
||||
|
|
@ -49,6 +51,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function undoRefinery(item: ShopItem) {
|
||||
undoing = item.id;
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/shop/items/${item.id}/refinery/undo`,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
alertMessage = data.error;
|
||||
return;
|
||||
}
|
||||
shopItemsStore.update((items) =>
|
||||
items.map((i) =>
|
||||
i.id === item.id
|
||||
? {
|
||||
...i,
|
||||
userBoostPercent: data.boostPercent,
|
||||
upgradeCount: data.upgradeCount,
|
||||
effectiveProbability: data.effectiveProbability,
|
||||
nextUpgradeCost: data.nextCost
|
||||
}
|
||||
: i
|
||||
)
|
||||
);
|
||||
await refreshUserScraps();
|
||||
alertMessage = `Refunded ${data.refundedCost} scraps`;
|
||||
} catch (e) {
|
||||
alertMessage = $t.refinery.failedToUndo || 'Failed to undo upgrade';
|
||||
} finally {
|
||||
undoing = null;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await getUser();
|
||||
fetchShopItems();
|
||||
|
|
@ -111,10 +150,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm:text-right">
|
||||
<div class="flex items-center gap-2 sm:text-right">
|
||||
{#if item.userBoostPercent > 0}
|
||||
<button
|
||||
onclick={() => undoRefinery(item)}
|
||||
disabled={undoing === item.id}
|
||||
class="cursor-pointer rounded-full border-4 border-black px-4 py-2 text-sm font-bold transition-all duration-200 hover:border-dashed disabled:opacity-50 sm:text-base"
|
||||
>
|
||||
{$t.refinery.undo}
|
||||
</button>
|
||||
{/if}
|
||||
{#if maxed}
|
||||
<span
|
||||
class="inline-block rounded-full bg-gray-200 px-4 py-2 font-bold text-gray-600"
|
||||
class="rounded-full bg-gray-200 px-4 py-2 font-bold text-gray-600"
|
||||
>{$t.refinery.maxed}</span
|
||||
>
|
||||
{:else if nextCost !== null}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue