Внесены корректировки
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
StatusImport = "Запланировано"
|
||||||
StatusWarehouse = "Склад"
|
StatusWarehouse = "Склад"
|
||||||
StatusSawing = "Пила"
|
StatusSawing = "Пила"
|
||||||
StatusEdging = "Кромка"
|
StatusEdging = "Кромка"
|
||||||
@@ -40,6 +41,7 @@ func IsValidStatus(s string) bool {
|
|||||||
|
|
||||||
type Part struct {
|
type Part struct {
|
||||||
ID string `gorm:"primaryKey" json:"id" binding:"required"` // № или Обозначение
|
ID string `gorm:"primaryKey" json:"id" binding:"required"` // № или Обозначение
|
||||||
|
Destignation string `json:"destignation" binding:"required"` // № или Обозначение
|
||||||
OrderNo string `json:"order_no" binding:"required"` // Заказ изделия
|
OrderNo string `json:"order_no" binding:"required"` // Заказ изделия
|
||||||
Name string `json:"name" binding:"required"` // Наименование детали
|
Name string `json:"name" binding:"required"` // Наименование детали
|
||||||
Material string `json:"material" binding:"required"` // Наименование материала
|
Material string `json:"material" binding:"required"` // Наименование материала
|
||||||
@@ -61,6 +63,6 @@ type Part struct {
|
|||||||
Note string `json:"note"` // Примечание
|
Note string `json:"note"` // Примечание
|
||||||
ProductName string `json:"product_name" binding:"required"` // Наимен. изделия
|
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",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5",
|
||||||
|
"react-router-dom": "^7.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -1366,6 +1368,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2586,6 +2601,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -2709,6 +2730,44 @@
|
|||||||
"react": "^19.2.5"
|
"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": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||||
@@ -2764,6 +2823,12 @@
|
|||||||
"semver": "bin/semver.js"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|||||||
@@ -13,8 +13,10 @@
|
|||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5",
|
||||||
|
"react-router-dom": "^7.14.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
+14
-8
@@ -1,19 +1,28 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import * as api from './api';
|
import * as api from './api';
|
||||||
import PartForm from './components/PartForm';
|
|
||||||
import PartsTable from './components/PartsTable';
|
import PartsTable from './components/PartsTable';
|
||||||
import Scanner from './components/Scanner';
|
import Scanner from './components/Scanner';
|
||||||
|
import CsvImporter from './components/CsvImporter';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [allParts, setAllParts] = useState([]);
|
|
||||||
const [isScannerOpen, setIsScannerOpen] = useState(false);
|
const [isScannerOpen, setIsScannerOpen] = useState(false);
|
||||||
const [selectedPart, setSelectedPart] = useState(null); // Добавили стейт
|
const [selectedPart, setSelectedPart] = useState(null); // Добавили стейт
|
||||||
|
const [parts, setParts] = useState([]);
|
||||||
|
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
try{
|
||||||
const res = await api.getAllParts();
|
const res = await api.getAllParts();
|
||||||
setAllParts(res.data);
|
setParts(res.data || []);
|
||||||
|
}catch(e){
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleScan = async (id) => {
|
const handleScan = async (id) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.getPart(id);
|
const res = await api.getPart(id);
|
||||||
@@ -36,8 +45,6 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { loadData(); }, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#0f172a] text-slate-200 p-6 flex flex-col items-center">
|
<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">
|
<header className="w-full max-w-4xl mb-12 flex justify-between items-center">
|
||||||
@@ -48,10 +55,10 @@ function App() {
|
|||||||
>
|
>
|
||||||
<span>📷</span> Сканировать QR
|
<span>📷</span> Сканировать QR
|
||||||
</button>
|
</button>
|
||||||
|
<CsvImporter onImported={loadData} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="w-full max-w-4xl space-y-8">
|
<main className="w-full max-w-4xl space-y-8">
|
||||||
{/* Исправлено: onClose={...} вместо onClos */}
|
|
||||||
{isScannerOpen && (
|
{isScannerOpen && (
|
||||||
<Scanner
|
<Scanner
|
||||||
onScan={handleScan}
|
onScan={handleScan}
|
||||||
@@ -59,7 +66,6 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Инфо о выбранной детали (если отсканировали) */}
|
|
||||||
{selectedPart && (
|
{selectedPart && (
|
||||||
<div className="bg-blue-900/30 border border-blue-500 p-4 rounded-lg flex justify-between items-center">
|
<div className="bg-blue-900/30 border border-blue-500 p-4 rounded-lg flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -71,7 +77,7 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<PartForm onCreated={loadData} />
|
<PartForm onCreated={loadData} />
|
||||||
<PartsTable parts={allParts} onRefresh={loadData} onDelete={handleDelete} />
|
<PartsTable parts={parts} onRefresh={loadData} onDelete={handleDelete} />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Papa from 'papaparse';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// Используем относительный путь, так как работаем через Traefik
|
// Используем относительный путь, так как работаем через 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 createPart = (data) => axios.post(`${API_URL}/parts`, data);
|
||||||
export const updatePartStatus = (id, status) => axios.patch(`${API_URL}/parts/${id}/status`, { status });
|
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 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';
|
import { createPart } from '../api';
|
||||||
|
|
||||||
export default function PartForm({ onCreated }) {
|
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) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
await createPart(formData);
|
// Преобразуем числовые поля, чтобы Go не ругался
|
||||||
setFormData({ id: '', name: '', order_no: '', material: '', status: 'Склад' });
|
const dataToSend = {
|
||||||
onCreated(); // Вызываем обновление списка в родителе
|
...formData,
|
||||||
} catch (err) { alert("Ошибка при создании"); }
|
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 (
|
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>
|
<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">
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<input
|
<input placeholder="ID" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
|
||||||
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 />
|
||||||
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
|
<input placeholder="Обозначение" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
|
||||||
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.destignation} onChange={e => setFormData({...formData, destignation: e.target.value})} required />
|
||||||
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.name} onChange={e => setFormData({...formData, name: e.target.value})} required />
|
||||||
<input
|
<input placeholder="Изделие" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
|
||||||
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.product_name} onChange={e => setFormData({...formData, product_name: e.target.value})} required />
|
||||||
value={formData.order_no} onChange={e => setFormData({...formData, order_no: e.target.value})} required
|
|
||||||
/>
|
{/* Числовые поля */}
|
||||||
<input
|
<input type="number" placeholder="L (Длина)" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-white"
|
||||||
placeholder="Материал" className="bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 text-sm focus:border-blue-500 outline-none"
|
onChange={e => setFormData({...formData, length: e.target.value})} required />
|
||||||
value={formData.material} onChange={e => setFormData({...formData, material: 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>
|
<button type="submit" className="bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3 rounded-xl transition-all">ДОБАВИТЬ</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,34 +2,50 @@ export default function PartsTable({ parts, onRefresh, onDelete }) {
|
|||||||
return (
|
return (
|
||||||
<section className="bg-slate-800/50 rounded-3xl border border-slate-700 p-6 shadow-xl">
|
<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">
|
<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>
|
<button onClick={onRefresh} className="text-xs text-blue-500 font-bold uppercase">Обновить ↻</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left">
|
<table className="w-full text-left">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-slate-500 text-[10px] uppercase border-b border-slate-700">
|
<tr className="text-slate-500 text-[10px] uppercase border-b border-slate-700">
|
||||||
<th className="pb-4">ID</th>
|
<th className="pb-4">Заказ / ID</th>
|
||||||
<th className="pb-4">Наименование</th>
|
<th className="pb-4">Деталь</th>
|
||||||
<th className="pb-4">Материал</th>
|
<th className="pb-4">Размеры (L x W)</th>
|
||||||
<th className="pb-4">Статус</th>
|
<th className="pb-4">Статус</th>
|
||||||
|
<th className="pb-4">Действие</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-700/50">
|
<tbody className="divide-y divide-slate-700/50">
|
||||||
{parts.map(p => (
|
{parts.map(p => (
|
||||||
<tr key={p.id} className="hover:bg-white/5 transition-colors">
|
<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">
|
<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>
|
<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>
|
||||||
<td>
|
<td className="py-4">
|
||||||
<button onClick={() => onDelete(p.id)} className="text-red-500 hover:text-red-400 text-xs font-bold">x</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user