feat: add form stock in stock out on front

This commit is contained in:
2026-06-27 21:52:03 +03:00
parent 46a0fd20db
commit 4089037fb7
2 changed files with 232 additions and 58 deletions
+1 -2
View File
@@ -116,7 +116,6 @@ function ItemFormModal({ item, onClose, onSaved }) {
placeholder="Остаток" placeholder="Остаток"
value={form.stock} value={form.stock}
onChange={handleChange("stock")} onChange={handleChange("stock")}
disabled
/> />
<input <input
className="bg-white/10 rounded px-3 py-2" className="bg-white/10 rounded px-3 py-2"
@@ -149,7 +148,7 @@ function ItemFormModal({ item, onClose, onSaved }) {
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={saving} disabled={saving}
className="bg-gold text-black px-6 py-2 rounded font-medium flex-grow disabled:opacity-50" className="bg-gold text-black px-6 py-2 rounded font-medium grow disabled:opacity-50"
> >
{saving ? "Сохранение..." : isEdit ? "Сохранить" : "Создать"} {saving ? "Сохранение..." : isEdit ? "Сохранить" : "Создать"}
</button> </button>
+187 -12
View File
@@ -1,5 +1,10 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { getStockMovements } from "../api/index"; import {
getStockMovements,
stockIn,
stockOut,
getAllItems,
} from "../api/index";
const REASON_LABELS = { const REASON_LABELS = {
purchase: "Закупка", purchase: "Закупка",
@@ -10,28 +15,197 @@ const REASON_LABELS = {
export default function AdminStock() { export default function AdminStock() {
const [movements, setMovements] = useState([]); const [movements, setMovements] = useState([]);
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const loadMovements = () => { // состояние форм
getStockMovements() const [inForm, setInForm] = useState({
.then((res) => { item_id: "",
setMovements(res.data); quantity: "",
comment: "",
});
const [outForm, setOutForm] = useState({
item_id: "",
quantity: "",
comment: "",
});
const [busy, setBusy] = useState(false);
const loadAll = () => {
Promise.all([getStockMovements(), getAllItems()])
.then(([movRes, itemsRes]) => {
setMovements(movRes.data);
setItems(itemsRes.data);
}) })
.catch(() => { }) .catch(() => { })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
useEffect(() => { useEffect(() => {
loadMovements(); loadAll();
}, []); }, []);
if (loading) return <p className="text-white">Загрузка...</p>; // приход
const handleStockIn = async () => {
if (!inForm.item_id || !inForm.quantity || Number(inForm.quantity) <= 0) {
alert("Выберите товар и укажите количество");
return;
}
setBusy(true);
try {
await stockIn({
item_id: Number(inForm.item_id),
quantity: Number(inForm.quantity),
comment: inForm.comment,
});
setInForm({ item_id: "", quantity: "", comment: "" });
loadAll(); // обновить историю и остатки
} catch {
alert("Не удалось оформить приход");
} finally {
setBusy(false);
}
};
// уход
const handleStockOut = async () => {
if (
!outForm.item_id ||
!outForm.quantity ||
Number(outForm.quantity) <= 0
) {
alert("Выберите товар и укажите количество");
return;
}
setBusy(true);
try {
await stockOut({
item_id: Number(outForm.item_id),
quantity: Number(outForm.quantity),
comment: outForm.comment,
});
setOutForm({ item_id: "", quantity: "", comment: "" });
loadAll();
} catch (err) {
// бэк возвращает "недостаточно товара" при нехватке
alert(err.response?.data?.error || "Не удалось оформить списание");
} finally {
setBusy(false);
}
};
if (loading) return <p className="text-white/60">Загрузка...</p>;
// общий класс инпутов
const inputClass =
"bg-white/5 border border-white/10 rounded px-3 py-2 text-white placeholder-white/40 focus:border-gold focus:outline-none";
return ( return (
<div className="flex flex-col gap-8">
{/* === Формы === */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Приход */}
<div className="bg-white/5 border border-white/10 rounded-lg p-5">
<h3 className="text-lg font-medium text-green-400 mb-4">
Приход товара
</h3>
<div className="flex flex-col gap-3">
<select
className={inputClass}
value={inForm.item_id}
onChange={(e) =>
setInForm({ ...inForm, item_id: e.target.value })
}
>
<option value="">Выберите товар</option>
{items.map((it) => (
<option key={it.id} value={it.id}>
{it.name} (остаток: {it.stock} {it.unit})
</option>
))}
</select>
<input
type="number"
className={inputClass}
placeholder="Количество"
value={inForm.quantity}
onChange={(e) =>
setInForm({ ...inForm, quantity: e.target.value })
}
/>
<input
className={inputClass}
placeholder="Комментарий (необязательно)"
value={inForm.comment}
onChange={(e) =>
setInForm({ ...inForm, comment: e.target.value })
}
/>
<button
onClick={handleStockIn}
disabled={busy}
className="bg-gold text-black px-6 py-2 rounded font-medium uppercase text-sm disabled:opacity-50"
>
Оприходовать
</button>
</div>
</div>
{/* Уход */}
<div className="bg-white/5 border border-white/10 rounded-lg p-5">
<h3 className="text-lg font-medium text-red-400 mb-4">
Списание товара
</h3>
<div className="flex flex-col gap-3">
<select
className={inputClass}
value={outForm.item_id}
onChange={(e) =>
setOutForm({ ...outForm, item_id: e.target.value })
}
>
<option value="">Выберите товар</option>
{items.map((it) => (
<option key={it.id} value={it.id}>
{it.name} (остаток: {it.stock} {it.unit})
</option>
))}
</select>
<input
type="number"
className={inputClass}
placeholder="Количество"
value={outForm.quantity}
onChange={(e) =>
setOutForm({ ...outForm, quantity: e.target.value })
}
/>
<input
className={inputClass}
placeholder="Комментарий (необязательно)"
value={outForm.comment}
onChange={(e) =>
setOutForm({ ...outForm, comment: e.target.value })
}
/>
<button
onClick={handleStockOut}
disabled={busy}
className="bg-gold text-black px-6 py-2 rounded font-medium uppercase text-sm disabled:opacity-50"
>
Списать
</button>
</div>
</div>
</div>
{/* === История (твоя таблица) === */}
<div> <div>
<h2 className="text-xl font-medium text-white md-4">История движения</h2> <h2 className="text-xl font-medium text-white mb-4">
История движений
</h2>
{movements.length === 0 ? ( {movements.length === 0 ? (
<p className="text-white">Движений нет</p> <p className="text-white/60">Движений нет</p>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm text-left"> <table className="w-full text-sm text-left">
@@ -48,10 +222,10 @@ export default function AdminStock() {
<tbody> <tbody>
{movements.map((m) => ( {movements.map((m) => (
<tr <tr
className="border-b border-white/5 hover:bg-white/5"
key={m.id} key={m.id}
className="border-b border-white/5 hover:bg-white/5 transition-colors"
> >
<td className="py-3 pr-4 text-whtie/70 whitespace-nowrap"> <td className="py-3 pr-4 text-white/70 whitespace-nowrap">
{new Date(m.created_at).toLocaleDateString("ru-RU")} {new Date(m.created_at).toLocaleDateString("ru-RU")}
</td> </td>
<td className="py-3 pr-4 text-white">{m.item?.name}</td> <td className="py-3 pr-4 text-white">{m.item?.name}</td>
@@ -71,7 +245,7 @@ export default function AdminStock() {
{REASON_LABELS[m.reason] || m.reason} {REASON_LABELS[m.reason] || m.reason}
</td> </td>
<td className="py-3 pr-4 text-white/50"> <td className="py-3 pr-4 text-white/50">
{m.comment || "-"} {m.comment || ""}
</td> </td>
</tr> </tr>
))} ))}
@@ -80,5 +254,6 @@ export default function AdminStock() {
</div> </div>
)} )}
</div> </div>
</div>
); );
} }