3D-Modelle Ordner + Dateianhang im Kalkulator
This commit is contained in:
@@ -198,6 +198,48 @@ router.post('/', authenticate, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── 3D-Modelle Hilfsendpoints ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Stellt sicher dass ein Ordner existiert (Name + optionaler parent), gibt ihn zurück
|
||||||
|
router.post('/ensure-folder', authenticate, (req, res) => {
|
||||||
|
const uid = req.user.id;
|
||||||
|
const { name, parent_id } = req.body;
|
||||||
|
if (!name?.trim()) return res.status(400).json({ error: 'Name erforderlich' });
|
||||||
|
const parentId = parent_id ? parseInt(parent_id) : null;
|
||||||
|
|
||||||
|
// Suche existierenden Ordner
|
||||||
|
const existing = parentId
|
||||||
|
? db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id=?').get(uid, name.trim(), parentId)
|
||||||
|
: db.prepare('SELECT * FROM folders WHERE user_id=? AND name=? AND parent_id IS NULL').get(uid, name.trim());
|
||||||
|
|
||||||
|
if (existing) return res.json(enrichFolder(existing));
|
||||||
|
|
||||||
|
const r = db.prepare('INSERT INTO folders (user_id,name,parent_id) VALUES (?,?,?)').run(uid, name.trim(), parentId);
|
||||||
|
res.json(enrichFolder(db.prepare('SELECT * FROM folders WHERE id=?').get(r.lastInsertRowid)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload 3D-Dateien (ohne Ext-Beschränkung, für den Kalkulator)
|
||||||
|
const upload3d = multer({
|
||||||
|
storage,
|
||||||
|
limits: { fileSize: parseInt(process.env.MAX_3D_SIZE_MB || '200') * 1024 * 1024 },
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/upload-3d', authenticate, (req, res) => {
|
||||||
|
upload3d.array('files', 20)(req, res, err => {
|
||||||
|
if (err) return res.status(400).json({ error: err.message });
|
||||||
|
if (!req.files?.length) return res.status(400).json({ error: 'Keine Dateien' });
|
||||||
|
const uid = req.user.id;
|
||||||
|
const folderId = req.body.folder_id ? parseInt(req.body.folder_id) : null;
|
||||||
|
const inserted = [];
|
||||||
|
for (const f of req.files) {
|
||||||
|
const r = db.prepare('INSERT INTO files (user_id,filename,originalname,mimetype,size,folder_id) VALUES (?,?,?,?,?,?)')
|
||||||
|
.run(uid, f.filename, f.originalname, f.mimetype || 'application/octet-stream', f.size, folderId);
|
||||||
|
inserted.push(r.lastInsertRowid);
|
||||||
|
}
|
||||||
|
res.json({ uploaded: inserted.length });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/storage', authenticate, (req, res) => {
|
router.get('/storage', authenticate, (req, res) => {
|
||||||
const uid = req.user.id;
|
const uid = req.user.id;
|
||||||
const used = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
|
const used = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
|
||||||
|
|||||||
@@ -192,6 +192,11 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
|
|
||||||
useEffect(()=>{load();},[load]);
|
useEffect(()=>{load();},[load]);
|
||||||
|
|
||||||
|
// "3D-Modelle" Ordner beim ersten Laden automatisch anlegen
|
||||||
|
useEffect(()=>{
|
||||||
|
api('/tools/dateien/ensure-folder',{body:{name:'3D-Modelle'}}).catch(()=>{});
|
||||||
|
},[]);
|
||||||
|
|
||||||
const doUpload = async file => {
|
const doUpload = async file => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useConfirm } from '../confirm.jsx';
|
|||||||
import { useState, useMemo, useEffect, useRef } from 'react';
|
import { useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import { api, S } from '../lib.js';
|
import { api, S } from '../lib.js';
|
||||||
|
|
||||||
|
const MODEL_EXTENSIONS = ['.stl','.3mf','.f3d','.step','.stp','.obj','.iges','.igs','.fcstd','.amf','.scad'];
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
materialpreis_pro_rolle: 10, // €/Rolle (1 kg = 1000 g → 0.01 €/g)
|
materialpreis_pro_rolle: 10, // €/Rolle (1 kg = 1000 g → 0.01 €/g)
|
||||||
stromverbrauch_kw: 0.15,
|
stromverbrauch_kw: 0.15,
|
||||||
@@ -138,7 +140,9 @@ export default function Kalkulator3D({ toast, editData, onEditDone, mobile, setA
|
|||||||
const [bemerkung, setBemerkung] = useState(editData?.bemerkung ?? '');
|
const [bemerkung, setBemerkung] = useState(editData?.bemerkung ?? '');
|
||||||
const [image, setImage] = useState(editData?.image ?? null);
|
const [image, setImage] = useState(editData?.image ?? null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const fileRef = useRef(null);
|
const [modelFiles, setModelFiles] = useState([]); // Array von File-Objekten
|
||||||
|
const fileRef = useRef(null);
|
||||||
|
const modelFileRef = useRef(null);
|
||||||
const sv = (k, v) => setVars(p => ({ ...p, [k]: v }));
|
const sv = (k, v) => setVars(p => ({ ...p, [k]: v }));
|
||||||
|
|
||||||
const calc = useMemo(() => {
|
const calc = useMemo(() => {
|
||||||
@@ -177,12 +181,36 @@ export default function Kalkulator3D({ toast, editData, onEditDone, mobile, setA
|
|||||||
preis_normal: calc.grund * 1.5,
|
preis_normal: calc.grund * 1.5,
|
||||||
preis_auftrag: calc.grund * 2.5,
|
preis_auftrag: calc.grund * 2.5,
|
||||||
image, bemerkung };
|
image, bemerkung };
|
||||||
|
|
||||||
if (editData?.id) {
|
if (editData?.id) {
|
||||||
await api(`/tools/kalkulator3d/${editData.id}`, { method:'PUT', body:payload });
|
await api(`/tools/kalkulator3d/${editData.id}`, { method:'PUT', body:payload });
|
||||||
toast('Aktualisiert ✓'); onEditDone?.();
|
toast('Aktualisiert ✓'); onEditDone?.();
|
||||||
} else {
|
} else {
|
||||||
await api('/tools/kalkulator3d', { body:payload });
|
await api('/tools/kalkulator3d', { body:payload });
|
||||||
toast('Gespeichert ✓'); setName(''); setBemerkung(''); setImage(null);
|
// 3D-Dateien hochladen wenn vorhanden
|
||||||
|
if (modelFiles.length > 0) {
|
||||||
|
try {
|
||||||
|
// 1. "3D-Modelle" Basisordner sicherstellen
|
||||||
|
const base = await api('/tools/dateien/ensure-folder', { body: { name: '3D-Modelle' } });
|
||||||
|
// 2. Unterordner mit Modellnamen anlegen
|
||||||
|
const sub = await api('/tools/dateien/ensure-folder', { body: { name: name.trim(), parent_id: base.id } });
|
||||||
|
// 3. Dateien hochladen
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('folder_id', sub.id);
|
||||||
|
for (const f of modelFiles) form.append('files', f);
|
||||||
|
await fetch('/api/tools/dateien/upload-3d', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${localStorage.getItem('sk_token')}` },
|
||||||
|
body: form,
|
||||||
|
});
|
||||||
|
toast(`Gespeichert ✓ · ${modelFiles.length} Datei(en) in 3D-Modelle/${name.trim()} abgelegt`);
|
||||||
|
} catch {
|
||||||
|
toast('Gespeichert ✓ (Datei-Upload fehlgeschlagen)', 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast('Gespeichert ✓');
|
||||||
|
}
|
||||||
|
setName(''); setBemerkung(''); setImage(null); setModelFiles([]);
|
||||||
setActive?.('kalkulator3d-saved');
|
setActive?.('kalkulator3d-saved');
|
||||||
}
|
}
|
||||||
} catch(e) { toast(e.message, 'error'); } finally { setSaving(false); }
|
} catch(e) { toast(e.message, 'error'); } finally { setSaving(false); }
|
||||||
@@ -340,6 +368,50 @@ export default function Kalkulator3D({ toast, editData, onEditDone, mobile, setA
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 3D-Modelldateien – nur beim Neuanlegen */}
|
||||||
|
{!editData?.id && (
|
||||||
|
<div style={{ marginBottom:14 }}>
|
||||||
|
<label style={{ ...S.head, display:'block', marginBottom:6 }}>
|
||||||
|
3D-DATEIEN (optional) – werden in 3D-Modelle/{name.trim()||'…'} abgelegt
|
||||||
|
</label>
|
||||||
|
<input ref={modelFileRef} type="file" multiple
|
||||||
|
accept={MODEL_EXTENSIONS.join(',')}
|
||||||
|
onChange={e => setModelFiles(Array.from(e.target.files||[]))}
|
||||||
|
style={{ display:'none' }} />
|
||||||
|
{modelFiles.length > 0 ? (
|
||||||
|
<div style={{ background:'rgba(255,255,255,0.03)', border:'1px solid rgba(255,255,255,0.1)',
|
||||||
|
borderRadius:8, padding:10 }}>
|
||||||
|
{modelFiles.map((f,i) => (
|
||||||
|
<div key={i} style={{ display:'flex', alignItems:'center', justifyContent:'space-between',
|
||||||
|
padding:'4px 0', borderBottom: i<modelFiles.length-1 ? '1px solid rgba(255,255,255,0.05)' : 'none' }}>
|
||||||
|
<span style={{ color:'rgba(255,255,255,0.7)', fontFamily:'monospace', fontSize:11,
|
||||||
|
overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', maxWidth:'80%' }}>
|
||||||
|
🖨 {f.name}
|
||||||
|
</span>
|
||||||
|
<span style={{ color:'rgba(255,255,255,0.3)', fontFamily:'monospace', fontSize:9, flexShrink:0 }}>
|
||||||
|
{(f.size/1024/1024).toFixed(1)} MB
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ display:'flex', gap:6, marginTop:8 }}>
|
||||||
|
<button onClick={() => modelFileRef.current?.click()}
|
||||||
|
style={{ ...S.btn('#4ecdc4', true), flex:1, textAlign:'center', fontSize:11 }}>
|
||||||
|
+ Weitere hinzufügen
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setModelFiles([])}
|
||||||
|
style={{ ...S.btn('#ff6b9d', true), fontSize:11 }}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button onClick={() => modelFileRef.current?.click()} style={{
|
||||||
|
width:'100%', padding:'16px', background:'rgba(255,255,255,0.03)',
|
||||||
|
border:'1px dashed rgba(255,255,255,0.15)', borderRadius:8,
|
||||||
|
color:'rgba(255,255,255,0.6)', cursor:'pointer', fontFamily:'monospace', fontSize:12,
|
||||||
|
}}>🖨 3D-Dateien anhängen (STL, 3MF, F3D, STEP …)</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<button onClick={save} disabled={saving} style={{
|
<button onClick={save} disabled={saving} style={{
|
||||||
width:'100%', background:'linear-gradient(135deg,#4ecdc4,#45b7d1)', border:'none',
|
width:'100%', background:'linear-gradient(135deg,#4ecdc4,#45b7d1)', border:'none',
|
||||||
borderRadius:8, padding:'12px 0', color:'#0d0d0f',
|
borderRadius:8, padding:'12px 0', color:'#0d0d0f',
|
||||||
|
|||||||
Reference in New Issue
Block a user