first commit

main
Cizz22 3 days ago
parent 023824f729
commit f64a0b4b7c

2
.gitignore vendored

@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/app/generated/prisma

@ -0,0 +1,45 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Check, X, Eye, Loader2 } from "lucide-react";
import { updateRequestStatus } from "@/app/lib/actions";
export function ApprovalActions({ id, type }: { id: string, type: 'reimbursement' | 'overtime' }) {
const [isUpdating, setIsUpdating] = useState<'APPROVED' | 'REJECTED' | null>(null);
async function handleUpdate(status: 'APPROVED' | 'REJECTED') {
setIsUpdating(status);
try {
await updateRequestStatus(id, type, status);
} catch (e) {
alert("Failed to update status.");
} finally {
setIsUpdating(null);
}
}
return (
<div className="flex gap-2">
<Button variant="ghost" className="rounded-xl w-10 h-10 p-0 text-neutral-400 hover:text-indigo-600 hover:bg-indigo-50 transition-all">
<Eye className="w-5 h-5" />
</Button>
<Button
onClick={() => handleUpdate('APPROVED')}
disabled={!!isUpdating}
variant="ghost"
className="rounded-xl w-10 h-10 p-0 text-neutral-400 hover:text-emerald-600 hover:bg-emerald-50 transition-all"
>
{isUpdating === 'APPROVED' ? <Loader2 className="w-5 h-5 animate-spin" /> : <Check className="w-5 h-5" />}
</Button>
<Button
onClick={() => handleUpdate('REJECTED')}
disabled={!!isUpdating}
variant="ghost"
className="rounded-xl w-10 h-10 p-0 text-neutral-400 hover:text-rose-600 hover:bg-rose-50 transition-all"
>
{isUpdating === 'REJECTED' ? <Loader2 className="w-5 h-5 animate-spin" /> : <X className="w-5 h-5" />}
</Button>
</div>
);
}

@ -0,0 +1,123 @@
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>
);
}

@ -0,0 +1,166 @@
"use client";
import { useState } from "react";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
PieChart,
Plus,
Settings2,
AlertTriangle,
CheckCircle2,
ArrowRight,
Target,
BarChart,
Edit2
} from "lucide-react";
const initialBudgets = [
{ department: 'Engineering (IT)', limit: 250000, spent: 185000, status: 'Healthy', color: 'bg-indigo-600' },
{ department: 'Marketing', limit: 120000, spent: 115000, status: 'Warning', color: 'bg-rose-500' },
{ department: 'Human Resources', limit: 80000, spent: 42000, status: 'Healthy', color: 'bg-emerald-500' },
{ department: 'Customer Success', limit: 60000, spent: 58000, status: 'Critical', color: 'bg-amber-500' },
];
export default function BudgetingPage() {
const [budgets, setBudgets] = useState(initialBudgets);
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">Department Budgeting</h2>
<p className="text-neutral-500 mt-1 font-medium">Set and monitor expenditure limits across the organization.</p>
</div>
<Button className="bg-neutral-900 text-white hover:bg-black rounded-2xl h-14 px-8 font-black shadow-2xl transition-all active:scale-95 group">
<Plus className="w-5 h-5 mr-1 group-hover:rotate-180 transition-transform duration-500" />
Create Budget
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<Card className="border-none shadow-2xl shadow-indigo-500/10 bg-white dark:bg-neutral-900 rounded-[3rem] p-10 overflow-hidden relative">
<div className="relative z-10 flex flex-col h-full">
<div className="flex items-center gap-4 mb-10">
<div className="w-16 h-16 bg-indigo-50 dark:bg-indigo-900/20 rounded-3xl flex items-center justify-center">
<Target className="w-8 h-8 text-indigo-600" />
</div>
<div>
<h3 className="text-2xl font-black text-neutral-900 dark:text-neutral-100 italic">Total Budget Allocation</h3>
<p className="font-bold text-neutral-400 uppercase tracking-widest text-xs mt-1">FY 2024 Q1 ACTIVE</p>
</div>
</div>
<div className="flex-1 flex flex-col justify-center gap-12">
<div>
<div className="flex justify-between items-end mb-4">
<p className="text-sm font-bold text-neutral-400 uppercase tracking-[0.2em]">Overall Utilization</p>
<p className="text-6xl font-black text-neutral-900 dark:text-neutral-100 tracking-tighter">78<span className="text-indigo-600 text-4xl">%</span></p>
</div>
<div className="h-6 w-full bg-neutral-50 dark:bg-neutral-800 rounded-full overflow-hidden p-1.5 border border-neutral-100 dark:border-neutral-800 shadow-inner">
<div className="h-full bg-gradient-to-r from-indigo-600 to-indigo-400 rounded-full shadow-[0_0_20px_rgba(79,70,229,0.3)]" style={{ width: '78%' }} />
</div>
</div>
<div className="grid grid-cols-2 gap-8">
<div className="space-y-1">
<p className="text-[10px] font-black text-neutral-400 uppercase tracking-widest">Total Limit</p>
<p className="text-2xl font-black text-neutral-900 dark:text-neutral-100">$510,000.00</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-black text-neutral-400 uppercase tracking-widest">Spent to Date</p>
<p className="text-2xl font-black text-indigo-600">$400,000.00</p>
</div>
</div>
</div>
</div>
{/* Decorative blobs */}
<div className="absolute top-[-10%] right-[-10%] w-[40%] h-[40%] bg-indigo-500/5 blur-[80px] rounded-full" />
</Card>
<Card className="border-none shadow-2xl shadow-black/5 bg-neutral-900 rounded-[3rem] p-10 text-white flex flex-col justify-center">
<div className="flex items-center gap-4 mb-8">
<div className="p-3 bg-white/10 rounded-2xl">
<AlertTriangle className="w-6 h-6 text-amber-500" />
</div>
<div>
<h3 className="text-xl font-bold">Budget Insights</h3>
<p className="text-neutral-400 text-sm">Automated analysis of current spend</p>
</div>
</div>
<div className="space-y-6">
<div className="p-6 bg-white/5 rounded-3xl border border-white/5 hover:bg-white/10 transition-colors group cursor-pointer">
<div className="flex justify-between items-start mb-2">
<span className="text-xs font-black text-rose-500 uppercase tracking-widest bg-rose-500/10 px-2 py-1 rounded-lg">Critical Alert</span>
<ArrowRight className="w-4 h-4 text-neutral-600 group-hover:text-white transition-colors" />
</div>
<p className="font-bold text-lg mb-1">Marketing Budget at 96%</p>
<p className="text-sm text-neutral-400 font-medium leading-relaxed">Marketing spend is significantly higher than projected. Approvals for non-essential spend restricted.</p>
</div>
<div className="p-6 bg-white/5 rounded-3xl border border-white/5 hover:bg-white/10 transition-colors group cursor-pointer">
<div className="flex justify-between items-start mb-2">
<span className="text-xs font-black text-emerald-500 uppercase tracking-widest bg-emerald-500/10 px-2 py-1 rounded-lg">Optimization</span>
<ArrowRight className="w-4 h-4 text-neutral-600 group-hover:text-white transition-colors" />
</div>
<p className="font-bold text-lg mb-1">HR Savings Opportunity</p>
<p className="text-sm text-neutral-400 font-medium leading-relaxed">Current HR spend is 45% below budget. Consider reallocating $15k to Engineering Q2.</p>
</div>
</div>
</Card>
</div>
<div className="grid grid-cols-1 gap-6">
<h3 className="text-xl font-black text-neutral-900 dark:text-neutral-100 flex items-center gap-3">
<BarChart className="w-6 h-6 text-indigo-600" />
Departmental Breakdown
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{budgets.map((b, i) => {
const percent = (b.spent / b.limit) * 100;
return (
<Card key={i} className="border-none shadow-xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[2.5rem] p-8 flex flex-col justify-between group hover:scale-[1.03] transition-all duration-300">
<div>
<div className="flex justify-between items-start mb-6">
<div className={`p-3 rounded-2xl ${b.color} text-white shadow-lg shadow-black/5`}>
<Settings2 className="w-5 h-5" />
</div>
<Button variant="ghost" size="icon" className="rounded-full hover:bg-neutral-100">
<Edit2 className="w-4 h-4 text-neutral-400" />
</Button>
</div>
<h4 className="font-black text-xl mb-1 text-neutral-900 dark:text-neutral-100">{b.department}</h4>
<p className="text-[10px] font-black text-neutral-400 uppercase tracking-[0.2em] mb-4">Current Period</p>
<div className="space-y-4 mb-8">
<div className="flex justify-between text-sm font-bold">
<span className="text-neutral-500">Utilization</span>
<span className={percent > 90 ? 'text-rose-600' : 'text-neutral-900 dark:text-neutral-100'}>{percent.toFixed(0)}%</span>
</div>
<div className="h-2 w-full bg-neutral-50 dark:bg-neutral-800 rounded-full overflow-hidden">
<div className={`h-full ${b.color} rounded-full transition-all duration-1000 ease-out shadow-[0_0_10px_rgba(0,0,0,0.1)]`} style={{ width: `${percent}%` }} />
</div>
</div>
</div>
<div className="pt-6 border-t border-neutral-100 dark:border-neutral-800 space-y-2">
<div className="flex justify-between items-center">
<span className="text-xs font-bold text-neutral-400 uppercase tracking-tighter">Spent</span>
<span className="font-black text-neutral-900 dark:text-neutral-100 text-lg">${(b.spent / 1000).toFixed(0)}k</span>
</div>
<div className="flex justify-between items-center opacity-40">
<span className="text-xs font-bold text-neutral-400 uppercase tracking-tighter">Limit</span>
<span className="font-bold text-sm">${(b.limit / 1000).toFixed(0)}k</span>
</div>
</div>
</Card>
);
})}
</div>
</div>
</div>
);
}

@ -0,0 +1,139 @@
import { prisma } from "@/lib/prisma";
import {
Users,
BarChart3,
Wallet,
Clock,
ArrowUpRight,
} from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { formatCurrency } from "@/lib/utils";
import Link from "next/link";
export default async function AdminDashboard() {
const [pendingReimbursements, pendingOvertime, totalBudget, recentTransactions] = await Promise.all([
prisma.reimbursement.count({ where: { status: 'PENDING' } }),
prisma.overtime.count({ where: { status: 'PENDING' } }),
prisma.budget.aggregate({ _sum: { amount: true } }),
prisma.companyTransaction.findMany({
orderBy: { date: 'desc' },
take: 5
})
]);
const totalPending = pendingReimbursements + pendingOvertime;
return (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h2 className="text-4xl font-extrabold tracking-tight text-neutral-900 dark:text-white">Admin Console</h2>
<p className="text-neutral-500 mt-2 font-medium text-lg">Real-time overview of company finances and worker requests.</p>
</div>
<Link href="/admin/approvals">
<Button className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-2xl h-14 px-8 font-black shadow-2xl shadow-emerald-200 dark:shadow-none transition-all active:scale-95 flex items-center gap-2">
<Users className="w-5 h-5" />
Review Requests ({totalPending})
</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{[
{ label: 'Total Budget Allocated', value: formatCurrency(totalBudget._sum.amount?.toString() || '0'), icon: Wallet, color: 'emerald', trend: '+5.2% vs last month' },
{ label: 'Pending Approvals', value: totalPending.toString(), icon: Clock, color: 'amber', trend: `${pendingReimbursements} claims, ${pendingOvertime} OT` },
{ label: 'Monthly Revenue', value: formatCurrency(245000), icon: BarChart3, color: 'indigo', trend: 'On track for Q1' },
{ label: 'Active Personnel', value: '142', icon: Users, color: 'violet', trend: '+3 new this week' },
].map((stat, i) => (
<Card key={i} className="group border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[2.5rem] p-8 hover:-translate-y-2 transition-all duration-500 overflow-hidden relative">
<div className="relative z-10">
<div className={`w-14 h-14 rounded-2xl bg-neutral-50 dark:bg-neutral-800 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-500`}>
<stat.icon className={`w-7 h-7 text-neutral-900 dark:text-neutral-100`} />
</div>
<p className="text-xs font-black text-neutral-400 uppercase tracking-widest mb-1">{stat.label}</p>
<div className="flex items-baseline gap-2">
<h3 className="text-3xl font-black text-neutral-900 dark:text-neutral-100">{stat.value}</h3>
</div>
<p className="mt-4 text-[10px] font-bold text-neutral-500 flex items-center gap-1">
{stat.trend}
</p>
</div>
</Card>
))}
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
<Card className="xl:col-span-2 border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[3rem] p-10 overflow-hidden">
<div className="flex items-center justify-between mb-10">
<h3 className="text-2xl font-black tracking-tight">Financial Ledger</h3>
<Link href="/admin/ledger">
<Button variant="ghost" className="text-emerald-600 font-bold hover:bg-emerald-50 rounded-xl px-4 py-2">View Full Ledger</Button>
</Link>
</div>
<div className="space-y-6">
{(recentTransactions as any[]).map((tx) => (
<div key={tx.id} className="flex items-center justify-between p-6 rounded-[2rem] hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-all group border border-transparent hover:border-neutral-100 dark:hover:border-neutral-800">
<div className="flex items-center gap-6">
<div className={`w-14 h-14 rounded-2xl ${tx.type === 'DEBIT' ? 'bg-rose-50 text-rose-600' : 'bg-emerald-50 text-emerald-600'} flex items-center justify-center font-black shadow-sm`}>
{tx.type === 'DEBIT' ? '-' : '+'}
</div>
<div>
<p className="font-bold text-lg text-neutral-900 dark:text-neutral-100">{tx.description}</p>
<p className="text-xs text-neutral-400 font-medium">{new Date(tx.date).toLocaleDateString()}</p>
</div>
</div>
<div className="text-right">
<p className={`font-black text-xl ${tx.type === 'DEBIT' ? 'text-rose-600' : 'text-emerald-600'}`}>
{tx.type === 'DEBIT' ? '-' : '+'}{formatCurrency(tx.amount.toString())}
</p>
<span className="text-[10px] font-black uppercase text-neutral-400">Success</span>
</div>
</div>
))}
</div>
</Card>
<div className="space-y-8">
<Card className="border-none shadow-2xl shadow-emerald-500/10 bg-gradient-to-br from-emerald-600 to-teal-700 rounded-[3rem] p-10 text-white relative overflow-hidden group">
<div className="relative z-10">
<h3 className="text-xl font-bold mb-6 flex items-center gap-2">
<BarChart3 className="w-5 h-5" />
Budget Health
</h3>
<p className="text-emerald-100 font-medium leading-relaxed mb-8 text-sm">
Total budget utilization is at <span className="text-white font-bold text-lg">68%</span>. Marketing and Sales are nearing their monthly limits.
</p>
<Link href="/admin/budgeting">
<Button className="w-full bg-white text-emerald-700 hover:bg-emerald-50 rounded-2xl h-14 font-black shadow-xl transition-all active:scale-95">
Manage Budgets
</Button>
</Link>
</div>
</Card>
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[3rem] p-10">
<h3 className="text-xl font-black mb-8">Departmental Spend</h3>
<div className="space-y-6">
{[
{ label: 'Marketing', value: 85, color: 'bg-emerald-500' },
{ label: 'Engineering', value: 62, color: 'bg-indigo-500' },
{ label: 'Operations', value: 48, color: 'bg-violet-500' },
].map((item) => (
<div key={item.label} className="space-y-2">
<div className="flex justify-between text-sm font-bold">
<span className="text-neutral-500">{item.label}</span>
<span className={item.value > 80 ? 'text-rose-500' : ''}>{item.value}%</span>
</div>
<div className="w-full h-2 bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden">
<div className={`h-full ${item.color} rounded-full transition-all duration-1000`} style={{ width: `${item.value}%` }} />
</div>
</div>
))}
</div>
</Card>
</div>
</div>
</div>
);
}

@ -0,0 +1,49 @@
import { Sidebar } from "@/components/sidebar";
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import {
LayoutDashboard,
UserCheck,
BookOpen,
PieChart,
BarChart3,
} from "lucide-react";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
// Double check role
// if (session.user.role !== 'ADMIN') {
// redirect('/me/dashboard');
// }
return (
<div className="flex h-screen bg-neutral-50 dark:bg-neutral-950">
<Sidebar
portal="admin"
user={{
name: session.user.name,
email: session.user.email,
role: "Administrator"
}}
/>
<main className="flex-1 overflow-y-auto p-8 relative">
<div className="max-w-7xl mx-auto space-y-8">
{children}
</div>
{/* Background Accents */}
<div className="fixed top-[-5%] right-[-5%] w-[30%] h-[30%] bg-emerald-500/5 blur-[100px] -z-10 rounded-full" />
<div className="fixed bottom-[-5%] left-[20%] w-[30%] h-[30%] bg-blue-500/5 blur-[100px] -z-10 rounded-full" />
</main>
</div>
);
}

@ -0,0 +1,142 @@
import { prisma } from "@/lib/prisma";
import {
Plus,
Search,
Filter,
ArrowUpRight,
ArrowDownRight,
Download,
Calendar,
Wallet,
ArrowRightLeft
} from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { formatCurrency } from "@/lib/utils";
export default async function LedgerPage() {
const transactions = await prisma.companyTransaction.findMany({
orderBy: { date: 'desc' },
include: {
category: true,
account: true
}
});
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">Company Ledger</h2>
<p className="text-neutral-500 mt-1">Full transaction history and cash flow tracking.</p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" className="rounded-xl border-neutral-200 gap-2 h-12 px-6 font-bold shadow-sm hover:bg-neutral-50 transition-all">
<Download className="w-4 h-4" />
Export CSV
</Button>
<Button className="bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl gap-2 h-12 px-8 font-bold shadow-lg shadow-emerald-100 dark:shadow-none transition-all active:scale-95">
<Plus className="w-4 h-4" />
New Entry
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{ label: 'Total Inflow', amount: 452000, color: 'emerald', icon: ArrowUpRight },
{ label: 'Total Outflow', amount: 128400, color: 'rose', icon: ArrowDownRight },
{ label: 'Net Balance', amount: 323600, color: 'indigo', icon: Wallet },
].map((stat, i) => (
<Card key={i} className="border-none shadow-xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-3xl p-8 relative overflow-hidden group">
<div className="relative z-10">
<div className={`w-12 h-12 rounded-2xl bg-${stat.color}-50 dark:bg-${stat.color}-900/20 flex items-center justify-center mb-4`}>
<stat.icon className={`w-6 h-6 text-${stat.color}-600 dark:text-${stat.color}-400`} />
</div>
<p className="text-xs font-bold text-neutral-400 uppercase tracking-widest mb-1">{stat.label}</p>
<h3 className="text-3xl font-black text-neutral-900 dark:text-neutral-100">{formatCurrency(stat.amount.toString())}</h3>
</div>
</Card>
))}
</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 border-b border-neutral-100 dark:border-neutral-800 flex flex-col md:flex-row md:items-center justify-between gap-4">
<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 transactions..." 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" />
All Accounts
</Button>
<Button variant="outline" className="rounded-xl border-neutral-200 h-12 flex items-center gap-2 font-bold transition-all hover:bg-neutral-50">
<Calendar className="w-4 h-4" />
This Month
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<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">Transaction</th>
<th className="p-6 text-xs font-black uppercase tracking-[0.15em] text-neutral-400">Account</th>
<th className="p-6 text-xs font-black uppercase tracking-[0.15em] text-neutral-400">Category</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">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
{transactions.map((tx) => (
<tr key={tx.id} className="hover:bg-neutral-50/50 transition-all group">
<td className="p-6 px-8">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${tx.type === 'DEBIT' ? 'bg-rose-50 text-rose-600' : 'bg-emerald-50 text-emerald-600'
}`}>
{tx.type === 'DEBIT' ? <ArrowDownRight className="w-5 h-5" /> : <ArrowUpRight className="w-5 h-5" />}
</div>
<div>
<p className="font-bold text-neutral-900 dark:text-neutral-100">{tx.description}</p>
<p className="text-[10px] text-neutral-400 font-bold uppercase tracking-tight">TX-{tx.id.substring(0, 8).toUpperCase()}</p>
</div>
</div>
</td>
<td className="p-6">
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">{tx.account.name}</span>
</td>
<td className="p-6">
<span className="px-2.5 py-1 rounded-lg bg-neutral-100 dark:bg-neutral-800 text-[10px] font-black uppercase text-neutral-500">
{tx.category?.name || 'Uncategorized'}
</span>
</td>
<td className="p-6">
<span className={`text-lg font-black ${tx.type === 'DEBIT' ? 'text-rose-600' : 'text-emerald-600'}`}>
{tx.type === 'DEBIT' ? '-' : '+'}{formatCurrency(tx.amount.toString())}
</span>
</td>
<td className="p-6 px-8 text-right">
<p className="text-sm font-bold text-neutral-900 dark:text-neutral-100">{new Date(tx.date).toLocaleDateString()}</p>
<p className="text-[10px] text-neutral-400 uppercase tracking-tighter">14:20 PM</p>
</td>
</tr>
))}
</tbody>
</table>
{transactions.length === 0 && (
<div className="p-20 text-center">
<div className="w-20 h-20 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-6">
<ArrowRightLeft className="w-10 h-10 text-neutral-300" />
</div>
<h3 className="text-xl font-black mb-2">No Transactions Found</h3>
<p className="text-neutral-500 font-medium">Start recording financial activities to see them here.</p>
</div>
)}
</div>
</Card>
</div>
);
}

@ -0,0 +1,198 @@
"use client";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
BarChart3,
PieChart as PieChartIcon,
Download,
Calendar,
ChevronDown,
TrendingUp,
ArrowUpRight,
FileText,
Filter
} from "lucide-react";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
AreaChart,
Area,
PieChart,
Cell,
Pie
} from "recharts";
const cashFlowData = [
{ name: 'Jul', incoming: 45000, outgoing: 32000 },
{ name: 'Aug', incoming: 52000, outgoing: 38000 },
{ name: 'Sep', incoming: 48000, outgoing: 41000 },
{ name: 'Oct', incoming: 61000, outgoing: 45000 },
{ name: 'Nov', incoming: 55000, outgoing: 42000 },
{ name: 'Dec', incoming: 67000, outgoing: 48000 },
{ name: 'Jan', incoming: 72000, outgoing: 51000 },
{ name: 'Feb', incoming: 69000, outgoing: 55000 },
];
const categoryData = [
{ name: 'Payroll', value: 45, color: '#4f46e5' },
{ name: 'Marketing', value: 20, color: '#8b5cf6' },
{ name: 'Infrastructure', value: 15, color: '#10b981' },
{ name: 'Operations', value: 12, color: '#f59e0b' },
{ name: 'Legal/Other', value: 8, color: '#ef4444' },
];
export default function ReportsPage() {
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500 pb-12">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-3xl font-black tracking-tight italic">Financial Intel</h2>
<p className="text-neutral-500 mt-1 font-medium text-lg">Detailed P&L and Cash Flow analysis for the current fiscal year.</p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" className="rounded-2xl border-neutral-200 h-14 px-6 gap-2 font-bold group">
<Calendar className="w-5 h-5 text-neutral-400" />
FY 2024
<ChevronDown className="w-4 h-4 text-neutral-400" />
</Button>
<Button className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl h-14 px-8 font-black shadow-2xl transition-all active:scale-95 flex items-center gap-2">
<Download className="w-5 h-5" />
Executive Summary
</Button>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
<Card className="xl:col-span-2 border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[3rem] p-10 overflow-hidden relative">
<div className="flex justify-between items-start mb-10 relative z-10">
<div>
<h3 className="text-2xl font-black tracking-tighter">Net Cash Flow Trajectory</h3>
<p className="text-neutral-500 font-medium text-sm mt-1">Comparison of gross inflows vs primary outflows</p>
</div>
<div className="p-4 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 rounded-2xl flex items-center gap-2 font-black text-sm shadow-sm">
<TrendingUp className="w-4 h-4" />
+18.4%
</div>
</div>
<div className="h-[400px] w-full relative z-10">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={cashFlowData}>
<defs>
<linearGradient id="colorInc" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#4f46e5" stopOpacity={0.1} />
<stop offset="95%" stopColor="#4f46e5" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorOut" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.05} />
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#a3a3a3', fontSize: 12, fontWeight: 700 }} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#a3a3a3', fontSize: 12, fontWeight: 700 }} tickFormatter={(v) => `$${v / 1000}k`} />
<Tooltip
contentStyle={{ borderRadius: '24px', border: 'none', boxShadow: '0 20px 25px -5px rgb(0 0 0 / 0.1)', padding: '20px' }}
itemStyle={{ fontWeight: 800, fontSize: '14px' }}
/>
<Area type="monotone" dataKey="incoming" stroke="#4f46e5" strokeWidth={4} fillOpacity={1} fill="url(#colorInc)" />
<Area type="monotone" dataKey="outgoing" stroke="#ef4444" strokeWidth={4} fillOpacity={1} fill="url(#colorOut)" strokeDasharray="8 8" />
</AreaChart>
</ResponsiveContainer>
</div>
<div className="absolute top-0 right-0 w-64 h-64 bg-indigo-500/5 blur-[100px] rounded-full -mr-32 -mt-32" />
</Card>
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[3rem] p-10 flex flex-col items-center">
<h3 className="text-xl font-black mb-8 w-full">Expense Allocation</h3>
<div className="h-[280px] w-full mb-10">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={categoryData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={120}
paddingAngle={8}
dataKey="value"
stroke="none"
>
{categoryData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{ borderRadius: '20px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }}
/>
</PieChart>
</ResponsiveContainer>
</div>
<div className="w-full space-y-4">
{categoryData.map((c) => (
<div key={c.name} className="flex justify-between items-center group cursor-pointer">
<div className="flex items-center gap-3">
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: c.color }} />
<span className="font-bold text-sm text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-900 transition-colors">{c.name}</span>
</div>
<div className="flex items-center gap-4">
<span className="font-black text-sm">{c.value}%</span>
<ChevronDown className="w-4 h-4 text-neutral-200" />
</div>
</div>
))}
</div>
</Card>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mt-8">
{[
{ label: 'EBITDA', value: '$240k', trend: '+12%', sub: 'Earnings' },
{ label: 'OPEX', value: '$180k', trend: '-2.4%', sub: 'Operating' },
{ label: 'LTV/CAC', value: '4.2x', trend: '+0.5x', sub: 'Efficiency' },
{ label: 'Burn Rate', value: '$45k', trend: 'Stable', sub: 'Monthly' },
].map((stat, i) => (
<Card key={i} className="border-none shadow-xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[2rem] p-8 hover:bg-neutral-50 transition-colors cursor-pointer group">
<p className="text-[10px] font-black text-neutral-400 uppercase tracking-widest mb-1">{stat.label}</p>
<p className="text-3xl font-black text-neutral-900 dark:text-neutral-100 group-hover:text-indigo-600 transition-colors">{stat.value}</p>
<div className="mt-4 flex justify-between items-center">
<span className="text-xs font-bold text-neutral-500">{stat.sub}</span>
<span className={`text-xs font-black p-1 px-2 rounded-lg ${stat.trend.startsWith('+') ? 'bg-emerald-50 text-emerald-600' :
stat.trend.startsWith('-') ? 'bg-rose-50 text-rose-600' : 'bg-neutral-100 text-neutral-400'
}`}>
{stat.trend}
</span>
</div>
</Card>
))}
</div>
<Card className="border-none shadow-2xl shadow-black/5 bg-neutral-900 rounded-[3rem] p-10 text-white mt-8 overflow-hidden relative">
<div className="relative z-10 flex flex-col lg:flex-row items-center justify-between gap-8">
<div className="flex-1">
<h3 className="text-2xl font-black mb-2">Automated P&L Audit</h3>
<p className="text-neutral-400 font-medium">Generate a comprehensive profit and loss statement for external audit in minutes.</p>
</div>
<div className="flex items-center gap-4">
<Button variant="outline" className="rounded-2xl h-14 px-8 border-white/20 text-white hover:bg-white/10 font-bold">
<Filter className="w-5 h-5 mr-1" />
Configure Rules
</Button>
<Button className="bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl h-14 px-10 font-black shadow-2xl shadow-indigo-500/20">
<FileText className="w-6 h-6 mr-2" />
Generate P&L
</Button>
</div>
</div>
<div className="absolute top-1/2 left-0 -translate-y-1/2 -ml-20 w-64 h-64 bg-indigo-500/10 rounded-full blur-[80px]" />
</Card>
</div>
);
}

@ -0,0 +1,2 @@
import { handlers } from "@/auth" // Referring to the auth.ts we just created
export const { GET, POST } = handlers;

@ -0,0 +1,44 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';
export async function GET() {
try {
const defaultPassword = await bcrypt.hash('password123', 10);
// Create an Admin user
const adminUser = await prisma.user.upsert({
where: { email: 'admin@tam-finance.com' },
update: {},
create: {
name: 'TAM Admin',
email: 'admin@tam-finance.com',
role: 'ADMIN',
department: 'Operations',
password: defaultPassword,
},
});
// Create a regular worker
const workerUser = await prisma.user.upsert({
where: { email: 'worker@tam-finance.com' },
update: {},
create: {
name: 'John Doe',
email: 'worker@tam-finance.com',
role: 'WORKER',
department: 'Engineering',
password: defaultPassword,
},
});
return NextResponse.json({
message: 'Database specifically seeded for TAM Finance',
adminUser,
workerUser
});
} catch (error) {
console.error('Failed to seed the database', error);
return NextResponse.json({ error: 'Failed to seed database' }, { status: 500 });
}
}

@ -0,0 +1,53 @@
import { auth, signOut } from '@/auth';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
export default async function DashboardPage() {
const session = await auth();
return (
<div className="flex flex-col min-h-screen p-8 bg-neutral-50 dark:bg-neutral-950">
<header className="flex justify-between items-center bg-white dark:bg-neutral-900 shadow-sm p-4 rounded-lg mb-8">
<h1 className="text-xl font-bold">Dashboard</h1>
<div className="flex gap-4 items-center">
<p className="text-sm font-medium">Logged in as: {session?.user?.email}</p>
<form
action={async () => {
'use server';
await signOut();
}}
>
<Button variant="outline">Sign out</Button>
</form>
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card>
<CardHeader>
<CardTitle>Total Balance</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">$124,500.00</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Monthly Spend</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-red-600">-$24,300.00</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Pending Approvals</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-yellow-600">5</p>
</CardContent>
</Card>
</div>
</div>
);
}

@ -1,26 +1,126 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
:root {
--background: #ffffff;
--foreground: #171717;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

@ -0,0 +1,128 @@
'use server';
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', Object.fromEntries(formData));
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
}
}
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
import { auth } from '@/auth';
export async function submitReimbursement(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const amount = formData.get('amount') as string;
const description = formData.get('description') as string;
const category = formData.get('category') as string;
const dateStr = formData.get('date') as string;
await prisma.reimbursement.create({
data: {
amount: parseFloat(amount),
description,
category,
createdAt: new Date(dateStr),
userId: (session.user as any).id,
},
});
revalidatePath('/me/reimbursements');
revalidatePath('/me/dashboard');
}
export async function submitOvertime(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const date = formData.get('date') as string;
const hours = formData.get('hours') as string;
const reason = formData.get('reason') as string;
await prisma.overtime.create({
data: {
date: new Date(date),
hours: parseFloat(hours),
reason,
userId: (session.user as any).id,
},
});
revalidatePath('/me/overtime');
revalidatePath('/me/dashboard');
}
export async function updateRequestStatus(id: string, type: 'reimbursement' | 'overtime', status: 'APPROVED' | 'REJECTED' | 'PAID') {
const session = await auth();
// Simple role check
if ((session?.user as any)?.role !== 'ADMIN') throw new Error('Admin only');
if (type === 'reimbursement') {
await prisma.reimbursement.update({
where: { id },
data: { status },
});
} else {
await prisma.overtime.update({
where: { id },
data: { status },
});
}
revalidatePath('/admin/approvals');
revalidatePath('/admin/dashboard');
}
export async function createBudget(formData: FormData) {
const department = formData.get('department') as string;
const amount = formData.get('amount') as string;
const period = formData.get('period') as string;
await prisma.budget.create({
data: {
department,
amount: parseFloat(amount),
period,
},
});
revalidatePath('/admin/budgeting');
}
export async function createLedgerEntry(formData: FormData) {
const amount = formData.get('amount') as string;
const type = formData.get('type') as 'CREDIT' | 'DEBIT';
const description = formData.get('description') as string;
const categoryId = formData.get('categoryId') as string;
const accountId = formData.get('accountId') as string;
await prisma.companyTransaction.create({
data: {
amount: parseFloat(amount),
type,
description,
categoryId,
accountId,
},
});
revalidatePath('/admin/ledger');
revalidatePath('/admin/dashboard');
}

@ -0,0 +1,16 @@
import LoginForm from '@/app/ui/login-form';
export default function LoginPage() {
return (
<main className="flex items-center justify-center min-h-screen bg-neutral-50 dark:bg-neutral-950">
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
<div className="flex w-full items-end rounded-lg bg-blue-600 p-3 mb-2 shadow-sm">
<div className="w-32 text-white font-bold text-2xl">
TAM Finance
</div>
</div>
<LoginForm />
</div>
</main>
);
}

@ -0,0 +1,165 @@
import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import {
DollarSign,
Receipt,
Clock,
Wallet,
ArrowUpRight,
TrendingUp,
CreditCard,
Zap
} from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { formatCurrency } from "@/lib/utils";
import Link from "next/link";
export default async function WorkerDashboard() {
const session = await auth();
const userId = (session?.user as any)?.id;
const [reimbursements, overtimes] = await Promise.all([
prisma.reimbursement.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
take: 5
}),
prisma.overtime.findMany({
where: { userId },
take: 20
})
]);
const pendingClaimsCount = await prisma.reimbursement.count({
where: { userId, status: 'PENDING' }
});
const totalSpendingAgg = await prisma.reimbursement.aggregate({
where: { userId, status: 'PAID' },
_sum: { amount: true }
});
const overtimeHours = overtimes.reduce((acc, curr) => acc + curr.hours, 0);
return (
<div className="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h2 className="text-4xl font-extrabold tracking-tight text-neutral-900 dark:text-white">
Welcome back, <span className="text-indigo-600">{(session?.user?.name || 'User').split(' ')[0]}</span>
</h2>
<p className="text-neutral-500 mt-2 font-medium text-lg">Your financial activity and pending requests at a glance.</p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" className="rounded-2xl border-neutral-200 h-14 px-6 gap-2 font-bold hover:bg-white hover:shadow-xl transition-all">
Download Report
</Button>
<Link href="/me/reimbursements">
<Button className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl h-14 px-8 font-black shadow-2xl shadow-indigo-200 dark:shadow-none transition-all active:scale-95 flex items-center gap-2">
<Zap className="w-5 h-5 fill-white" />
New Request
</Button>
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
{[
{ label: 'Total Spending', value: formatCurrency(totalSpendingAgg._sum.amount?.toString() || '0'), icon: DollarSign, color: 'indigo', trend: '+12% this month' },
{ label: 'Pending Claims', value: pendingClaimsCount.toString(), icon: Receipt, color: 'amber', trend: 'Wait time: ~2 days' },
{ label: 'Overtime Hours', value: `${overtimeHours}h`, icon: Clock, color: 'violet', trend: 'YTD tracking' },
{ label: 'Next Payout', value: formatCurrency(2450), icon: Wallet, color: 'emerald', trend: 'Due in 12 days' },
].map((stat, i) => (
<Card key={i} className="group border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[2.5rem] p-8 hover:-translate-y-2 transition-all duration-500 overflow-hidden relative">
<div className="relative z-10">
<div className={`w-14 h-14 rounded-2xl bg-indigo-50 dark:bg-indigo-900/20 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-500`}>
<stat.icon className={`w-7 h-7 text-indigo-600 dark:text-indigo-400`} />
</div>
<p className="text-xs font-black text-neutral-400 uppercase tracking-widest mb-1">{stat.label}</p>
<div className="flex items-baseline gap-2">
<h3 className="text-3xl font-black text-neutral-900 dark:text-neutral-100">{stat.value}</h3>
</div>
<p className="mt-4 text-[10px] font-bold text-neutral-500 flex items-center gap-1">
<TrendingUp className="w-3 h-3 text-emerald-500" />
{stat.trend}
</p>
</div>
</Card>
))}
</div>
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
<Card className="xl:col-span-2 border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[3rem] p-10">
<div className="flex items-center justify-between mb-10">
<h3 className="text-2xl font-black tracking-tight">Recent Activity</h3>
<Button variant="ghost" className="text-indigo-600 font-bold hover:bg-indigo-50 rounded-xl px-4 py-2">View All</Button>
</div>
<div className="space-y-6">
{reimbursements.map((item) => (
<div key={item.id} className="flex items-center justify-between p-6 rounded-[2rem] hover:bg-neutral-50 dark:hover:bg-neutral-800/50 transition-all group cursor-pointer border border-transparent hover:border-neutral-100 dark:hover:border-neutral-800">
<div className="flex items-center gap-6">
<div className="w-14 h-14 rounded-2xl bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center group-hover:bg-white dark:group-hover:bg-neutral-700 transition-colors shadow-sm">
<CreditCard className="w-6 h-6 text-neutral-400 group-hover:text-indigo-600 transition-colors" />
</div>
<div>
<p className="font-bold text-lg text-neutral-900 dark:text-neutral-100">{item.description}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] font-black uppercase text-neutral-400 tracking-wider bg-neutral-100 dark:bg-neutral-800 px-2 py-0.5 rounded-md">{item.category}</span>
<span className="text-xs text-neutral-400 font-medium">{new Date(item.createdAt).toLocaleDateString()}</span>
</div>
</div>
</div>
<div className="text-right">
<p className="font-black text-xl text-neutral-900 dark:text-neutral-100">{formatCurrency(item.amount.toString())}</p>
<span className={`text-[10px] font-black uppercase ${item.status === 'PAID' ? 'text-emerald-500' : 'text-amber-500'
}`}>{item.status}</span>
</div>
</div>
))}
</div>
</Card>
<div className="space-y-8">
<Card className="border-none shadow-2xl shadow-indigo-500/10 bg-gradient-to-br from-indigo-700 to-violet-800 rounded-[3rem] p-10 text-white relative overflow-hidden group">
<div className="relative z-10">
<h3 className="text-xl font-bold mb-6 flex items-center gap-2">
<ArrowUpRight className="w-5 h-5" />
Quick Wallet Tip
</h3>
<p className="text-indigo-100 font-medium leading-relaxed mb-8">
You have <span className="text-white font-bold underline decoration-indigo-400 underline-offset-4">{pendingClaimsCount} pending reimbursement requests</span>. Claims are usually processed within 48 hours.
</p>
<Link href="/me/reimbursements">
<Button className="w-full bg-white text-indigo-700 hover:bg-indigo-50 rounded-2xl h-14 font-black shadow-xl transition-all active:scale-95">
Submit New Claim
</Button>
</Link>
</div>
</Card>
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-[3rem] p-10">
<h3 className="text-xl font-black mb-8">Spending Snapshot</h3>
<div className="space-y-6">
{[
{ label: 'Travel', value: 45, color: 'bg-indigo-500' },
{ label: 'Dining', value: 30, color: 'bg-emerald-500' },
{ label: 'Supplies', value: 25, color: 'bg-amber-500' },
].map((item) => (
<div key={item.label} className="space-y-2">
<div className="flex justify-between text-sm font-bold">
<span className="text-neutral-500">{item.label}</span>
<span>{item.value}%</span>
</div>
<div className="w-full h-2 bg-neutral-100 dark:bg-neutral-800 rounded-full overflow-hidden">
<div className={`h-full ${item.color} rounded-full transition-all duration-1000`} style={{ width: `${item.value}%` }} />
</div>
</div>
))}
</div>
</Card>
</div>
</div>
</div>
);
}

@ -0,0 +1,43 @@
import { Sidebar } from "@/components/sidebar";
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import {
LayoutDashboard,
Receipt,
Clock,
FileText,
} from "lucide-react";
export default async function WorkerLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<div className="flex h-screen bg-neutral-50 dark:bg-neutral-950">
<Sidebar
portal="worker"
user={{
name: session.user.name,
email: session.user.email,
role: "Worker Account"
}}
/>
<main className="flex-1 overflow-y-auto p-8 relative">
<div className="max-w-7xl mx-auto space-y-8">
{children}
</div>
{/* Subtle Background Elements for Modern Look */}
<div className="fixed top-[-10%] left-[-10%] w-[40%] h-[40%] bg-indigo-500/5 blur-[120px] -z-10 rounded-full" />
<div className="fixed bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-violet-500/5 blur-[120px] -z-10 rounded-full" />
</main>
</div>
);
}

@ -0,0 +1,64 @@
"use client";
import { useState } from "react";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Zap, Loader2 } from "lucide-react";
import { submitOvertime } from "@/app/lib/actions";
export function OvertimeForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
try {
await submitOvertime(formData);
alert("Overtime logged successfully!");
(document.getElementById('overtime-form') as HTMLFormElement).reset();
} catch (e) {
alert("Failed to log overtime.");
} finally {
setIsSubmitting(false);
}
}
return (
<Card className="border-none shadow-2xl shadow-indigo-500/10 bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden">
<CardHeader className="p-8 border-b border-neutral-100 dark:border-neutral-800 flex flex-row items-center justify-between">
<div>
<CardTitle>Log Extra Hours</CardTitle>
<CardDescription>Enter details for your additional work hours</CardDescription>
</div>
<div className="p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-2xl">
<Zap className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
</CardHeader>
<CardContent className="p-8">
<form id="overtime-form" action={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="date" className="text-sm font-semibold">Work Date</Label>
<Input name="date" type="date" required className="rounded-xl" />
</div>
<div className="space-y-2">
<Label htmlFor="hours" className="text-sm font-semibold">Hours Worked</Label>
<Input name="hours" type="number" step="0.5" placeholder="e.g. 2.5" required className="rounded-xl" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="reason" className="text-sm font-semibold">Reason / Project</Label>
<Textarea name="reason" placeholder="Briefly describe the work you performed..." className="rounded-2xl min-h-[120px] bg-neutral-50 dark:bg-neutral-800/50 border-none resize-none px-4 py-3" />
</div>
<div className="pt-4">
<Button type="submit" disabled={isSubmitting} className="w-full bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl py-6 h-auto text-lg font-bold transition-all shadow-lg shadow-indigo-100 dark:shadow-none">
{isSubmitting ? <Loader2 className="w-5 h-5 animate-spin mx-auto" /> : "Log Overtime"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}

@ -0,0 +1,111 @@
import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { OvertimeForm } from "./overtime-form";
import {
Clock,
Calendar,
CheckCircle2,
AlertCircle,
Zap
} from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export default async function OvertimePage() {
const session = await auth();
const userId = (session?.user as any)?.id;
const logs = await prisma.overtime.findMany({
where: { userId },
orderBy: { date: 'desc' },
take: 10
});
const totalYTD = logs.reduce((acc, curr) => acc + curr.hours, 0);
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div>
<h2 className="text-3xl font-bold tracking-tight">Overtime Management</h2>
<p className="text-neutral-500 mt-1">Log your extra hours and track approval status.</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<OvertimeForm />
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden">
<div className="p-8 border-b border-neutral-100 dark:border-neutral-800 flex justify-between items-center">
<h3 className="text-xl font-bold">Recent Logs</h3>
<Button variant="ghost" className="text-indigo-600 font-semibold hover:bg-indigo-50 rounded-xl px-4 py-2 h-auto text-sm">
View History
</Button>
</div>
<div className="divide-y divide-neutral-100 dark:divide-neutral-800">
{logs.map((log, i) => (
<div key={log.id} className="p-8 hover:bg-neutral-50/50 transition-colors flex items-center justify-between group">
<div className="flex items-center gap-6">
<div className="hidden sm:flex flex-col items-center">
<span className="text-xs font-bold text-neutral-400 uppercase tracking-tighter">Day</span>
<span className="text-2xl font-black text-neutral-300 group-hover:text-indigo-200 transition-colors">
{new Date(log.date).getDate().toString().padStart(2, '0')}
</span>
</div>
<div>
<p className="font-bold text-neutral-900 dark:text-neutral-100">{log.reason}</p>
<div className="flex items-center gap-3 mt-1 text-sm text-neutral-500">
<span className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(log.date).toLocaleDateString()}
</span>
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{log.hours} hours
</span>
</div>
</div>
</div>
<div className="flex flex-col items-end gap-2">
<span className={`text-[10px] font-bold uppercase px-2 py-1 rounded-full ${log.status === 'PAID' ? 'bg-emerald-100 text-emerald-700' :
log.status === 'APPROVED' ? 'bg-indigo-100 text-indigo-700' : 'bg-amber-100 text-amber-700'
}`}>
{log.status}
</span>
<p className="text-xs font-medium text-neutral-400">Payroll: Current Cycle</p>
</div>
</div>
))}
</div>
</Card>
</div>
<div className="space-y-8">
<Card className="border-none shadow-2xl shadow-indigo-500/10 bg-gradient-to-br from-indigo-600 to-violet-700 rounded-3xl p-8 text-white relative overflow-hidden">
<div className="relative z-10">
<div className="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center mb-6 backdrop-blur-md">
<Clock className="w-6 h-6 text-white" />
</div>
<h3 className="text-xl font-bold mb-2">Total Overtime (YTD)</h3>
<p className="text-4xl font-black mb-4">{totalYTD}h</p>
<div className="w-full bg-white/10 rounded-full h-2 mb-2">
<div className="bg-white h-full rounded-full w-[65%]" style={{ width: `${Math.min(100, (totalYTD / 60) * 100)}%` }} />
</div>
<p className="text-xs text-indigo-100">Relative to monthly capacity</p>
</div>
<div className="absolute top-[-20%] right-[-20%] w-48 h-48 bg-white/5 rounded-full blur-3xl pointer-events-none" />
</Card>
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-3xl p-8">
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-amber-500" />
Important Note
</h3>
<p className="text-sm text-neutral-500 leading-relaxed">
Overtime logs must be submitted within 48 hours of completion. All logs are subject to manager approval before being processed for payroll.
</p>
</Card>
</div>
</div>
</div>
);
}

@ -0,0 +1,134 @@
import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import {
FileText,
Download,
Eye,
Search,
ChevronRight,
CreditCard
} from "lucide-react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { formatCurrency } from "@/lib/utils";
export default async function PayslipsPage() {
const session = await auth();
const userId = (session?.user as any)?.id;
const payslips = await prisma.payslip.findMany({
where: { userId },
orderBy: [
{ year: 'desc' },
{ month: 'desc' }
]
});
return (
<div className="space-y-10 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-6">
<div>
<h2 className="text-4xl font-extrabold tracking-tight">My Payslips</h2>
<p className="text-neutral-500 mt-2 font-medium">Securely access and download your past salary statements.</p>
</div>
<Button className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl h-14 px-8 font-black shadow-2xl shadow-indigo-100 dark:shadow-none transition-all active:scale-95 flex items-center gap-2">
<Download className="w-5 h-5" />
Download All (2024)
</Button>
</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 border-b border-neutral-100 dark:border-neutral-800 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400" />
<Input placeholder="Search payslips..." className="pl-12 h-14 rounded-2xl bg-neutral-50 dark:bg-neutral-800 border-none shadow-none text-base" />
</div>
<div className="flex items-center gap-3">
<Button variant="outline" className="rounded-2xl border-neutral-200 h-14 px-6 font-bold hover:bg-white transition-all">
2024
</Button>
<Button variant="outline" className="rounded-2xl border-neutral-200 h-14 px-6 font-bold hover:bg-white transition-all">
All Years
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-neutral-100 dark:border-neutral-800 bg-neutral-50/50 dark:bg-neutral-800/50">
<th className="p-8 text-xs font-black uppercase tracking-[0.2em] text-neutral-400">Statement Ref</th>
<th className="p-8 text-xs font-black uppercase tracking-[0.2em] text-neutral-400">Period</th>
<th className="p-8 text-xs font-black uppercase tracking-[0.2em] text-neutral-400">Net Pay</th>
<th className="p-8 text-xs font-black uppercase tracking-[0.2em] text-neutral-400">Date Issued</th>
<th className="p-8 text-xs font-black uppercase tracking-[0.2em] text-neutral-400 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
{payslips.map((slip) => (
<tr key={slip.id} className="hover:bg-neutral-50/50 transition-all group">
<td className="p-8">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-900/20 flex items-center justify-center">
<FileText className="w-6 h-6 text-indigo-600 dark:text-indigo-400" />
</div>
<div>
<p className="font-bold text-neutral-900 dark:text-neutral-100">PAY-{slip.year}-{slip.month.substring(0, 3).toUpperCase()}</p>
<p className="text-[10px] font-black text-neutral-400 uppercase tracking-tighter">PDF Statement</p>
</div>
</div>
</td>
<td className="p-8">
<p className="font-bold text-neutral-900 dark:text-neutral-100">{slip.month} {slip.year}</p>
</td>
<td className="p-8">
<span className="text-xl font-black text-indigo-600">
{formatCurrency(slip.amount.toString())}
</span>
</td>
<td className="p-8 text-neutral-500 font-medium">
{new Date(slip.createdAt).toLocaleDateString()}
</td>
<td className="p-8 flex justify-end gap-3 px-8">
<Button variant="ghost" className="rounded-2xl w-14 h-14 p-0 text-neutral-400 hover:text-indigo-600 hover:bg-white transition-all shadow-sm border border-transparent hover:border-neutral-100">
<Eye className="w-6 h-6" />
</Button>
<Button variant="ghost" className="rounded-2xl w-14 h-14 p-0 text-neutral-400 hover:text-emerald-600 hover:bg-white transition-all shadow-sm border border-transparent hover:border-neutral-100">
<Download className="w-6 h-6" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
{payslips.length === 0 && (
<div className="p-20 text-center">
<div className="w-20 h-20 bg-neutral-100 dark:bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-6">
<FileText className="w-10 h-10 text-neutral-300" />
</div>
<h3 className="text-xl font-black mb-2">No Payslips Yet</h3>
<p className="text-neutral-500 font-medium">Your payslips will appear here once issued by payroll.</p>
</div>
)}
</div>
</Card>
<Card className="border-none shadow-2xl shadow-indigo-500/5 bg-gradient-to-br from-white to-indigo-50/30 dark:from-neutral-900 dark:to-indigo-950/10 rounded-[2.5rem] p-10 flex flex-col md:flex-row items-center gap-8 border border-indigo-100/50">
<div className="w-20 h-20 bg-indigo-600 rounded-3xl flex items-center justify-center shrink-0 shadow-2xl shadow-indigo-200">
<CreditCard className="w-10 h-10 text-white" />
</div>
<div className="flex-1 text-center md:text-left">
<h3 className="text-2xl font-black mb-2">Payroll Information</h3>
<p className="text-neutral-500 font-medium leading-relaxed">
Your salary is deposited into your primary bank account ending in <span className="text-indigo-600 font-bold font-mono">****4590</span> on the last business day of each month.
</p>
</div>
<Button variant="outline" className="rounded-2xl h-14 px-8 border-neutral-200 font-bold hover:bg-white shrink-0 group">
Update Banking
<ChevronRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Card>
</div>
);
}

@ -0,0 +1,106 @@
import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";
import { submitReimbursement } from "@/app/lib/actions";
import { ReimbursementForm } from "./reimbursement-form";
import {
CheckCircle2,
Clock,
Tag,
Calendar,
Search,
Filter
} 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 ReimbursementsPage() {
const session = await auth();
const userId = (session?.user as any)?.id;
const reimbursements = await prisma.reimbursement.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
});
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">Reimbursements</h2>
<p className="text-neutral-500 mt-1">Manage and track your business expense claims.</p>
</div>
<ReimbursementForm />
</div>
<Card className="border-none shadow-2xl shadow-black/5 bg-white dark:bg-neutral-900 rounded-3xl 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 claims..." className="pl-10 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 flex items-center gap-2">
<Filter className="w-4 h-4" />
Filter
</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">
<th className="p-4 px-8 text-xs font-bold uppercase tracking-wider text-neutral-500">Status</th>
<th className="p-4 text-xs font-bold uppercase tracking-wider text-neutral-500">Details</th>
<th className="p-4 text-xs font-bold uppercase tracking-wider text-neutral-500">Category</th>
<th className="p-4 text-xs font-bold uppercase tracking-wider text-neutral-500">Date</th>
<th className="p-4 px-8 text-xs font-bold uppercase tracking-wider text-neutral-500 text-right">Amount</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-100 dark:divide-neutral-800">
{reimbursements.map((item) => (
<tr key={item.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors group cursor-pointer">
<td className="p-4 px-8">
<div className="flex items-center gap-2">
{item.status === 'PAID' && <CheckCircle2 className="w-4 h-4 text-emerald-500" />}
{item.status === 'APPROVED' && <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />}
{item.status === 'PENDING' && <Clock className="w-4 h-4 text-amber-500" />}
<span className={`text-[10px] font-bold uppercase px-2 py-1 rounded-full ${item.status === 'PAID' ? 'bg-emerald-100 text-emerald-700' :
item.status === 'APPROVED' ? 'bg-indigo-100 text-indigo-700' :
'bg-amber-100 text-amber-700'
}`}>
{item.status}
</span>
</div>
</td>
<td className="p-4">
<p className="font-semibold text-neutral-900 dark:text-neutral-100">{item.description}</p>
<p className="text-[10px] text-neutral-400 uppercase tracking-tight">Ref: #{item.id.slice(-8)}</p>
</td>
<td className="p-4 border-none">
<span className="inline-flex items-center gap-1 text-sm text-neutral-500">
<Tag className="w-3 h-3" />
{item.category}
</span>
</td>
<td className="p-4 border-none">
<span className="inline-flex items-center gap-1 text-sm text-neutral-500">
<Calendar className="w-3 h-3" />
{new Date(item.createdAt).toLocaleDateString()}
</span>
</td>
<td className="p-4 px-8 border-none text-right">
<span className="font-bold text-lg text-neutral-900 dark:text-neutral-100">
{formatCurrency(item.amount.toString())}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
);
}

@ -0,0 +1,104 @@
"use client";
import { useState } from "react";
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Upload,
Plus,
Loader2,
} from "lucide-react";
import { submitReimbursement } from "@/app/lib/actions";
export function ReimbursementForm() {
const [showForm, setShowForm] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(formData: FormData) {
setIsSubmitting(true);
try {
await submitReimbursement(formData);
setShowForm(false);
alert("Claim submitted successfully!");
} catch (e) {
alert("Failed to submit claim.");
} finally {
setIsSubmitting(false);
}
}
return (
<>
<Button
onClick={() => setShowForm(!showForm)}
className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl px-6 py-6 h-auto shadow-lg shadow-indigo-200 dark:shadow-none transition-all active:scale-95"
>
{showForm ? "Cancel Request" : (
<span className="flex items-center gap-2">
<Plus className="w-5 h-5" />
New Request
</span>
)}
</Button>
{showForm && (
<Card className="border-none shadow-2xl shadow-indigo-500/10 bg-white dark:bg-neutral-900 rounded-3xl overflow-hidden animate-in zoom-in-95 duration-300 absolute top-24 right-0 left-0 z-20 mx-8">
<CardHeader className="p-8">
<CardTitle>Submit New Request</CardTitle>
<CardDescription>Fill in the details and attach your receipt</CardDescription>
</CardHeader>
<CardContent className="p-8 pt-0">
<form action={handleSubmit} className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="description" className="text-sm font-semibold">Description</Label>
<Input name="description" placeholder="e.g. Flight to Berlin" required className="rounded-xl border-neutral-200 focus:ring-indigo-500" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="amount" className="text-sm font-semibold">Amount ($)</Label>
<Input name="amount" type="number" step="0.01" placeholder="0.00" required className="rounded-xl" />
</div>
<div className="space-y-2">
<Label htmlFor="category" className="text-sm font-semibold">Category</Label>
<select name="category" className="w-full rounded-xl border-neutral-200 bg-white dark:bg-neutral-950 p-2 text-sm focus:ring-2 focus:ring-indigo-500 outline-none h-10 border">
<option>Travel</option>
<option>Food</option>
<option>Equipment</option>
<option>Medical</option>
<option>Other</option>
</select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="date" className="text-sm font-semibold">Transaction Date</Label>
<Input name="date" type="date" required className="rounded-xl" />
</div>
</div>
<div className="space-y-6">
<div className="space-y-2">
<Label className="text-sm font-semibold">Receipt Image / PDF</Label>
<div className="border-2 border-dashed border-neutral-200 dark:border-neutral-800 rounded-3xl p-12 text-center hover:border-indigo-400 hover:bg-indigo-50/50 transition-all cursor-pointer group">
<div className="w-16 h-16 bg-neutral-50 dark:bg-neutral-800 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:bg-white dark:group-hover:bg-neutral-700 transition-colors shadow-sm">
<Upload className="w-8 h-8 text-neutral-400 group-hover:text-indigo-600 transition-colors" />
</div>
<p className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">Click to upload or drag and drop</p>
<p className="text-xs text-neutral-500 mt-1">PNG, JPG or PDF up to 10MB</p>
</div>
</div>
<div className="pt-4 flex justify-end">
<Button type="submit" disabled={isSubmitting} className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-700 text-white rounded-2xl px-12 py-6 h-auto transition-all">
{isSubmitting ? <Loader2 className="w-5 h-5 animate-spin" /> : "Submit Claim"}
</Button>
</div>
</div>
</form>
</CardContent>
</Card>
)}
</>
);
}

@ -1,65 +1,5 @@
import Image from "next/image";
import { redirect } from 'next/navigation';
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
redirect('/dashboard');
}

@ -0,0 +1,67 @@
'use client';
import { useActionState } from 'react';
import { authenticate } from '@/app/lib/actions';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { AlertCircle } from 'lucide-react';
import { useFormStatus } from 'react-dom';
export default function LoginForm() {
const [errorMessage, dispatch] = useActionState(authenticate, undefined);
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>
Enter your email below to login to your account.
</CardDescription>
</CardHeader>
<form action={dispatch}>
<CardContent className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
name="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
required
/>
</div>
{errorMessage && (
<div className="flex h-8 items-end space-x-1" aria-live="polite" aria-atomic="true">
<AlertCircle className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</div>
)}
</CardContent>
<CardFooter>
<LoginButton />
</CardFooter>
</form>
</Card>
);
}
function LoginButton() {
const { pending } = useFormStatus();
return (
<Button className="w-full" aria-disabled={pending} disabled={pending}>
{pending ? "Signing in..." : "Sign in"}
</Button>
);
}

@ -0,0 +1,26 @@
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnLogin = nextUrl.pathname.startsWith('/login');
const isPublicPath = nextUrl.pathname === '/' || nextUrl.pathname.startsWith('/api') || isOnLogin;
if (isOnLogin) {
if (isLoggedIn) return Response.redirect(new URL('/dashboard', nextUrl));
return true;
}
if (!isLoggedIn && !isPublicPath) {
return false; // Redirect unauthenticated users to login page
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;

@ -0,0 +1,52 @@
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { prisma } from './lib/prisma';
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await prisma.user.findUnique({
where: { email: credentials.email as string }
});
if (!user || !user.password) return null;
const passwordsMatch = await bcrypt.compare(
credentials.password as string,
user.password
);
if (passwordsMatch) return user;
return null;
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as any).role;
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (token) {
(session.user as any).role = token.role;
(session.user as any).id = token.id;
}
return session;
},
},
});

@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

@ -0,0 +1,242 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import {
LayoutDashboard,
Receipt,
Clock,
FileText,
UserCheck,
BookOpen,
PieChart,
BarChart3,
Settings,
LogOut,
ChevronRight,
Wallet,
Shield,
Briefcase,
Layers
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { signOut } from "next-auth/react";
import { motion, AnimatePresence } from "framer-motion";
interface SidebarItem {
title: string;
href: string;
icon: any;
badge?: string | number;
}
interface SidebarGroup {
label: string;
items: SidebarItem[];
}
const workerGroups: SidebarGroup[] = [
{
label: "Main",
items: [
{ title: "Dashboard", href: "/me/dashboard", icon: LayoutDashboard },
]
},
{
label: "Activities",
items: [
{ title: "Reimbursements", href: "/me/reimbursements", icon: Receipt, badge: "3" },
{ title: "Overtime", href: "/me/overtime", icon: Clock },
]
},
{
label: "Documents",
items: [
{ title: "Payslips", href: "/me/payslips", icon: FileText },
]
}
];
const adminGroups: SidebarGroup[] = [
{
label: "Management",
items: [
{ title: "Dashboard", href: "/admin/dashboard", icon: LayoutDashboard },
{ title: "Approvals", href: "/admin/approvals", icon: UserCheck, badge: "12" },
]
},
{
label: "Accounting",
items: [
{ title: "Company Ledger", href: "/admin/ledger", icon: BookOpen },
{ title: "Budgeting", href: "/admin/budgeting", icon: PieChart },
]
},
{
label: "Analytics",
items: [
{ title: "Reports", href: "/admin/reports", icon: BarChart3 },
]
}
];
interface SidebarProps {
portal: "worker" | "admin";
user: {
name?: string | null;
email?: string | null;
role?: string;
};
}
export function Sidebar({ portal, user }: SidebarProps) {
const pathname = usePathname();
const isAdmin = portal === 'admin';
const groups = isAdmin ? adminGroups : workerGroups;
// Role-based theme configuration
const theme = {
accent: isAdmin ? "emerald" : "indigo",
gradient: isAdmin
? "from-emerald-600 to-teal-600"
: "from-indigo-600 to-violet-600",
bgActive: isAdmin
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400"
: "bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400",
iconActive: isAdmin ? "text-emerald-600 dark:text-emerald-400" : "text-indigo-600 dark:text-indigo-400",
glow: isAdmin ? "shadow-emerald-200/50" : "shadow-indigo-200/50"
};
return (
<div className="w-72 h-screen flex flex-col bg-white dark:bg-neutral-900 border-r border-neutral-100 dark:border-neutral-800 shadow-sm z-50">
{/* Brand Section */}
<div className="p-8 pb-6">
<div className="flex items-center gap-4 group cursor-pointer">
<div className={cn(
"w-12 h-12 rounded-2xl bg-gradient-to-br flex items-center justify-center shadow-xl transition-all duration-500 group-hover:scale-110 group-hover:rotate-3",
theme.gradient,
theme.glow
)}>
<Wallet className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="font-extrabold text-xl tracking-tight text-neutral-900 dark:text-white leading-none">
Finance<span className={isAdmin ? "text-emerald-600" : "text-indigo-600"}>TAM</span>
</h1>
<div className="flex items-center gap-1.5 mt-1.5 text-[10px] font-bold uppercase tracking-[0.15em] text-neutral-400">
{isAdmin ? <Shield className="w-3 h-3" /> : <Layers className="w-3 h-3" />}
{user.role || 'Portal'}
</div>
</div>
</div>
</div>
{/* Navigation Groups */}
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-8 scrollbar-none">
{groups.map((group, groupIdx) => (
<div key={groupIdx} className="space-y-3">
<h3 className="px-4 text-[10px] font-black uppercase tracking-[0.2em] text-neutral-400/80">
{group.label}
</h3>
<div className="space-y-1">
{group.items.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
"relative flex items-center gap-3.5 px-4 py-3.5 rounded-2xl text-[13px] font-bold transition-all duration-300 group",
isActive
? theme.bgActive
: "text-neutral-500 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100 hover:bg-neutral-50 dark:hover:bg-neutral-800/50"
)}
>
{/* Active Background Pill */}
{isActive && (
<motion.div
layoutId="active-pill"
className={cn(
"absolute inset-0 rounded-2xl z-0",
isAdmin ? "bg-emerald-500/5" : "bg-indigo-500/5"
)}
transition={{ type: "spring", bounce: 0.2, duration: 0.6 }}
/>
)}
<item.icon className={cn(
"w-5 h-5 z-10 transition-transform duration-300 group-hover:scale-110",
isActive ? theme.iconActive : "text-neutral-400 group-hover:text-neutral-600 dark:group-hover:text-neutral-300"
)} />
<span className="z-10">{item.title}</span>
<AnimatePresence>
{item.badge && (
<motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className={cn(
"ml-auto px-2 py-0.5 rounded-lg text-[10px] font-black tracking-tight z-10",
isActive
? (isAdmin ? "bg-emerald-600 text-white" : "bg-indigo-600 text-white")
: "bg-neutral-100 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400"
)}
>
{item.badge}
</motion.span>
)}
</AnimatePresence>
{isActive && (
<div className={cn(
"absolute left-0 w-1 h-6 rounded-r-full",
isAdmin ? "bg-emerald-600 shadow-[2px_0_8px_rgba(16,185,129,0.4)]" : "bg-indigo-600 shadow-[2px_0_8px_rgba(79,70,229,0.4)]"
)} />
)}
</Link>
);
})}
</div>
</div>
))}
</div>
{/* User & Footer */}
<div className="p-4 mt-auto">
<div className="bg-neutral-50 dark:bg-neutral-800/50 rounded-3xl p-4 border border-neutral-100 dark:border-neutral-800 relative group transition-all hover:bg-white dark:hover:bg-neutral-800 hover:shadow-xl hover:shadow-black/5">
<div className="flex items-center gap-3">
<div className={cn(
"w-10 h-10 rounded-2xl bg-gradient-to-br flex items-center justify-center text-sm font-black text-white shadow-md",
theme.gradient
)}>
{user.name?.charAt(0) || user.email?.charAt(0) || 'U'}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-black text-neutral-900 dark:text-neutral-100 truncate tracking-tight">{user.name || 'User Name'}</p>
<p className="text-[10px] font-bold text-neutral-400 truncate tracking-tight">{user.email}</p>
</div>
</div>
<div className="mt-4 pt-4 border-t border-neutral-200/50 dark:border-neutral-700/50">
<Button
variant="ghost"
className="w-full justify-start gap-3 text-neutral-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-900/10 rounded-2xl px-3 py-2.5 h-auto transition-all duration-300 font-bold text-xs group"
onClick={() => signOut({ callbackUrl: '/login' })}
>
<div className="w-8 h-8 rounded-xl bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center group-hover:bg-rose-100 dark:group-hover:bg-rose-900/30 transition-colors">
<LogOut className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
</div>
Sign out
</Button>
</div>
</div>
<p className="text-[9px] text-center mt-4 font-bold uppercase tracking-[0.25em] text-neutral-300 dark:text-neutral-600">
Vers. 2.4.0 Secured
</p>
</div>
</div>
);
}

@ -0,0 +1,64 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client"
import { PrismaPg } from "@prisma/adapter-pg"
import pg from "pg"
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }
const connectionString = process.env.DATABASE_URL
const pool = new pg.Pool({ connectionString })
const adapter = new PrismaPg(pool)
export const prisma =
globalForPrisma.prisma ?? new PrismaClient({ adapter })
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

@ -0,0 +1,13 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatCurrency(amount: number | string) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(Number(amount));
}

@ -0,0 +1,9 @@
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};

7636
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -9,18 +9,39 @@
"lint": "eslint"
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
"@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.4.2",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"dotenv": "^17.3.1",
"framer-motion": "^12.35.0",
"jsonwebtoken": "^9.0.3",
"lucide-react": "^0.577.0",
"next": "16.1.6",
"next-auth": "^5.0.0-beta.30",
"pg": "^8.20.0",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"recharts": "^3.7.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"prisma": "^7.4.2",
"shadcn": "^3.8.5",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}

@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

@ -0,0 +1,122 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
enum Role {
ADMIN
MANAGER
WORKER
}
enum Status {
PENDING
APPROVED
REJECTED
PAID
}
model User {
id String @id @default(cuid())
name String
email String @unique
password String?
role Role @default(WORKER)
department String?
reimbursements Reimbursement[]
overtimes Overtime[]
payslips Payslip[]
logs AuditLog[]
deletedAt DateTime?
}
model Account {
id String @id @default(cuid())
name String
balance Decimal @db.Decimal(20, 2)
type String // "ASSET" or "LIABILITY"
transactions CompanyTransaction[]
deletedAt DateTime?
}
model Category {
id String @id @default(cuid())
name String
parentId String?
parent Category? @relation("CategoryToCategory", fields: [parentId], references: [id])
children Category[] @relation("CategoryToCategory")
transactions CompanyTransaction[]
deletedAt DateTime?
}
model Reimbursement {
id String @id @default(cuid())
amount Decimal @db.Decimal(20, 2)
description String
category String // e.g., "Travel", "Medical"
receiptUrl String? // URL from S3/Uploadthing
status Status @default(PENDING)
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
deletedAt DateTime?
}
model Overtime {
id String @id @default(cuid())
date DateTime
hours Float
reason String
status Status @default(PENDING)
userId String
user User @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
deletedAt DateTime?
}
model CompanyTransaction {
id String @id @default(cuid())
amount Decimal @db.Decimal(20, 2)
type String // "CREDIT" or "DEBIT"
description String
accountId String
account Account @relation(fields: [accountId], references: [id])
categoryId String?
category Category? @relation(fields: [categoryId], references: [id])
date DateTime @default(now())
deletedAt DateTime?
}
model AuditLog {
id String @id @default(cuid())
action String // e.g., "UPDATE_REIMBURSEMENT"
entityId String?
oldValue String?
newValue String?
userId String
user User @relation(fields: [userId], references: [id])
timestamp DateTime @default(now())
}
model Budget {
id String @id @default(cuid())
department String
amount Decimal @db.Decimal(20, 2)
period String // e.g., "2024-Q1", "2024-01"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
}
model Payslip {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
month Int // 1-12
year Int
pdfUrl String
amount Decimal @db.Decimal(20, 2)
createdAt DateTime @default(now())
}
Loading…
Cancel
Save