From 8b1f73693dfa36f0a59f82b0ec57cfadae5c3915 Mon Sep 17 00:00:00 2001 From: LeonG11 Date: Tue, 14 Jul 2026 11:04:44 +0300 Subject: [PATCH] feat:add admin order page --- frontend/src/App.jsx | 9 ++ frontend/src/api/index.js | 1 + frontend/src/components/AdminOrders.jsx | 8 +- frontend/src/pages/AdminOrderPage.jsx | 189 ++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/AdminOrderPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 340bc4e..ee06134 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -10,6 +10,7 @@ import Cart from "./pages/Cart"; import Profile from "./pages/Profile"; import ItemPage from "./pages/ItemPage"; import AdminPage from "./pages/AdminPage"; +import AdminOrderDetail from "./pages/AdminOrderPage"; const PrivateRoute = ({ children }) => { const token = localStorage.getItem("token"); @@ -62,6 +63,14 @@ export default function App() { } /> + + + + } + /> } /> } /> diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 3dddee5..40ef738 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -71,3 +71,4 @@ export const stockOut = (data) => export const getStockMovements = () => axios.get(`${API_URL}/admin/stock/movements`); export const getTopItems = () => axios.get(`${API_URL}/admin/items/top`); +export const getAdminOrder = (id) => axios.get(`${API_URL}/admin/orders/${id}`); diff --git a/frontend/src/components/AdminOrders.jsx b/frontend/src/components/AdminOrders.jsx index c047849..6c502da 100644 --- a/frontend/src/components/AdminOrders.jsx +++ b/frontend/src/components/AdminOrders.jsx @@ -1,9 +1,10 @@ import { useState, useEffect } from "react"; import { getAllOrders, updateOrderStatus } from "../api/index"; +import { useNavigate, useParams } from "react-router-dom"; const STATUSES = [ { value: "new", label: "Новый" }, - { value: "packed", label: "Собран" }, + { value: "packed", label: "Собран" }, { value: "delivered", label: "Доставлен" }, { value: "cancel", label: "Отменён" }, ]; @@ -34,6 +35,8 @@ export default function AdminOrders() { } }; + const navigate = useNavigate(); + if (loading) return

Загрузка...

; return ( @@ -44,7 +47,8 @@ export default function AdminOrders() { orders.map((order) => (
navigate(`/admin/orders/${order.id}`)} > {/* шапка: номер + селект статуса */}
diff --git a/frontend/src/pages/AdminOrderPage.jsx b/frontend/src/pages/AdminOrderPage.jsx new file mode 100644 index 0000000..a2af5e8 --- /dev/null +++ b/frontend/src/pages/AdminOrderPage.jsx @@ -0,0 +1,189 @@ +import { useState, useEffect } from "react"; +import { getAdminOrder } from "../api"; +import { useParams, useNavigate } from "react-router-dom"; + +// Статусы заказа — ПОДСТАВЬ свои, если в БД они другие +const STATUSES = [ + { value: "new", label: "Новый" }, + { value: "prepared", label: "Собран" }, + { value: "shipped", label: "Отправлен" }, + { value: "delivered", label: "Доставлен" }, + { value: "canceled", label: "Отменён" }, +]; + +export default function AdminOrderDetail({ orderId }) { + const [order, setOrder] = useState(null); + const [status, setStatus] = useState(""); + const [items, setItems] = useState([]); // редактируемая копия позиций + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + + const { id } = useParams(); + const navigate = useNavigate(); + + // Загружаем заказ при монтировании / смене orderId + useEffect(() => { + setError(""); + setLoading(true); + + getAdminOrder(id) + .then((res) => { + setOrder(res.data); + setStatus(res.data.status); + setItems((res.data.items ?? []).map((it) => ({ ...it }))); + }) + .catch((err) => { + setError(err.response?.data?.error ?? "Не удалось загрузить заказ"); + }) + .finally(() => { }); + }, [orderId]); + + function handleQuantityChange(itemId, value) { + setItems((prev) => + prev.map((it) => + it.item_id === itemId + ? { ...it, quantity: value === "" ? "" : parseFloat(value) } + : it, + ), + ); + } + + // Пересчёт итога на лету + const total = items.reduce((sum, it) => { + const q = typeof it.quantity === "number" ? it.quantity : 0; + return sum + it.price * q; + }, 0); + + if (!loading) { + return
Загрузка заказа…
; + } + + if (!order) { + return ( +
+

{error || "Заказ не найден"}

+ +
+ ); + } + + const created = order.created_at + ? new Date(order.created_at).toLocaleString("ru-RU") + : ""; + + return ( +
+ {/* Шапка */} +
+ +

+ Заказ #{order.order_id} +

+
+ + {error && ( +
+ {error} +
+ )} + + {/* Мета заказа */} +
+ {order.address && ( +
+ Адрес + {order.address} +
+ )} + {created && ( +
+ Создан + {created} +
+ )} + + {/* Редактируемый статус */} +
+ Статус + +
+
+ + {/* Позиции с редактируемым весом */} +
+ {items.map((it) => ( +
+
+

{it.name}

+

+ {it.price} ₽ / {it.unit} +

+
+ +
+ + handleQuantityChange(it.item_id, e.target.value) + } + className="w-20 bg-slate-700 text-white rounded-lg px-2 py-1.5 text-right outline-none focus:ring-2 focus:ring-sky-500" + /> + {it.unit} +
+ +
+ {( + it.price * (typeof it.quantity === "number" ? it.quantity : 0) + ).toFixed(2)}{" "} + ₽ +
+
+ ))} +
+ + {/* Итог */} +
+ Итого + + {total.toFixed(2)} ₽ + +
+ + {/* Сохранить */} + +
+ ); +}