wulf-pulse/app/admin/data-browser/time-entries/page.tsx.backup

394 lines
12 KiB
Text
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import DataTable from '@/components/admin/DataTable';
import DetailModal from '@/components/admin/DetailModal';
import {
Clock,
Users,
Ticket,
Calendar,
ArrowLeft,
Home,
RefreshCw,
Download,
Filter
} from 'lucide-react';
import Link from 'next/link';
import { TimeEntry } from '@/lib/types/database';
export default function TimeEntriesPage() {
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedEntry, setSelectedEntry] = useState<TimeEntry | null>(null);
const [showDetailModal, setShowDetailModal] = useState(false);
// Pagination states
const [totalCount, setTotalCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(100);
// Filter states
const [search, setSearch] = useState('');
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [billable, setBillable] = useState<string>('all');
const [approved, setApproved] = useState<string>('all');
const [limit, setLimit] = useState('100');
const fetchTimeEntries = async (page: number = 1) => {
setLoading(true);
setError(null);
try {
const offset = (page - 1) * pageSize;
const params = new URLSearchParams();
if (search) params.append('search', search);
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (billable !== 'all') params.append('billable', billable);
if (approved !== 'all') params.append('approved', approved);
params.append('limit', pageSize.toString());
params.append('offset', offset.toString());
const response = await fetch(`/api/data/time-entries?${params}`);
if (!response.ok) {
throw new Error(`Failed to fetch time entries: ${response.statusText}`);
}
const data = await response.json();
setTimeEntries(data.timeEntries || []);
setTotalCount(data.pagination?.total || 0);
setCurrentPage(page);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setError(errorMessage);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTimeEntries(1);
}, [pageSize]);
const handleRefresh = () => {
fetchTimeEntries(currentPage);
};
const handlePageChange = (newPage: number) => {
fetchTimeEntries(newPage);
};
const handleExport = async () => {
try {
const params = new URLSearchParams({ format: 'csv' });
if (search) params.append('search', search);
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (billable !== 'all') params.append('billable', billable);
if (approved !== 'all') params.append('approved', approved);
const response = await fetch(`/api/data/time-entries/export?${params}`);
if (!response.ok) {
throw new Error(`Failed to export data: ${response.statusText}`);
}
// Download file
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `time-entries-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setError(errorMessage);
}
};
const handleRowClick = (entry: TimeEntry) => {
setSelectedEntry(entry);
setShowDetailModal(true);
};
const columns = [
{
key: 'id',
label: 'ID',
sortable: true,
},
{
key: 'resource_id',
label: 'Resource',
sortable: true,
render: (value: number) => (
<Badge variant="outline">
<Users className="w-3 h-3 mr-1" />
{value}
</Badge>
),
},
{
key: 'ticket_id',
label: 'Ticket',
sortable: true,
render: (value: number) => (
<Badge variant="outline">
<Ticket className="w-3 h-3 mr-1" />
{value}
</Badge>
),
},
{
key: 'entry_date',
label: 'Date',
sortable: true,
render: (value: string) => (
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{new Date(value).toLocaleDateString()}
</div>
),
},
{
key: 'hours_worked',
label: 'Hours',
sortable: true,
render: (value: number | string) => {
const hours = typeof value === 'string' ? parseFloat(value) : value;
return (
<Badge variant={hours > 4 ? 'destructive' : 'secondary'}>
<Clock className="w-3 h-3 mr-1" />
{hours.toFixed(1)}h
</Badge>
);
},
},
{
key: 'title',
label: 'Title',
sortable: true,
render: (value: string) => (
<div className="max-w-48 truncate" title={value}>
{value || 'No title'}
</div>
),
},
{
key: 'billable',
label: 'Billable',
sortable: true,
render: (value: boolean) => (
<Badge variant={value ? 'default' : 'secondary'}>
{value ? 'Yes' : 'No'}
</Badge>
),
},
{
key: 'approved',
label: 'Approved',
sortable: true,
render: (value: boolean) => (
<Badge variant={value ? 'default' : 'destructive'}>
{value ? 'Yes' : 'No'}
</Badge>
),
},
];
return (
<div className="container mx-auto p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/admin/data-browser">
<Button variant="outline" size="sm" className="gap-2">
<ArrowLeft className="w-4 h-4" />
<span className="hidden sm:inline">Back</span>
</Button>
</Link>
<Clock className="w-8 h-8 text-blue-600" />
<div>
<h1 className="text-3xl font-bold">Time Entries</h1>
<p className="text-muted-foreground">Browse and analyze time tracking data</p>
</div>
</div>
<div className="flex items-center gap-2">
<Link href="/admin/analytics/time-entries">
<Button variant="outline">
<Filter className="w-4 h-4 mr-2" />
Analytics
</Button>
</Link>
<Button variant="outline" onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<Button variant="outline" onClick={handleRefresh} disabled={loading}>
<RefreshCw className={cn("w-4 h-4 mr-2", loading ? "animate-spin" : "")} />
Refresh
</Button>
</div>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Filter className="w-5 h-5" />
Filters
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<Label>Search</Label>
<Input
placeholder="Search notes or title..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Start Date</Label>
<Input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>End Date</Label>
<Input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Page Size</Label>
<Select value={pageSize.toString()} onValueChange={(val) => setPageSize(parseInt(val))}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="250">250</SelectItem>
<SelectItem value="500">500</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Billable</Label>
<Select value={billable} onValueChange={setBillable}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="true">Billable</SelectItem>
<SelectItem value="false">Non-billable</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Approved</Label>
<Select value={approved} onValueChange={setApproved}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="true">Approved</SelectItem>
<SelectItem value="false">Not approved</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Button variant="outline" onClick={() => {
setBillable('all');
setApproved('all');
setPageSize(100);
setCurrentPage(1);
}}>
Clear
</Button>
</div>
</div>
<div className="flex justify-end mt-4">
<Button variant="outline" onClick={handleRefresh} disabled={loading}>
<RefreshCw className={cn("w-4 h-4 mr-2", loading ? "animate-spin" : "")} />
Apply Filters
</Button>
</div>
</CardContent>
</Card>
{/* Data Table */}
<Card>
<CardHeader>
<CardTitle>Time Entries ({timeEntries.length})</CardTitle>
<CardDescription>
Click on any row to view detailed information
</CardDescription>
</CardHeader>
<CardContent>
{error && (
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-4">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<DataTable
data={timeEntries}
columns={columns}
isLoading={loading}
onRowClick={handleRowClick}
totalCount={totalCount}
page={currentPage}
pageSize={pageSize}
onPageChange={handlePageChange}
/>
</CardContent>
</Card>
{/* Detail Modal */}
<DetailModal
open={showDetailModal}
onOpenChange={(open) => setShowDetailModal(open)}
title={`Time Entry #${selectedEntry?.id}`}
data={selectedEntry}
/>
</div>
);
}
function cn(...classes: string[]) {
return classes.filter(Boolean).join(' ');
}