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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/app/dashboard/invoices/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
formatTimeInTimeZone,
} from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';
import { PageBreadcrumb } from '@/components/layout/PageBreadcrumb';

export default function InvoiceDetailPage() {
const params = useParams();
Expand Down
132 changes: 67 additions & 65 deletions frontend/app/dashboard/invoices/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,35 @@ import { useState } from 'react';
import { useDashboardData } from '@/lib/hooks/useDashboardData';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { CheckCircle2, Clock, AlertCircle, Filter, FileText, Loader2 } from 'lucide-react';
import {
CheckCircle2,
Clock,
AlertCircle,
Filter,
FileText,
Loader2,
} from 'lucide-react';
import { motion } from 'framer-motion';
import Link from 'next/link';
import { InvoiceCardSkeleton } from '@/components/ui/loading-skeletons';
import { EmptyState } from '@/components/empty/EmptyState';
import { formatDateInTimeZone } from '@/lib/utils';
import { useAuthStore } from '@/store/useAuthStore';
import { useRouter } from 'next/navigation';

export default function InvoicesPage() {
const router = useRouter();
const { invoices, loading } = useDashboardData();
const timezone = useAuthStore((state) => state.timezone);
const [filter, setFilter] = useState<'all' | 'paid' | 'pending' | 'overdue'>('all');

const [filter, setFilter] = useState<
'all' | 'paid' | 'pending' | 'overdue'
>('all');

const filteredInvoices =
filter === 'all' ? invoices : invoices.filter((inv) => inv.status === filter);
filter === 'all'
? invoices
: invoices.filter((inv) => inv.status === filter);

const getStatusIcon = (status: string) => {
switch (status) {
Expand Down Expand Up @@ -52,12 +65,15 @@ export default function InvoicesPage() {
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Invoices</h1>
<p className="text-gray-600 mt-1">View and manage your invoices</p>
<p className="text-gray-600 mt-1">
View and manage your invoices
</p>
<div className="mt-2 inline-flex items-center gap-2 text-sm text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading invoices...
</div>
</div>

<div className="grid grid-cols-1 gap-4">
{[1, 2, 3, 4].map((i) => (
<InvoiceCardSkeleton key={i} />
Expand All @@ -69,83 +85,55 @@ export default function InvoicesPage() {

return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-3xl font-bold text-gray-900">Invoices</h1>
<p className="text-gray-600 mt-1">View and manage your invoices</p>
<p className="text-gray-600 mt-1">
View and manage your invoices
</p>
</div>

{/* Filters */}
<div className="flex items-center gap-2">
<Filter className="h-4 w-4 text-gray-500" />
<div className="flex gap-2">
{(['all', 'paid', 'pending', 'overdue'] as const).map((status) => (
<Button
key={status}
variant={filter === status ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter(status)}
className="capitalize"
>
{status}
</Button>
))}
{(['all', 'paid', 'pending', 'overdue'] as const).map(
(status) => (
<Button
key={status}
variant={filter === status ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter(status)}
className="capitalize"
>
{status}
</Button>
)
)}
</div>
</div>

<div className="grid grid-cols-1 gap-4">
{filteredInvoices.map((invoice, index) => (
<motion.div
key={invoice.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Link href={`/dashboard/projects/${invoice.projectId}`}>
<Card className="hover:shadow-lg transition-all cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
{getStatusIcon(invoice.status)}
<div className="flex-1">
<h3 className="font-semibold text-gray-900">{invoice.projectTitle}</h3>
<p className="text-sm text-gray-600">{invoice.milestoneTitle}</p>
<p className="text-xs text-gray-500 mt-1">
Ref #{invoice.id} • {formatDateInTimeZone(invoice.generatedAt, timezone)}
</p>
</div>
</div>
<div className="text-right">
<p className="text-xl font-bold text-gray-900">
{invoice.amount} {invoice.currency}
</p>
<span
className={`inline-block px-3 py-1 rounded-full text-xs font-medium border mt-2 ${getStatusColor(
invoice.status
)}`}
>
{invoice.status}
</span>
</div>
</div>
</CardContent>
</Card>
</Link>
</motion.div>
))}
</div>

{filteredInvoices.length === 0 && (
{/* Content */}
{filteredInvoices.length === 0 ? (
<Card>
<CardContent>
<EmptyState
icon={FileText}
title={filter === 'all' ? 'No invoices yet' : `No ${filter} invoices`}
title={
filter === 'all'
? 'No invoices yet'
: `No ${filter} invoices`
}
description={
filter === 'all'
? 'Your invoices will appear here once projects generate them.'
: `You don't have any ${filter} invoices at the moment.`
}
action={{
label: filter === 'all' ? 'View Projects' : 'Show All Invoices',
label:
filter === 'all'
? 'View Projects'
: 'Show All Invoices',
onClick: () => {
if (filter === 'all') {
router.push('/dashboard/projects');
Expand All @@ -166,25 +154,39 @@ export default function InvoicesPage() {
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Link href={`/dashboard/projects/${invoice.projectId}`}>
<Link
href={`/dashboard/projects/${invoice.projectId}`}
>
<Card className="hover:shadow-lg transition-all cursor-pointer">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
{getStatusIcon(invoice.status)}

<div className="flex-1">
<h3 className="font-semibold text-gray-900">{invoice.projectTitle}</h3>
<p className="text-sm text-gray-600">{invoice.milestoneTitle}</p>
<h3 className="font-semibold text-gray-900">
{invoice.projectTitle}
</h3>

<p className="text-sm text-gray-600">
{invoice.milestoneTitle}
</p>

<p className="text-xs text-gray-500 mt-1">
Ref #{invoice.id} •{' '}
{new Date(invoice.generatedAt).toLocaleDateString()}
{formatDateInTimeZone(
invoice.generatedAt,
timezone
)}
</p>
</div>
</div>

<div className="text-right">
<p className="text-xl font-bold text-gray-900">
{invoice.amount} {invoice.currency}
</p>

<span
className={`inline-block px-3 py-1 rounded-full text-xs font-medium border mt-2 ${getStatusColor(
invoice.status
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import React from 'react';
import { useAuthStore } from '@/store/useAuthStore';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect } from 'react';
Expand All @@ -16,6 +17,9 @@ export default function DashboardLayout({
const router = useRouter();
const pathname = usePathname();

const mainRef = React.useRef<HTMLDivElement>(null);
const scrollPositions = React.useRef<Record<string, number>>({});

useEffect(() => {
if (!isAuthenticated) {
router.push('/auth');
Expand Down
Loading
Loading