Skip to content
4 changes: 2 additions & 2 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
45 changes: 38 additions & 7 deletions frontend/src/pages/ClientsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { type Client } from '../types/api';
const ClientsPage: React.FC = () => {
const [open, setOpen] = useState(false);
const [editingClient, setEditingClient] = useState<Client | null>(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();
Expand All @@ -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'] });
Expand All @@ -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'] });
Expand Down Expand Up @@ -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);
Expand All @@ -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('');
};

Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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),
});
}
};
Expand Down Expand Up @@ -202,6 +213,7 @@ const ClientsPage: React.FC = () => {
<TableCell>Name</TableCell>
<TableCell>Department</TableCell>
<TableCell>Email</TableCell>
<TableCell>Billing Rate</TableCell>
<TableCell>Description</TableCell>
<TableCell>Created</TableCell>
<TableCell align="right">Actions</TableCell>
Expand Down Expand Up @@ -234,6 +246,15 @@ const ClientsPage: React.FC = () => {
<Chip label="-" size="small" variant="outlined" />
)}
</TableCell>
<TableCell>
{client.billingRate > 0 ? (
<Typography variant="body2" color="text.secondary">
{client.billingRate.toFixed(2)}
</Typography>
) : (
<Chip label="-" size="small" variant="outlined" />
)}
</TableCell>
<TableCell>
{client.description ? (
<Typography variant="body2" color="text.secondary">
Expand Down Expand Up @@ -268,7 +289,7 @@ const ClientsPage: React.FC = () => {
))
) : (
<TableRow>
<TableCell colSpan={6} align="center">
<TableCell colSpan={7} align="center">
<Typography color="text.secondary" sx={{ py: 3 }}>
No clients found. Create your first client to get started.
</Typography>
Expand Down Expand Up @@ -313,6 +334,16 @@ const ClientsPage: React.FC = () => {
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
disabled={createMutation.isPending || updateMutation.isPending}
/>
<TextField
margin="dense"
label="Billing Rate"
type="number"
fullWidth
inputProps={{ min: 0, step: 0.01 }}
value={formData.billingRate}
onChange={(e) => setFormData({ ...formData, billingRate: e.target.value })}
disabled={createMutation.isPending || updateMutation.isPending}
/>
<TextField
margin="dense"
label="Description"
Expand Down
26 changes: 22 additions & 4 deletions frontend/src/pages/ReportsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const ReportsPage: React.FC = () => {
{selectedClient && report && (
<>
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
Expand All @@ -189,7 +189,7 @@ const ReportsPage: React.FC = () => {
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
Expand All @@ -201,7 +201,7 @@ const ReportsPage: React.FC = () => {
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
Expand All @@ -213,6 +213,18 @@ const ReportsPage: React.FC = () => {
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
<Card>
<CardContent>
<Typography color="textSecondary" gutterBottom>
Total Billed
</Typography>
<Typography variant="h4" component="div">
{(report.totalAmount ?? 0).toFixed(2)}
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>

<Paper>
Expand All @@ -222,6 +234,7 @@ const ReportsPage: React.FC = () => {
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Hours</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Description</TableCell>
<TableCell>Created</TableCell>
</TableRow>
Expand All @@ -242,6 +255,11 @@ const ReportsPage: React.FC = () => {
variant="outlined"
/>
</TableCell>
<TableCell>
<Typography variant="body2">
{(entry.amount ?? 0).toFixed(2)}
</Typography>
</TableCell>
<TableCell>
{entry.description ? (
<Typography variant="body2" color="text.secondary">
Expand All @@ -260,7 +278,7 @@ const ReportsPage: React.FC = () => {
))
) : (
<TableRow>
<TableCell colSpan={4} align="center">
<TableCell colSpan={5} align="center">
<Typography color="text.secondary" sx={{ py: 3 }}>
No work entries found for this client.
</Typography>
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Client {
description: string | null;
department: string | null;
email: string | null;
billingRate: number;
created_at: string;
updated_at: string;
}
Expand All @@ -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 {
Expand All @@ -32,6 +35,7 @@ export interface ClientReport {
client: Client;
workEntries: WorkEntry[];
totalHours: number;
totalAmount: number;
entryCount: number;
}

Expand All @@ -40,13 +44,15 @@ export interface CreateClientRequest {
description?: string;
department?: string;
email?: string;
billingRate?: number;
}

export interface UpdateClientRequest {
name?: string;
description?: string;
department?: string;
email?: string;
billingRate?: number;
}

export interface CreateWorkEntryRequest {
Expand Down