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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user