chore: standardize security and quality foundations
This commit is contained in:
@@ -1,115 +1,111 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
|
||||
import AdminShell from '../../ui/AdminShell'
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { authFetch, clearToken, getApiBase, getToken } from "../../lib/auth";
|
||||
import AdminShell from "../../ui/AdminShell";
|
||||
|
||||
type RequestRow = {
|
||||
id: number
|
||||
title?: string | null
|
||||
year?: number | null
|
||||
type?: string | null
|
||||
statusLabel?: string | null
|
||||
requestedBy?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
id: number;
|
||||
title?: string | null;
|
||||
year?: number | null;
|
||||
type?: string | null;
|
||||
statusLabel?: string | null;
|
||||
requestedBy?: string | null;
|
||||
createdAt?: string | null;
|
||||
};
|
||||
|
||||
const REQUEST_STAGE_OPTIONS = [
|
||||
{ value: 'all', label: 'All stages' },
|
||||
{ value: 'pending', label: 'Waiting for approval' },
|
||||
{ value: 'approved', label: 'Approved' },
|
||||
{ value: 'in_progress', label: 'In progress' },
|
||||
{ value: 'working', label: 'Working on it' },
|
||||
{ value: 'partial', label: 'Partially ready' },
|
||||
{ value: 'ready', label: 'Ready to watch' },
|
||||
{ value: 'declined', label: 'Declined' },
|
||||
]
|
||||
{ value: "all", label: "All stages" },
|
||||
{ value: "pending", label: "Waiting for approval" },
|
||||
{ value: "approved", label: "Approved" },
|
||||
{ value: "in_progress", label: "In progress" },
|
||||
{ value: "working", label: "Working on it" },
|
||||
{ value: "partial", label: "Partially ready" },
|
||||
{ value: "ready", label: "Ready to watch" },
|
||||
{ value: "declined", label: "Declined" },
|
||||
];
|
||||
|
||||
const formatDateTime = (value?: string | null) => {
|
||||
if (!value) return 'Unknown'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return value
|
||||
return date.toLocaleString()
|
||||
}
|
||||
if (!value) return "Unknown";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.valueOf())) return value;
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
export default function AdminRequestsAllPage() {
|
||||
const router = useRouter()
|
||||
const [rows, setRows] = useState<RequestRow[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [page, setPage] = useState(1)
|
||||
const [stage, setStage] = useState('all')
|
||||
const router = useRouter();
|
||||
const [rows, setRows] = useState<RequestRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [page, setPage] = useState(1);
|
||||
const [stage, setStage] = useState("all");
|
||||
|
||||
const pageCount = useMemo(() => {
|
||||
if (!total || pageSize <= 0) return 1
|
||||
return Math.max(1, Math.ceil(total / pageSize))
|
||||
}, [total, pageSize])
|
||||
if (!total || pageSize <= 0) return 1;
|
||||
return Math.max(1, Math.ceil(total / pageSize));
|
||||
}, [total, pageSize]);
|
||||
|
||||
const load = async () => {
|
||||
const load = useCallback(async () => {
|
||||
if (!getToken()) {
|
||||
router.push('/login')
|
||||
return
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const skip = (page - 1) * pageSize
|
||||
const baseUrl = getApiBase();
|
||||
const skip = (page - 1) * pageSize;
|
||||
const params = new URLSearchParams({
|
||||
take: String(pageSize),
|
||||
skip: String(skip),
|
||||
})
|
||||
if (stage !== 'all') {
|
||||
params.set('stage', stage)
|
||||
});
|
||||
if (stage !== "all") {
|
||||
params.set("stage", stage);
|
||||
}
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/requests/all?${params.toString()}`
|
||||
)
|
||||
const response = await authFetch(`${baseUrl}/admin/requests/all?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
clearToken()
|
||||
router.push('/login')
|
||||
return
|
||||
clearToken();
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
if (response.status === 403) {
|
||||
router.push('/')
|
||||
return
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
throw new Error(`Load failed: ${response.status}`)
|
||||
throw new Error(`Load failed: ${response.status}`);
|
||||
}
|
||||
const data = await response.json()
|
||||
setRows(Array.isArray(data?.results) ? data.results : [])
|
||||
setTotal(Number(data?.total ?? 0))
|
||||
const data = await response.json();
|
||||
setRows(Array.isArray(data?.results) ? data.results : []);
|
||||
setTotal(Number(data?.total ?? 0));
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Unable to load requests.')
|
||||
console.error(err);
|
||||
setError("Unable to load requests.");
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [page, pageSize, router, stage]);
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [page, pageSize, stage])
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page > pageCount) {
|
||||
setPage(pageCount)
|
||||
setPage(pageCount);
|
||||
}
|
||||
}, [pageCount, page])
|
||||
}, [pageCount, page]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [stage])
|
||||
void stage;
|
||||
setPage(1);
|
||||
}, [stage]);
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
title="All requests"
|
||||
subtitle="Paginated view of every cached request."
|
||||
>
|
||||
<AdminShell title="All requests" subtitle="Paginated view of every cached request.">
|
||||
<section className="admin-section">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar-info">
|
||||
@@ -160,10 +156,10 @@ export default function AdminRequestsAllPage() {
|
||||
>
|
||||
<span>
|
||||
{row.title || `Request #${row.id}`}
|
||||
{row.year ? ` (${row.year})` : ''}
|
||||
{row.year ? ` (${row.year})` : ""}
|
||||
</span>
|
||||
<span>{row.statusLabel || 'Unknown'}</span>
|
||||
<span>{row.requestedBy || 'Unknown'}</span>
|
||||
<span>{row.statusLabel || "Unknown"}</span>
|
||||
<span>{row.requestedBy || "Unknown"}</span>
|
||||
<span>{formatDateTime(row.createdAt)}</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -179,22 +175,14 @@ export default function AdminRequestsAllPage() {
|
||||
<span>
|
||||
Page {page} of {pageCount}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page >= pageCount}
|
||||
>
|
||||
<button type="button" onClick={() => setPage(page + 1)} disabled={page >= pageCount}>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPage(pageCount)}
|
||||
disabled={page >= pageCount}
|
||||
>
|
||||
<button type="button" onClick={() => setPage(pageCount)} disabled={page >= pageCount}>
|
||||
Last
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user