diff --git a/backend/models/part.go b/backend/models/part.go
index 6a08997..9ea3463 100644
--- a/backend/models/part.go
+++ b/backend/models/part.go
@@ -1,6 +1,7 @@
package models
const (
+ StatusImport = "Запланировано"
StatusWarehouse = "Склад"
StatusSawing = "Пила"
StatusEdging = "Кромка"
@@ -40,6 +41,7 @@ func IsValidStatus(s string) bool {
type Part struct {
ID string `gorm:"primaryKey" json:"id" binding:"required"` // № или Обозначение
+ Destignation string `json:"destignation" binding:"required"` // № или Обозначение
OrderNo string `json:"order_no" binding:"required"` // Заказ изделия
Name string `json:"name" binding:"required"` // Наименование детали
Material string `json:"material" binding:"required"` // Наименование материала
@@ -61,6 +63,6 @@ type Part struct {
Note string `json:"note"` // Примечание
ProductName string `json:"product_name" binding:"required"` // Наимен. изделия
- Status string `json:"status" gorm:"default:'Ожидает распила'"`
+ Status string `json:"status" gorm:"default:Создан"`
}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index cdb11b1..40978d5 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index 42e8b5c..b5d48bc 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 426e6c8..8dbb11a 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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 (
@@ -48,10 +55,10 @@ function App() {
>
📷 Сканировать QR
+
- {/* Исправлено: onClose={...} вместо onClos */}
{isScannerOpen && (
)}
- {/* Инфо о выбранной детали (если отсканировали) */}
{selectedPart && (
@@ -71,7 +77,7 @@ function App() {
)}
-
+
);
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index d197312..3007ac1 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -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)
+ });
+ });
+};
diff --git a/frontend/src/components/CsvImporter.jsx b/frontend/src/components/CsvImporter.jsx
new file mode 100644
index 0000000..e2b4f62
--- /dev/null
+++ b/frontend/src/components/CsvImporter.jsx
@@ -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 (
+
+
Импорт заказа из Базиса
+
Выберите .csv файл для массовой загрузки
+
+
+
+
+
+ );
+}
+
diff --git a/frontend/src/components/PartForm.jsx b/frontend/src/components/PartForm.jsx
index f7bf3ad..0cb8645 100644
--- a/frontend/src/components/PartForm.jsx
+++ b/frontend/src/components/PartForm.jsx
@@ -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 (
-
+
Регистрация новой панели
-
diff --git a/frontend/src/components/PartsTable.jsx b/frontend/src/components/PartsTable.jsx
index 5e12ef1..6f9cc4f 100644
--- a/frontend/src/components/PartsTable.jsx
+++ b/frontend/src/components/PartsTable.jsx
@@ -2,34 +2,50 @@ export default function PartsTable({ parts, onRefresh, onDelete }) {
return (
-
Текущее состояние производства
+ Цех: Поток деталей
-
-
-
- | ID |
- Наименование |
- Материал |
- Статус |
-
-
-
- {parts.map(p => (
-
- | {p.id} |
- {p.name} |
- {p.material} |
-
- {p.status}
- |
-
-
- |
+
+
+
+
+ | Заказ / ID |
+ Деталь |
+ Размеры (L x W) |
+ Статус |
+ Действие |
- ))}
-
-
+
+
+ {parts.map(p => (
+
+ |
+ {p.id}
+ {p.order_no}
+ {p.destignation}
+ |
+
+ {p.name}
+ {p.material}
+ |
+
+ {p.length} × {p.width} x{p.quantity}
+ |
+
+
+ {p.status}
+
+ |
+
+
+ |
+
+ ))}
+
+
+
);
}