mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 19:45:14 +00:00
typos and stuff
This commit is contained in:
parent
a3feec9263
commit
5d6e7eab52
5 changed files with 144 additions and 34 deletions
|
|
@ -216,6 +216,7 @@ export async function notifyProjectReview({
|
|||
projectId,
|
||||
action,
|
||||
feedbackForAuthor,
|
||||
rejectionReason,
|
||||
reviewerSlackId,
|
||||
adminSlackIds,
|
||||
scrapsAwarded,
|
||||
|
|
@ -227,6 +228,7 @@ export async function notifyProjectReview({
|
|||
projectId: number;
|
||||
action: "approved" | "denied" | "permanently_rejected";
|
||||
feedbackForAuthor: string;
|
||||
rejectionReason?: string;
|
||||
reviewerSlackId?: string | null;
|
||||
adminSlackIds: string[];
|
||||
scrapsAwarded?: number;
|
||||
|
|
@ -301,14 +303,19 @@ export async function notifyProjectReview({
|
|||
},
|
||||
];
|
||||
} else if (action === "permanently_rejected") {
|
||||
fallbackText = `:scraps: hey <@${userSlackId}>! your scraps project ${projectName} has been unshipped by an admin.`;
|
||||
const reasonSuffix = rejectionReason ? ` reason: ${rejectionReason}` : "";
|
||||
const reasonBlock = rejectionReason
|
||||
? `\n\n*reason:* ${rejectionReason}`
|
||||
: "";
|
||||
|
||||
fallbackText = `:scraps: hey <@${userSlackId}>! your scraps project ${projectName} has been unshipped by an admin.${reasonSuffix}`;
|
||||
|
||||
blocks = [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `:scraps: hey <@${userSlackId}>!\n\nyour scraps project *<${projectUrl}|${projectName}>* has been unshipped by an admin.`,
|
||||
text: `:scraps: hey <@${userSlackId}>!\n\nyour scraps project *<${projectUrl}|${projectName}>* has been unshipped by an admin.${reasonBlock}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -45,7 +45,12 @@ const admin = new Elysia({ prefix: "/admin" });
|
|||
async function requireReviewer(headers: Record<string, string>) {
|
||||
const user = await getUserFromSession(headers);
|
||||
if (!user) return null;
|
||||
if (user.role !== "reviewer" && user.role !== "admin" && user.role !== "creator") return null;
|
||||
if (
|
||||
user.role !== "reviewer" &&
|
||||
user.role !== "admin" &&
|
||||
user.role !== "creator"
|
||||
)
|
||||
return null;
|
||||
return user;
|
||||
}
|
||||
|
||||
|
|
@ -293,8 +298,9 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
const fulfilledConsolationDollarCost =
|
||||
Number(fulfilledConsolationCount[0]?.count || 0) * 2;
|
||||
const fulfilledUpgradeDollarCost =
|
||||
Number((fulfilledUpgrades.rows[0] as { total_cost: string })?.total_cost || 0) /
|
||||
SCRAPS_PER_DOLLAR;
|
||||
Number(
|
||||
(fulfilledUpgrades.rows[0] as { total_cost: string })?.total_cost || 0,
|
||||
) / SCRAPS_PER_DOLLAR;
|
||||
const totalActualCost =
|
||||
fulfilledLuckWinDollarCost +
|
||||
fulfilledConsolationDollarCost +
|
||||
|
|
@ -313,7 +319,9 @@ admin.get("/stats", async ({ headers, status }) => {
|
|||
{ headers: { Accept: "application/json" } },
|
||||
);
|
||||
if (hcbRes.ok) {
|
||||
const hcbData = await hcbRes.json() as { balances?: { balance_cents?: number } };
|
||||
const hcbData = (await hcbRes.json()) as {
|
||||
balances?: { balance_cents?: number };
|
||||
};
|
||||
hcbBalanceCents = hcbData.balances?.balance_cents ?? 0;
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -439,7 +447,10 @@ admin.get("/users", async ({ headers, query, status }) => {
|
|||
username: u.username,
|
||||
avatar: u.avatar,
|
||||
slackId: u.slackId,
|
||||
email: (user.role === "admin" || user.role === "creator") ? u.email : undefined,
|
||||
email:
|
||||
user.role === "admin" || user.role === "creator"
|
||||
? u.email
|
||||
: undefined,
|
||||
scraps: u.scraps,
|
||||
role: u.role,
|
||||
internalNotes: u.internalNotes,
|
||||
|
|
@ -514,7 +525,10 @@ admin.get("/users/:id", async ({ params, headers, status }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (targetUser[0].email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(targetUser[0].email, targetUser[0].slackId);
|
||||
const htUser = await getHackatimeUser(
|
||||
targetUser[0].email,
|
||||
targetUser[0].slackId,
|
||||
);
|
||||
if (htUser) {
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
hackatimeBanned = htUser.banned || false;
|
||||
|
|
@ -530,7 +544,10 @@ admin.get("/users/:id", async ({ params, headers, status }) => {
|
|||
username: targetUser[0].username,
|
||||
avatar: targetUser[0].avatar,
|
||||
slackId: targetUser[0].slackId,
|
||||
email: (user.role === "admin" || user.role === "creator") ? targetUser[0].email : undefined,
|
||||
email:
|
||||
user.role === "admin" || user.role === "creator"
|
||||
? targetUser[0].email
|
||||
: undefined,
|
||||
scraps: scrapsBalance.balance,
|
||||
role: targetUser[0].role,
|
||||
internalNotes: targetUser[0].internalNotes,
|
||||
|
|
@ -876,7 +893,10 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (projectUser[0]?.email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
|
||||
const htUser = await getHackatimeUser(
|
||||
projectUser[0].email,
|
||||
projectUser[0].slackId,
|
||||
);
|
||||
if (htUser) {
|
||||
hackatimeUserId = htUser.user_id;
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
|
|
@ -944,6 +964,7 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
hoursOverride,
|
||||
tierOverride,
|
||||
userInternalNotes,
|
||||
rejectionReason,
|
||||
} = body as {
|
||||
action: "approved" | "denied" | "permanently_rejected";
|
||||
feedbackForAuthor: string;
|
||||
|
|
@ -951,6 +972,7 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
hoursOverride?: number;
|
||||
tierOverride?: number;
|
||||
userInternalNotes?: string;
|
||||
rejectionReason?: string;
|
||||
};
|
||||
|
||||
if (!["approved", "denied", "permanently_rejected"].includes(action)) {
|
||||
|
|
@ -961,6 +983,12 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
return { error: "Feedback for author is required" };
|
||||
}
|
||||
|
||||
if (action === "permanently_rejected" && !rejectionReason?.trim()) {
|
||||
return {
|
||||
error: "Reason shown to user is required for permanent rejection",
|
||||
};
|
||||
}
|
||||
|
||||
const projectId = parseInt(params.id);
|
||||
|
||||
// Get project to find user
|
||||
|
|
@ -1133,8 +1161,11 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
|
||||
// Trigger immediate Airtable sync when a project is shipped or updated
|
||||
if (canShipDirectly && action === "approved") {
|
||||
syncProjectsToAirtable().catch(err =>
|
||||
console.error("[ADMIN] Failed to trigger Airtable sync after ship:", err)
|
||||
syncProjectsToAirtable().catch((err) =>
|
||||
console.error(
|
||||
"[ADMIN] Failed to trigger Airtable sync after ship:",
|
||||
err,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1158,7 +1189,12 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
const admins = await db
|
||||
.select({ slackId: usersTable.slackId })
|
||||
.from(usersTable)
|
||||
.where(or(eq(usersTable.role, "admin"), eq(usersTable.role, "creator")));
|
||||
.where(
|
||||
or(
|
||||
eq(usersTable.role, "admin"),
|
||||
eq(usersTable.role, "creator"),
|
||||
),
|
||||
);
|
||||
|
||||
adminSlackIds = admins
|
||||
.map((a) => a.slackId)
|
||||
|
|
@ -1174,6 +1210,7 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
|
|||
projectId,
|
||||
action,
|
||||
feedbackForAuthor,
|
||||
rejectionReason: rejectionReason?.trim() || undefined,
|
||||
reviewerSlackId,
|
||||
adminSlackIds,
|
||||
scrapsAwarded,
|
||||
|
|
@ -1349,7 +1386,10 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (projectUser[0]?.email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
|
||||
const htUser = await getHackatimeUser(
|
||||
projectUser[0].email,
|
||||
projectUser[0].slackId,
|
||||
);
|
||||
if (htUser) {
|
||||
hackatimeUserId = htUser.user_id;
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
|
|
@ -1524,8 +1564,11 @@ admin.post("/second-pass/:id", async ({ params, body, headers }) => {
|
|||
}
|
||||
|
||||
// Trigger immediate Airtable sync after shipping
|
||||
syncProjectsToAirtable().catch(err =>
|
||||
console.error("[ADMIN] Failed to trigger Airtable sync after second-pass ship:", err)
|
||||
syncProjectsToAirtable().catch((err) =>
|
||||
console.error(
|
||||
"[ADMIN] Failed to trigger Airtable sync after second-pass ship:",
|
||||
err,
|
||||
),
|
||||
);
|
||||
|
||||
return { success: true, scrapsAwarded };
|
||||
|
|
@ -2332,7 +2375,9 @@ admin.get("/orders", async ({ headers, query, status }) => {
|
|||
const rows = await ordersQuery;
|
||||
|
||||
// Batch-check Hackatime ban status for unique user emails
|
||||
const uniqueEmails = [...new Set(rows.map((r) => r.userEmail).filter(Boolean))] as string[];
|
||||
const uniqueEmails = [
|
||||
...new Set(rows.map((r) => r.userEmail).filter(Boolean)),
|
||||
] as string[];
|
||||
const emailToSlackId = new Map<string, string | null>();
|
||||
for (const row of rows) {
|
||||
if (row.userEmail && !emailToSlackId.has(row.userEmail)) {
|
||||
|
|
@ -2343,7 +2388,10 @@ admin.get("/orders", async ({ headers, query, status }) => {
|
|||
await Promise.all(
|
||||
uniqueEmails.map(async (email) => {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(email, emailToSlackId.get(email));
|
||||
const htUser = await getHackatimeUser(
|
||||
email,
|
||||
emailToSlackId.get(email),
|
||||
);
|
||||
banMap.set(email, htUser?.banned ?? false);
|
||||
} catch {
|
||||
banMap.set(email, false);
|
||||
|
|
@ -2488,7 +2536,10 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
const [orderItem] = await db
|
||||
.select({ name: shopItemsTable.name })
|
||||
.from(shopItemsTable)
|
||||
.innerJoin(shopOrdersTable, eq(shopOrdersTable.shopItemId, shopItemsTable.id))
|
||||
.innerJoin(
|
||||
shopOrdersTable,
|
||||
eq(shopOrdersTable.shopItemId, shopItemsTable.id),
|
||||
)
|
||||
.where(eq(shopOrdersTable.id, parseInt(params.id)))
|
||||
.limit(1);
|
||||
|
||||
|
|
@ -2498,7 +2549,7 @@ admin.patch("/orders/:id", async ({ params, body, headers, status }) => {
|
|||
itemName: orderItem.name,
|
||||
trackingNumber: updated[0].trackingNumber,
|
||||
token: config.slackBotToken,
|
||||
}).catch((err) => console.error('Failed to send fulfillment DM:', err));
|
||||
}).catch((err) => console.error("Failed to send fulfillment DM:", err));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3254,7 +3305,9 @@ admin.post(
|
|||
async ({ params, headers, body, status }) => {
|
||||
try {
|
||||
const admin = await requireAdmin(headers as Record<string, string>);
|
||||
const creator = !admin ? await requireCreator(headers as Record<string, string>) : null;
|
||||
const creator = !admin
|
||||
? await requireCreator(headers as Record<string, string>)
|
||||
: null;
|
||||
const user = admin || creator;
|
||||
if (!user) return status(401, { error: "Unauthorized" });
|
||||
|
||||
|
|
@ -3642,25 +3695,37 @@ admin.delete("/users/:id", async ({ params, headers, status }) => {
|
|||
await db.delete(sessionsTable).where(eq(sessionsTable.userId, targetId));
|
||||
|
||||
// 2. Delete user activity
|
||||
await db.delete(userActivityTable).where(eq(userActivityTable.userId, targetId));
|
||||
await db
|
||||
.delete(userActivityTable)
|
||||
.where(eq(userActivityTable.userId, targetId));
|
||||
|
||||
// 3. Delete shop hearts
|
||||
await db.delete(shopHeartsTable).where(eq(shopHeartsTable.userId, targetId));
|
||||
await db
|
||||
.delete(shopHeartsTable)
|
||||
.where(eq(shopHeartsTable.userId, targetId));
|
||||
|
||||
// 4. Delete shop penalties
|
||||
await db.delete(shopPenaltiesTable).where(eq(shopPenaltiesTable.userId, targetId));
|
||||
await db
|
||||
.delete(shopPenaltiesTable)
|
||||
.where(eq(shopPenaltiesTable.userId, targetId));
|
||||
|
||||
// 5. Delete shop rolls
|
||||
await db.delete(shopRollsTable).where(eq(shopRollsTable.userId, targetId));
|
||||
|
||||
// 6. Delete refinery spending history
|
||||
await db.delete(refinerySpendingHistoryTable).where(eq(refinerySpendingHistoryTable.userId, targetId));
|
||||
await db
|
||||
.delete(refinerySpendingHistoryTable)
|
||||
.where(eq(refinerySpendingHistoryTable.userId, targetId));
|
||||
|
||||
// 7. Delete refinery orders
|
||||
await db.delete(refineryOrdersTable).where(eq(refineryOrdersTable.userId, targetId));
|
||||
await db
|
||||
.delete(refineryOrdersTable)
|
||||
.where(eq(refineryOrdersTable.userId, targetId));
|
||||
|
||||
// 8. Delete shop orders
|
||||
await db.delete(shopOrdersTable).where(eq(shopOrdersTable.userId, targetId));
|
||||
await db
|
||||
.delete(shopOrdersTable)
|
||||
.where(eq(shopOrdersTable.userId, targetId));
|
||||
|
||||
// 9. Null out givenBy on bonuses given by this user (so other users' bonuses remain)
|
||||
await db
|
||||
|
|
@ -3669,21 +3734,29 @@ admin.delete("/users/:id", async ({ params, headers, status }) => {
|
|||
.where(eq(userBonusesTable.givenBy, targetId));
|
||||
|
||||
// 10. Delete user's own bonuses
|
||||
await db.delete(userBonusesTable).where(eq(userBonusesTable.userId, targetId));
|
||||
await db
|
||||
.delete(userBonusesTable)
|
||||
.where(eq(userBonusesTable.userId, targetId));
|
||||
|
||||
// 11. Delete reviews done by this user as reviewer
|
||||
await db.delete(reviewsTable).where(eq(reviewsTable.reviewerId, targetId));
|
||||
|
||||
if (projectIds.length > 0) {
|
||||
// 12. Delete project activity for user's projects (from any user)
|
||||
await db.delete(projectActivityTable).where(inArray(projectActivityTable.projectId, projectIds));
|
||||
await db
|
||||
.delete(projectActivityTable)
|
||||
.where(inArray(projectActivityTable.projectId, projectIds));
|
||||
|
||||
// 13. Delete reviews on user's projects
|
||||
await db.delete(reviewsTable).where(inArray(reviewsTable.projectId, projectIds));
|
||||
await db
|
||||
.delete(reviewsTable)
|
||||
.where(inArray(reviewsTable.projectId, projectIds));
|
||||
}
|
||||
|
||||
// 14. Delete project activity by this user (on any project)
|
||||
await db.delete(projectActivityTable).where(eq(projectActivityTable.userId, targetId));
|
||||
await db
|
||||
.delete(projectActivityTable)
|
||||
.where(eq(projectActivityTable.userId, targetId));
|
||||
|
||||
// 15. Delete user's projects
|
||||
await db.delete(projectsTable).where(eq(projectsTable.userId, targetId));
|
||||
|
|
|
|||
|
|
@ -455,6 +455,7 @@ export default {
|
|||
noRefinements: 'no refinements to show',
|
||||
roles: {
|
||||
admin: 'admin',
|
||||
creator: 'creator',
|
||||
reviewer: 'reviewer',
|
||||
member: 'member',
|
||||
banned: 'banned'
|
||||
|
|
|
|||
|
|
@ -457,6 +457,7 @@ export default {
|
|||
noRefinements: 'no hay refinamientos para mostrar',
|
||||
roles: {
|
||||
admin: 'admin',
|
||||
creator: 'creador',
|
||||
reviewer: 'revisor',
|
||||
member: 'miembro',
|
||||
banned: 'baneado'
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@
|
|||
|
||||
let confirmAction = $state<'approved' | 'denied' | 'permanently_rejected' | null>(null);
|
||||
let errorModal = $state<string | null>(null);
|
||||
let rejectionReason = $state('');
|
||||
|
||||
let projectId = $derived(page.params.id);
|
||||
|
||||
|
|
@ -227,6 +228,10 @@
|
|||
error = 'Internal justification is required';
|
||||
return;
|
||||
}
|
||||
if (action === 'permanently_rejected' && !rejectionReason.trim()) {
|
||||
error = 'Reason shown to user is required for permanent rejection';
|
||||
return;
|
||||
}
|
||||
confirmAction = action;
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +252,10 @@
|
|||
error = 'Internal justification is required';
|
||||
return;
|
||||
}
|
||||
if (confirmAction === 'permanently_rejected' && !rejectionReason.trim()) {
|
||||
error = 'Reason shown to user is required for permanent rejection';
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
error = null;
|
||||
|
|
@ -262,7 +271,11 @@
|
|||
internalJustification: internalJustification || undefined,
|
||||
hoursOverride: hoursOverride !== undefined ? hoursOverride : undefined,
|
||||
tierOverride: tierOverride !== undefined ? tierOverride : undefined,
|
||||
userInternalNotes: userInternalNotes || undefined
|
||||
userInternalNotes: userInternalNotes || undefined,
|
||||
rejectionReason:
|
||||
confirmAction === 'permanently_rejected' && rejectionReason.trim()
|
||||
? rejectionReason.trim()
|
||||
: undefined
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -931,8 +944,23 @@
|
|||
class="w-full resize-none rounded-lg border-2 border-black px-4 py-2 focus:border-dashed focus:outline-none"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
⚠️ for permanent rejection: this is kept internal — the user will only be told their
|
||||
project was unshipped by an admin
|
||||
⚠️ for permanent rejection: the above field is kept internal only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-bold">
|
||||
reason shown to user <span class="text-red-500">*</span>
|
||||
<span class="text-gray-400">(for permanent rejection)</span>
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={rejectionReason}
|
||||
rows="3"
|
||||
placeholder="This reason will be included in the Slack DM to the user when permanently rejecting."
|
||||
class="w-full resize-none rounded-lg border-2 border-black px-4 py-2 focus:border-dashed focus:outline-none"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
required for permanent rejection — sent to the user via Slack DM
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue