feat(raw-materials): add inline price editing with keyboard shortcuts and partial update support

This commit is contained in:
Your Name committed 2026-10-18 11:56:38 +07:00
1 parent a5584d0d46
commit b030c08f68
2 files changed
+128 -10

No files matched your search

+112 -4
View File
@@ -10,6 +10,10 @@ import {
Boxes, Boxes,
Layers, Layers,
Sparkles, Sparkles,
Check,
X,
Loader2,
Pencil,
} from 'lucide-react'; } from 'lucide-react';
import { API_BASE, type RawMaterial } from '@homaroma/types'; import { API_BASE, type RawMaterial } from '@homaroma/types';
import RawMaterialBulkJsonModal from '../components/RawMaterialBulkJsonModal'; import RawMaterialBulkJsonModal from '../components/RawMaterialBulkJsonModal';
@@ -21,6 +25,56 @@ export default function RawMaterialsPage() {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isBulkModalOpen, setIsBulkModalOpen] = useState(false); const [isBulkModalOpen, setIsBulkModalOpen] = useState(false);
// Inline Price Editing State
const [editingId, setEditingId] = useState<number | null>(null);
const [editingPrice, setEditingPrice] = useState<string>('');
const [savingId, setSavingId] = useState<number | null>(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 () => { const fetchMaterials = async () => {
setLoading(true); setLoading(true);
try { try {
@@ -240,10 +294,64 @@ export default function RawMaterialsPage() {
{item.unit} {item.unit}
</span> </span>
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-3">
<p className="text-sm font-bold text-emerald-400 font-mono"> {editingId === item.id ? (
฿{Number(item.pricePerUnit).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <div className="flex items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
</p> <div className="relative">
<span className="absolute left-2.5 top-1/2 -translate-y-1/2 text-xs text-slate-500 font-mono font-bold">
฿
</span>
<input
type="number"
step="0.01"
min="0"
autoFocus
value={editingPrice}
onChange={(e) => 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"
/>
</div>
<button
type="button"
disabled={savingId === item.id}
onClick={() => handleSavePrice(item)}
className="p-1.5 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 border border-emerald-500/40 transition-all disabled:opacity-50"
title="บันทึกราคา (Enter)"
>
{savingId === item.id ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Check className="w-3.5 h-3.5" />
)}
</button>
<button
type="button"
disabled={savingId === item.id}
onClick={cancelEditing}
className="p-1.5 rounded-lg bg-white/5 hover:bg-rose-500/20 text-slate-400 hover:text-rose-300 border border-white/10 transition-all disabled:opacity-50"
title="ยกเลิก (Esc)"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
) : (
<div
onClick={() => 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)"
>
<p className="text-sm font-bold text-emerald-400 font-mono">
฿{Number(item.pricePerUnit).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</p>
<Pencil className="w-3 h-3 text-slate-500 opacity-0 group-hover/price:opacity-100 transition-opacity" />
</div>
)}
</td> </td>
<td className="px-6 py-4 text-right"> <td className="px-6 py-4 text-right">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
+16 -6
View File
@@ -137,20 +137,30 @@ export const rawMaterialsRoute = new Elysia({ prefix: "/raw-materials" })
if (isNaN(id)) return status(400, { message: "Invalid ID" }); if (isNaN(id)) return status(400, { message: "Invalid ID" });
try { 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({ const rawMaterial = await prisma.rawMaterial.update({
where: { id }, where: { id },
data: body, data: updateData,
}); });
return rawMaterial; return rawMaterial;
} catch (err) { } catch (err: any) {
console.error(err); 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({ body: t.Object({
name: t.String(), name: t.Optional(t.String()),
unit: t.String(), unit: t.Optional(t.String()),
pricePerUnit: t.Number(), pricePerUnit: t.Optional(t.Union([t.Number(), t.String()])),
}), }),
detail: { tags: ["raw-materials"] } detail: { tags: ["raw-materials"] }
}) })