Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions app/(protected)/invoices/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,15 @@ export default function InvoicesPage() {
const bMap = new Map(bidders.map((b) => [b.id!, b]));
return invs
.map((inv) => ({ ...inv, bidder: bMap.get(inv.bidderId) }))
.sort(
(a, b) =>
new Date(b.generatedAt).getTime() -
new Date(a.generatedAt).getTime()
);
.sort((a, b) => {
// Sort by invoice number (ascending) for a stable order that
// doesn't reshuffle on every recalc/sync. Falls back to id.
const an = a.invoiceNumber ?? "";
const bn = b.invoiceNumber ?? "";
const cmp = an.localeCompare(bn, undefined, { numeric: true });
if (cmp !== 0) return cmp;
return (a.id ?? 0) - (b.id ?? 0);
});
}, []),
[currentEventId, dbReady, db]
);
Expand Down
147 changes: 98 additions & 49 deletions components/bidders/BidderForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import type { Bidder } from "@/lib/db";
import { useUserDb } from "@/components/providers/UserDbProvider";
import { useCloudSync } from "@/components/providers/CloudSyncProvider";
Expand All @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { getSuggestedPaddleNumber } from "@/lib/hooks/useBidders";
import { mutateWithParentEventTouch } from "@/lib/db/mutateWithParentEventTouch";
import { flushSingleEventToCloudSnapshot } from "@/lib/services/cloudSync";

type Props = {
open: boolean;
Expand All @@ -33,32 +34,49 @@ export function BidderForm({
const [phone, setPhone] = useState("");
const [email, setEmail] = useState("");
const [error, setError] = useState<string | null>(null);
const [paddleReady, setPaddleReady] = useState(false);
const [submitting, setSubmitting] = useState(false);
const submittingRef = useRef(false);

useEffect(() => {
if (!open) return;
setError(null);
setPaddleReady(false);
(async () => {
if (!db) return;
if (!db) {
setError("Local database is unavailable. Reload and try again.");
return;
}
if (editing) {
setPaddleNumber(String(editing.paddleNumber));
setFirstName(editing.firstName);
setLastName(editing.lastName);
setPhone(editing.phone ?? "");
setEmail(editing.email ?? "");
setPaddleReady(true);
} else {
const next = await getSuggestedPaddleNumber(db, eventId);
setPaddleNumber(String(next));
setFirstName("");
setLastName("");
setPhone("");
setEmail("");
setPaddleReady(true);
}
})();
}, [open, editing, eventId, db]);

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!db) return;
if (submittingRef.current) return;
if (!db) {
setError("Local database is unavailable. Reload and try again.");
return;
}
if (!paddleReady) {
setError("One moment — still loading. Try again.");
return;
}
setError(null);
const paddle = parseInt(paddleNumber.trim(), 10);
if (!Number.isFinite(paddle) || paddle < 1) {
Expand All @@ -71,52 +89,70 @@ export function BidderForm({
setError("First and last name are required.");
return;
}
const taken = await db.bidders
.where("[eventId+paddleNumber]")
.equals([eventId, paddle])
.first();
const editingId = editing?.id;
if (
taken != null &&
(typeof editingId !== "number" || taken.id !== editingId)
) {
setError(`Paddle #${paddle} is already registered for this event.`);
return;
}
const now = new Date();
submittingRef.current = true;
setSubmitting(true);
try {
await mutateWithParentEventTouch(db, eventId, "bidders", async () => {
if (editing?.id != null) {
await db.bidders.update(editing.id, {
paddleNumber: paddle,
firstName: fn,
lastName: ln,
phone: phone.trim() || undefined,
email: email.trim() || undefined,
updatedAt: now,
});
} else {
await db.bidders.add({
eventId,
paddleNumber: paddle,
firstName: fn,
lastName: ln,
phone: phone.trim() || undefined,
email: email.trim() || undefined,
createdAt: now,
updatedAt: now,
});
const taken = await db.bidders
.where("[eventId+paddleNumber]")
.equals([eventId, paddle])
.first();
const editingId = editing?.id;
if (
taken != null &&
(typeof editingId !== "number" || taken.id !== editingId)
) {
setError(`Paddle #${paddle} is already registered for this event.`);
return;
}
const now = new Date();
try {
await mutateWithParentEventTouch(db, eventId, "bidders", async () => {
if (editing?.id != null) {
await db.bidders.update(editing.id, {
paddleNumber: paddle,
firstName: fn,
lastName: ln,
phone: phone.trim() || undefined,
email: email.trim() || undefined,
updatedAt: now,
});
} else {
await db.bidders.add({
eventId,
paddleNumber: paddle,
firstName: fn,
lastName: ln,
phone: phone.trim() || undefined,
email: email.trim() || undefined,
createdAt: now,
updatedAt: now,
});
}
});
} catch (err) {
setError(
err instanceof Error ? err.message : "Could not save bidder. Try again."
);
return;
}
// Bidder ops are not tracked in the per-event op log, so a background
// pull can full-replace local bidders before the debounced cloud push
// runs. Flush a snapshot immediately when online so the new/edited
// bidder is durable on the server before the next pull.
if (typeof navigator !== "undefined" && navigator.onLine) {
try {
await flushSingleEventToCloudSnapshot(db, eventId);
} catch {
/* fall back to debounced push */
}
});
} catch (e) {
setError(
e instanceof Error ? e.message : "Could not save bidder. Try again."
);
return;
}
scheduleCloudPush();
onSaved();
onClose();
} finally {
submittingRef.current = false;
setSubmitting(false);
}
scheduleCloudPush();
onSaved();
onClose();
}

return (
Expand All @@ -126,11 +162,24 @@ export function BidderForm({
onClose={onClose}
footer={
<>
<Button variant="secondary" type="button" onClick={onClose}>
<Button
variant="secondary"
type="button"
onClick={onClose}
disabled={submitting}
>
Cancel
</Button>
<Button type="submit" form="bidder-form">
{editing ? "Save" : "Add bidder"}
<Button
type="submit"
form="bidder-form"
disabled={submitting || !paddleReady}
>
{submitting
? "Saving…"
: editing
? "Save"
: "Add bidder"}
</Button>
</>
}
Expand Down
Loading