Dateien nach "frontend/src" hochladen
This commit is contained in:
@@ -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
49
frontend/src/confirm.jsx
Normal 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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user