feat(timesheets): move upload status to its own unlinked page
Timesheets now shows only entries + filters; an "Upload status" button next to Import Timesheets opens the sheet-by-sheet processing view on its own route, not listed in the sidebar.
This commit is contained in:
parent
c3914165af
commit
607de98e89
4 changed files with 157 additions and 98 deletions
|
|
@ -1,7 +1,10 @@
|
|||
import Link from "next/link";
|
||||
import { ClipboardList } from "lucide-react";
|
||||
import { pbAdmin } from "@/lib/pb";
|
||||
import { Topbar } from "@/components/layout/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ImportTimesheetsButton } from "@/components/import-timesheets-button";
|
||||
import { TimesheetsView, type TimesheetEntryRow, type UploadRow } from "./timesheets-view";
|
||||
import { TimesheetsView, type TimesheetEntryRow } from "./timesheets-view";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
|
|
@ -16,9 +19,7 @@ export default async function TimesheetsPage() {
|
|||
pb.collection("employees").getFullList({ sort: "name" }),
|
||||
pb.collection("sites").getFullList({ sort: "name" }),
|
||||
pb.collection("timesheets").getFullList({
|
||||
sort: "-date_uploaded",
|
||||
fields:
|
||||
"id,source_file,date_uploaded,processing_status,rows_created,rows_failed,last_error",
|
||||
fields: "id,processing_status",
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
@ -48,34 +49,31 @@ export default async function TimesheetsPage() {
|
|||
};
|
||||
});
|
||||
|
||||
const uploads: UploadRow[] = documents.map((d) => ({
|
||||
id: d.id,
|
||||
sourceFile: (d.source_file as string) ?? "",
|
||||
dateUploaded: (d.date_uploaded as string) ?? "",
|
||||
processingStatus: (d.processing_status as string) ?? "",
|
||||
rowsCreated: (d.rows_created as number) ?? 0,
|
||||
rowsFailed: (d.rows_failed as number) ?? 0,
|
||||
lastError: (d.last_error as string) ?? "",
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Topbar
|
||||
title="Timesheets"
|
||||
subtitle="Every entry, every site, every source — flat list with upload status surfaced."
|
||||
subtitle="Every entry, every site, every source."
|
||||
right={
|
||||
<ImportTimesheetsButton
|
||||
sites={sites.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
project_code: s.project_code,
|
||||
}))}
|
||||
/>
|
||||
<>
|
||||
<Button variant="outline" size="sm" asChild className="gap-1.5">
|
||||
<Link href="/dashboard/workforce/timesheets/upload-status">
|
||||
<ClipboardList className="h-3.5 w-3.5" />
|
||||
Upload status
|
||||
</Link>
|
||||
</Button>
|
||||
<ImportTimesheetsButton
|
||||
sites={sites.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
project_code: s.project_code,
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<TimesheetsView
|
||||
rows={rows}
|
||||
uploads={uploads}
|
||||
sites={sites.map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
|
|
|
|||
|
|
@ -47,24 +47,11 @@ export interface TimesheetEntryRow {
|
|||
documentStatus?: string | null;
|
||||
}
|
||||
|
||||
export interface UploadRow {
|
||||
id: string;
|
||||
sourceFile: string;
|
||||
/** ISO date string (YYYY-MM-DD) or empty. */
|
||||
dateUploaded: string;
|
||||
processingStatus: string;
|
||||
rowsCreated: number;
|
||||
rowsFailed: number;
|
||||
lastError: string;
|
||||
}
|
||||
|
||||
export function TimesheetsView({
|
||||
rows,
|
||||
uploads,
|
||||
sites,
|
||||
}: {
|
||||
rows: TimesheetEntryRow[];
|
||||
uploads: UploadRow[];
|
||||
sites: { id: string; name: string; project_code: string }[];
|
||||
}) {
|
||||
const [siteFilter, setSiteFilter] = React.useState<string>("all");
|
||||
|
|
@ -197,68 +184,6 @@ export function TimesheetsView({
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Upload status</CardTitle>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Every uploaded sheet and whether it processed cleanly — separate from
|
||||
the individual hour entries below.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{uploads.length === 0 ? (
|
||||
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
No timesheets uploaded yet.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>File</TableHead>
|
||||
<TableHead>Uploaded</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Rows created</TableHead>
|
||||
<TableHead className="text-right">Rows failed</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{uploads.map((u) => (
|
||||
<TableRow key={u.id}>
|
||||
<TableCell className="max-w-[220px] truncate text-sm" title={u.sourceFile}>
|
||||
{u.sourceFile || "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs tabular-nums">
|
||||
{u.dateUploaded ? format(parseISO(u.dateUploaded), "yyyy-MM-dd") : "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"capitalize",
|
||||
u.processingStatus === "failed" &&
|
||||
"border-red-500/30 bg-red-500/15 text-red-700 dark:text-red-400",
|
||||
u.processingStatus === "processed" &&
|
||||
"border-emerald-500/30 bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
|
||||
)}
|
||||
title={u.lastError || undefined}
|
||||
>
|
||||
{u.processingStatus || "pending"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatNumber(u.rowsCreated)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatNumber(u.rowsFailed)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { pbAdmin } from "@/lib/pb";
|
||||
import { Topbar } from "@/components/layout/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UploadStatusView, type UploadRow } from "./upload-status-view";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function UploadStatusPage() {
|
||||
const pb = await pbAdmin();
|
||||
|
||||
const documents = await pb.collection("timesheets").getFullList({
|
||||
sort: "-date_uploaded",
|
||||
fields: "id,source_file,date_uploaded,processing_status,rows_created,rows_failed,last_error",
|
||||
});
|
||||
|
||||
const uploads: UploadRow[] = documents.map((d) => ({
|
||||
id: d.id,
|
||||
sourceFile: (d.source_file as string) ?? "",
|
||||
dateUploaded: (d.date_uploaded as string) ?? "",
|
||||
processingStatus: (d.processing_status as string) ?? "",
|
||||
rowsCreated: (d.rows_created as number) ?? 0,
|
||||
rowsFailed: (d.rows_failed as number) ?? 0,
|
||||
lastError: (d.last_error as string) ?? "",
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Topbar
|
||||
title="Upload status"
|
||||
subtitle="Every uploaded timesheet and whether it processed cleanly."
|
||||
right={
|
||||
<Button variant="outline" size="sm" asChild className="gap-1.5">
|
||||
<Link href="/dashboard/workforce/timesheets">
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
Back to Timesheets
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<UploadStatusView uploads={uploads} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { format, parseISO } from "date-fns";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cn, formatNumber } from "@/lib/utils";
|
||||
|
||||
export interface UploadRow {
|
||||
id: string;
|
||||
sourceFile: string;
|
||||
/** ISO date string (YYYY-MM-DD) or empty. */
|
||||
dateUploaded: string;
|
||||
processingStatus: string;
|
||||
rowsCreated: number;
|
||||
rowsFailed: number;
|
||||
lastError: string;
|
||||
}
|
||||
|
||||
export function UploadStatusView({ uploads }: { uploads: UploadRow[] }) {
|
||||
return (
|
||||
<div className="space-y-6 p-4 sm:p-6">
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">Upload status</CardTitle>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Every uploaded sheet and whether it processed cleanly — separate from
|
||||
the individual hour entries on the Timesheets page.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{uploads.length === 0 ? (
|
||||
<div className="px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
No timesheets uploaded yet.
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>File</TableHead>
|
||||
<TableHead>Uploaded</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Rows created</TableHead>
|
||||
<TableHead className="text-right">Rows failed</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{uploads.map((u) => (
|
||||
<TableRow key={u.id}>
|
||||
<TableCell className="max-w-[220px] truncate text-sm" title={u.sourceFile}>
|
||||
{u.sourceFile || "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs tabular-nums">
|
||||
{u.dateUploaded ? format(parseISO(u.dateUploaded), "yyyy-MM-dd") : "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"capitalize",
|
||||
u.processingStatus === "failed" &&
|
||||
"border-red-500/30 bg-red-500/15 text-red-700 dark:text-red-400",
|
||||
u.processingStatus === "processed" &&
|
||||
"border-emerald-500/30 bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
|
||||
)}
|
||||
title={u.lastError || undefined}
|
||||
>
|
||||
{u.processingStatus || "pending"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatNumber(u.rowsCreated)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatNumber(u.rowsFailed)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue