feat(raw-materials): add inline price editing with keyboard shortcuts and partial update support
This commit is contained in:
1 parent
a5584d0d46
commit
b030c08f68
2 files changed
+128
-10
No files matched your search
@@ -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<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 () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -240,10 +294,64 @@ export default function RawMaterialsPage() {
|
||||
{item.unit}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="text-sm font-bold text-emerald-400 font-mono">
|
||||
฿{Number(item.pricePerUnit).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
<td className="px-6 py-3">
|
||||
{editingId === item.id ? (
|
||||
<div className="flex items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
<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 className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
|
||||
@@ -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"] }
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user