Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export const ApplicationDetailPanel = memo(function ApplicationDetailPanel({
<Badge className={getStatusColor(application.status)}>
{application.status}
</Badge>
<Badge variant="secondary" className="tabular-nums">
{application.points ?? 0} pts
</Badge>
</>
) : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ export const ApplicationsTable = memo(function ApplicationsTable({
<TableHead>Created</TableHead>
<TableHead>Updated</TableHead>
<TableHead>AI Percent</TableHead>
<TableHead>Points</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{applications.length === 0 ? (
<TableRow>
<TableCell colSpan={14} className="text-center text-gray-500">
<TableCell colSpan={16} className="text-center text-gray-500">
No applications found
</TableCell>
</TableRow>
Expand Down Expand Up @@ -107,6 +108,7 @@ export const ApplicationsTable = memo(function ApplicationsTable({
<TableCell>
{app.ai_percent != null ? `${app.ai_percent}%` : "-"}
</TableCell>
<TableCell className="tabular-nums">{app.points}</TableCell>
</TableRow>
))
)}
Expand Down
1 change: 1 addition & 0 deletions client/web/src/pages/admin/all-applicants/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ApplicationListItem {
reviews_assigned: number;
reviews_completed: number;
has_resume: boolean;
points: number;
}

export interface ApplicationListResult {
Expand Down
4 changes: 3 additions & 1 deletion client/web/src/pages/admin/scans/ScansPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default function ScansPage() {
rebalancing,
fetchTypes,
fetchStats,
fetchPointsName,
saveScanTypes,
rebalanceStats,
setActiveScanType,
Expand All @@ -31,12 +32,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 (
Expand Down
17 changes: 17 additions & 0 deletions client/web/src/pages/admin/scans/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getRequest, postRequest, putRequest } from "@/shared/lib/api";
import type { ApiResponse } from "@/types";

import type {
PointsNameResponse,
Scan,
ScanStatsResponse,
ScanType,
Expand Down Expand Up @@ -61,3 +62,19 @@ export async function saveScanTypes(
"scan types",
);
}

export async function fetchPointsName(
signal?: AbortSignal,
): Promise<ApiResponse<PointsNameResponse>> {
return getRequest<PointsNameResponse>("/points-name", "points name", signal);
}

export async function savePointsName(
name: string,
): Promise<ApiResponse<PointsNameResponse>> {
return postRequest<PointsNameResponse>(
"/superadmin/settings/points-name",
{ name },
"points name",
);
}
71 changes: 71 additions & 0 deletions client/web/src/pages/admin/scans/components/PointsNameCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex items-center gap-2">
<Coins className="size-4 text-muted-foreground" />
<Input
value={draft}
onChange={(e) => 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 && (
<Button
size="sm"
variant="outline"
className="cursor-pointer"
disabled={savingPointsName}
onClick={handleSave}
>
{savingPointsName ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
"Save"
)}
</Button>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
DoorOpen,
Gift,
MoreHorizontal,
ShoppingBag,
UserCheck,
Utensils,
} from "lucide-react";
Expand All @@ -19,6 +20,7 @@ const categoryIcons: Record<ScanTypeCategory, typeof UserCheck> = {
check_in: UserCheck,
meal: Utensils,
swag: Gift,
shop: ShoppingBag,
other: MoreHorizontal,
walk_in: DoorOpen,
};
Expand Down
47 changes: 44 additions & 3 deletions client/web/src/pages/admin/scans/components/ScanTypesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function ScanTypesTable({
}: ScanTypesTableProps) {
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [editDisplayName, setEditDisplayName] = useState("");
const [editPoints, setEditPoints] = useState("0");
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
const [pendingNew, setPendingNew] = useState<ScanType | null>(null);
const [rebalanceOpen, setRebalanceOpen] = useState(false);
Expand All @@ -90,13 +91,16 @@ export function ScanTypesTable({
setEditingIndex(index);
if (scanTypes[index]) {
setEditDisplayName(scanTypes[index].display_name);
setEditPoints(String(scanTypes[index].points ?? 0));
}
};

const saveDisplayName = useCallback(() => {
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) {
Expand All @@ -105,6 +109,7 @@ export function ScanTypesTable({
...pendingNew,
display_name: trimmed,
name: toSnakeCase(trimmed),
points,
};
const updated = [...scanTypes, newType];

Expand All @@ -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,
);

Expand All @@ -139,7 +144,14 @@ 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);
Expand Down Expand Up @@ -197,10 +209,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) => {
Expand Down Expand Up @@ -295,6 +309,7 @@ export function ScanTypesTable({
<TableHead className="w-24">Action</TableHead>
<TableHead className="w-48">Name</TableHead>
<TableHead className="w-150">Category</TableHead>
<TableHead className="w-24">Points</TableHead>
<TableHead className="w-24">Scans</TableHead>
{isSuperAdmin && <TableHead>Active</TableHead>}
</TableRow>
Expand Down Expand Up @@ -375,6 +390,29 @@ export function ScanTypesTable({
})}
</div>
</TableCell>
<TableCell>
<Input
type="number"
min={0}
value={editPoints}
onChange={(e) => 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"
/>
</TableCell>
<TableCell className="tabular-nums">{count}</TableCell>
<TableCell>
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -461,6 +499,9 @@ export function ScanTypesTable({
</Badge>
</div>
</TableCell>
<TableCell className="tabular-nums">
{scanType.points ?? 0}
</TableCell>
<TableCell className="tabular-nums">{count}</TableCell>
{isSuperAdmin && (
<TableCell>
Expand Down
16 changes: 16 additions & 0 deletions client/web/src/pages/admin/scans/components/ScannerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function ScannerDialog() {
activeScanType,
lastScanResult,
scanning,
pointsName,
setActiveScanType,
performScan,
clearLastResult,
Expand Down Expand Up @@ -103,6 +104,21 @@ export function ScannerDialog() {
<XCircle className="size-12" />
)}
<p className="text-lg font-medium">{lastScanResult.message}</p>
{lastScanResult.success &&
(lastScanResult.scan?.points ?? 0) !== 0 && (
<p className="text-sm font-medium">
{(lastScanResult.scan?.points ?? 0) > 0
? `+${lastScanResult.scan?.points}`
: `−${Math.abs(lastScanResult.scan?.points ?? 0)}`}{" "}
{pointsName}
</p>
)}
{lastScanResult.success &&
lastScanResult.scan?.balance !== undefined && (
<p className="text-sm font-light">
Balance: {lastScanResult.scan.balance} {pointsName}
</p>
)}
<Button variant="outline" onClick={handleResume}>
<ScanLine className="mr-2 size-4" />
Scan Next
Expand Down
Loading
Loading