You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
7.6 KiB
TypeScript
124 lines
7.6 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
|
import { ApprovalActions } from "./approval-actions";
|
|
import {
|
|
CheckCircle2,
|
|
Clock,
|
|
Calendar,
|
|
Search,
|
|
Filter,
|
|
ArrowRight
|
|
} from "lucide-react";
|
|
import { Card } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { formatCurrency } from "@/lib/utils";
|
|
|
|
export default async function ApprovalsPage() {
|
|
const [reimbursements, overtimes] = await Promise.all([
|
|
prisma.reimbursement.findMany({
|
|
where: { status: 'PENDING' },
|
|
include: { user: true },
|
|
orderBy: { createdAt: 'desc' }
|
|
}),
|
|
prisma.overtime.findMany({
|
|
where: { status: 'PENDING' },
|
|
include: { user: true },
|
|
orderBy: { date: 'desc' }
|
|
})
|
|
]);
|
|
|
|
const allRequests = [
|
|
...reimbursements.map(r => ({ ...r, type: 'reimbursement' as const })),
|
|
...overtimes.map(o => ({ ...o, type: 'overtime' as const, amount: o.hours * 45, description: o.reason }))
|
|
].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
|
|
return (
|
|
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-3xl font-bold tracking-tight">Approval Queue</h2>
|
|
<p className="text-neutral-500 mt-1">Review and process worker reimbursement and overtime requests.</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 bg-neutral-100 dark:bg-neutral-800 p-1 rounded-2xl">
|
|
<Button variant="ghost" className="bg-white dark:bg-neutral-700 shadow-sm rounded-xl px-6 font-bold text-sm">Pending ({allRequests.length})</Button>
|
|
<Button variant="ghost" className="text-neutral-500 rounded-xl px-6 font-bold text-sm">Processed</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[2.5rem] overflow-hidden">
|
|
<div className="p-8 flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-neutral-100 dark:border-neutral-800">
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400" />
|
|
<Input placeholder="Search requests..." className="pl-10 h-12 rounded-xl bg-neutral-50 dark:bg-neutral-800 border-none shadow-none" />
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button variant="outline" className="rounded-xl border-neutral-200 h-12 flex items-center gap-2 font-bold transition-all hover:bg-neutral-50">
|
|
<Filter className="w-4 h-4" />
|
|
Request Type
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="bg-neutral-50/50 dark:bg-neutral-800/50 border-b border-neutral-100 dark:border-neutral-800">
|
|
<th className="p-6 px-8 text-xs font-black uppercase tracking-[0.15em] text-neutral-400">User</th>
|
|
<th className="p-6 text-xs font-black uppercase tracking-[0.15em] text-neutral-400">Type</th>
|
|
<th className="p-6 text-xs font-black uppercase tracking-[0.15em] text-neutral-400">Description</th>
|
|
<th className="p-6 text-xs font-black uppercase tracking-[0.15em] text-neutral-400">Amount</th>
|
|
<th className="p-6 px-8 text-xs font-black uppercase tracking-[0.15em] text-neutral-400 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
|
|
{allRequests.map((req) => (
|
|
<tr key={req.id} className="hover:bg-neutral-50/50 transition-all group">
|
|
<td className="p-6 px-8">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-indigo-50 dark:bg-indigo-900/20 flex items-center justify-center text-indigo-600 font-bold text-xs">
|
|
{req.user.name[0]}
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-neutral-900 dark:text-neutral-100">{req.user.name}</p>
|
|
<p className="text-[10px] text-neutral-400 font-bold uppercase tracking-tight">{req.user.department}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="p-6">
|
|
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase ${req.type === 'reimbursement' ? 'bg-amber-50 text-amber-600' : 'bg-purple-50 text-purple-600'
|
|
}`}>
|
|
{req.type === 'reimbursement' ? <ArrowRight className="w-3 h-3" /> : <Clock className="w-3 h-3" />}
|
|
{req.type}
|
|
</span>
|
|
</td>
|
|
<td className="p-6">
|
|
<p className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">{req.description}</p>
|
|
<p className="text-[10px] text-neutral-400 mt-0.5">{new Date(req.createdAt).toLocaleDateString()}</p>
|
|
</td>
|
|
<td className="p-6">
|
|
<span className="text-lg font-black text-neutral-900 dark:text-neutral-100">
|
|
{formatCurrency(req.amount.toString())}
|
|
</span>
|
|
</td>
|
|
<td className="p-6 px-8 flex justify-end gap-2">
|
|
<ApprovalActions id={req.id} type={req.type} />
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{allRequests.length === 0 && (
|
|
<div className="p-20 text-center">
|
|
<div className="w-20 h-20 bg-emerald-50 dark:bg-emerald-900/20 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<CheckCircle2 className="w-10 h-10 text-emerald-500" />
|
|
</div>
|
|
<h3 className="text-xl font-black mb-2">Queue is Clear!</h3>
|
|
<p className="text-neutral-500 font-medium">All worker requests have been processed.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|