Add duplicate pattern check when adding budget rules
Client side catches it immediately (case-insensitive match against existing rules) and shows an inline error. API also rejects with 409 as the authoritative guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,11 @@ export async function POST(req: Request) {
|
|||||||
const budget = await prisma.budget.findFirst({ where: { id: budgetId, userId: session.user.id } })
|
const budget = await prisma.budget.findFirst({ where: { id: budgetId, userId: session.user.id } })
|
||||||
if (!budget) return NextResponse.json({ error: 'Budget not found' }, { status: 404 })
|
if (!budget) return NextResponse.json({ error: 'Budget not found' }, { status: 404 })
|
||||||
|
|
||||||
|
const existing = await prisma.budgetRule.findFirst({
|
||||||
|
where: { budgetId, pattern: { equals: pattern, mode: 'insensitive' } },
|
||||||
|
})
|
||||||
|
if (existing) return NextResponse.json({ error: 'Duplicate rule pattern' }, { status: 409 })
|
||||||
|
|
||||||
const rule = await prisma.budgetRule.create({
|
const rule = await prisma.budgetRule.create({
|
||||||
data: { userId: session.user.id, budgetId, pattern },
|
data: { userId: session.user.id, budgetId, pattern },
|
||||||
select: { id: true, budgetId: true, pattern: true },
|
select: { id: true, budgetId: true, pattern: true },
|
||||||
|
|||||||
@@ -24,10 +24,14 @@ export function BudgetRulesDialog({ open, onOpenChange, budgetId, budgetName, ru
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [pattern, setPattern] = useState('')
|
const [pattern, setPattern] = useState('')
|
||||||
const [adding, setAdding] = useState(false)
|
const [adding, setAdding] = useState(false)
|
||||||
|
const [dupError, setDupError] = useState(false)
|
||||||
|
|
||||||
async function handleAdd(e: React.FormEvent) {
|
async function handleAdd(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!pattern.trim()) return
|
if (!pattern.trim()) return
|
||||||
|
const isDup = rules.some((r) => r.pattern.toLowerCase() === pattern.trim().toLowerCase())
|
||||||
|
if (isDup) { setDupError(true); return }
|
||||||
|
setDupError(false)
|
||||||
setAdding(true)
|
setAdding(true)
|
||||||
await fetch('/api/budget-rules', {
|
await fetch('/api/budget-rules', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -36,6 +40,7 @@ export function BudgetRulesDialog({ open, onOpenChange, budgetId, budgetName, ru
|
|||||||
})
|
})
|
||||||
setPattern('')
|
setPattern('')
|
||||||
setAdding(false)
|
setAdding(false)
|
||||||
|
setDupError(false)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,17 +83,22 @@ export function BudgetRulesDialog({ open, onOpenChange, budgetId, budgetName, ru
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleAdd} className="flex gap-2">
|
<form onSubmit={handleAdd} className="space-y-1.5">
|
||||||
<Input
|
<div className="flex gap-2">
|
||||||
placeholder="e.g. Amazon, Netflix, Starbucks"
|
<Input
|
||||||
value={pattern}
|
placeholder="e.g. Amazon, Netflix, Starbucks"
|
||||||
onChange={(e) => setPattern(e.target.value)}
|
value={pattern}
|
||||||
className="flex-1"
|
onChange={(e) => { setPattern(e.target.value); setDupError(false) }}
|
||||||
/>
|
className={dupError ? 'flex-1 border-destructive focus-visible:ring-destructive' : 'flex-1'}
|
||||||
<Button type="submit" disabled={adding || !pattern.trim()} size="sm">
|
/>
|
||||||
<Plus className="h-4 w-4 mr-1" />
|
<Button type="submit" disabled={adding || !pattern.trim()} size="sm">
|
||||||
Add
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{dupError && (
|
||||||
|
<p className="text-xs text-destructive">That pattern already exists for this budget.</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user