From 2da72f12462b336ff1c011be5c094ce2fd5d65a4 Mon Sep 17 00:00:00 2001 From: Dicken Date: Thu, 28 May 2026 13:39:33 +0200 Subject: [PATCH] =?UTF-8?q?Archiv:=20Geteilt-von-mir=20Tab=20+=203D-Datei?= =?UTF-8?q?=20Downloads=20f=C3=BCr=20Owner=20und=20Empf=C3=A4nger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/tools/kalkulator3d/routes.js | 73 +++++++++++++- frontend/src/tools/kalkulator3d.jsx | 119 ++++++++++++++++++----- 2 files changed, 167 insertions(+), 25 deletions(-) diff --git a/backend/src/tools/kalkulator3d/routes.js b/backend/src/tools/kalkulator3d/routes.js index 298f50c..773f13e 100644 --- a/backend/src/tools/kalkulator3d/routes.js +++ b/backend/src/tools/kalkulator3d/routes.js @@ -31,12 +31,81 @@ router.get('/', authenticate, (req, res) => { ORDER BY c.created_at DESC `).all(uid); + // Geteilt von mir + const sharedByMe = db.prepare(` + SELECT c.*, u.username as shared_with_name, cs.created_at as shared_at + FROM calculations c + JOIN calculation_shares cs ON cs.calc_id = c.id + JOIN users u ON u.id = cs.shared_with + WHERE c.user_id = ? + ORDER BY c.name ASC + `).all(uid); + + // has_files auch für shared items prüfen (anhand owner's Ordner) + const sharedWithFiles = shared.map(i => { + const ownerBase = db.prepare( + "SELECT id FROM folders WHERE user_id=? AND name='3D-Modelle' AND parent_id IS NULL" + ).get(i.user_id); + let has_files = false; + if (ownerBase) { + const sub = db.prepare( + 'SELECT id FROM folders WHERE user_id=? AND name=? AND parent_id=?' + ).get(i.user_id, i.name, ownerBase.id); + if (sub) { + const cnt = db.prepare('SELECT COUNT(*) AS c FROM files WHERE folder_id=?').get(sub.id); + has_files = cnt.c > 0; + } + } + return { ...i, has_files }; + }); + res.json({ - own: items.map(i => ({ ...i, has_files: !!filesMap[i.name] })), - shared: shared, + own: items.map(i => ({ ...i, has_files: !!filesMap[i.name] })), + shared: sharedWithFiles, + sharedByMe: sharedByMe.map(i => ({ ...i, has_files: !!filesMap[i.name] })), }); }); +// Dateien eines Modells abrufen (für Owner + shared users) +router.get('/:id/files', authenticate, (req, res) => { + const uid = req.user.id; + const calc = db.prepare('SELECT * FROM calculations WHERE id=?').get(req.params.id); + if (!calc) return res.status(404).json({ error: 'Nicht gefunden' }); + const isOwner = calc.user_id === uid; + const isShared = db.prepare('SELECT id FROM calculation_shares WHERE calc_id=? AND shared_with=?').get(calc.id, uid); + if (!isOwner && !isShared) return res.status(403).json({ error: 'Kein Zugriff' }); + + const baseFolder = db.prepare( + "SELECT id FROM folders WHERE user_id=? AND name='3D-Modelle' AND parent_id IS NULL" + ).get(calc.user_id); + if (!baseFolder) return res.json([]); + const sub = db.prepare('SELECT id FROM folders WHERE user_id=? AND name=? AND parent_id=?') + .get(calc.user_id, calc.name, baseFolder.id); + if (!sub) return res.json([]); + res.json(db.prepare('SELECT * FROM files WHERE folder_id=? ORDER BY created_at ASC').all(sub.id)); +}); + +// Einzelne Datei herunterladen (Owner + shared users) +router.get('/:id/files/:fileId/download', authenticate, (req, res) => { + const uid = req.user.id; + const calc = db.prepare('SELECT * FROM calculations WHERE id=?').get(req.params.id); + if (!calc) return res.status(404).json({ error: 'Nicht gefunden' }); + const isOwner = calc.user_id === uid; + const isShared = db.prepare('SELECT id FROM calculation_shares WHERE calc_id=? AND shared_with=?').get(calc.id, uid); + if (!isOwner && !isShared) return res.status(403).json({ error: 'Kein Zugriff' }); + + const file = db.prepare('SELECT * FROM files WHERE id=?').get(req.params.fileId); + if (!file) return res.status(404).json({ error: 'Datei nicht gefunden' }); + + const path = require('path'); + const fs = require('fs'); + const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads'; + const fp = path.join(UPLOAD_DIR, file.filename); + if (!fs.existsSync(fp)) return res.status(404).json({ error: 'Datei fehlt' }); + res.download(fp, file.originalname); +}); + + // Share-Management router.get('/:id/shares', authenticate, (req, res) => { const calc = db.prepare('SELECT * FROM calculations WHERE id=? AND user_id=?').get(req.params.id, req.user.id); diff --git a/frontend/src/tools/kalkulator3d.jsx b/frontend/src/tools/kalkulator3d.jsx index 775b6b4..0269eb8 100644 --- a/frontend/src/tools/kalkulator3d.jsx +++ b/frontend/src/tools/kalkulator3d.jsx @@ -541,6 +541,68 @@ export default function Kalkulator3D({ toast, editData, onEditDone, mobile, setA } // ── Archiv ────────────────────────────────────────────────────────────── +// ── Datei-Downloads für ein Archiv-Modell ──────────────────────────────────── +function ModelFiles({ calcId, toast }) { + const [files, setFiles] = useState(null); // null = noch nicht geladen + const [open, setOpen] = useState(false); + + const load = async () => { + if (files !== null) return; + try { setFiles(await api(`/tools/kalkulator3d/${calcId}/files`)); } + catch { setFiles([]); } + }; + + const download = async f => { + try { + const res = await fetch(`/api/tools/kalkulator3d/${calcId}/files/${f.id}/download`, { + headers:{ Authorization:`Bearer ${localStorage.getItem('sk_token')}` } + }); + if (!res.ok) { toast('Download fehlgeschlagen','error'); return; } + const blob = await res.blob(); + const a = Object.assign(document.createElement('a'),{ href:URL.createObjectURL(blob), download:f.originalname }); + document.body.appendChild(a); a.click(); document.body.removeChild(a); + setTimeout(()=>URL.revokeObjectURL(a.href),1000); + } catch(e){ toast(e.message,'error'); } + }; + + const toggle = () => { if (!open) load(); setOpen(v=>!v); }; + + return ( +
+ + {open && ( +
+ {files === null ? ( +
Lädt…
+ ) : files.length === 0 ? ( +
Keine Dateien vorhanden
+ ) : files.map((f,i) => ( +
+ + {f.originalname} + + + {(f.size/1024/1024).toFixed(1)} MB + + +
+ ))} +
+ )} +
+ ); +} + // ── Share Modal ─────────────────────────────────────────────────────────────── function CalcShareModal({ item, onClose, toast }) { const [username, setUsername] = useState(''); @@ -602,19 +664,20 @@ function CalcShareModal({ item, onClose, toast }) { export function SavedList({ toast, mobile }) { const { confirm, ConfirmDialog } = useConfirm(); - const [own, setOwn] = useState([]); - const [shared, setShared] = useState([]); - const [loading, setLoading] = useState(true); - const [tab, setTab] = useState('own'); - const [editData, setEditData] = useState(null); - const [search, setSearch] = useState(''); - const [lightbox, setLightbox] = useState(null); - const [shareItem,setShareItem]= useState(null); + const [own, setOwn] = useState([]); + const [shared, setShared] = useState([]); + const [sharedByMe, setSharedByMe] = useState([]); + const [loading, setLoading] = useState(true); + const [tab, setTab] = useState('own'); + const [editData, setEditData] = useState(null); + const [search, setSearch] = useState(''); + const [lightbox, setLightbox] = useState(null); + const [shareItem, setShareItem]= useState(null); const load = () => { setLoading(true); api('/tools/kalkulator3d') - .then(d => { setOwn(d.own||[]); setShared(d.shared||[]); }) + .then(d => { setOwn(d.own||[]); setShared(d.shared||[]); setSharedByMe(d.sharedByMe||[]); }) .catch(e => toast(e.message,'error')) .finally(()=>setLoading(false)); }; @@ -625,7 +688,7 @@ export function SavedList({ toast, mobile }) { catch(e) { toast(e.message,'error'); } }; - const items = tab==='own' ? own : shared; + const items = tab==='own' ? own : tab==='shared' ? shared : sharedByMe; const filtered = search.trim() ? items.filter(i => i.name.toLowerCase().includes(search.toLowerCase()) || (i.bemerkung&&i.bemerkung.toLowerCase().includes(search.toLowerCase()))) : items; @@ -648,8 +711,8 @@ export function SavedList({ toast, mobile }) { {/* Tabs */} -
- {[['own','Meine',own.length],['shared','Geteilt mit mir',shared.length]].map(([k,l,cnt])=>( +
+ {[['own','Meine',own.length],['shared','Geteilt mit mir',shared.length],['sharedByMe','Geteilt von mir',sharedByMe.length]].map(([k,l,cnt])=>(
{/* Aktionen */} -
- {!item.is_shared ? ( - <> - - - - - ) : ( -
- von {item.owner_name} · nur Ansicht -
+
+ {/* Datei-Downloads */} + {item.has_files && ( + )} +
+ {!item.is_shared && tab !== 'sharedByMe' ? ( + <> + + + + + ) : tab === 'sharedByMe' ? ( +
+ → {item.shared_with_name} +
+ ) : ( +
+ von {item.owner_name} · nur Ansicht +
+ )} +
))}