ui fixes, input fixes, better dev workflow
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
type FormEvent,
|
||||
} from "react";
|
||||
import { Money } from "../../components/ui";
|
||||
import CurrencyInput from "../../components/CurrencyInput";
|
||||
import { useDashboard } from "../../hooks/useDashboard";
|
||||
import { useToast } from "../../components/Toast";
|
||||
import {
|
||||
@@ -41,6 +42,15 @@ type FixedPlan = {
|
||||
|
||||
type LocalPlan = FixedPlan & { _isNew?: boolean; _isDeleted?: boolean };
|
||||
|
||||
function parseCurrencyToCents(value: string): number {
|
||||
const cleaned = value.replace(/,/g, ".").replace(/[^0-9.]/g, "");
|
||||
const [whole, ...fractionParts] = cleaned.split(".");
|
||||
const fraction = fractionParts.join("");
|
||||
const normalized = fraction.length > 0 ? `${whole}.${fraction.slice(0, 2)}` : whole;
|
||||
const parsed = Number.parseFloat(normalized || "0");
|
||||
return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0;
|
||||
}
|
||||
|
||||
export type PlansSettingsHandle = {
|
||||
save: () => Promise<boolean>;
|
||||
};
|
||||
@@ -132,14 +142,14 @@ const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
|
||||
|
||||
// Form state for adding new plan
|
||||
const [name, setName] = useState("");
|
||||
const [total, setTotal] = useState("");
|
||||
const [totalInput, setTotalInput] = useState("");
|
||||
const [priority, setPriority] = useState("");
|
||||
const [due, setDue] = useState(getTodayInTimezone(userTimezone));
|
||||
const [frequency, setFrequency] = useState<"" | FixedPlan["frequency"]>("monthly");
|
||||
const [autoPayEnabled, setAutoPayEnabled] = useState(false);
|
||||
const [amountMode, setAmountMode] = useState<"fixed" | "estimated">("fixed");
|
||||
|
||||
const totalCents = Math.max(0, Math.round((Number(total) || 0) * 100));
|
||||
const totalCents = Math.max(0, parseCurrencyToCents(totalInput));
|
||||
const parsedPriority = Math.max(0, Math.floor(Number(priority) || 0));
|
||||
const addDisabled = !name.trim() || totalCents <= 0 || !due || isSaving;
|
||||
|
||||
@@ -188,7 +198,7 @@ const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
|
||||
};
|
||||
setLocalPlans((prev) => [...prev, newPlan]);
|
||||
setName("");
|
||||
setTotal("");
|
||||
setTotalInput("");
|
||||
setPriority("");
|
||||
setDue(getTodayInTimezone(userTimezone));
|
||||
setFrequency("monthly");
|
||||
@@ -540,8 +550,8 @@ const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
|
||||
push("err", "Save this plan before applying an actual amount.");
|
||||
return;
|
||||
}
|
||||
const raw = actualInputs[plan.id];
|
||||
const actualCents = Math.max(0, Math.round((Number(raw) || 0) * 100));
|
||||
const raw = actualInputs[plan.id] ?? "";
|
||||
const actualCents = Math.max(0, parseCurrencyToCents(raw));
|
||||
setApplyingActualByPlan((prev) => ({ ...prev, [plan.id]: true }));
|
||||
setTrueUpMessages((prev) => ({ ...prev, [plan.id]: "" }));
|
||||
try {
|
||||
@@ -629,18 +639,15 @@ const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
<CurrencyInput
|
||||
className="input"
|
||||
placeholder={amountMode === "estimated" ? "Estimated $" : "Total $"}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={total}
|
||||
onChange={(e) => setTotal(e.target.value)}
|
||||
placeholder={amountMode === "estimated" ? "Estimated amount" : "Total amount"}
|
||||
value={totalInput}
|
||||
onValue={setTotalInput}
|
||||
/>
|
||||
{amountMode === "estimated" && (
|
||||
<div className="text-xs muted col-span-full">
|
||||
Tip: Always over-estimate variable bills to avoid due-date shortfalls.
|
||||
<div className="settings-add-form-tip">
|
||||
Tip: For variable bills (utilities, usage-based charges), set a slightly higher estimate to avoid due-date shortfalls.
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
@@ -777,14 +784,11 @@ const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="stack gap-1">
|
||||
<span className="label">Actual this cycle</span>
|
||||
<input
|
||||
<CurrencyInput
|
||||
className="input w-36"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={actualInputs[plan.id] ?? ""}
|
||||
onChange={(e) =>
|
||||
setActualInputs((prev) => ({ ...prev, [plan.id]: e.target.value }))
|
||||
onValue={(nextValue) =>
|
||||
setActualInputs((prev) => ({ ...prev, [plan.id]: nextValue }))
|
||||
}
|
||||
placeholder={((plan.totalCents ?? 0) / 100).toFixed(2)}
|
||||
/>
|
||||
@@ -799,7 +803,7 @@ const PlansSettings = forwardRef<PlansSettingsHandle, PlansSettingsProps>(
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs muted mt-2">
|
||||
Using a slightly higher estimate helps prevent last-minute shortages.
|
||||
Keep this estimate slightly high for variable bills, then true-up with the actual amount when posted.
|
||||
</div>
|
||||
{trueUpMessages[plan.id] ? (
|
||||
<div className="text-xs mt-2">{trueUpMessages[plan.id]}</div>
|
||||
@@ -958,7 +962,7 @@ function InlineEditMoney({
|
||||
useEffect(() => setV((cents / 100).toFixed(2)), [cents]);
|
||||
|
||||
const commit = () => {
|
||||
const newCents = Math.max(0, Math.round((Number(v) || 0) * 100));
|
||||
const newCents = Math.max(0, parseCurrencyToCents(v));
|
||||
if (newCents !== cents) onChange(newCents);
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user