feat: Add upsert behavior for battery groups

When adding batteries with the same brand-type-chemistry combination as
an existing group, the quantity is now added to the existing group instead
of failing with a duplicate error.

Changes:
- Update POST /api/batteries to check for existing group and upsert
- Add isNew flag to API response for UI feedback
- Update AddBatteryModal to show contextual success message
- Document upsert behavior in FSD.md and TSD.md

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
root 2026-01-19 17:11:55 +00:00
parent 9c4a9a141a
commit febdc8beab
4 changed files with 114 additions and 15 deletions

View File

@ -54,6 +54,7 @@ Each brand-type-chemistry combination can have batteries in three states:
- Can select from predefined chemistries (NiMH, Li-ion, etc.) or add custom
- Can enter brand name
- Can set initial quantity and status
- If the same brand-type-chemistry combination already exists, the quantity is added to the existing group (upsert behavior)
### US-02: View Inventory
> As a user, I want to see all my batteries at a glance so I know what's available.
@ -87,6 +88,14 @@ Each brand-type-chemistry combination can have batteries in three states:
- Mark charging complete (moves to available)
- Optional: track charging start time
### US-06: Add Batteries to Existing Group
> As a user, I want to add more batteries to an existing group when I purchase more or find additional batteries.
**Acceptance Criteria:**
- When adding batteries with same brand-type-chemistry as existing group, quantity is added to that group
- User receives feedback indicating batteries were added to existing group vs new group created
- Works seamlessly whether user intentionally or accidentally selects existing combination
## 5. Data Model (Conceptual)
```

View File

@ -193,7 +193,7 @@ POST /api/brands # Create brand
# Battery Groups
GET /api/batteries # List all battery groups (with brand/type/chemistry info)
POST /api/batteries # Create battery group
POST /api/batteries # Create or add to battery group (upsert)
GET /api/batteries/:id # Get single battery group
PATCH /api/batteries/:id # Update battery group
DELETE /api/batteries/:id # Delete battery group
@ -309,6 +309,49 @@ async function assignToDevice(groupId: number, deviceId: number, quantity: numbe
}
```
### 6.4 Add Batteries (Upsert Behavior)
When adding batteries, the system checks if a group with the same brand-type-chemistry combination exists:
- **If exists**: Adds the quantity to the existing group's available count
- **If not exists**: Creates a new battery group
```typescript
// POST /api/batteries - Upsert behavior
async function addBatteries(brandId: number, typeId: number, chemistryId: number, quantity: number) {
// Check for existing group
const existing = await db
.select()
.from(batteryGroups)
.where(and(
eq(batteryGroups.brandId, brandId),
eq(batteryGroups.typeId, typeId),
eq(batteryGroups.chemistryId, chemistryId)
))
.get();
if (existing) {
// Add to existing group
await db
.update(batteryGroups)
.set({
availableCount: sql`available_count + ${quantity}`,
updatedAt: new Date()
})
.where(eq(batteryGroups.id, existing.id));
return { ...existing, isNew: false };
} else {
// Create new group
const result = await db
.insert(batteryGroups)
.values({ brandId, typeId, chemistryId, availableCount: quantity })
.returning();
return { ...result[0], isNew: true };
}
}
```
Response includes `isNew` flag to allow UI to show appropriate feedback message.
## 7. UI/UX Design Details
### Color Scheme

View File

@ -1,6 +1,6 @@
import { NextResponse } from 'next/server';
import { db, schema } from '@/lib/db';
import { eq, sql } from 'drizzle-orm';
import { eq, sql, and } from 'drizzle-orm';
export async function GET() {
try {
@ -56,19 +56,58 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Brand, type, and chemistry are required' }, { status: 400 });
}
const result = await db
.insert(schema.batteryGroups)
.values({
brandId,
typeId,
chemistryId,
availableCount: availableCount || 0,
chargingCount: chargingCount || 0,
notes,
})
.returning();
// Check if a group with the same brand-type-chemistry already exists
const existing = await db
.select()
.from(schema.batteryGroups)
.where(
and(
eq(schema.batteryGroups.brandId, brandId),
eq(schema.batteryGroups.typeId, typeId),
eq(schema.batteryGroups.chemistryId, chemistryId)
)
)
.get();
return NextResponse.json(result[0], { status: 201 });
if (existing) {
// Add to existing group
const quantityToAdd = availableCount || 0;
const chargingToAdd = chargingCount || 0;
await db
.update(schema.batteryGroups)
.set({
availableCount: sql`${schema.batteryGroups.availableCount} + ${quantityToAdd}`,
chargingCount: sql`${schema.batteryGroups.chargingCount} + ${chargingToAdd}`,
notes: notes || existing.notes,
updatedAt: sql`(datetime('now'))`,
})
.where(eq(schema.batteryGroups.id, existing.id));
// Fetch updated record
const updated = await db
.select()
.from(schema.batteryGroups)
.where(eq(schema.batteryGroups.id, existing.id))
.get();
return NextResponse.json({ ...updated, isNew: false }, { status: 200 });
} else {
// Create new group
const result = await db
.insert(schema.batteryGroups)
.values({
brandId,
typeId,
chemistryId,
availableCount: availableCount || 0,
chargingCount: chargingCount || 0,
notes,
})
.returning();
return NextResponse.json({ ...result[0], isNew: true }, { status: 201 });
}
} catch (error) {
console.error('Failed to create battery group:', error);
return NextResponse.json({ error: 'Failed to create battery group' }, { status: 500 });

View File

@ -93,7 +93,15 @@ export function AddBatteryModal({ isOpen, onClose, types, brands, chemistries }:
throw new Error(error.error || 'Failed to add battery');
}
showToast('success', 'Battery added successfully');
const result = await res.json();
const quantity = (parseInt(formData.availableCount) || 0) + (parseInt(formData.chargingCount) || 0);
if (result.isNew) {
showToast('success', `New battery group created with ${quantity} batteries`);
} else {
showToast('success', `${quantity} batteries added to existing group`);
}
onClose();
router.refresh();