diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e423f753..ad43b951 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -66,12 +66,12 @@ class ApiClient { return response.data; } - async createClient(clientData: { name: string; description?: string; department?: string; email?: string }) { + async createClient(clientData: { name: string; description?: string; department?: string; email?: string; billingRate?: number }) { const response = await this.client.post('/api/clients', clientData); return response.data; } - async updateClient(id: number, clientData: { name?: string; description?: string; department?: string; email?: string }) { + async updateClient(id: number, clientData: { name?: string; description?: string; department?: string; email?: string; billingRate?: number }) { const response = await this.client.put(`/api/clients/${id}`, clientData); return response.data; } diff --git a/frontend/src/pages/ClientsPage.tsx b/frontend/src/pages/ClientsPage.tsx index 822ca867..6dfa37db 100644 --- a/frontend/src/pages/ClientsPage.tsx +++ b/frontend/src/pages/ClientsPage.tsx @@ -33,7 +33,7 @@ import { type Client } from '../types/api'; const ClientsPage: React.FC = () => { const [open, setOpen] = useState(false); const [editingClient, setEditingClient] = useState(null); - const [formData, setFormData] = useState({ name: '', description: '', department: '', email: '' }); + const [formData, setFormData] = useState({ name: '', description: '', department: '', email: '', billingRate: '' }); const [error, setError] = useState(''); const queryClient = useQueryClient(); @@ -44,7 +44,7 @@ const ClientsPage: React.FC = () => { }); const createMutation = useMutation({ - mutationFn: (clientData: { name: string; description?: string; department?: string; email?: string }) => + mutationFn: (clientData: { name: string; description?: string; department?: string; email?: string; billingRate?: number }) => apiClient.createClient(clientData), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['clients'] }); @@ -57,7 +57,7 @@ const ClientsPage: React.FC = () => { }); const updateMutation = useMutation({ - mutationFn: ({ id, data }: { id: number; data: { name?: string; description?: string; department?: string; email?: string } }) => + mutationFn: ({ id, data }: { id: number; data: { name?: string; description?: string; department?: string; email?: string; billingRate?: number } }) => apiClient.updateClient(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['clients'] }); @@ -100,11 +100,12 @@ const ClientsPage: React.FC = () => { name: client.name, description: client.description || '', department: client.department || '', - email: client.email || '' + email: client.email || '', + billingRate: client.billingRate ? String(client.billingRate) : '' }); } else { setEditingClient(null); - setFormData({ name: '', description: '', department: '', email: '' }); + setFormData({ name: '', description: '', department: '', email: '', billingRate: '' }); } setError(''); setOpen(true); @@ -113,7 +114,7 @@ const ClientsPage: React.FC = () => { const handleClose = () => { setOpen(false); setEditingClient(null); - setFormData({ name: '', description: '', department: '', email: '' }); + setFormData({ name: '', description: '', department: '', email: '', billingRate: '' }); setError(''); }; @@ -126,6 +127,14 @@ const ClientsPage: React.FC = () => { return; } + if ( + formData.billingRate !== '' && + (Number.isNaN(Number(formData.billingRate)) || Number(formData.billingRate) < 0) + ) { + setError('Billing rate must be a positive number'); + return; + } + if (editingClient) { updateMutation.mutate({ id: editingClient.id, @@ -134,6 +143,7 @@ const ClientsPage: React.FC = () => { description: formData.description || undefined, department: formData.department || undefined, email: formData.email || undefined, + billingRate: formData.billingRate === '' ? undefined : Number(formData.billingRate), }, }); } else { @@ -142,6 +152,7 @@ const ClientsPage: React.FC = () => { description: formData.description || undefined, department: formData.department || undefined, email: formData.email || undefined, + billingRate: formData.billingRate === '' ? undefined : Number(formData.billingRate), }); } }; @@ -202,6 +213,7 @@ const ClientsPage: React.FC = () => { Name Department Email + Billing Rate Description Created Actions @@ -234,6 +246,15 @@ const ClientsPage: React.FC = () => { )} + + {client.billingRate > 0 ? ( + + {client.billingRate.toFixed(2)} + + ) : ( + + )} + {client.description ? ( @@ -268,7 +289,7 @@ const ClientsPage: React.FC = () => { )) ) : ( - + No clients found. Create your first client to get started. @@ -313,6 +334,16 @@ const ClientsPage: React.FC = () => { onChange={(e) => setFormData({ ...formData, email: e.target.value })} disabled={createMutation.isPending || updateMutation.isPending} /> + setFormData({ ...formData, billingRate: e.target.value })} + disabled={createMutation.isPending || updateMutation.isPending} + /> { {selectedClient && report && ( <> - + @@ -189,7 +189,7 @@ const ReportsPage: React.FC = () => { - + @@ -201,7 +201,7 @@ const ReportsPage: React.FC = () => { - + @@ -213,6 +213,18 @@ const ReportsPage: React.FC = () => { + + + + + Total Billed + + + {(report.totalAmount ?? 0).toFixed(2)} + + + + @@ -222,6 +234,7 @@ const ReportsPage: React.FC = () => { Date Hours + Amount Description Created @@ -242,6 +255,11 @@ const ReportsPage: React.FC = () => { variant="outlined" /> + + + {(entry.amount ?? 0).toFixed(2)} + + {entry.description ? ( @@ -260,7 +278,7 @@ const ReportsPage: React.FC = () => { )) ) : ( - + No work entries found for this client. diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 51869d45..010a4bd0 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -9,6 +9,7 @@ export interface Client { description: string | null; department: string | null; email: string | null; + billingRate: number; created_at: string; updated_at: string; } @@ -22,6 +23,8 @@ export interface WorkEntry { created_at: string; updated_at: string; client_name?: string; + billable?: number; + amount?: number; } export interface WorkEntryWithClient extends WorkEntry { @@ -32,6 +35,7 @@ export interface ClientReport { client: Client; workEntries: WorkEntry[]; totalHours: number; + totalAmount: number; entryCount: number; } @@ -40,6 +44,7 @@ export interface CreateClientRequest { description?: string; department?: string; email?: string; + billingRate?: number; } export interface UpdateClientRequest { @@ -47,6 +52,7 @@ export interface UpdateClientRequest { description?: string; department?: string; email?: string; + billingRate?: number; } export interface CreateWorkEntryRequest {