diff --git a/src/api/equipment.test.ts b/src/api/equipment.test.ts index 5559574..cf1a0d3 100644 --- a/src/api/equipment.test.ts +++ b/src/api/equipment.test.ts @@ -3,6 +3,7 @@ import equipment from './equipment' const mockEquipmentService = { listEquipments: mock(), + updateEquipment: mock(), } mock.module('../service/equipment', () => ({ @@ -12,6 +13,7 @@ mock.module('../service/equipment', () => ({ describe('Equipment API', () => { afterEach(() => { mockEquipmentService.listEquipments.mockClear() + mockEquipmentService.updateEquipment.mockClear() }) it('GET / should list equipments with filters', async () => { @@ -28,4 +30,20 @@ describe('Equipment API', () => { { category: ['Headwear'], subCategory: undefined }, ) }) + + it('PATCH /:id should update equipment', async () => { + mockEquipmentService.updateEquipment.mockResolvedValue({ id: '1', name: 'Updated' }) + + const req = new Request('http://localhost/1', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'Updated' }), + }) + + const res = await equipment.fetch(req) + expect(res.status).toBe(200) + expect(mockEquipmentService.updateEquipment).toHaveBeenCalledWith('1', { + name: 'Updated', + }) + }) }) diff --git a/src/api/equipment.ts b/src/api/equipment.ts index 7d4e8df..4c7cbc8 100644 --- a/src/api/equipment.ts +++ b/src/api/equipment.ts @@ -24,4 +24,22 @@ const route = app.get('/', zValidator('query', listSchema), async (c) => { return c.json(result) }) -export default route +const patchSchema = z.object({ + name: z.string().optional(), + description: z.string().optional(), +}) + +const routeWithPatch = route.patch('/:id', zValidator('json', patchSchema), async (c) => { + const { id } = c.req.param() + const { name, description } = c.req.valid('json') + + try { + const result = await equipmentService.updateEquipment(id, { name, description }) + return c.json(result) + } catch (e) { + console.error('Failed to update equipment', e) + return c.json({ error: 'Failed to update equipment' }, 500) + } +}) + +export default routeWithPatch diff --git a/src/service/equipment.test.ts b/src/service/equipment.test.ts index 6bcdc7d..dd2c715 100644 --- a/src/service/equipment.test.ts +++ b/src/service/equipment.test.ts @@ -4,6 +4,7 @@ const mockPrisma = { equipment: { findMany: mock(), count: mock(), + update: mock(), }, } @@ -15,6 +16,7 @@ describe('EquipmentService', () => { afterEach(() => { mockPrisma.equipment.findMany.mockClear() mockPrisma.equipment.count.mockClear() + mockPrisma.equipment.update.mockClear() }) it('listEquipments should filter by category', async () => { @@ -33,4 +35,26 @@ describe('EquipmentService', () => { }), ) }) + + it('updateEquipment should update equipment details', async () => { + // @ts-ignore + const { equipmentService } = await import(`./equipment?v=${Date.now()}`) + mockPrisma.equipment.update.mockResolvedValue({ + id: '1', + name: 'New Name', + description: 'New Description', + }) + + await equipmentService.updateEquipment('1', { name: 'New Name', description: 'New Description' }) + + expect(mockPrisma.equipment.update).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: '1' }, + data: expect.objectContaining({ + name: 'New Name', + description: 'New Description', + }), + }), + ) + }) }) diff --git a/src/service/equipment.ts b/src/service/equipment.ts index eac4a08..33aa457 100644 --- a/src/service/equipment.ts +++ b/src/service/equipment.ts @@ -65,6 +65,28 @@ export class EquipmentService { }, }) } + + /** + * Updates an existing equipment item. + */ + async updateEquipment( + id: string, + data: { + name?: string + description?: string + }, + ) { + return prisma.equipment.update({ + where: { id }, + data: { + name: data.name, + description: data.description, + }, + include: { + image: true, + }, + }) + } } export const equipmentService = new EquipmentService() diff --git a/src/service/generation.ts b/src/service/generation.ts index 0465324..2a6b9e2 100644 --- a/src/service/generation.ts +++ b/src/service/generation.ts @@ -139,7 +139,11 @@ category: ${eq.category} character: true, equipments: { include: { - equipment: true, + equipment: { + include: { + image: true, + }, + }, }, }, }, @@ -177,7 +181,11 @@ category: ${eq.category} character: true, equipments: { include: { - equipment: true, + equipment: { + include: { + image: true, + }, + }, }, }, }, diff --git a/src/ui/components/EquipmentDetailsDialog.tsx b/src/ui/components/EquipmentDetailsDialog.tsx new file mode 100644 index 0000000..863e9d5 --- /dev/null +++ b/src/ui/components/EquipmentDetailsDialog.tsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react' +import { InferResponseType } from 'hono/client' +import { client } from '../client' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from './ui/dialog' +import { Button } from './ui/button' +import { Input } from './ui/input' +import { Textarea } from './ui/textarea' +import { Label } from './ui/label' +import { toast } from 'sonner' +import { Loader2 } from 'lucide-react' + +type EquipmentResponse = InferResponseType +type EquipmentItem = EquipmentResponse['items'][number] + +interface EquipmentDetailsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + equipment: EquipmentItem | null + onUpdate: () => void +} + +export const EquipmentDetailsDialog: React.FC = ({ + open, + onOpenChange, + equipment, + onUpdate, +}) => { + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (equipment) { + setName(equipment.name) + setDescription(equipment.description || '') + } + }, [equipment]) + + const handleSave = async () => { + if (!equipment) return + setSaving(true) + try { + const res = await client.equipments[':id'].$patch({ + param: { id: equipment.id }, + json: { name, description }, + }) + if (res.ok) { + toast.success('Equipment updated') + onUpdate() + onOpenChange(false) + } else { + toast.error('Failed to update equipment') + } + } catch (e) { + console.error(e) + toast.error('An error occurred') + } finally { + setSaving(false) + } + } + + if (!equipment) return null + + return ( + + + + + Equipment Details + + + {equipment.category} {equipment.subCategory && `> ${equipment.subCategory}`} + + + +
+ {/* Top: Full Image */} +
+
+ {equipment.name} +
+ + {/* Bottom: Edit Form */} +
+
+
+ + setName(e.target.value)} + placeholder="Equipment Name" + /> +
+ +
+ +