fix(automation-rules): hint amount sign for expenses vs income (#524)
## Problem A user reported automation rules not applying. The rule matched a description **and** `Valor es igual a 21,99`, but never fired. Root cause: the matching engine evaluates the **signed** transaction amount (`amount / 100`), and expenses are stored negative. So an expense of `-21,99` is compared as `-21.99 == 21.99` → false. The rule editor showed a plain number input with no hint about the sign convention, so entering a positive `21,99` to match an expense silently builds a rule that can never match. This is a common trap — likely affecting many users with amount-based rules. ## Change Add a small helper note under the numeric `amount`/`Valor` value input: > Usa un valor negativo para gastos (ej. -21,99) y un valor positivo para ingresos. - Renders only for the numeric `amount` field; text conditions are unaffected. - No change to how rules are stored or evaluated — existing rules keep working. - Added the Spanish translation key (`es.json` is CI-enforced). ## Testing - `bun run format` and `eslint` clean on the component. - `LocalizationTest` passes (translation key resolves, no missing keys). ## Follow-up (not in this PR) The hint guides new rules but doesn't retroactively fix rules already saved wrong. A future enhancement could warn when saving a positive-value amount condition, since most finance rules target expenses.
This commit is contained in:
parent
d2a4412118
commit
e526f861b2
|
|
@ -1645,6 +1645,7 @@
|
|||
"Uruguayan Peso": "Peso uruguayo",
|
||||
"Usage Information:": "Información de Uso:",
|
||||
"Use Defaults": "Usar Valores Predeterminados",
|
||||
"Use a negative value for expenses (e.g. -21.99) and a positive value for income.": "Usa un valor negativo para gastos (ej. -21,99) y un valor positivo para ingresos.",
|
||||
"Use a strong password (minimum 12 characters). This password will encrypt your data.": "Usa una contraseña fuerte (mínimo 12 caracteres). Esta contraseña encriptará tus datos.",
|
||||
"Use a strong password (minimum 12 characters). This\\n password will encrypt your data.": "Usa una contraseña segura (mínimo 12 caracteres). Esta contraseña cifrará tus datos.",
|
||||
"Use a view-only key.": "Usa una clave de solo lectura.",
|
||||
|
|
|
|||
|
|
@ -275,62 +275,79 @@ function ConditionRow({
|
|||
|
||||
const inputType = fieldConfig?.type === 'number' ? 'number' : 'text';
|
||||
|
||||
const showAmountHint = showValueInput && inputType === 'number';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Select value={condition.field} onValueChange={handleFieldChange}>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(FIELD_CONFIG).map(([key, config]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{__(config.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
<Select
|
||||
value={condition.field}
|
||||
onValueChange={handleFieldChange}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(FIELD_CONFIG).map(([key, config]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
{__(config.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={condition.operator}
|
||||
onValueChange={handleOperatorChange}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableOperators.map((op) => (
|
||||
<SelectItem key={op} value={op}>
|
||||
{__(OPERATOR_LABELS[op])}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={condition.operator}
|
||||
onValueChange={handleOperatorChange}
|
||||
>
|
||||
<SelectTrigger className="w-full sm:w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableOperators.map((op) => (
|
||||
<SelectItem key={op} value={op}>
|
||||
{__(OPERATOR_LABELS[op])}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{showValueInput && (
|
||||
<Input
|
||||
type={inputType}
|
||||
value={condition.value}
|
||||
onChange={(e) =>
|
||||
onChange({ ...condition, value: e.target.value })
|
||||
}
|
||||
placeholder={__('Value')}
|
||||
className="w-full sm:flex-1"
|
||||
step={inputType === 'number' ? 'any' : undefined}
|
||||
/>
|
||||
{showValueInput && (
|
||||
<Input
|
||||
type={inputType}
|
||||
value={condition.value}
|
||||
onChange={(e) =>
|
||||
onChange({ ...condition, value: e.target.value })
|
||||
}
|
||||
placeholder={__('Value')}
|
||||
className="w-full sm:flex-1"
|
||||
step={inputType === 'number' ? 'any' : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!showValueInput && (
|
||||
<div className="hidden sm:flex sm:flex-1" />
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onRemove}
|
||||
disabled={!canRemove}
|
||||
className={`self-end sm:self-auto${!canRemove ? 'opacity-30' : ''}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showAmountHint && (
|
||||
<p className="text-xs pl-2 py-2 text-muted-foreground">
|
||||
{__(
|
||||
'Use a negative value for expenses (e.g. -21.99) and a positive value for income.',
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!showValueInput && <div className="hidden sm:flex sm:flex-1" />}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onRemove}
|
||||
disabled={!canRemove}
|
||||
className={`self-end sm:self-auto${!canRemove ? 'opacity-30' : ''}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue