Внесены корректировки

This commit is contained in:
2026-04-30 13:24:33 +03:00
parent 1d0816fcd9
commit 5417092796
8 changed files with 254 additions and 60 deletions
+66 -1
View File
@@ -11,8 +11,10 @@
"@tailwindcss/vite": "^4.2.4",
"axios": "^1.15.2",
"html5-qrcode": "^2.3.8",
"papaparse": "^5.5.3",
"react": "^19.2.5",
"react-dom": "^19.2.5"
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -1366,6 +1368,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2586,6 +2601,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/papaparse": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2709,6 +2730,44 @@
"react": "^19.2.5"
}
},
"node_modules/react-router": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
"integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz",
"integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==",
"license": "MIT",
"dependencies": {
"react-router": "7.14.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.17",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
@@ -2764,6 +2823,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+3 -1
View File
@@ -13,8 +13,10 @@
"@tailwindcss/vite": "^4.2.4",
"axios": "^1.15.2",
"html5-qrcode": "^2.3.8",
"papaparse": "^5.5.3",
"react": "^19.2.5",
"react-dom": "^19.2.5"
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
+15 -9
View File
@@ -1,19 +1,28 @@
import { useState, useEffect } from 'react';
import * as api from './api';
import PartForm from './components/PartForm';
import PartsTable from './components/PartsTable';
import Scanner from './components/Scanner';
import CsvImporter from './components/CsvImporter';
function App() {
const [allParts, setAllParts] = useState([]);
const [isScannerOpen, setIsScannerOpen] = useState(false);
const [selectedPart, setSelectedPart] = useState(null); // Добавили стейт
const [parts, setParts] = useState([]);
const loadData = async () => {
const res = await api.getAllParts();
setAllParts(res.data);
try{
const res = await api.getAllParts();
setParts(res.data || []);
}catch(e){
console.error(e);
}
};
useEffect(()=>{
loadData();
}, []);
const handleScan = async (id) => {
try {
const res = await api.getPart(id);
@@ -36,8 +45,6 @@ function App() {
}
};
useEffect(() => { loadData(); }, []);
return (
<div className="min-h-screen bg-[#0f172a] text-slate-200 p-6 flex flex-col items-center">
<header className="w-full max-w-4xl mb-12 flex justify-between items-center">
@@ -48,10 +55,10 @@ function App() {
>
<span>📷</span> Сканировать QR
</button>
<CsvImporter onImported={loadData} />
</header>
<main className="w-full max-w-4xl space-y-8">
{/* Исправлено: onClose={...} вместо onClos */}
{isScannerOpen && (
<Scanner
onScan={handleScan}
@@ -59,7 +66,6 @@ function App() {
/>
)}
{/* Инфо о выбранной детали (если отсканировали) */}
{selectedPart && (
<div className="bg-blue-900/30 border border-blue-500 p-4 rounded-lg flex justify-between items-center">
<div>
@@ -71,7 +77,7 @@ function App() {
)}
<PartForm onCreated={loadData} />
<PartsTable parts={allParts} onRefresh={loadData} onDelete={handleDelete} />
<PartsTable parts={parts} onRefresh={loadData} onDelete={handleDelete} />
</main>
</div>
);
+40
View File
@@ -1,3 +1,4 @@
import Papa from 'papaparse';
import axios from 'axios';
// Используем относительный путь, так как работаем через Traefik
@@ -8,4 +9,43 @@ export const getAllParts = () => axios.get(`${API_URL}/parts`);
export const createPart = (data) => axios.post(`${API_URL}/parts`, data);
export const updatePartStatus = (id, status) => axios.patch(`${API_URL}/parts/${id}/status`, { status });
export const deletePart = (id) => axios.delete(`${API_URL}/parts/${id}`);
export const createPartBulk = (data) => axios.post(`${API_URL}/parts/bulk`, data);
export const parseAndImportCsv = (file, onProgress) => {
return new Promise((resolve, reject) => {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
encoding: "CP1251", // Стандарт для РФ выгрузок
complete: async (results) => {
const mappedParts = results.data.map(row => ({
id: row['№'] ,
destignation: row['Обозначение детали'],
order_no: row['Заказ изделия'],
name: row['Наименование детали'],
material: row['Наименование материала'],
thickness: parseFloat(row['Толщина с учетом облицовки пласти']?.replace(',', '.') || 0),
quantity: parseInt(row['Количество'] || 1),
length: parseFloat(row['Готовая деталь [L]']?.replace(',', '.') || 0),
width: parseFloat(row['Готовая деталь [W]']?.replace(',', '.') || 0),
edge_l1: row['Обозначение облицовки кромки [L1]'],
edge_l2: row['Обозначение облицовки кромки [L2]'],
edge_w1: row['Обозначение облицовки кромки [W1]'],
edge_w2: row['Обозначение облицовки кромки [W2]'],
groove: row['Паз'],
note: row['Примечание'],
product_name: row['Наимен. изделия'],
status: "Пила" // Стартовый статус по твоим константам
}));
try {
const response = await createPartBulk(mappedParts);
resolve(response.data);
} catch (err) {
reject(err.response?.data?.error || "Ошибка сервера");
}
},
error: (err) => reject(err.message)
});
});
};
+46
View File
@@ -0,0 +1,46 @@
import React, { useRef } from 'react';
import { parseAndImportCsv } from '../api';
export default function CsvImporter({ onImported }) {
const fileInputRef = useRef(null);
const handleUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await parseAndImportCsv(file);
alert(`Успешно! Загружено деталей: ${result.count}`);
if (onImported) onImported(); // Обновляем список деталей на странице
} catch (err) {
alert("Ошибка импорта: " + err);
} finally {
// Очищаем инпут, чтобы можно было загрузить тот же файл повторно
if (fileInputRef.current) fileInputRef.current.value = '';
}
};
return (
<div className="bg-slate-800/50 border-2 border-dashed border-slate-700 rounded-3xl p-6 mb-8 flex flex-col items-center justify-center transition-all hover:border-blue-500/50">
<h3 className="text-white font-bold mb-2">Импорт заказа из Базиса</h3>
<p className="text-slate-400 text-sm mb-4">Выберите .csv файл для массовой загрузки</p>
<input
type="file"
ref={fileInputRef}
onChange={handleUpload}
accept=".csv"
className="hidden"
id="csv-upload"
/>
<label
htmlFor="csv-upload"
className="bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-xl cursor-pointer transition-all shadow-lg active:scale-95"
>
ВЫБРАТЬ ФАЙЛ
</label>
</div>
);
}
+40 -23
View File
@@ -2,37 +2,54 @@ import { useState } from 'react';
import { createPart } from '../api';
export default function PartForm({ onCreated }) {
const [formData, setFormData] = useState({ id: '', name: '', order_no: '', material: '', status: 'Склад' });
const initialState = {
id: '', destignation: '', order_no: '', material: '',
length: 0, width: 0, thickness: 0, quantity: 1,
product_name: '', status: 'Создан', name: ''
};
const [formData, setFormData] = useState(initialState);
const handleSubmit = async (e) => {
e.preventDefault();
try {
await createPart(formData);
setFormData({ id: '', name: '', order_no: '', material: '', status: 'Склад' });
onCreated(); // Вызываем обновление списка в родителе
} catch (err) { alert("Ошибка при создании"); }
// Преобразуем числовые поля, чтобы Go не ругался
const dataToSend = {
...formData,
length: parseFloat(formData.length),
width: parseFloat(formData.width),
thickness: parseFloat(formData.thickness),
quantity: parseInt(formData.quantity)
};
await createPart(dataToSend);
setFormData(initialState);
onCreated();
} catch (err) { alert("Ошибка: " + err.response?.data?.error); }
};
return (
<section className="bg-slate-800/80 rounded-3xl border border-slate-700 p-8 shadow-xl">
<section className="bg-slate-800/80 rounded-3xl border border-slate-700 p-8 shadow-xl mb-8">
<h3 className="text-lg font-bold text-white mb-6">Регистрация новой панели</h3>
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-5 gap-4">
<input
placeholder="ID" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm focus:border-blue-500 outline-none"
value={formData.id} onChange={e => setFormData({...formData, id: e.target.value})} required
/>
<input
placeholder="Название" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm focus:border-blue-500 outline-none"
value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} required
/>
<input
placeholder="Заказ" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm focus:border-blue-500 outline-none"
value={formData.order_no} onChange={e => setFormData({...formData, order_no: e.target.value})} required
/>
<input
placeholder="Материал" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm focus:border-blue-500 outline-none"
value={formData.material} onChange={e => setFormData({...formData, material: e.target.value})} required
/>
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4">
<input placeholder="ID" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
value={formData.id} onChange={e => setFormData({...formData, id: e.target.value})} required />
<input placeholder="Заказ №" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
value={formData.order_no} onChange={e => setFormData({...formData, order_no: e.target.value})} required />
<input placeholder="Обозначение" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
value={formData.destignation} onChange={e => setFormData({...formData, destignation: e.target.value})} required />
<input placeholder="Наименование" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
value={formData.name} onChange={e => setFormData({...formData, name: e.target.value})} required />
<input placeholder="Изделие" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
value={formData.product_name} onChange={e => setFormData({...formData, product_name: e.target.value})} required />
{/* Числовые поля */}
<input type="number" placeholder="L (Длина)" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
onChange={e => setFormData({...formData, length: e.target.value})} required />
<input type="number" placeholder="W (Ширина)" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
onChange={e => setFormData({...formData, width: e.target.value})} required />
<input type="number" placeholder="Кол-во" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
value={formData.quantity} onChange={e => setFormData({...formData, quantity: e.target.value})} required />
<button type="submit" className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3 rounded-xl transition-all">ДОБАВИТЬ</button>
</form>
</section>
+41 -25
View File
@@ -2,34 +2,50 @@ export default function PartsTable({ parts, onRefresh, onDelete }) {
return (
<section className="bg-slate-800/50 rounded-3xl border border-slate-700 p-6 shadow-xl">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-bold text-white text-[10px] uppercase tracking-widest">Текущее состояние производства</h3>
<h3 className="text-lg font-bold text-white text-[10px] uppercase tracking-widest">Цех: Поток деталей</h3>
<button onClick={onRefresh} className="text-xs text-blue-500 font-bold uppercase">Обновить </button>
</div>
<table className="w-full text-left">
<thead>
<tr className="text-slate-500 text-[10px] uppercase border-b border-slate-700">
<th className="pb-4">ID</th>
<th className="pb-4">Наименование</th>
<th className="pb-4">Материал</th>
<th className="pb-4">Статус</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/50">
{parts.map(p => (
<tr key={p.id} className="hover:bg-white/5 transition-colors">
<td className="py-4 font-mono text-blue-400 text-xs">{p.id}</td>
<td className="py-4 font-bold text-sm">{p.name}</td>
<td className="py-4 text-slate-400 text-xs">{p.material}</td>
<td className="py-4">
<span className="px-2 py-1 rounded text-[9px] font-black uppercase border border-slate-600 text-slate-400">{p.status}</span>
</td>
<td>
<button onClick={() => onDelete(p.id)} className="text-red-500 hover:text-red-400 text-xs font-bold">x</button>
</td>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="text-slate-500 text-[10px] uppercase border-b border-slate-700">
<th className="pb-4">Заказ / ID</th>
<th className="pb-4">Деталь</th>
<th className="pb-4">Размеры (L x W)</th>
<th className="pb-4">Статус</th>
<th className="pb-4">Действие</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody className="divide-y divide-slate-700/50">
{parts.map(p => (
<tr key={p.id} className="hover:bg-white/5 transition-colors">
<td className="py-4">
<div className="font-mono text-blue-400 text-xs">{p.id}</div>
<div className="text-[10px] text-slate-500">{p.order_no}</div>
<div className="text-[10px] text-slate-500">{p.destignation}</div>
</td>
<td className="py-4">
<div className="font-bold text-sm text-white">{p.name}</div>
<div className="text-[10px] text-slate-500">{p.material}</div>
</td>
<td className="py-4 font-mono text-emerald-400 text-sm">
{p.length} × {p.width} <span className="text-[10px] text-slate-600">x{p.quantity}</span>
</td>
<td className="py-4">
<span className="px-2 py-1 rounded text-[9px] font-black uppercase border border-emerald-500/50 text-emerald-500 bg-emerald-500/10">
{p.status}
</span>
</td>
<td className="py-4">
<button onClick={() => onDelete(p.id)} className="hover:scale-125 transition-transform text-red-500">
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
);
}