feat: add onboarding walkthrough and wire all views into main page
This commit is contained in:
@@ -0,0 +1,770 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Paper,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
IconButton,
|
||||||
|
Chip,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Drawer,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Divider,
|
||||||
|
useMediaQuery,
|
||||||
|
Tooltip,
|
||||||
|
InputAdornment,
|
||||||
|
Avatar,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
ToggleButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
|
Languages,
|
||||||
|
LogOut,
|
||||||
|
User as UserIcon,
|
||||||
|
Settings,
|
||||||
|
Menu as MenuIcon,
|
||||||
|
MessageSquare,
|
||||||
|
Wrench,
|
||||||
|
ShieldCheck,
|
||||||
|
Lock,
|
||||||
|
ChevronRight,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import Logo from '../components/Logo';
|
||||||
|
|
||||||
|
import { useAuthStore } from '../services/auth-store';
|
||||||
|
import { useColorMode } from '../services/color-mode';
|
||||||
|
import { useChatStore } from '../services/chat-store';
|
||||||
|
import { useI18n } from '../services/i18n';
|
||||||
|
import { useToolsStore } from '../services/tools-store';
|
||||||
|
import { Tool, ToolCategory, CATEGORY_LABEL } from '../types';
|
||||||
|
|
||||||
|
import ChatSidebar from '../components/ChatSidebar';
|
||||||
|
import ChatWindow from '../components/ChatWindow';
|
||||||
|
import ToolCard from '../components/ToolCard';
|
||||||
|
import ReservationModal from '../components/ReservationModal';
|
||||||
|
import Onboarding from '../components/Onboarding';
|
||||||
|
import HeroSection from '../components/HeroSection';
|
||||||
|
import AdminDashboard from '../components/AdminDashboard';
|
||||||
|
import AnimatedBackground from '../components/AnimatedBackground';
|
||||||
|
|
||||||
|
type View = 'tools' | 'chat' | 'admin';
|
||||||
|
|
||||||
|
const APPBAR_HEIGHT = 64;
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const { user, login, logout } = useAuthStore();
|
||||||
|
const { mode, toggleColorMode } = useColorMode();
|
||||||
|
const { t, lang, setLang } = useI18n();
|
||||||
|
const { createSession, addMessage } = useChatStore();
|
||||||
|
const tools = useToolsStore((s) => s.tools);
|
||||||
|
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [loginError, setLoginError] = useState<string | null>(null);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const openLangMenu = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const [view, setView] = useState<View>('tools');
|
||||||
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||||
|
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// Tools page filters
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState<ToolCategory | 'all'>('all');
|
||||||
|
|
||||||
|
const navItems = useMemo(
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
{ id: 'tools' as View, label: t('tools'), icon: <Wrench size={18} /> },
|
||||||
|
{ id: 'chat' as View, label: t('chat'), icon: <MessageSquare size={18} /> },
|
||||||
|
...(user?.role === 'admin'
|
||||||
|
? [{ id: 'admin' as View, label: t('admin'), icon: <Settings size={18} /> }]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
[user, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredTools = useMemo(() => {
|
||||||
|
const q = search.trim().toLowerCase();
|
||||||
|
return tools.filter((tool) => {
|
||||||
|
if (categoryFilter !== 'all' && tool.category !== categoryFilter) return false;
|
||||||
|
if (!q) return true;
|
||||||
|
return (
|
||||||
|
tool.name.toLowerCase().includes(q) ||
|
||||||
|
tool.description.toLowerCase().includes(q) ||
|
||||||
|
tool.location.toLowerCase().includes(q) ||
|
||||||
|
CATEGORY_LABEL[tool.category].toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [tools, search, categoryFilter]);
|
||||||
|
|
||||||
|
const handleLangClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleLangClose = (newLang?: 'de' | 'en') => {
|
||||||
|
if (newLang) setLang(newLang);
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSubmitting(true);
|
||||||
|
setLoginError(null);
|
||||||
|
const ok = login(username.trim(), password);
|
||||||
|
if (ok) {
|
||||||
|
setShowOnboarding(true);
|
||||||
|
} else {
|
||||||
|
setLoginError(t('loginError'));
|
||||||
|
}
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openReservation = (tool: Tool) => {
|
||||||
|
setSelectedTool(tool);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAISend = (message: string) => {
|
||||||
|
const sessionId = createSession(message.substring(0, 30) || 'Neue Reparatur');
|
||||||
|
setView('chat');
|
||||||
|
addMessage(sessionId, { role: 'user', content: message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigate = (next: View) => {
|
||||||
|
setView(next);
|
||||||
|
setDrawerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<LoginScreen
|
||||||
|
username={username}
|
||||||
|
password={password}
|
||||||
|
setUsername={setUsername}
|
||||||
|
setPassword={setPassword}
|
||||||
|
submitting={submitting}
|
||||||
|
loginError={loginError}
|
||||||
|
onSubmit={handleLogin}
|
||||||
|
t={t}
|
||||||
|
mode={mode}
|
||||||
|
toggleColorMode={toggleColorMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const usedCategories = Array.from(new Set(tools.map((t) => t.category))) as ToolCategory[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedBackground intensity={view === 'tools' ? 1 : 0.6} />
|
||||||
|
|
||||||
|
<Box sx={{ position: 'relative', zIndex: 1, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||||
|
<AppBar position="sticky" elevation={0} sx={{ color: 'text.primary' }}>
|
||||||
|
<Toolbar sx={{ minHeight: { xs: APPBAR_HEIGHT, sm: APPBAR_HEIGHT }, gap: 1 }}>
|
||||||
|
{isMobile && (
|
||||||
|
<IconButton
|
||||||
|
edge="start"
|
||||||
|
onClick={() => setDrawerOpen(true)}
|
||||||
|
aria-label="Menü öffnen"
|
||||||
|
sx={{ mr: 0.5 }}
|
||||||
|
>
|
||||||
|
<MenuIcon size={22} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1.25,
|
||||||
|
cursor: 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
flexGrow: { xs: 1, md: 0 },
|
||||||
|
}}
|
||||||
|
onClick={() => navigate('tools')}
|
||||||
|
>
|
||||||
|
<Logo size={36} />
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 900,
|
||||||
|
letterSpacing: '-0.02em',
|
||||||
|
fontSize: { xs: '1.05rem', sm: '1.15rem' },
|
||||||
|
display: { xs: isSmall ? 'none' : 'block', sm: 'block' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
PATCH<Box component="span" sx={{ color: 'primary.main' }}>PILOT</Box>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!isMobile && (
|
||||||
|
<Box sx={{ ml: 3, display: 'flex', gap: 0.5, flexGrow: 1 }}>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => navigate(item.id)}
|
||||||
|
startIcon={item.icon}
|
||||||
|
variant={view === item.id ? 'contained' : 'text'}
|
||||||
|
color={view === item.id ? 'primary' : 'inherit'}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
fontWeight: view === item.id ? 700 : 500,
|
||||||
|
boxShadow: view === item.id ? 'var(--pp-shadow-brand)' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMobile && <Box sx={{ flexGrow: 1 }} />}
|
||||||
|
|
||||||
|
<Tooltip title="Sprache wechseln">
|
||||||
|
<IconButton onClick={handleLangClick} size="small">
|
||||||
|
<Languages size={20} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu anchorEl={anchorEl} open={openLangMenu} onClose={() => handleLangClose()}>
|
||||||
|
<MenuItem onClick={() => handleLangClose('de')} selected={lang === 'de'}>
|
||||||
|
Deutsch
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => handleLangClose('en')} selected={lang === 'en'}>
|
||||||
|
English
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Tooltip title={mode === 'dark' ? 'Light' : 'Dark'}>
|
||||||
|
<IconButton onClick={toggleColorMode} size="small">
|
||||||
|
{mode === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{!isSmall && (
|
||||||
|
<Chip
|
||||||
|
icon={<UserIcon size={14} />}
|
||||||
|
label={user.username}
|
||||||
|
sx={{ ml: 1, fontWeight: 600, borderRadius: 1.5 }}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Tooltip title={t('logout')}>
|
||||||
|
<IconButton onClick={logout} size="small">
|
||||||
|
<LogOut size={20} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
<Drawer anchor="left" open={drawerOpen} onClose={() => setDrawerOpen(false)}>
|
||||||
|
<Box sx={{ width: 280, pt: 2, height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Box sx={{ px: 2.5, pb: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.username.charAt(0).toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
|
||||||
|
{user.username}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{user.role === 'admin' ? t('administrator') : user.role === 'staff' ? t('staffRole') : t('guestRole')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<List sx={{ flexGrow: 1, p: 1 }}>
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<ListItem key={item.id} disablePadding sx={{ mb: 0.5 }}>
|
||||||
|
<ListItemButton
|
||||||
|
selected={view === item.id}
|
||||||
|
onClick={() => navigate(item.id)}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
'&.Mui-selected': {
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
color: 'white',
|
||||||
|
'& .MuiListItemIcon-root': { color: 'white' },
|
||||||
|
'&:hover': { bgcolor: 'primary.dark' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={item.label} primaryTypographyProps={{ fontWeight: 600 }} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<Box sx={{ p: 1.5 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<LogOut size={18} />}
|
||||||
|
onClick={logout}
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
{t('logout')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={view}
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -8 }}
|
||||||
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||||
|
style={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}
|
||||||
|
>
|
||||||
|
{view === 'tools' ? (
|
||||||
|
<Box sx={{ pb: { xs: 6, md: 10 } }}>
|
||||||
|
<HeroSection onAISend={handleAISend} />
|
||||||
|
<Container maxWidth="lg" sx={{ mt: { xs: 2, md: 4 } }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: { xs: 'stretch', md: 'center' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
mb: { xs: 2, md: 3 },
|
||||||
|
flexDirection: { xs: 'column', md: 'row' },
|
||||||
|
gap: { xs: 1.5, md: 2 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.015em' }}>
|
||||||
|
{t('inventory')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{filteredTools.length} / {tools.length} {t('inventoryHint')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
placeholder={t('search')}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
sx={{ minWidth: { xs: '100%', sm: 280 } }}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Search size={16} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: search ? (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton size="small" onClick={() => setSearch('')}>
|
||||||
|
<X size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
) : null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mb: { xs: 2.5, md: 3 }, overflowX: 'auto', pb: 0.5 }}>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
value={categoryFilter}
|
||||||
|
exclusive
|
||||||
|
onChange={(_, v) => v && setCategoryFilter(v)}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
'& .MuiToggleButton-root': {
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
borderRadius: '999px !important',
|
||||||
|
mx: 0.25,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.5,
|
||||||
|
height: 30,
|
||||||
|
'&.Mui-selected': {
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
color: 'white',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
'&:hover': { bgcolor: 'primary.dark' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value="all">{t('all')}</ToggleButton>
|
||||||
|
{usedCategories.map((cat) => (
|
||||||
|
<ToggleButton key={cat} value={cat}>
|
||||||
|
{CATEGORY_LABEL[cat]}
|
||||||
|
</ToggleButton>
|
||||||
|
))}
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{filteredTools.length === 0 ? (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 6,
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: 3,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography color="text.secondary">{t('noToolsFound')}</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: {
|
||||||
|
xs: '1fr',
|
||||||
|
sm: 'repeat(2, 1fr)',
|
||||||
|
lg: 'repeat(3, 1fr)',
|
||||||
|
},
|
||||||
|
gap: { xs: 2, md: 2.5 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredTools.map((tool, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={tool.id}
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.03 * idx, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<ToolCard tool={tool} user={user} onReserve={openReservation} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
) : view === 'chat' ? (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: `calc(100vh - ${APPBAR_HEIGHT}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChatSidebar />
|
||||||
|
<ChatWindow />
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<AdminDashboard />
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ReservationModal
|
||||||
|
open={isModalOpen}
|
||||||
|
tool={selectedTool}
|
||||||
|
user={user}
|
||||||
|
onClose={() => setIsModalOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Onboarding open={showOnboarding} onFinish={() => setShowOnboarding(false)} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Login Screen ---------- */
|
||||||
|
|
||||||
|
interface LoginScreenProps {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
setUsername: (v: string) => void;
|
||||||
|
setPassword: (v: string) => void;
|
||||||
|
submitting: boolean;
|
||||||
|
loginError: string | null;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
t: (k: any) => string;
|
||||||
|
mode: 'light' | 'dark';
|
||||||
|
toggleColorMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginScreen({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
setUsername,
|
||||||
|
setPassword,
|
||||||
|
submitting,
|
||||||
|
loginError,
|
||||||
|
onSubmit,
|
||||||
|
t,
|
||||||
|
mode,
|
||||||
|
toggleColorMode,
|
||||||
|
}: LoginScreenProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatedBackground intensity={0.7} />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
sx={{ position: 'absolute', top: 16, right: 16, zIndex: 5 }}
|
||||||
|
>
|
||||||
|
{mode === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
display: { xs: 'none', md: 'flex' },
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
color: 'white',
|
||||||
|
p: 6,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 0% 0%, rgba(255,107,0,0.95), rgba(229,95,0,0.95) 45%, rgba(45,90,39,0.9) 100%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="pp-grid-bg" sx={{ position: 'absolute', inset: 0, opacity: 0.18 }} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, position: 'relative' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
bgcolor: 'rgba(255,255,255,0.95)',
|
||||||
|
p: 0.5,
|
||||||
|
border: '1px solid rgba(255,255,255,0.28)',
|
||||||
|
boxShadow: '0 8px 20px -8px rgba(0,0,0,0.35)',
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo size={36} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 900, letterSpacing: '-0.02em' }}>
|
||||||
|
PATCHPILOT
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ position: 'relative', maxWidth: 460 }}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{ fontWeight: 900, letterSpacing: '-0.03em', lineHeight: 1.05, mb: 2 }}
|
||||||
|
>
|
||||||
|
{t('heroTitlePart1')}<br />{t('heroTitlePart2')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ opacity: 0.92, fontSize: '1.05rem', mb: 4 }}>
|
||||||
|
{t('heroSubtitle')}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 2 }}>
|
||||||
|
<Feature icon={<Wrench size={18} />} title={t('miniStatInventory')} desc="Werkzeug-Inventar" />
|
||||||
|
<Feature icon={<MessageSquare size={18} />} title={t('chat')} desc="Schritt für Schritt" />
|
||||||
|
<Feature icon={<ShieldCheck size={18} />} title="Sicher" desc="Einweisungs-Tracking" />
|
||||||
|
<Feature icon={<Settings size={18} />} title={t('admin')} desc="Verwaltung" />
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.75, position: 'relative' }}>
|
||||||
|
© {new Date().getFullYear()} PatchPilot · Hackathon Edition
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: { xs: 3, sm: 4, md: 6 },
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, ease: 'easeOut' }}
|
||||||
|
style={{ width: '100%', maxWidth: 420, position: 'relative' }}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: { md: 'none' }, mb: 4, textAlign: 'center' }}>
|
||||||
|
<Box sx={{ display: 'inline-block', mb: 2 }}>
|
||||||
|
<Logo size={64} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 900, letterSpacing: '-0.02em' }}>
|
||||||
|
PATCHPILOT
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 800, mb: 1, letterSpacing: '-0.02em' }}>
|
||||||
|
{t('loginWelcomeBack')}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
{t('loginSubtitle')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t('username')}
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
autoComplete="username"
|
||||||
|
margin="normal"
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<UserIcon size={18} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={t('password')}
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
autoComplete="current-password"
|
||||||
|
margin="normal"
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Lock size={18} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{loginError && (
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
p: 1.5,
|
||||||
|
bgcolor: 'error.main',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: 2,
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loginError}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
size="large"
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
disabled={submitting || !username || !password}
|
||||||
|
endIcon={<ChevronRight size={18} />}
|
||||||
|
sx={{ mt: 3, py: 1.5, fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
{submitting ? t('loginSubmitting') : t('loginSubmit')}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
mt: 4,
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
border: '1px dashed',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||||
|
{t('loginDemoTitle')}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ display: 'block', color: 'text.secondary', mt: 0.5, whiteSpace: 'pre-line' }}
|
||||||
|
>
|
||||||
|
{t('loginDemoBody')}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</motion.div>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Feature({ icon, title, desc }: { icon: React.ReactNode; title: string; desc: string }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(255,255,255,0.12)',
|
||||||
|
backdropFilter: 'blur(8px)',
|
||||||
|
borderRadius: 2.5,
|
||||||
|
p: 2,
|
||||||
|
border: '1px solid rgba(255,255,255,0.18)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||||
|
{icon}
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.85 }}>
|
||||||
|
{desc}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,701 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
LinearProgress,
|
||||||
|
Chip,
|
||||||
|
Paper,
|
||||||
|
TextField,
|
||||||
|
Avatar,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
Hammer,
|
||||||
|
MessageSquare,
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
Calendar,
|
||||||
|
Shield,
|
||||||
|
X,
|
||||||
|
Sparkles,
|
||||||
|
ShieldCheck,
|
||||||
|
MapPin,
|
||||||
|
Send,
|
||||||
|
Bot,
|
||||||
|
User as UserIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import Logo from './Logo';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { useI18n } from '../services/i18n';
|
||||||
|
import { useAuthStore } from '../services/auth-store';
|
||||||
|
|
||||||
|
interface OnboardingProps {
|
||||||
|
open: boolean;
|
||||||
|
onFinish: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
visual: React.ReactNode;
|
||||||
|
action?: { label: string; hint?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Onboarding({ open, onFinish }: OnboardingProps) {
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [pickedExample, setPickedExample] = useState<string | null>(null);
|
||||||
|
const [demoInput, setDemoInput] = useState('');
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { user } = useAuthStore();
|
||||||
|
|
||||||
|
const steps = useMemo<Step[]>(() => {
|
||||||
|
const base: Step[] = [
|
||||||
|
{
|
||||||
|
key: 'welcome',
|
||||||
|
title: `Willkommen, ${user?.username ?? 'Helfer'}!`,
|
||||||
|
subtitle:
|
||||||
|
'In wenigen Sekunden zeige ich dir, wie du Werkzeuge ausleihst, dich vom KI-Assistenten beraten lässt und alles auf einen Blick siehst.',
|
||||||
|
icon: <Hammer size={22} />,
|
||||||
|
visual: <WelcomeVisual />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tools',
|
||||||
|
title: 'Werkzeuge entdecken',
|
||||||
|
subtitle:
|
||||||
|
'Wähle aus unserem Inventar — jedes Werkzeug zeigt dir Standort, Leihdauer und ob eine Einweisung nötig ist.',
|
||||||
|
icon: <Hammer size={22} />,
|
||||||
|
visual: (
|
||||||
|
<ToolsVisual
|
||||||
|
picked={pickedExample}
|
||||||
|
onPick={(name) => setPickedExample(name)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
action: {
|
||||||
|
label: 'Probier es: klick auf ein Werkzeug',
|
||||||
|
hint: pickedExample ? `✓ Gewählt: ${pickedExample}` : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'chat',
|
||||||
|
title: 'KI-Assistent nutzen',
|
||||||
|
subtitle:
|
||||||
|
'Beschreibe dein Problem in eigenen Worten — RepairBuddy hilft dir bei der Diagnose und schlägt Reparaturansätze vor.',
|
||||||
|
icon: <MessageSquare size={22} />,
|
||||||
|
visual: <ChatVisual demoInput={demoInput} onChange={setDemoInput} />,
|
||||||
|
action: {
|
||||||
|
label: 'Tippe einen Beispiel-Satz oben ein',
|
||||||
|
hint: demoInput.length > 4 ? `✓ Beispiel erkannt (${demoInput.length} Zeichen)` : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'calendar',
|
||||||
|
title: 'Verfügbarkeit prüfen',
|
||||||
|
subtitle:
|
||||||
|
'Im interaktiven Kalender siehst du sofort, wann Werkzeuge frei sind und wann sie belegt sind.',
|
||||||
|
icon: <Calendar size={22} />,
|
||||||
|
visual: <CalendarVisual />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (user?.role === 'admin') {
|
||||||
|
base.push({
|
||||||
|
key: 'admin',
|
||||||
|
title: 'Admin-Bereich',
|
||||||
|
subtitle:
|
||||||
|
'Als Admin verwaltest du Werkzeuge, Nutzer, das KI-Wissen und siehst alle Reservierungen auf einen Blick.',
|
||||||
|
icon: <Shield size={22} />,
|
||||||
|
visual: <AdminVisual />,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}, [user, pickedExample, demoInput]);
|
||||||
|
|
||||||
|
const totalSteps = steps.length;
|
||||||
|
const isLast = activeStep === totalSteps - 1;
|
||||||
|
const current = steps[activeStep];
|
||||||
|
const progress = ((activeStep + 1) / totalSteps) * 100;
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (isLast) {
|
||||||
|
reset();
|
||||||
|
onFinish();
|
||||||
|
} else {
|
||||||
|
setActiveStep((p) => p + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => setActiveStep((p) => Math.max(0, p - 1));
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
reset();
|
||||||
|
onFinish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setActiveStep(0);
|
||||||
|
setPickedExample(null);
|
||||||
|
setDemoInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleSkip}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="md"
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: { xs: 3, md: 4 },
|
||||||
|
overflow: 'hidden',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ position: 'relative' }}>
|
||||||
|
{/* Header: progress + close */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
px: { xs: 2, md: 3 },
|
||||||
|
py: 2,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
background: 'linear-gradient(135deg, #FF6B00, #E55F00)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sparkles size={14} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 700, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
|
||||||
|
Onboarding · Schritt {activeStep + 1} von {totalSteps}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={progress}
|
||||||
|
sx={{
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
background: 'linear-gradient(90deg, #FF6B00, #E55F00)',
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<IconButton onClick={handleSkip} size="small" aria-label="Überspringen">
|
||||||
|
<X size={16} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
|
||||||
|
minHeight: { md: 400 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Visual */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
p: { xs: 3, md: 4 },
|
||||||
|
bgcolor: 'background.default',
|
||||||
|
backgroundImage: 'none',
|
||||||
|
borderRight: { md: '1px solid' },
|
||||||
|
borderBottom: { xs: '1px solid', md: 'none' },
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={current.key}
|
||||||
|
initial={{ opacity: 0, scale: 0.96 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 1.02 }}
|
||||||
|
transition={{ duration: 0.28, ease: 'easeOut' }}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{current.visual}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Text */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: { xs: 3, md: 4 },
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={current.key}
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -6 }}
|
||||||
|
transition={{ duration: 0.22 }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.75,
|
||||||
|
color: 'primary.main',
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
letterSpacing: '0.06em',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
mb: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{current.icon}
|
||||||
|
<span>{current.key}</span>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800, mb: 1.5, letterSpacing: '-0.015em' }}>
|
||||||
|
{current.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" sx={{ lineHeight: 1.6, mb: 2 }}>
|
||||||
|
{current.subtitle}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{current.action && (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Sparkles size={16} color="#FF6B00" />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', fontWeight: 700 }}>
|
||||||
|
{current.action.label}
|
||||||
|
</Typography>
|
||||||
|
{current.action.hint && (
|
||||||
|
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 600 }}>
|
||||||
|
{current.action.hint}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
mt: 'auto',
|
||||||
|
pt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={activeStep === 0 ? handleSkip : handleBack}
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{activeStep === 0 ? 'Überspringen' : 'Zurück'}
|
||||||
|
</Button>
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<Box
|
||||||
|
key={s.key}
|
||||||
|
sx={{
|
||||||
|
width: i === activeStep ? 20 : 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: i === activeStep ? 'primary.main' : 'action.disabledBackground',
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
onClick={handleNext}
|
||||||
|
variant="contained"
|
||||||
|
endIcon={isLast ? <Check size={16} /> : <ArrowRight size={16} />}
|
||||||
|
>
|
||||||
|
{isLast ? 'Loslegen' : 'Weiter'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------ Visuals ------------ */
|
||||||
|
|
||||||
|
function WelcomeVisual() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ textAlign: 'center', width: '100%' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 110,
|
||||||
|
height: 110,
|
||||||
|
mx: 'auto',
|
||||||
|
borderRadius: 4,
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
boxShadow: 'var(--pp-shadow-elevated)',
|
||||||
|
position: 'relative',
|
||||||
|
p: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Logo size={76} />
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 14, repeat: Infinity, ease: 'linear' }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: -10,
|
||||||
|
borderRadius: 26,
|
||||||
|
border: '2px dashed rgba(255,107,0,0.35)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'center', gap: 0.75 }}>
|
||||||
|
<Chip label="🔧 Werkzeuge" size="small" />
|
||||||
|
<Chip label="🤖 KI-Chat" size="small" />
|
||||||
|
<Chip label="📅 Kalender" size="small" />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolsVisual({
|
||||||
|
picked,
|
||||||
|
onPick,
|
||||||
|
}: {
|
||||||
|
picked: string | null;
|
||||||
|
onPick: (name: string) => void;
|
||||||
|
}) {
|
||||||
|
const samples = [
|
||||||
|
{ name: 'Bohrmaschine', loc: 'Werkstatt A', cert: true },
|
||||||
|
{ name: '3D-Drucker', loc: 'Labor 1', cert: true },
|
||||||
|
{ name: 'Hammer', loc: 'Werkzeugwand', cert: false },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1.25 }}>
|
||||||
|
{samples.map((s) => {
|
||||||
|
const selected = picked === s.name;
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
key={s.name}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onPick(s.name)}
|
||||||
|
sx={{
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1.5,
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.18s ease',
|
||||||
|
borderColor: selected ? 'primary.main' : 'divider',
|
||||||
|
bgcolor: selected ? 'rgba(255,107,0,0.08)' : 'background.paper',
|
||||||
|
boxShadow: selected ? 'var(--pp-shadow-brand)' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 1.75,
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
background: 'linear-gradient(135deg, #FF6B00, #E55F00)',
|
||||||
|
color: 'white',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Hammer size={16} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 700 }}>
|
||||||
|
{s.name}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: 'text.secondary' }}>
|
||||||
|
<MapPin size={11} />
|
||||||
|
<Typography variant="caption">{s.loc}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{s.cert ? (
|
||||||
|
<Chip
|
||||||
|
icon={<ShieldCheck size={11} />}
|
||||||
|
label="Einweisung"
|
||||||
|
size="small"
|
||||||
|
color="warning"
|
||||||
|
sx={{ height: 22, fontSize: '0.7rem', borderRadius: 1.25 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
label="Frei"
|
||||||
|
size="small"
|
||||||
|
color="success"
|
||||||
|
sx={{ height: 22, fontSize: '0.7rem', borderRadius: 1.25 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatVisual({
|
||||||
|
demoInput,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
demoInput: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1.25 }}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1.25, alignItems: 'flex-start' }}>
|
||||||
|
<Avatar sx={{ width: 28, height: 28, bgcolor: 'secondary.main' }}>
|
||||||
|
<Bot size={14} />
|
||||||
|
</Avatar>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 1.25,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
borderTopLeftRadius: 6,
|
||||||
|
maxWidth: '85%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" sx={{ fontSize: '0.82rem' }}>
|
||||||
|
Hi! Was möchtest du heute reparieren?
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
{demoInput.length > 0 && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1.25, alignItems: 'flex-start', flexDirection: 'row-reverse' }}>
|
||||||
|
<Avatar sx={{ width: 28, height: 28, bgcolor: 'primary.main' }}>
|
||||||
|
<UserIcon size={14} />
|
||||||
|
</Avatar>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: 1.25,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
borderTopRightRadius: 6,
|
||||||
|
maxWidth: '85%',
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
}}
|
||||||
|
elevation={0}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" sx={{ fontSize: '0.82rem' }}>
|
||||||
|
{demoInput}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
variant="standard"
|
||||||
|
placeholder="z. B. Mein Fahrrad bremst nicht mehr"
|
||||||
|
value={demoInput}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
InputProps={{ disableUnderline: true, sx: { px: 1, fontSize: '0.85rem' } }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: 2,
|
||||||
|
'&:hover': { bgcolor: 'primary.dark' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Send size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarVisual() {
|
||||||
|
const days = Array.from({ length: 28 }, (_, i) => i + 1);
|
||||||
|
const reserved = new Set([5, 6, 12, 18, 19, 20, 25]);
|
||||||
|
const today = 10;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 320,
|
||||||
|
mx: 'auto',
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 3,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1.5 }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 700 }}>
|
||||||
|
Mai 2026
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4 }}>
|
||||||
|
<Box sx={{ width: 8, height: 8, borderRadius: 0.75, bgcolor: '#F59E0B' }} />
|
||||||
|
<Typography variant="caption" sx={{ fontSize: '0.65rem' }}>
|
||||||
|
Belegt
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(7, 1fr)',
|
||||||
|
gap: 0.4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((d) => (
|
||||||
|
<Box
|
||||||
|
key={d}
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '0.6rem',
|
||||||
|
color: 'text.secondary',
|
||||||
|
fontWeight: 700,
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{d}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{days.map((d) => {
|
||||||
|
const isReserved = reserved.has(d);
|
||||||
|
const isToday = d === today;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={d}
|
||||||
|
sx={{
|
||||||
|
aspectRatio: '1',
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: 1,
|
||||||
|
bgcolor: isToday
|
||||||
|
? 'primary.main'
|
||||||
|
: isReserved
|
||||||
|
? 'rgba(245,158,11,0.18)'
|
||||||
|
: 'transparent',
|
||||||
|
color: isToday ? 'white' : isReserved ? '#B45309' : 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{d}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdminVisual() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 1.25 }}>
|
||||||
|
{[
|
||||||
|
{ label: 'Werkzeuge', value: '3', accent: '#FF6B00' },
|
||||||
|
{ label: 'Aktive Reservierungen', value: '2', accent: '#3B82F6' },
|
||||||
|
{ label: 'Offene Anfragen', value: '1', accent: '#F59E0B' },
|
||||||
|
{ label: 'Nutzer', value: '2', accent: '#22C55E' },
|
||||||
|
].map((s) => (
|
||||||
|
<Paper
|
||||||
|
key={s.label}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2.5,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: s.accent,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
|
||||||
|
{s.label}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 800, letterSpacing: '-0.02em' }}>
|
||||||
|
{s.value}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user