From 835c70809470a238096aa17f438b373b7ca21e14 Mon Sep 17 00:00:00 2001 From: axv2655 Date: Fri, 24 Jul 2026 18:23:57 -0500 Subject: [PATCH 1/5] feat: scan points --- .../components/ApplicationDetailPanel.tsx | 3 + .../components/ApplicationsTable.tsx | 4 +- .../src/pages/admin/all-applicants/types.ts | 1 + .../web/src/pages/admin/scans/ScansPage.tsx | 6 +- client/web/src/pages/admin/scans/api.ts | 21 ++ .../admin/scans/components/PointsNameCard.tsx | 71 ++++++ .../admin/scans/components/ScanTypesTable.tsx | 40 +++- .../admin/scans/components/ScannerDialog.tsx | 7 + client/web/src/pages/admin/scans/store.ts | 35 +++ client/web/src/pages/admin/scans/types.ts | 6 + client/web/src/pages/admin/scans/utils.ts | 3 + .../src/pages/hacker/profile/ProfilePage.tsx | 37 ++- client/web/src/types.ts | 3 + cmd/api/api.go | 3 + cmd/api/applications.go | 23 ++ cmd/api/applications_test.go | 16 ++ cmd/api/resume.go | 7 + cmd/api/resume_test.go | 1 + cmd/api/scans.go | 1 + cmd/api/scans_test.go | 81 ++++++- cmd/api/settings.go | 97 ++++++++ cmd/api/settings_test.go | 99 ++++++++ .../000024_alter_scans_add_points.down.sql | 1 + .../000024_alter_scans_add_points.up.sql | 1 + docs/docs.go | 219 ++++++++++++++++++ internal/store/applications.go | 6 +- internal/store/mock_store.go | 15 ++ internal/store/scans.go | 27 ++- internal/store/settings.go | 50 ++++ internal/store/storage.go | 3 + 30 files changed, 861 insertions(+), 26 deletions(-) create mode 100644 client/web/src/pages/admin/scans/components/PointsNameCard.tsx create mode 100644 cmd/migrate/migrations/000024_alter_scans_add_points.down.sql create mode 100644 cmd/migrate/migrations/000024_alter_scans_add_points.up.sql diff --git a/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx b/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx index 423ecc0f..4a6eaca5 100644 --- a/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx +++ b/client/web/src/pages/admin/all-applicants/components/ApplicationDetailPanel.tsx @@ -76,6 +76,9 @@ export const ApplicationDetailPanel = memo(function ApplicationDetailPanel({ {application.status} + + {application.points ?? 0} pts + ) : null} diff --git a/client/web/src/pages/admin/all-applicants/components/ApplicationsTable.tsx b/client/web/src/pages/admin/all-applicants/components/ApplicationsTable.tsx index f7067016..69d602df 100644 --- a/client/web/src/pages/admin/all-applicants/components/ApplicationsTable.tsx +++ b/client/web/src/pages/admin/all-applicants/components/ApplicationsTable.tsx @@ -51,12 +51,13 @@ export const ApplicationsTable = memo(function ApplicationsTable({ Created Updated AI Percent + Points {applications.length === 0 ? ( - + No applications found @@ -107,6 +108,7 @@ export const ApplicationsTable = memo(function ApplicationsTable({ {app.ai_percent != null ? `${app.ai_percent}%` : "-"} + {app.points} )) )} diff --git a/client/web/src/pages/admin/all-applicants/types.ts b/client/web/src/pages/admin/all-applicants/types.ts index 50f6b836..fe19a7aa 100644 --- a/client/web/src/pages/admin/all-applicants/types.ts +++ b/client/web/src/pages/admin/all-applicants/types.ts @@ -30,6 +30,7 @@ export interface ApplicationListItem { reviews_assigned: number; reviews_completed: number; has_resume: boolean; + points: number; } export interface ApplicationListResult { diff --git a/client/web/src/pages/admin/scans/ScansPage.tsx b/client/web/src/pages/admin/scans/ScansPage.tsx index b7db5e03..94b56482 100644 --- a/client/web/src/pages/admin/scans/ScansPage.tsx +++ b/client/web/src/pages/admin/scans/ScansPage.tsx @@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { useUserStore } from "@/shared/stores/user"; +import { PointsNameCard } from "./components/PointsNameCard"; import { ScannerDialog } from "./components/ScannerDialog"; import { ScanStatsCards } from "./components/ScanStatsCards"; import { ScanTypesTable } from "./components/ScanTypesTable"; @@ -20,6 +21,7 @@ export default function ScansPage() { rebalancing, fetchTypes, fetchStats, + fetchPointsName, saveScanTypes, rebalanceStats, setActiveScanType, @@ -31,12 +33,13 @@ export default function ScansPage() { const controller = new AbortController(); fetchTypes(controller.signal); fetchStats(controller.signal); + fetchPointsName(controller.signal); return () => { controller.abort(); // Reset active scan type so dialog doesn't reopen on navigate back setActiveScanType(null); }; - }, [fetchTypes, fetchStats, setActiveScanType]); + }, [fetchTypes, fetchStats, fetchPointsName, setActiveScanType]); if (typesLoading && scanTypes.length === 0) { return ( @@ -74,6 +77,7 @@ export default function ScansPage() { stats={stats} loading={typesLoading || statsLoading} /> + > { + return getRequest( + "/points-name", + "points name", + signal, + ); +} + +export async function savePointsName( + name: string, +): Promise> { + return postRequest( + "/superadmin/settings/points-name", + { name }, + "points name", + ); +} diff --git a/client/web/src/pages/admin/scans/components/PointsNameCard.tsx b/client/web/src/pages/admin/scans/components/PointsNameCard.tsx new file mode 100644 index 00000000..9616712e --- /dev/null +++ b/client/web/src/pages/admin/scans/components/PointsNameCard.tsx @@ -0,0 +1,71 @@ +import { Coins, Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +import { useScansStore } from "../store"; + +interface PointsNameCardProps { + isSuperAdmin: boolean; +} + +export function PointsNameCard({ isSuperAdmin }: PointsNameCardProps) { + const { pointsName, savingPointsName, savePointsName } = useScansStore(); + const [draft, setDraft] = useState(pointsName); + + useEffect(() => { + setDraft(pointsName); + }, [pointsName]); + + if (!isSuperAdmin) { + return null; + } + + const dirty = draft.trim() !== pointsName; + + const handleSave = async () => { + const trimmed = draft.trim(); + if (!trimmed) { + toast.error("Points system name must not be empty"); + return; + } + if (trimmed.length > 30) { + toast.error("Points system name must be at most 30 characters"); + return; + } + await savePointsName(trimmed); + }; + + return ( +
+ + setDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && dirty) handleSave(); + }} + placeholder="Points system name" + className="h-8 w-44 text-sm font-light" + maxLength={30} + /> + {dirty && ( + + )} +
+ ); +} diff --git a/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx b/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx index 9ac11bba..e7c67942 100644 --- a/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx +++ b/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx @@ -72,6 +72,7 @@ export function ScanTypesTable({ }: ScanTypesTableProps) { const [editingIndex, setEditingIndex] = useState(null); const [editDisplayName, setEditDisplayName] = useState(""); + const [editPoints, setEditPoints] = useState("0"); const [deleteIndex, setDeleteIndex] = useState(null); const [pendingNew, setPendingNew] = useState(null); const [rebalanceOpen, setRebalanceOpen] = useState(false); @@ -90,6 +91,7 @@ export function ScanTypesTable({ setEditingIndex(index); if (scanTypes[index]) { setEditDisplayName(scanTypes[index].display_name); + setEditPoints(String(scanTypes[index].points ?? 0)); } }; @@ -97,6 +99,8 @@ export function ScanTypesTable({ if (editingIndex === null) return; const trimmed = editDisplayName.trim(); + const parsedPoints = Number.parseInt(editPoints, 10); + const points = Number.isNaN(parsedPoints) ? 0 : parsedPoints; // Pending new row — save only if user typed something, otherwise no-op if (pendingNew) { @@ -105,6 +109,7 @@ export function ScanTypesTable({ ...pendingNew, display_name: trimmed, name: toSnakeCase(trimmed), + points, }; const updated = [...scanTypes, newType]; @@ -124,11 +129,11 @@ export function ScanTypesTable({ if (!current) return; // No change — skip save - if (trimmed === current.display_name) return; + if (trimmed === current.display_name && points === current.points) return; const updated = scanTypes.map((st, i) => i === editingIndex - ? { ...st, display_name: trimmed, name: toSnakeCase(trimmed) } + ? { ...st, display_name: trimmed, name: toSnakeCase(trimmed), points } : st, ); @@ -139,7 +144,7 @@ export function ScanTypesTable({ } onSave(updated); - }, [editingIndex, editDisplayName, scanTypes, pendingNew, onSave]); + }, [editingIndex, editDisplayName, editPoints, scanTypes, pendingNew, onSave]); // Ref to avoid stale closures in event listeners const saveDisplayNameRef = useRef(saveDisplayName); @@ -197,10 +202,12 @@ export function ScanTypesTable({ display_name: "", category: "other", is_active: true, + points: 0, }; setPendingNew(newType); setEditingIndex(scanTypes.length); setEditDisplayName(""); + setEditPoints("0"); }; const handleDelete = (index: number) => { @@ -295,6 +302,7 @@ export function ScanTypesTable({ Action Name Category + Points Scans {isSuperAdmin && Active} @@ -375,6 +383,29 @@ export function ScanTypesTable({ })} + + setEditPoints(e.target.value)} + onBlur={(e) => { + if ( + editRowRef.current?.contains( + e.relatedTarget as Node, + ) + ) + return; + saveDisplayName(); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + closeEditing(); + } + }} + className="h-8 w-20 text-sm font-light shadow-none bg-transparent pl-2 rounded-sm focus-visible:ring-1" + /> + {count}
@@ -461,6 +492,9 @@ export function ScanTypesTable({
+ + {scanType.points ?? 0} + {count} {isSuperAdmin && ( diff --git a/client/web/src/pages/admin/scans/components/ScannerDialog.tsx b/client/web/src/pages/admin/scans/components/ScannerDialog.tsx index bc291c38..00d6ddae 100644 --- a/client/web/src/pages/admin/scans/components/ScannerDialog.tsx +++ b/client/web/src/pages/admin/scans/components/ScannerDialog.tsx @@ -19,6 +19,7 @@ export function ScannerDialog() { activeScanType, lastScanResult, scanning, + pointsName, setActiveScanType, performScan, clearLastResult, @@ -103,6 +104,12 @@ export function ScannerDialog() { )}

{lastScanResult.message}

+ {lastScanResult.success && + (lastScanResult.scan?.points ?? 0) > 0 && ( +

+ +{lastScanResult.scan?.points} {pointsName} +

+ )} + )} + + + + ); +} From 0e581f3fd96d718183f67044d7ff20571642f17f Mon Sep 17 00:00:00 2001 From: axv2655 Date: Fri, 24 Jul 2026 21:32:32 -0500 Subject: [PATCH 4/5] refactor: ran prettier for ci --- client/web/src/pages/admin/scans/api.ts | 6 +----- .../src/pages/admin/scans/components/ScanTypesTable.tsx | 9 ++++++++- .../web/src/pages/superadmin/settings/tabs/ScansTab.tsx | 6 +----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/client/web/src/pages/admin/scans/api.ts b/client/web/src/pages/admin/scans/api.ts index 8fdd833d..e9f5ccd3 100644 --- a/client/web/src/pages/admin/scans/api.ts +++ b/client/web/src/pages/admin/scans/api.ts @@ -66,11 +66,7 @@ export async function saveScanTypes( export async function fetchPointsName( signal?: AbortSignal, ): Promise> { - return getRequest( - "/points-name", - "points name", - signal, - ); + return getRequest("/points-name", "points name", signal); } export async function savePointsName( diff --git a/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx b/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx index e7c67942..b7ebccbb 100644 --- a/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx +++ b/client/web/src/pages/admin/scans/components/ScanTypesTable.tsx @@ -144,7 +144,14 @@ export function ScanTypesTable({ } onSave(updated); - }, [editingIndex, editDisplayName, editPoints, scanTypes, pendingNew, onSave]); + }, [ + editingIndex, + editDisplayName, + editPoints, + scanTypes, + pendingNew, + onSave, + ]); // Ref to avoid stale closures in event listeners const saveDisplayNameRef = useRef(saveDisplayName); diff --git a/client/web/src/pages/superadmin/settings/tabs/ScansTab.tsx b/client/web/src/pages/superadmin/settings/tabs/ScansTab.tsx index d436dab6..17dfd97c 100644 --- a/client/web/src/pages/superadmin/settings/tabs/ScansTab.tsx +++ b/client/web/src/pages/superadmin/settings/tabs/ScansTab.tsx @@ -105,11 +105,7 @@ export default function ScansTab() { disabled={saving} onClick={handleSave} > - {saving ? ( - - ) : ( - "Save" - )} + {saving ? : "Save"} )} From a32e8f33282ec5f6e5b7d6280505736f554e3ab8 Mon Sep 17 00:00:00 2001 From: axv2655 Date: Fri, 24 Jul 2026 21:35:24 -0500 Subject: [PATCH 5/5] fix: fixed build --- client/web/src/pages/admin/scans/components/ScanStatsCards.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/web/src/pages/admin/scans/components/ScanStatsCards.tsx b/client/web/src/pages/admin/scans/components/ScanStatsCards.tsx index 37bf6747..c2c6f7a4 100644 --- a/client/web/src/pages/admin/scans/components/ScanStatsCards.tsx +++ b/client/web/src/pages/admin/scans/components/ScanStatsCards.tsx @@ -2,6 +2,7 @@ import { DoorOpen, Gift, MoreHorizontal, + ShoppingBag, UserCheck, Utensils, } from "lucide-react"; @@ -19,6 +20,7 @@ const categoryIcons: Record = { check_in: UserCheck, meal: Utensils, swag: Gift, + shop: ShoppingBag, other: MoreHorizontal, walk_in: DoorOpen, };