Add draft-only invoicing rules, send reminders, and time clock billing.
Restrict line item edits to draft invoices, auto-create drafts on clock-out, and add sendReminderAt scheduling with dashboard due reminders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -52,6 +52,7 @@ import { Separator } from "~/components/ui/separator";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { DatePicker } from "~/components/ui/date-picker";
|
||||
import {
|
||||
getEffectiveInvoiceStatus,
|
||||
isInvoiceOverdue,
|
||||
@@ -166,6 +167,15 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
onError: (e) => toast.error(e.message ?? "Failed to send reminder"),
|
||||
});
|
||||
|
||||
const updateInvoice = api.invoices.update.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Reminder saved");
|
||||
void utils.invoices.getById.invalidate({ id: invoiceId });
|
||||
void utils.dashboard.getStats.invalidate();
|
||||
},
|
||||
onError: (e) => toast.error(e.message ?? "Failed to save reminder"),
|
||||
});
|
||||
|
||||
if (isLoading) return <InvoiceDetailsSkeleton />;
|
||||
if (!invoice) notFound();
|
||||
|
||||
@@ -522,7 +532,7 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
|
||||
{/* Right Column - Actions */}
|
||||
<div className="space-y-6">
|
||||
{effectiveStatus !== "paid" && (
|
||||
{storedStatus === "draft" && (
|
||||
<InvoiceTimerCard invoiceId={invoiceId} clientId={invoice.clientId} />
|
||||
)}
|
||||
|
||||
@@ -553,6 +563,25 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{effectiveStatus === "draft" && (
|
||||
<SendReminderEditor
|
||||
key={`${invoiceId}-${invoice.sendReminderAt?.toISOString() ?? "none"}`}
|
||||
invoiceId={invoiceId}
|
||||
savedReminderAt={invoice.sendReminderAt}
|
||||
formatDate={formatDate}
|
||||
isSaving={updateInvoice.isPending}
|
||||
onSave={(sendReminderAt) =>
|
||||
updateInvoice.mutate({
|
||||
id: invoiceId,
|
||||
sendReminderAt,
|
||||
})
|
||||
}
|
||||
onClear={() =>
|
||||
updateInvoice.mutate({ id: invoiceId, sendReminderAt: null })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(effectiveStatus === "sent" || effectiveStatus === "overdue") && (
|
||||
<EnhancedSendInvoiceButton
|
||||
invoiceId={invoice.id}
|
||||
@@ -808,6 +837,67 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SendReminderEditor({
|
||||
invoiceId,
|
||||
savedReminderAt,
|
||||
formatDate,
|
||||
isSaving,
|
||||
onSave,
|
||||
onClear,
|
||||
}: {
|
||||
invoiceId: string;
|
||||
savedReminderAt: Date | null | undefined;
|
||||
formatDate: (date: Date) => string;
|
||||
isSaving: boolean;
|
||||
onSave: (sendReminderAt: Date | null) => void;
|
||||
onClear: () => void;
|
||||
}) {
|
||||
const [sendReminderAt, setSendReminderAt] = useState<Date | undefined>(() =>
|
||||
savedReminderAt ? new Date(savedReminderAt) : undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-lg border p-3">
|
||||
<Label htmlFor={`send-reminder-at-${invoiceId}`}>Remind me to send</Label>
|
||||
<DatePicker
|
||||
date={sendReminderAt}
|
||||
onDateChange={setSendReminderAt}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => onSave(sendReminderAt ?? null)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Save reminder
|
||||
</Button>
|
||||
{sendReminderAt ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSendReminderAt(undefined);
|
||||
onClear();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{savedReminderAt ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{new Date(savedReminderAt) <= new Date()
|
||||
? "Reminder is due — time to send this invoice."
|
||||
: `Scheduled for ${formatDate(savedReminderAt)}`}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InvoiceViewPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
Reference in New Issue
Block a user