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 ( -
+

Регистрация новой панели

-
- setFormData({...formData, id: e.target.value})} required - /> - setFormData({...formData, name: e.target.value})} required - /> - setFormData({...formData, order_no: e.target.value})} required - /> - setFormData({...formData, material: e.target.value})} required - /> + + setFormData({...formData, id: e.target.value})} required /> + setFormData({...formData, order_no: e.target.value})} required /> + setFormData({...formData, destignation: e.target.value})} required /> + setFormData({...formData, name: e.target.value})} required /> + setFormData({...formData, product_name: e.target.value})} required /> + + {/* Числовые поля */} + setFormData({...formData, length: e.target.value})} required /> + setFormData({...formData, width: e.target.value})} required /> + setFormData({...formData, quantity: e.target.value})} required /> +
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 (
-

Текущее состояние производства

+

Цех: Поток деталей

- - - - - - - - - - - {parts.map(p => ( - - - - - - +
+
IDНаименованиеМатериалСтатус
{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} + + + + + + + ))} + + +
); }