Dateien nach "frontend/src" hochladen

This commit is contained in:
2026-05-26 01:12:07 +02:00
parent 360deb7257
commit 9bf8415a43
2 changed files with 112 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { api, S } from './lib.js'; import { api, S } from './lib.js';
import { TOOLS, getGroupedTools } from './toolRegistry.js'; import { TOOLS, getGroupedTools } from './toolRegistry.js';
import { useConfirm } from './confirm.jsx';
import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon } from './icons.jsx'; import { HomeIcon, AppsIcon, AdminIcon, MoreIcon, UserIcon, LogOutIcon, ChevronIcon, UpdateIcon } from './icons.jsx';
const ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡']; const ICONS = ['🔗','🌐','📊','📁','🛠','📧','🗓','💾','🖥','📡','🔒','📖','🎯','⚡','🏠','🔧','📱','🎮','🚀','💡'];
@@ -330,6 +331,7 @@ function BottomNav({ active, setActive, user, onLogout, updateInfo, onUpdateClic
// ── Dashboard Widgets ───────────────────────────────────────────────────────── // ── Dashboard Widgets ─────────────────────────────────────────────────────────
function QuickLinks({ toast, mobile }) { function QuickLinks({ toast, mobile }) {
const { confirm, ConfirmDialog } = useConfirm();
const [links, setLinks] = useState([]); const [links, setLinks] = useState([]);
const [editMode, setEdit] = useState(false); const [editMode, setEdit] = useState(false);
const [showForm, setForm] = useState(false); const [showForm, setForm] = useState(false);
@@ -356,12 +358,14 @@ function QuickLinks({ toast, mobile }) {
}; };
const del = async id => { const del = async id => {
if (!await confirm('Link wirklich löschen?')) return;
try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id!==id)); toast('Gelöscht'); } try { await api(`/dashboard/links/${id}`, { method:'DELETE' }); setLinks(p => p.filter(l => l.id!==id)); toast('Gelöscht'); }
catch(e) { toast(e.message, 'error'); } catch(e) { toast(e.message, 'error'); }
}; };
return ( return (
<div style={{ ...S.card, marginBottom:10 }}> <div style={{ ...S.card, marginBottom:10 }}>
<ConfirmDialog/>
<div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}> <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:10 }}>
<div style={S.head}>SCHNELLZUGRIFF</div> <div style={S.head}>SCHNELLZUGRIFF</div>
<div style={{ display:'flex', gap:6 }}> <div style={{ display:'flex', gap:6 }}>
@@ -539,6 +543,64 @@ function Notepad({ toast }) {
); );
} }
// ── Bestell-Statistik Widget ─────────────────────────────────────────────────
function BestellStats() {
const [stats, setStats] = useState(null);
const [open, setOpen] = useState(false);
const [loaded, setLoaded] = useState(false);
const load = () => {
if (loaded) return;
api('/dashboard/stats').then(s => { setStats(s); setLoaded(true); }).catch(() => {});
};
const toggle = () => { setOpen(v => !v); if (!open) load(); };
const Row = ({label, val, color='#fff'}) => (
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'7px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
<span style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:12}}>{label}</span>
<span style={{color,fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>{val}</span>
</div>
);
return (
<div style={{...S.card,marginBottom:10}}>
<button onClick={toggle} style={{width:'100%',background:'transparent',border:'none',
display:'flex',justifyContent:'space-between',alignItems:'center',cursor:'pointer',padding:0}}>
<div style={S.head}>STATISTIK</div>
<span style={{color:'rgba(255,255,255,0.3)',fontSize:11,display:'inline-block',
transform:open?'rotate(90deg)':'rotate(0)',transition:'transform 0.2s'}}></span>
</button>
{open && (
<div style={{marginTop:12}}>
{!stats ? (
<div style={{color:'rgba(255,255,255,0.25)',fontFamily:'monospace',fontSize:12}}>Lädt</div>
) : (
<>
<div style={{marginBottom:12}}>
<div style={{...S.head,marginBottom:6}}>BESTELLUNGEN</div>
<Row label="Gesamt" val={stats.totalOrders}/>
<Row label="Diesen Monat" val={stats.ordersThisMonth} color="#4ecdc4"/>
<Row label="Warteliste" val={stats.byStatus.warteliste||0} color="#ffe66d"/>
<Row label="In Arbeit" val={stats.byStatus.in_arbeit||0} color="#4ecdc4"/>
<Row label="Fertig (unbezahlt)" val={stats.byStatus.fertig||0} color="#6bcb77"/>
<Row label="Bezahlt" val={stats.bezahltOrders} color="#c084fc"/>
</div>
<div>
<div style={{...S.head,marginBottom:6}}>EINNAHMEN</div>
<Row label="Eingenommen (bezahlt)" val={`${stats.totalRevenue.toFixed(2)}`} color="#6bcb77"/>
<Row label="Offen (in Arbeit/Fertig)" val={`${stats.offeneRevenue.toFixed(2)}`} color="#ffe66d"/>
<Row label="Archiv-Einträge" val={stats.totalCalcs} color="rgba(255,255,255,0.5)"/>
</div>
</>
)}
</div>
)}
</div>
);
}
function Dashboard({ toast, mobile }) { function Dashboard({ toast, mobile }) {
return ( return (
<div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}> <div style={{ padding: mobile ? '14px 12px 90px' : '36px 44px', maxWidth:1100 }}>
@@ -549,6 +611,7 @@ function Dashboard({ toast, mobile }) {
</p> </p>
</div> </div>
<QuickLinks toast={toast} mobile={mobile} /> <QuickLinks toast={toast} mobile={mobile} />
<BestellStats/>
<TodoList toast={toast} /> <TodoList toast={toast} />
<Notepad toast={toast} /> <Notepad toast={toast} />
</div> </div>

49
frontend/src/confirm.jsx Normal file
View File

@@ -0,0 +1,49 @@
// ── Globaler Bestätigungs-Dialog ─────────────────────────────────────────────
// Verwendung: const { confirm, ConfirmDialog } = useConfirm();
// await confirm('Wirklich löschen?') && deleteFn();
import { useState, useCallback } from 'react';
export function useConfirm() {
const [state, setState] = useState({ open:false, msg:'', resolve:null });
const confirm = useCallback((msg='Wirklich fortfahren?') => {
return new Promise(resolve => {
setState({ open:true, msg, resolve });
});
}, []);
const handle = yes => {
state.resolve?.(yes);
setState({ open:false, msg:'', resolve:null });
};
const ConfirmDialog = () => !state.open ? null : (
<div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.75)',
zIndex:9000, display:'flex', alignItems:'flex-end', justifyContent:'center' }}
onClick={() => handle(false)}>
<div style={{ background:'#1a1a1e', borderRadius:'16px 16px 0 0', width:'100%',
maxWidth:500, padding:'20px 20px 32px',
border:'1px solid rgba(255,255,255,0.1)' }}
onClick={e => e.stopPropagation()}>
<div style={{ width:36, height:4, background:'rgba(255,255,255,0.15)',
borderRadius:2, margin:'0 auto 18px' }}/>
<p style={{ color:'#fff', fontFamily:'monospace', fontSize:14,
textAlign:'center', marginBottom:20, lineHeight:1.6 }}>{state.msg}</p>
<div style={{ display:'flex', gap:10 }}>
<button onClick={() => handle(false)} style={{
flex:1, padding:'12px 0', background:'rgba(255,255,255,0.06)',
border:'1px solid rgba(255,255,255,0.1)', borderRadius:10,
color:'rgba(255,255,255,0.7)', fontFamily:'monospace', fontSize:13, cursor:'pointer',
}}>Abbrechen</button>
<button onClick={() => handle(true)} style={{
flex:1, padding:'12px 0', background:'rgba(255,107,157,0.15)',
border:'1px solid rgba(255,107,157,0.4)', borderRadius:10,
color:'#ff6b9d', fontFamily:'monospace', fontSize:13, cursor:'pointer', fontWeight:700,
}}>Ja, löschen</button>
</div>
</div>
</div>
);
return { confirm, ConfirmDialog };
}