Files
mm.com.tm-frontend/src/pages/CarconfiguratorAdmin/index.jsx
2026-03-27 23:01:33 +05:00

435 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from "react";
import styles from "./Adminpage.module.scss";
// ─── PASSWORD — change this ───────────────────────────────────────────────────
const ADMIN_PASSWORD = "shumoff2024";
// ─── LOGIN ────────────────────────────────────────────────────────────────────
function LoginScreen({ onLogin }) {
const [input, setInput] = useState("");
const [error, setError] = useState(false);
const [shake, setShake] = useState(false);
function handleSubmit(e) {
e.preventDefault();
if (input === ADMIN_PASSWORD) {
onLogin();
} else {
setError(true);
setShake(true);
setTimeout(() => setShake(false), 500);
}
}
return (
<div className={styles.loginWrapper}>
<div className={`${styles.loginCard} ${shake ? styles.shake : ""}`}>
<div className={styles.loginLogo}></div>
<h1 className={styles.loginTitle}>Панель администратора</h1>
<p className={styles.loginSub}>Введите пароль для доступа</p>
<form onSubmit={handleSubmit} className={styles.loginForm}>
<input
type="password"
className={`${styles.loginInput} ${error ? styles.inputError : ""}`}
placeholder="Пароль"
value={input}
onChange={(e) => { setInput(e.target.value); setError(false); }}
autoFocus
/>
{error && <p className={styles.errorMsg}>Неверный пароль. Попробуйте ещё раз.</p>}
<button type="submit" className={styles.loginBtn}>Войти</button>
</form>
</div>
</div>
);
}
// ─── TABS ─────────────────────────────────────────────────────────────────────
const ADMIN_TABS = [
{ id: "products", label: "📦 Товары и цены" },
{ id: "bodytypes", label: "🚗 Типы кузова" },
];
// ─── ADMIN PANEL ─────────────────────────────────────────────────────────────
function AdminPanel({ onLogout }) {
const [data, setData] = useState(null);
const [tab, setTab] = useState("products");
const [zone, setZone] = useState(null);
const [bodyType, setBodyType] = useState(null);
const [pkg, setPkg] = useState(null);
const [saved, setSaved] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Load data.json from /public
useEffect(() => {
fetch("/frontend-api/data")
.then((r) => r.json())
.then((d) => {
setData(d);
setZone(Object.keys(d.zones)[0]);
setBodyType(d.bodyTypes[0]?.id);
setPkg(d.packages[0]);
setLoading(false);
})
.catch((e) => { setError(e.message); setLoading(false); });
}, []);
// ── helpers ────────────────────────────────────────────────
function updateData(fn) {
setData((prev) => {
const next = JSON.parse(JSON.stringify(prev));
fn(next);
return next;
});
}
// Products tab
function getProducts() {
return data?.zones?.[zone]?.products?.[bodyType]?.[pkg] || [];
}
function setProducts(products) {
updateData((d) => {
if (!d.zones[zone].products[bodyType]) d.zones[zone].products[bodyType] = {};
d.zones[zone].products[bodyType][pkg] = products;
});
}
function updateProduct(idx, field, val) {
const ps = JSON.parse(JSON.stringify(getProducts()));
if (field === "price" || field === "qty") ps[idx][field] = Number(val) || 0;
else ps[idx][field] = val;
setProducts(ps);
}
function deleteProduct(idx) {
const ps = JSON.parse(JSON.stringify(getProducts()));
ps.splice(idx, 1);
setProducts(ps);
}
function addProduct() {
const ps = JSON.parse(JSON.stringify(getProducts()));
ps.push({ name: "Новый товар", price: 0, qty: 1, unit: "Л" });
setProducts(ps);
}
// Body types tab
function updateBodyType(idx, field, val) {
updateData((d) => { d.bodyTypes[idx][field] = val; });
}
function deleteBodyType(idx) {
const bt = data.bodyTypes[idx];
updateData((d) => {
d.bodyTypes.splice(idx, 1);
// remove products for this body type in all zones
Object.values(d.zones).forEach((z) => { delete z.products[bt.id]; });
});
if (bodyType === bt.id) setBodyType(data.bodyTypes[0]?.id);
}
function addBodyType() {
const newId = `body_${Date.now()}`;
updateData((d) => {
d.bodyTypes.push({ id: newId, label: "Новый тип", icon: "🚗" });
});
}
// Zone label
function updateZoneLabel(val) {
updateData((d) => { d.zones[zone].label = val; });
}
function handleImageUpload(e, idx) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
updateBodyType(idx, "image", event.target.result);
updateBodyType(idx, "icon", null); // Eski icon verisini temizle
};
reader.readAsDataURL(file);
}
// ── Save = download updated data.json ──────────────────────
// Since there's no backend, admin downloads the JSON and replaces public/data.json
async function handleSave() {
const res = await fetch("/frontend-api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const result = await res.json();
if (!result.ok) { alert("Hata: " + result.error); return; }
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}
// ── Render ─────────────────────────────────────────────────
if (loading) return <div className={styles.loadingScreen}>Загрузка...</div>;
if (error) return <div className={styles.loadingScreen}>Ошибка: {error}</div>;
const products = getProducts();
return (
<div className={styles.adminWrapper}>
{/* Header */}
<header className={styles.adminHeader}>
<div className={styles.adminHeaderLeft}>
<span className={styles.adminHeaderIcon}></span>
<div>
<h1 className={styles.adminHeaderTitle}>Панель администратора</h1>
<p className={styles.adminHeaderSub}>Управление товарами, ценами и типами кузова</p>
</div>
</div>
<div className={styles.adminHeaderRight}>
{saved && (
<span className={styles.savedBadge}>
Сохранено
</span>
)}
<button className={styles.btnSaveHeader} onClick={handleSave}>
💾 Сохранить
</button>
<button className={styles.btnLogout} onClick={onLogout}>Выйти</button>
</div>
</header>
{/* Tab bar */}
<div className={styles.tabBar}>
{ADMIN_TABS.map((t) => (
<button
key={t.id}
className={`${styles.tabBtn} ${tab === t.id ? styles.tabActive : ""}`}
onClick={() => setTab(t.id)}
>
{t.label}
</button>
))}
</div>
{/* ── PRODUCTS TAB ─────────────────────────────────────── */}
{tab === "products" && (
<div className={styles.adminBody}>
{/* Sidebar: zones */}
<aside className={styles.sidebar}>
<p className={styles.sidebarLabel}>Зоны</p>
{Object.entries(data.zones).map(([zid, z]) => (
<button
key={zid}
className={`${styles.sidebarItem} ${zone === zid ? styles.sidebarActive : ""}`}
onClick={() => setZone(zid)}
>
{z.label}
</button>
))}
</aside>
{/* Main content */}
<main className={styles.content}>
{/* Zone label */}
<div className={styles.fieldRow}>
<label className={styles.fieldLabel}>Название зоны</label>
<input
className={styles.fieldInput}
value={data.zones[zone]?.label || ""}
onChange={(e) => updateZoneLabel(e.target.value)}
/>
</div>
{/* Body type selector */}
<div className={styles.bodyTypeRow}>
<span className={styles.fieldLabel}>Тип кузова</span>
<div className={styles.bodyTypePills}>
{data.bodyTypes.map((b) => (
<button
key={b.id}
className={`${styles.bodyPill} ${bodyType === b.id ? styles.bodyPillActive : ""}`}
onClick={() => setBodyType(b.id)}
>
{b.image ? <img src={b.image} alt={b.label} className={styles.bodyPillIcon} /> : (b.icon && <span className={styles.bodyPillEmoji}>{b.icon}</span>)}
{b.label}
</button>
))}
</div>
</div>
{/* Package tabs */}
<div className={styles.pkgTabs}>
{data.packages.map((p) => (
<button
key={p}
className={`${styles.pkgTab} ${pkg === p ? styles.pkgActive : ""}`}
onClick={() => setPkg(p)}
>
{p}
</button>
))}
</div>
{/* Products table */}
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th className={styles.th}>Название товара</th>
<th className={`${styles.th} ${styles.thNum}`}>Цена (m)</th>
<th className={`${styles.th} ${styles.thNum}`}>Кол-во</th>
<th className={`${styles.th} ${styles.thNum}`}>Ед.</th>
<th className={`${styles.th} ${styles.thNum}`}>Итого</th>
<th className={styles.th}></th>
</tr>
</thead>
<tbody>
{products.map((p, i) => (
<tr key={i} className={styles.tr}>
<td className={styles.td}>
<input
className={styles.cellInput}
value={p.name}
onChange={(e) => updateProduct(i, "name", e.target.value)}
/>
</td>
<td className={styles.td}>
<input
className={`${styles.cellInput} ${styles.cellNum}`}
type="number" min="0"
value={p.price}
onChange={(e) => updateProduct(i, "price", e.target.value)}
/>
</td>
<td className={styles.td}>
<input
className={`${styles.cellInput} ${styles.cellNum}`}
type="number" min="0"
value={p.qty}
onChange={(e) => updateProduct(i, "qty", e.target.value)}
/>
</td>
<td className={styles.td}>
<input
className={`${styles.cellInput} ${styles.cellNum}`}
value={p.unit}
onChange={(e) => updateProduct(i, "unit", e.target.value)}
/>
</td>
<td className={`${styles.td} ${styles.tdTotal}`}>
{(p.price * p.qty).toLocaleString("ru")} m
</td>
<td className={styles.td}>
<button className={styles.btnDel} onClick={() => deleteProduct(i)}></button>
</td>
</tr>
))}
{products.length === 0 && (
<tr>
<td colSpan={6} className={styles.emptyRow}>
Нет товаров для этой комбинации
</td>
</tr>
)}
</tbody>
</table>
</div>
<button className={styles.btnAdd} onClick={addProduct}>
+ Добавить товар
</button>
</main>
</div>
)}
{/* ── BODY TYPES TAB ───────────────────────────────────── */}
{tab === "bodytypes" && (
<div className={styles.bodyTypesPage}>
<div className={styles.bodyTypesHeader}>
<h2 className={styles.bodyTypesTitle}>Типы кузова</h2>
<p className={styles.bodyTypesSub}>
Добавляйте, удаляйте и редактируйте типы кузова. Изменения применяются
ко всем зонам и пакетам.
</p>
</div>
<div className={styles.bodyTypesGrid}>
{data.bodyTypes.map((b, i) => (
<div key={b.id} className={styles.bodyTypeCard}>
<div className={styles.imageUploader}>
{b.image ? (
<img src={b.image} alt="Preview" className={styles.iconPreview} />
) : (
b.icon && <span className={styles.iconPreview}>{b.icon}</span>
)}
<label className={styles.uploadLabel}>
<span>{b.image || b.icon ? 'Изменить' : 'Загрузить'}</span>
<input
type="file"
accept="image/png, image/jpeg, image/svg+xml"
onChange={(e) => handleImageUpload(e, i)}
style={{ display: 'none' }}
/>
</label>
</div>
<div className={styles.bodyTypeCardBottom}>
<input
className={styles.fieldInput}
value={b.label}
onChange={(e) => updateBodyType(i, "label", e.target.value)}
placeholder="Название типа"
/>
<button
className={styles.btnDel}
onClick={() => {
if (window.confirm(`Удалить тип "${b.label}"? Все товары для этого типа будут удалены.`)) {
deleteBodyType(i);
}
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
</button>
</div>
</div>
))}
{/* Add new */}
<button className={styles.bodyTypeAddCard} onClick={addBodyType}>
<span>+</span>
<span>Добавить тип</span>
</button>
</div>
<div className={styles.saveHint}>
<p>
После всех изменений нажмите <strong>«Сохранить»</strong> в шапке.
</p>
</div>
</div>
)}
{/* Floating save */}
<div className={styles.floatSave}>
<button className={styles.btnSaveFloat} onClick={handleSave}>
💾 Сохранить
</button>
</div>
</div>
);
}
// ─── PAGE EXPORT ─────────────────────────────────────────────────────────────
export default function AdminPage() {
const [authed, setAuthed] = useState(false);
return authed
? <AdminPanel onLogout={() => setAuthed(false)} />
: <LoginScreen onLogin={() => setAuthed(true)} />;
}