Внесены корректировки
This commit is contained in:
@@ -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:Создан"`
|
||||
}
|
||||
|
||||
|
||||
Generated
+66
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user