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:
parent
9c4a9a141a
commit
febdc8beab
|
|
@ -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 select from predefined chemistries (NiMH, Li-ion, etc.) or add custom
|
||||||
- Can enter brand name
|
- Can enter brand name
|
||||||
- Can set initial quantity and status
|
- 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
|
### US-02: View Inventory
|
||||||
> As a user, I want to see all my batteries at a glance so I know what's available.
|
> 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)
|
- Mark charging complete (moves to available)
|
||||||
- Optional: track charging start time
|
- 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)
|
## 5. Data Model (Conceptual)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
45
docs/TSD.md
45
docs/TSD.md
|
|
@ -193,7 +193,7 @@ POST /api/brands # Create brand
|
||||||
|
|
||||||
# Battery Groups
|
# Battery Groups
|
||||||
GET /api/batteries # List all battery groups (with brand/type/chemistry info)
|
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
|
GET /api/batteries/:id # Get single battery group
|
||||||
PATCH /api/batteries/:id # Update battery group
|
PATCH /api/batteries/:id # Update battery group
|
||||||
DELETE /api/batteries/:id # Delete 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
|
## 7. UI/UX Design Details
|
||||||
|
|
||||||
### Color Scheme
|
### Color Scheme
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { db, schema } from '@/lib/db';
|
import { db, schema } from '@/lib/db';
|
||||||
import { eq, sql } from 'drizzle-orm';
|
import { eq, sql, and } from 'drizzle-orm';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -56,19 +56,58 @@ export async function POST(request: Request) {
|
||||||
return NextResponse.json({ error: 'Brand, type, and chemistry are required' }, { status: 400 });
|
return NextResponse.json({ error: 'Brand, type, and chemistry are required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db
|
// Check if a group with the same brand-type-chemistry already exists
|
||||||
.insert(schema.batteryGroups)
|
const existing = await db
|
||||||
.values({
|
.select()
|
||||||
brandId,
|
.from(schema.batteryGroups)
|
||||||
typeId,
|
.where(
|
||||||
chemistryId,
|
and(
|
||||||
availableCount: availableCount || 0,
|
eq(schema.batteryGroups.brandId, brandId),
|
||||||
chargingCount: chargingCount || 0,
|
eq(schema.batteryGroups.typeId, typeId),
|
||||||
notes,
|
eq(schema.batteryGroups.chemistryId, chemistryId)
|
||||||
})
|
)
|
||||||
.returning();
|
)
|
||||||
|
.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) {
|
} catch (error) {
|
||||||
console.error('Failed to create battery group:', error);
|
console.error('Failed to create battery group:', error);
|
||||||
return NextResponse.json({ error: 'Failed to create battery group' }, { status: 500 });
|
return NextResponse.json({ error: 'Failed to create battery group' }, { status: 500 });
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,15 @@ export function AddBatteryModal({ isOpen, onClose, types, brands, chemistries }:
|
||||||
throw new Error(error.error || 'Failed to add battery');
|
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();
|
onClose();
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue