feat: make MH Paid column inline-editable
Add PATCH /api/payments/:id/mh-paid-amount for direct value updates. Clicking the MH Paid cell opens an input; Enter/blur saves and refreshes, Escape cancels. Dash placeholder is also clickable to enter a value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -427,6 +427,34 @@ router.patch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PATCH /api/payments/:id/mh-paid-amount
|
||||||
|
router.patch(
|
||||||
|
"/:id/mh-paid-amount",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const paymentId = parseIntOrError(req.params.id, "Payment ID");
|
||||||
|
const raw = req.body.mhPaidAmount;
|
||||||
|
const mhPaidAmount = parseFloat(raw);
|
||||||
|
if (isNaN(mhPaidAmount) || mhPaidAmount < 0) {
|
||||||
|
return res.status(400).json({ message: "Invalid mhPaidAmount value" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.payment.update({
|
||||||
|
where: { id: paymentId },
|
||||||
|
data: { mhPaidAmount, updatedById: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ ...updated, mhPaidAmount: Number(updated.mhPaidAmount) });
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : "Failed to update MH paid amount";
|
||||||
|
return res.status(500).json({ message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// PATCH /api/payments/:id/mh-payment-check
|
// PATCH /api/payments/:id/mh-payment-check
|
||||||
router.patch(
|
router.patch(
|
||||||
"/:id/mh-payment-check",
|
"/:id/mh-payment-check",
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export default function PaymentsRecentTable({
|
|||||||
const [selectedPaymentId, setSelectedPaymentId] = useState<number | null>(null);
|
const [selectedPaymentId, setSelectedPaymentId] = useState<number | null>(null);
|
||||||
const [checkedPaymentIds, setCheckedPaymentIds] = useState<Set<number>>(new Set());
|
const [checkedPaymentIds, setCheckedPaymentIds] = useState<Set<number>>(new Set());
|
||||||
const [isMhChecking, setIsMhChecking] = useState(false);
|
const [isMhChecking, setIsMhChecking] = useState(false);
|
||||||
|
const [editingMhPaidId, setEditingMhPaidId] = useState<number | null>(null);
|
||||||
|
const [editingMhPaidValue, setEditingMhPaidValue] = useState<string>("");
|
||||||
|
|
||||||
const [isRevertOpen, setIsRevertOpen] = useState(false);
|
const [isRevertOpen, setIsRevertOpen] = useState(false);
|
||||||
const [revertPaymentId, setRevertPaymentId] = useState<number | null>(null);
|
const [revertPaymentId, setRevertPaymentId] = useState<number | null>(null);
|
||||||
@@ -741,12 +743,60 @@ export default function PaymentsRecentTable({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{payment.mhPaidAmount != null ? (
|
{editingMhPaidId === payment.id ? (
|
||||||
<span className="text-sm font-medium text-green-700">
|
<input
|
||||||
${Number(payment.mhPaidAmount).toFixed(2)}
|
type="number"
|
||||||
</span>
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
autoFocus
|
||||||
|
className="w-24 border border-blue-400 rounded px-1 py-0.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||||
|
value={editingMhPaidValue}
|
||||||
|
onChange={(e) => setEditingMhPaidValue(e.target.value)}
|
||||||
|
onKeyDown={async (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
setEditingMhPaidId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={async () => {
|
||||||
|
const val = parseFloat(editingMhPaidValue);
|
||||||
|
if (!isNaN(val) && val >= 0) {
|
||||||
|
try {
|
||||||
|
const res = await apiRequest(
|
||||||
|
"PATCH",
|
||||||
|
`/api/payments/${payment.id}/mh-paid-amount`,
|
||||||
|
{ mhPaidAmount: val }
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: QK_PAYMENTS_RECENT_BASE });
|
||||||
|
} else {
|
||||||
|
toast({ title: "Error", description: "Failed to save MH paid amount.", variant: "destructive" });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast({ title: "Error", description: "Failed to save MH paid amount.", variant: "destructive" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEditingMhPaidId(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-gray-400">—</span>
|
<span
|
||||||
|
className="text-sm font-medium text-green-700 cursor-pointer hover:underline hover:text-green-900"
|
||||||
|
title="Click to edit"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingMhPaidId(payment.id);
|
||||||
|
setEditingMhPaidValue(
|
||||||
|
payment.mhPaidAmount != null
|
||||||
|
? Number(payment.mhPaidAmount).toFixed(2)
|
||||||
|
: "0.00"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{payment.mhPaidAmount != null
|
||||||
|
? `$${Number(payment.mhPaidAmount).toFixed(2)}`
|
||||||
|
: <span className="text-gray-400 font-normal">—</span>}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user