From b030c08f68c7e487cec7dcb7715fcaf56229d303 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 18 Oct 2026 11:56:38 +0700 Subject: [PATCH] feat(raw-materials): add inline price editing with keyboard shortcuts and partial update support --- apps/admin/src/pages/RawMaterialsPage.tsx | 116 +++++++++++++++++++++- apps/backend/src/routes/raw-materials.ts | 22 ++-- 2 files changed, 128 insertions(+), 10 deletions(-) diff --git a/apps/admin/src/pages/RawMaterialsPage.tsx b/apps/admin/src/pages/RawMaterialsPage.tsx index 0c2ae0e..7fe3247 100644 --- a/apps/admin/src/pages/RawMaterialsPage.tsx +++ b/apps/admin/src/pages/RawMaterialsPage.tsx @@ -10,6 +10,10 @@ import { Boxes, Layers, Sparkles, + Check, + X, + Loader2, + Pencil, } from 'lucide-react'; import { API_BASE, type RawMaterial } from '@homaroma/types'; import RawMaterialBulkJsonModal from '../components/RawMaterialBulkJsonModal'; @@ -21,6 +25,56 @@ export default function RawMaterialsPage() { const [searchQuery, setSearchQuery] = useState(''); const [isBulkModalOpen, setIsBulkModalOpen] = useState(false); + // Inline Price Editing State + const [editingId, setEditingId] = useState(null); + const [editingPrice, setEditingPrice] = useState(''); + const [savingId, setSavingId] = useState(null); + + const startEditing = (item: RawMaterial) => { + setEditingId(item.id); + setEditingPrice(String(item.pricePerUnit ?? '')); + }; + + const cancelEditing = () => { + setEditingId(null); + setEditingPrice(''); + }; + + const handleSavePrice = async (item: RawMaterial) => { + const val = parseFloat(editingPrice); + if (isNaN(val) || val < 0) { + alert('กรุณาระบุราคาต้นทุนให้ถูกต้อง (ต้องเป็นตัวเลขมากกว่าหรือเท่ากับ 0)'); + return; + } + + setSavingId(item.id); + try { + const res = await fetch(`${API_BASE}/raw-materials/${item.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + pricePerUnit: val, + }), + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.message || 'บันทึกราคาไม่สำเร็จ'); + } + + setMaterials((prev) => + prev.map((m) => (m.id === item.id ? { ...m, pricePerUnit: val } : m)) + ); + setEditingId(null); + setEditingPrice(''); + } catch (err: any) { + console.error(err); + alert(err.message || 'เกิดข้อผิดพลาดในการบันทึกราคา'); + } finally { + setSavingId(null); + } + }; + const fetchMaterials = async () => { setLoading(true); try { @@ -240,10 +294,64 @@ export default function RawMaterialsPage() { {item.unit} - -

- ฿{Number(item.pricePerUnit).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -

+ + {editingId === item.id ? ( +
e.stopPropagation()}> +
+ + ฿ + + setEditingPrice(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSavePrice(item); + if (e.key === 'Escape') cancelEditing(); + }} + disabled={savingId === item.id} + className="w-28 pl-6 pr-2 py-1 text-sm font-bold font-mono text-emerald-400 bg-black/80 border border-cyan-500/60 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500/40 shadow-inner" + placeholder="0.00" + /> +
+ + +
+ ) : ( +
startEditing(item)} + className="group/price inline-flex items-center gap-2 cursor-pointer px-2.5 py-1 -ml-2.5 rounded-lg hover:bg-white/5 border border-transparent hover:border-white/10 transition-all" + title="คลิกที่นี่เพื่อแก้ไขราคาต่อหน่วย (Inline Edit)" + > +

+ ฿{Number(item.pricePerUnit).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +

+ +
+ )}
diff --git a/apps/backend/src/routes/raw-materials.ts b/apps/backend/src/routes/raw-materials.ts index 14f2682..4064052 100644 --- a/apps/backend/src/routes/raw-materials.ts +++ b/apps/backend/src/routes/raw-materials.ts @@ -137,20 +137,30 @@ export const rawMaterialsRoute = new Elysia({ prefix: "/raw-materials" }) if (isNaN(id)) return status(400, { message: "Invalid ID" }); try { + const updateData: any = {}; + if (body.name !== undefined) updateData.name = String(body.name).trim(); + if (body.unit !== undefined) updateData.unit = String(body.unit).trim(); + if (body.pricePerUnit !== undefined) { + updateData.pricePerUnit = + typeof body.pricePerUnit === "string" + ? parseFloat(body.pricePerUnit) || 0 + : Number(body.pricePerUnit) || 0; + } + const rawMaterial = await prisma.rawMaterial.update({ where: { id }, - data: body, + data: updateData, }); return rawMaterial; - } catch (err) { + } catch (err: any) { console.error(err); - return status(500, { message: "Failed to update raw material" }); + return status(500, { message: "Failed to update raw material", error: err.message }); } }, { body: t.Object({ - name: t.String(), - unit: t.String(), - pricePerUnit: t.Number(), + name: t.Optional(t.String()), + unit: t.Optional(t.String()), + pricePerUnit: t.Optional(t.Union([t.Number(), t.String()])), }), detail: { tags: ["raw-materials"] } })