match the budget picker to the rules picker

This commit is contained in:
Owen
2026-08-10 13:55:05 -04:00
parent 7e6e0a8a9c
commit f2e7f83b3d
+161 -209
View File
@@ -1,13 +1,10 @@
"use client"; "use client";
import { import {
SettingsFormCell,
SettingsFormGrid,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionFooter, SettingsSectionFooter,
SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
@@ -281,217 +278,172 @@ export function BudgetsEditor({
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <div className="space-y-4">
<SettingsFormGrid> <Table>
<SettingsFormCell span="half"> <TableHeader>
<div className="space-y-4"> <TableRow>
<Table> <TableHead>{t("aiBudgetAmount")}</TableHead>
<TableHeader> <TableHead>{t("aiBudgetUnit")}</TableHead>
<TableRow> <TableHead>{t("aiBudgetPeriod")}</TableHead>
<TableHead> <TableHead></TableHead>
{t("aiBudgetUnit")} </TableRow>
</TableHead> </TableHeader>
<TableHead> <TableBody>
{t("aiBudgetPeriod")} {rows.length === 0 ? (
</TableHead> <DataTableEmptyState
<TableHead> colSpan={4}
{t("aiBudgetAmount")} message={t("aiBudgetEmpty")}
</TableHead> action={addRowButton}
<TableHead></TableHead> compact
</TableRow> />
</TableHeader> ) : (
<TableBody> rows.map((row) => {
{rows.length === 0 ? ( const showConflict = conflictingKeys.has(
<DataTableEmptyState row.key
colSpan={4} );
message={t("aiBudgetEmpty")} const showInvalidAmount =
action={addRowButton} attemptedSave &&
compact invalidAmountKeys.has(row.key);
/> return (
) : ( <TableRow key={row.key}>
rows.map((row) => { <TableCell>
const showConflict = <Input
conflictingKeys.has( type="number"
row.key min="0"
); step="any"
const showInvalidAmount = placeholder={t(
attemptedSave && "aiBudgetAmountPlaceholder"
invalidAmountKeys.has( )}
row.key value={row.amount}
); aria-invalid={
return ( showInvalidAmount
<TableRow key={row.key}> }
<TableCell> disabled={
<Select saveLoading ||
value={row.unit} budgetsQuery.isLoading
onValueChange={( }
value onChange={(e) =>
) => updateRow(row.key, {
updateRow( amount: e.target
row.key, .value
{ })
unit: value as AiBudgetUnit }
} className="w-full min-w-0"
) />
} </TableCell>
disabled={ <TableCell>
saveLoading || <Select
budgetsQuery.isLoading value={row.unit}
} onValueChange={(value) =>
> updateRow(row.key, {
<SelectTrigger unit: value as AiBudgetUnit
className="w-full min-w-0" })
aria-invalid={ }
showConflict disabled={
} saveLoading ||
budgetsQuery.isLoading
}
>
<SelectTrigger
className="w-full min-w-0"
aria-invalid={
showConflict
}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{AI_BUDGET_UNITS.map(
(unit) => (
<SelectItem
key={unit}
value={unit}
> >
<SelectValue /> {
</SelectTrigger> unitLabels[
<SelectContent>
{AI_BUDGET_UNITS.map(
(
unit unit
) => ( ]
<SelectItem }
key={ </SelectItem>
unit )
} )}
value={ </SelectContent>
unit </Select>
} </TableCell>
> <TableCell>
{ <Select
unitLabels[ value={row.period}
unit onValueChange={(value) =>
] updateRow(row.key, {
} period: value as AiBudgetPeriod
</SelectItem> })
) }
)} disabled={
</SelectContent> saveLoading ||
</Select> budgetsQuery.isLoading
</TableCell> }
<TableCell> >
<Select <SelectTrigger
value={ className="w-full min-w-0"
row.period aria-invalid={
} showConflict
onValueChange={( }
value >
) => <SelectValue />
updateRow( </SelectTrigger>
row.key, <SelectContent>
{ {AI_BUDGET_PERIODS.map(
period: value as AiBudgetPeriod (period) => (
} <SelectItem
) key={period}
} value={
disabled={ period
saveLoading ||
budgetsQuery.isLoading
}
>
<SelectTrigger
className="w-full min-w-0"
aria-invalid={
showConflict
} }
> >
<SelectValue /> {
</SelectTrigger> periodLabels[
<SelectContent>
{AI_BUDGET_PERIODS.map(
(
period period
) => ( ]
<SelectItem }
key={ </SelectItem>
period )
} )}
value={ </SelectContent>
period </Select>
} </TableCell>
> <TableCell>
{ <div className="flex items-center justify-end space-x-2">
periodLabels[ <Button
period type="button"
] variant="outline"
} disabled={
</SelectItem> saveLoading ||
) budgetsQuery.isLoading
)} }
</SelectContent> onClick={() =>
</Select> removeRow(row.key)
</TableCell> }
<TableCell> >
<Input Delete
type="number" </Button>
min="0" </div>
step="any" </TableCell>
placeholder={t( </TableRow>
"aiBudgetAmountPlaceholder" );
)} })
value={ )}
row.amount </TableBody>
} </Table>
aria-invalid={ {(conflictingKeys.size > 0 ||
showInvalidAmount (attemptedSave && invalidAmountKeys.size > 0)) && (
} <p className="text-xs text-destructive">
disabled={ {conflictingKeys.size > 0
saveLoading || ? t("aiBudgetConflictError")
budgetsQuery.isLoading : t("aiBudgetInvalidAmountError")}
} </p>
onChange={(e) => )}
updateRow( {rows.length > 0 && addRowButton}
row.key, </div>
{
amount: e
.target
.value
}
)
}
className="w-full min-w-0"
/>
</TableCell>
<TableCell>
<Button
type="button"
variant="ghost"
size="icon"
disabled={
saveLoading ||
budgetsQuery.isLoading
}
onClick={() =>
removeRow(
row.key
)
}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
{(conflictingKeys.size > 0 ||
(attemptedSave &&
invalidAmountKeys.size > 0)) && (
<p className="text-xs text-destructive">
{conflictingKeys.size > 0
? t("aiBudgetConflictError")
: t("aiBudgetInvalidAmountError")}
</p>
)}
{rows.length > 0 && addRowButton}
</div>
</SettingsFormCell>
</SettingsFormGrid>
</SettingsSectionForm>
</SettingsSectionBody> </SettingsSectionBody>
<SettingsSectionFooter> <SettingsSectionFooter>