Added 1/6-hour rounding

X-Lovable-Edit-ID: edt-6e5dc534-7212-4129-b5b4-55525e4bf27c
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 18:34:29 +00:00
co-authored by renee-png
2 changed files with 32 additions and 12 deletions
+25 -12
View File
@@ -9,7 +9,7 @@ import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Plus, Trash2, Loader2, FilePlus } from "lucide-react";
import { formatCurrency, formatDate } from "@/lib/format";
import { roundToTenth } from "@/lib/timer";
import { roundToSixth } from "@/lib/timer";
import { toast } from "sonner";
import {
Select,
@@ -41,11 +41,16 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
const [form, setForm] = useState({
work_date: new Date().toISOString().slice(0, 10),
hours: "",
minutes: "",
hourly_rate: caseRecord.default_hourly_rate?.toString() ?? "",
description: "",
billable: true,
});
// Compute total raw hours from hours + minutes inputs, then round UP to 1/6.
const rawHours = (parseFloat(form.hours || "0") || 0) + (parseFloat(form.minutes || "0") || 0) / 60;
const roundedHours = roundToSixth(rawHours);
const [fees, setFees] = useState<FeeItem[]>([]);
const [selectedFeeId, setSelectedFeeId] = useState<string>("");
@@ -96,10 +101,9 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
const submit = async (e: React.FormEvent) => {
e.preventDefault();
const rawHours = parseFloat(form.hours);
const hours = roundToTenth(rawHours);
const hours = roundedHours;
const rate = parseFloat(form.hourly_rate || "0");
if (!hours || hours <= 0) { toast.error("Hours must be greater than 0"); return; }
if (!hours || hours <= 0) { toast.error("Time must be greater than 0"); return; }
if (!form.description.trim()) { toast.error("Description required"); return; }
if (!user?.id) { toast.error("Not signed in"); return; }
setSubmitting(true);
@@ -115,9 +119,9 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
setSubmitting(false);
if (error) toast.error(error.message);
else {
toast.success("Time entry added");
toast.success(`Time entry added (${hours.toFixed(2)} hrs)`);
setShowForm(false);
setForm({ ...form, hours: "", description: "" });
setForm({ ...form, hours: "", minutes: "", description: "" });
load();
}
};
@@ -141,7 +145,7 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
<div className="space-y-4">
<div className="flex flex-wrap gap-3 items-center justify-between">
<div className="flex gap-4 text-sm">
<Stat label="Hours" value={totals.hours.toFixed(1)} />
<Stat label="Hours" value={totals.hours.toFixed(2)} />
<Stat label="Billed total" value={formatCurrency(totals.billable)} />
<Stat label="Unbilled" value={formatCurrency(totals.unbilled)} />
</div>
@@ -191,14 +195,23 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
</Select>
</div>
)}
<form onSubmit={submit} className="grid grid-cols-1 md:grid-cols-4 gap-3">
<form onSubmit={submit} className="grid grid-cols-1 md:grid-cols-5 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Date</Label>
<Input type="date" value={form.work_date} onChange={(e) => setForm({ ...form, work_date: e.target.value })} required />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Hours</Label>
<Input type="number" step="0.1" min="0.1" value={form.hours} onChange={(e) => setForm({ ...form, hours: e.target.value })} required />
<Input type="number" step="1" min="0" placeholder="0" value={form.hours} onChange={(e) => setForm({ ...form, hours: e.target.value })} />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Minutes</Label>
<Input type="number" step="1" min="0" placeholder="0" value={form.minutes} onChange={(e) => setForm({ ...form, minutes: e.target.value })} />
{rawHours > 0 && (
<p className="text-[11px] text-muted-foreground">
Billed as {roundedHours.toFixed(2)} hrs (1/6 hr)
</p>
)}
</div>
<div className="space-y-1.5">
<Label className="text-xs">Hourly rate ($)</Label>
@@ -210,11 +223,11 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
Billable
</label>
</div>
<div className="md:col-span-4 space-y-1.5">
<div className="md:col-span-5 space-y-1.5">
<Label className="text-xs">Description</Label>
<Textarea rows={2} value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} required maxLength={1000} />
</div>
<div className="md:col-span-4 flex justify-end">
<div className="md:col-span-5 flex justify-end">
<Button type="submit" disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save entry
@@ -247,7 +260,7 @@ export function CaseTimeTab({ caseRecord, onInvoice }: CaseTimeTabProps) {
<td className="px-4 py-3 text-muted-foreground whitespace-nowrap">{formatDate(e.work_date)}</td>
<td className="px-4 py-3 text-muted-foreground">{e.user?.full_name || e.user?.email}</td>
<td className="px-4 py-3 max-w-md">{e.description}</td>
<td className="px-4 py-3 text-right tabular-nums">{Number(e.hours).toFixed(1)}</td>
<td className="px-4 py-3 text-right tabular-nums">{Number(e.hours).toFixed(2)}</td>
<td className="px-4 py-3 text-right tabular-nums text-muted-foreground">{formatCurrency(e.hourly_rate)}</td>
<td className="px-4 py-3 text-right tabular-nums font-medium">{e.billable ? formatCurrency(Number(e.hours) * Number(e.hourly_rate)) : "—"}</td>
<td className="px-4 py-3 text-xs text-muted-foreground">{!e.billable ? "Non-billable" : e.invoice_id ? "Invoiced" : "Unbilled"}</td>
+7
View File
@@ -184,3 +184,10 @@ export function roundToTenth(hours: number): number {
if (!hours || hours <= 0) return 0;
return Math.max(0.1, Math.round(Math.ceil(hours * 10)) / 10);
}
/** Round hours UP to the nearest 1/6 hour (10-minute) increment, with 1/6 minimum. */
export function roundToSixth(hours: number): number {
if (!hours || hours <= 0) return 0;
const sixths = Math.max(1, Math.ceil(hours * 6));
return Math.round((sixths / 6) * 1000) / 1000;
}