Darkmode verbessert

This commit is contained in:
mathusan
2025-06-07 18:34:52 +02:00
parent c64ea0ef66
commit 3b184acc51
16 changed files with 633 additions and 349 deletions

View File

@@ -1,20 +1,13 @@
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Menu from '@mui/material/Menu';
import {
AppBar, Box, Toolbar, IconButton, Typography, Menu,
Container, Avatar, Button, Tooltip, MenuItem, InputBase, alpha, styled
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import Tooltip from '@mui/material/Tooltip';
import MenuItem from '@mui/material/MenuItem';
import AdbIcon from '@mui/icons-material/Adb';
import { alpha, InputBase, styled } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
const pages = ['Categories', 'Checkout', 'Contact'];
const settings = ['Account', 'Orders', 'Logout'];
@@ -23,18 +16,21 @@ const Search = styled('div')(({ theme }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: alpha(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
},
border: '1px solid black', // ✅ immer schwarz
boxShadow: '0 0 4px rgba(0,0,0,0.2)',
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(1),
width: 'auto',
marginLeft: theme.spacing(1),
width: 'auto',
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
'&:hover': {
backgroundColor: alpha(theme.palette.common.white, 0.25),
borderColor: 'black', // ✅ auch beim Hover
},
}));
const SearchIconWrapper = styled('div')(({ theme }) => ({
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
@@ -42,26 +38,31 @@ const Search = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: 'inherit',
}));
const StyledInputBase = styled(InputBase)(({ theme }) => ({
color: theme.palette.text.primary,
width: '100%',
'& .MuiInputBase-input': {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
padding: theme.spacing(1, 1, 1, 0),
paddingLeft: `calc(1em + ${theme.spacing(4)})`,
transition: theme.transitions.create('width'),
color: theme.palette.text.primary,
'&::placeholder': {
color: theme.palette.text.primary,
opacity: 0.7,
},
[theme.breakpoints.up('sm')]: {
width: '12ch',
'&:focus': {
width: '20ch',
},
},
},
},
}));
}));
export default function NavBar() {
const theme = useTheme();
const navigate = useNavigate();
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(null);
@@ -72,19 +73,16 @@ export default function NavBar() {
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget);
};
const handleCloseNavMenu = (link: string) => {
setAnchorElNav(null);
navigate(`/${link.toLowerCase()}`);
};
const handleCloseUserMenu = () => {
setAnchorElUser(null);
};
return (
<AppBar>
<AppBar position="static" color="primary">
<Container maxWidth="xl">
<Toolbar disableGutters>
<AdbIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
@@ -108,10 +106,10 @@ export default function NavBar() {
<Search>
<SearchIconWrapper>
<SearchIcon />
<SearchIcon sx={{ color: theme.palette.text.primary }} />
</SearchIconWrapper>
<StyledInputBase
placeholder="Search…"
placeholder="Suchen…"
inputProps={{ 'aria-label': 'search' }}
/>
</Search>
@@ -130,26 +128,21 @@ export default function NavBar() {
<Menu
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
open={Boolean(anchorElNav)}
onClose={handleCloseNavMenu}
onClose={() => setAnchorElNav(null)}
sx={{ display: { xs: 'block', md: 'none' } }}
>
{pages.map((page) => (
<MenuItem key={page} onClick={() => handleCloseNavMenu(page)}>
<Typography sx={{ textAlign: 'center' }}>{page}</Typography>
<Typography textAlign="center">{page}</Typography>
</MenuItem>
))}
</Menu>
</Box>
<AdbIcon sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
<Typography
variant="h5"
@@ -169,6 +162,7 @@ export default function NavBar() {
>
DPS
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map((page) => (
<Button
@@ -180,31 +174,26 @@ export default function NavBar() {
</Button>
))}
</Box>
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt="Florian Speicher" src="/static/images/avatar/2.jpg" />
<Avatar alt="User" src="/static/images/avatar/2.jpg" />
</IconButton>
</Tooltip>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
{settings.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography sx={{ textAlign: 'center' }}>{setting}</Typography>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
))}
</Menu>
@@ -213,4 +202,4 @@ export default function NavBar() {
</Container>
</AppBar>
);
}
}

View File

@@ -13,6 +13,7 @@ import {
TableRow,
TextField,
Typography,
useTheme
} from "@mui/material";
import { useState } from "react";
import AccountType from "../../components/Account";
@@ -24,6 +25,8 @@ const mockAccounts: AccountType[] = [
];
export default function AccountsInfo() {
const theme = useTheme();
const [accounts, setAccounts] = useState<AccountType[]>(mockAccounts);
const [searchTerm, setSearchTerm] = useState<string>("");
const [editMode, setEditMode] = useState<{ [key: string]: boolean }>({});
@@ -33,31 +36,34 @@ export default function AccountsInfo() {
};
const handleDelete = (id: string) => {
setAccounts((prevAccounts) => prevAccounts.filter((account) => account.id !== id));
setAccounts((prev) => prev.filter((account) => account.id !== id));
};
const handleToggleStatus = (id: string) => {
setAccounts((prevAccounts) =>
prevAccounts.map((account) =>
setAccounts((prev) =>
prev.map((account) =>
account.id === id
? { ...account, status: account.status === "active" ? "inactive" : "active" }
? {
...account,
status: account.status === "active" ? "inactive" : "active"
}
: account
)
);
};
const handleEdit = (id: string, field: keyof AccountType, value: string) => {
setAccounts((prevAccounts) =>
prevAccounts.map((account) =>
setAccounts((prev) =>
prev.map((account) =>
account.id === id ? { ...account, [field]: value } : account
)
);
};
const toggleEditMode = (id: string) => {
setEditMode((prevEditMode) => ({
...prevEditMode,
[id]: !prevEditMode[id],
setEditMode((prev) => ({
...prev,
[id]: !prev[id]
}));
};
@@ -68,28 +74,60 @@ export default function AccountsInfo() {
);
return (
<Box>
<Box sx={{ color: theme.palette.text.primary }}>
<Typography variant="h4" gutterBottom align="center">
Accounts Management
</Typography>
<TextField
label="Search Accounts"
variant="outlined"
fullWidth
sx={{ mb: 2 }}
sx={{
mb: 2,
backgroundColor: theme.palette.background.paper,
'& input': {
color: theme.palette.text.primary
},
'& label': {
color: theme.palette.text.secondary
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: theme.palette.divider
},
'&:hover fieldset': {
borderColor: theme.palette.text.primary
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main
}
}
}}
onChange={handleSearch}
/>
<TableContainer component={Paper}>
<TableContainer
component={Paper}
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary
}}
>
<Table>
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>Name</TableCell>
<TableCell>Email</TableCell>
<TableCell>Status</TableCell>
<TableCell align="center">Actions</TableCell>
{["ID", "Name", "Email", "Status", "Actions"].map((col) => (
<TableCell
key={col}
sx={{ color: theme.palette.text.primary, fontWeight: "bold" }}
>
{col}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{filteredAccounts.map((account) => (
<TableRow key={account.id}>
@@ -98,10 +136,11 @@ export default function AccountsInfo() {
{editMode[account.id] ? (
<TextField
value={account.name}
onChange={(e) =>
handleEdit(account.id, "name", e.target.value)
}
onChange={(e) => handleEdit(account.id, "name", e.target.value)}
variant="standard"
InputProps={{
sx: { color: theme.palette.text.primary }
}}
/>
) : (
account.name
@@ -111,10 +150,11 @@ export default function AccountsInfo() {
{editMode[account.id] ? (
<TextField
value={account.email}
onChange={(e) =>
handleEdit(account.id, "email", e.target.value)
}
onChange={(e) => handleEdit(account.id, "email", e.target.value)}
variant="standard"
InputProps={{
sx: { color: theme.palette.text.primary }
}}
/>
) : (
account.email
@@ -122,27 +162,18 @@ export default function AccountsInfo() {
</TableCell>
<TableCell>{account.status}</TableCell>
<TableCell align="center">
<IconButton
color="error"
onClick={() => handleDelete(account.id)}
>
<IconButton color="error" onClick={() => handleDelete(account.id)}>
<DeleteIcon />
</IconButton>
<Button
variant="contained"
color={
account.status === "active" ? "warning" : "success"
}
color={account.status === "active" ? "warning" : "success"}
onClick={() => handleToggleStatus(account.id)}
sx={{ mx: 1 }}
>
{account.status === "active"
? "Set Inactive"
: "Set Active"}
{account.status === "active" ? "Set Inactive" : "Set Active"}
</Button>
<IconButton
color="primary"
onClick={() => toggleEditMode(account.id)}
>
<IconButton color="primary" onClick={() => toggleEditMode(account.id)}>
<EditIcon />
</IconButton>
</TableCell>
@@ -153,4 +184,4 @@ export default function AccountsInfo() {
</TableContainer>
</Box>
);
}
}

View File

@@ -1,13 +1,15 @@
import { useDroppable } from "@dnd-kit/core";
import { Box } from "@mui/material";
import { Box, useTheme } from "@mui/material";
import { ReactNode } from "react";
type DroppableContainerProps = {
id: string;
children: React.ReactNode;
children: ReactNode;
};
export function DroppableContainer({ id, children }: DroppableContainerProps) {
const { setNodeRef } = useDroppable({ id });
const theme = useTheme();
return (
<Box
@@ -15,13 +17,14 @@ export function DroppableContainer({ id, children }: DroppableContainerProps) {
sx={{
flex: 1,
minHeight: 400,
bgcolor: "background.default",
border: "1px solid #ccc",
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
p: 2,
transition: "background-color 0.3s, border-color 0.3s",
}}
>
{children}
</Box>
);
}
}

View File

@@ -1,10 +1,27 @@
import { Typography } from "@mui/material";
import { Typography, Box, useTheme } from "@mui/material";
export default function ItemInfo() {
const theme = useTheme();
return (
<Typography >
Under construction...
</Typography>
<Box
sx={{
minHeight: 200,
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
borderRadius: 2,
boxShadow: 2,
mt: 4,
p: 4,
textAlign: "center",
}}
>
<Typography variant="h6">
🚧 Under construction...
</Typography>
</Box>
);
}
}

View File

@@ -1,10 +1,18 @@
import { closestCenter, DndContext } from "@dnd-kit/core";
import { restrictToParentElement } from "@dnd-kit/modifiers";
import { Box, Button, Dialog, DialogContent, DialogTitle, Typography } from "@mui/material";
import {
Box,
Button,
Dialog,
DialogContent,
DialogTitle,
Typography,
useTheme
} from "@mui/material";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { DroppableContainer } from "./DroppableContainer.tsx"; // Hilfskomponente für Droppable-Bereiche
import SortableItem from "./SortableItem.tsx"; // Hilfskomponente für Sortable Items
import { DroppableContainer } from "./DroppableContainer";
import SortableItem from "./SortableItem";
import OrderType from "../../components/Order";
const mockOrders: OrderType[] = [
@@ -14,10 +22,10 @@ const mockOrders: OrderType[] = [
status: "active",
items: [
{ name: "Tomatensamen", quantity: 2, price: 3.99 },
{ name: "Blumenerde", quantity: 1, price: 7.49 },
{ name: "Blumenerde", quantity: 1, price: 7.49 }
],
total: 15.47,
address: "Musterstraße 1, 12345 Musterstadt",
address: "Musterstraße 1, 12345 Musterstadt"
},
{
id: "1000",
@@ -25,7 +33,7 @@ const mockOrders: OrderType[] = [
status: "inactive",
items: [{ name: "Gießkanne", quantity: 1, price: 12.99 }],
total: 12.99,
address: "Musterstraße 1, 12345 Musterstadt",
address: "Musterstraße 1, 12345 Musterstadt"
},
{
id: "1002",
@@ -33,7 +41,7 @@ const mockOrders: OrderType[] = [
status: "running",
items: [{ name: "Pflanzendünger", quantity: 1, price: 8.99 }],
total: 8.99,
address: "Musterstraße 1, 12345 Musterstadt",
address: "Musterstraße 1, 12345 Musterstadt"
},
{
id: "1003",
@@ -41,25 +49,25 @@ const mockOrders: OrderType[] = [
status: "cancelled",
items: [{ name: "Blumentopf", quantity: 2, price: 5.99 }],
total: 11.98,
address: "Musterstraße 1, 12345 Musterstadt",
},
address: "Musterstraße 1, 12345 Musterstadt"
}
];
const statusOrder = ["active", "running", "inactive", "cancelled"];
export default function OrdersInfo() {
const { t } = useTranslation();
const theme = useTheme();
const [orders, setOrders] = useState<OrderType[]>(mockOrders);
const [selectedOrder, setSelectedOrder] = useState<OrderType | null>(null);
const handleDragEnd = (event: any) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const newStatus = over.id; // Zielspalte (status)
setOrders((prevOrders) =>
prevOrders.map((order) =>
const newStatus = over.id;
setOrders((prev) =>
prev.map((order) =>
order.id === active.id ? { ...order, status: newStatus } : order
)
);
@@ -68,10 +76,10 @@ export default function OrdersInfo() {
const handleNextStatus = (order: OrderType) => {
const currentIndex = statusOrder.indexOf(order.status);
if (currentIndex < statusOrder.length - 1) {
setOrders((prevOrders) =>
prevOrders.map((o) =>
setOrders((prev) =>
prev.map((o) =>
o.id === order.id
? { ...o, status: statusOrder[currentIndex + 1] as OrderType['status'] }
? { ...o, status: statusOrder[currentIndex + 1] as OrderType["status"] }
: o
)
);
@@ -79,22 +87,27 @@ export default function OrdersInfo() {
};
const renderOrders = (status: string) => {
const filteredOrders = orders.filter((order) => order.status === status);
const filtered = orders.filter((o) => o.status === status);
return (
<Box
sx={{
minHeight: 300,
p: 2,
bgcolor: "background.paper",
border: "1px solid #ccc",
borderRadius: 2,
bgcolor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2
}}
>
<Typography variant="h6" align="center" gutterBottom>
<Typography
variant="h6"
align="center"
gutterBottom
sx={{ color: theme.palette.text.primary }}
>
{t(status)}
</Typography>
{filteredOrders.map((order) => (
{filtered.map((order) => (
<SortableItem
key={order.id}
id={order.id}
@@ -111,6 +124,7 @@ export default function OrdersInfo() {
<Typography variant="h4" align="center" gutterBottom>
{t("Orders Management")}
</Typography>
<DndContext
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
@@ -125,12 +139,11 @@ export default function OrdersInfo() {
</Box>
</DndContext>
{/* Dialog für Bestelldetails */}
<Dialog open={!!selectedOrder} onClose={() => setSelectedOrder(null)}>
<DialogTitle>Order Details</DialogTitle>
<DialogContent>
<DialogContent sx={{ bgcolor: theme.palette.background.paper }}>
{selectedOrder && (
<Box>
<Box sx={{ color: theme.palette.text.primary }}>
<Typography variant="body1">
<strong>Order ID:</strong> {selectedOrder.id}
</Typography>
@@ -143,9 +156,9 @@ export default function OrdersInfo() {
<Typography variant="body1" sx={{ mt: 2 }}>
<strong>Items:</strong>
</Typography>
{selectedOrder.items.map((item, index) => (
<Typography key={index} variant="body2">
{item.quantity}x {item.name} - {item.price.toFixed(2)}
{selectedOrder.items.map((item, idx) => (
<Typography key={idx} variant="body2">
{item.quantity}x {item.name} {item.price.toFixed(2)}
</Typography>
))}
<Typography variant="body1" sx={{ mt: 2 }}>
@@ -168,4 +181,4 @@ export default function OrdersInfo() {
</Dialog>
</Box>
);
}
}

View File

@@ -1,6 +1,6 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Paper, Typography } from "@mui/material";
import { Paper, Typography, useTheme } from "@mui/material";
type SortableItemProps = {
id: string;
@@ -13,6 +13,7 @@ type SortableItemProps = {
export default function SortableItem({ id, order, onClick }: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const theme = useTheme();
const style = {
transform: CSS.Transform.toString(transform),
@@ -26,18 +27,22 @@ export default function SortableItem({ id, order, onClick }: SortableItemProps)
style={style}
{...attributes}
{...listeners}
onClick={onClick}
sx={{
p: 2,
mb: 2,
cursor: "pointer",
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.04)",
backgroundColor:
theme.palette.mode === "dark"
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.04)",
},
}}
onClick={onClick}
>
<Typography variant="body1">Order ID: {order.id}</Typography>
<Typography variant="body2">Total: {order.total.toFixed(2)} </Typography>
</Paper>
);
}
}

View File

@@ -1,4 +1,4 @@
import { Box, Typography } from "@mui/material";
import { Box, Typography, useTheme } from "@mui/material";
import {
Chart as ChartJS,
CategoryScale,
@@ -26,76 +26,123 @@ ChartJS.register(
PointElement
);
// Fake-Daten
const weeklySalesData = {
labels: ["Week 1", "Week 2", "Week 3", "Week 4"],
datasets: [
{
label: "Weekly Sales (€)",
data: [1200, 2100, 800, 1600],
backgroundColor: "rgba(75, 192, 192, 0.5)",
borderColor: "rgba(75, 192, 192, 1)",
borderWidth: 1,
},
],
};
const itemSalesData = {
labels: ["Tomatensamen", "Blumenerde", "Gießkanne", "Pflanzendünger"],
datasets: [
{
label: "Item Sales",
data: [400, 300, 200, 100],
backgroundColor: ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
hoverBackgroundColor: ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
},
],
};
const userSalesData = {
labels: ["John Doe", "Jane Smith", "Alice Johnson", "Bob Brown"],
datasets: [
{
label: "Sales by User",
data: [5, 8, 3, 6],
fill: false,
borderColor: "rgba(153, 102, 255, 1)",
backgroundColor: "rgba(153, 102, 255, 0.5)",
tension: 0.1,
},
],
};
export default function StatisticsInfo() {
const theme = useTheme();
const weeklySalesData = {
labels: ["Week 1", "Week 2", "Week 3", "Week 4"],
datasets: [
{
label: "Weekly Sales (€)",
data: [1200, 2100, 800, 1600],
backgroundColor: theme.palette.mode === "dark"
? "rgba(0, 230, 255, 0.5)"
: "rgba(75, 192, 192, 0.5)",
borderColor: theme.palette.mode === "dark"
? "rgba(0, 230, 255, 1)"
: "rgba(75, 192, 192, 1)",
borderWidth: 1,
},
],
};
const itemSalesData = {
labels: ["Tomatensamen", "Blumenerde", "Gießkanne", "Pflanzendünger"],
datasets: [
{
label: "Item Sales",
data: [400, 300, 200, 100],
backgroundColor: [
theme.palette.warning.main,
theme.palette.info.main,
theme.palette.success.main,
theme.palette.secondary.main,
],
hoverBackgroundColor: [
theme.palette.warning.light,
theme.palette.info.light,
theme.palette.success.light,
theme.palette.secondary.light,
],
},
],
};
const userSalesData = {
labels: ["John Doe", "Jane Smith", "Alice Johnson", "Bob Brown"],
datasets: [
{
label: "Sales by User",
data: [5, 8, 3, 6],
fill: false,
borderColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.light,
tension: 0.1,
},
],
};
const baseOptions = {
responsive: true,
plugins: {
legend: {
position: "top" as const,
labels: {
color: theme.palette.text.primary,
},
},
title: {
display: false,
},
},
scales: {
x: {
ticks: { color: theme.palette.text.primary },
grid: {
color: theme.palette.divider,
},
},
y: {
ticks: { color: theme.palette.text.primary },
grid: {
color: theme.palette.divider,
},
},
},
};
return (
<Box>
<Typography variant="h4" align="center" gutterBottom sx={{ color: "text.primary" }}>
<Box sx={{ color: theme.palette.text.primary }}>
<Typography variant="h4" align="center" gutterBottom>
Sales Statistics
</Typography>
{/* Gesamtverkaufszahlen pro Woche */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" align="center" gutterBottom>
Weekly Sales
</Typography>
<Bar data={weeklySalesData} options={{ responsive: true, plugins: { legend: { position: "top" } } }} />
<Bar data={weeklySalesData} options={baseOptions} />
</Box>
{/* Verkaufsanzahl der einzelnen Items */}
<Box sx={{ mb: 4 }}>
<Typography variant="h6" align="center" gutterBottom>
Item Sales Distribution
</Typography>
<Pie data={itemSalesData} options={{ responsive: true, plugins: { legend: { position: "top" } } }} />
<Pie
data={itemSalesData}
options={{
...baseOptions,
scales: undefined, // Pie braucht keine Achsen
}}
/>
</Box>
{/* Verkäufe pro Benutzer */}
<Box>
<Typography variant="h6" align="center" gutterBottom>
Sales by User
</Typography>
<Line data={userSalesData} options={{ responsive: true, plugins: { legend: { position: "top" } } }} />
<Line data={userSalesData} options={baseOptions} />
</Box>
</Box>
);
}
}

View File

@@ -1,4 +1,12 @@
import { FormControl, FormControlLabel, Radio, RadioGroup, Rating } from "@mui/material";
import {
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
Rating,
useTheme,
} from "@mui/material";
import React from "react";
type FilterItemOption = {
@@ -14,11 +22,13 @@ type FilterItemProps = {
};
export default function FilterItem({
filterName,
filterItems,
value,
onChange
}: FilterItemProps) {
filterName,
filterItems,
value,
onChange,
}: FilterItemProps) {
const theme = useTheme();
if (!value && filterItems.length > 0) {
value = filterItems[0].value;
}
@@ -30,8 +40,18 @@ export default function FilterItem({
};
return (
<div>
<h3>{filterName}</h3>
<div style={{ marginBottom: "1.5rem" }}>
<FormLabel
component="legend"
sx={{
fontWeight: "bold",
color: theme.palette.text.primary,
mb: 1,
}}
>
{filterName}
</FormLabel>
<FormControl>
<RadioGroup value={value} onChange={handleChange}>
{filterItems.map((item, idx) => (
@@ -41,15 +61,23 @@ export default function FilterItem({
control={<Radio />}
label={
/^[1-5]$/.test(item.value) ? (
<Rating readOnly value={Number(item.value)} precision={1} size="small" />
<Rating
readOnly
value={Number(item.value)}
precision={1}
size="small"
/>
) : (
item.label
)
}
sx={{
color: theme.palette.text.primary,
}}
/>
))}
</RadioGroup>
</FormControl>
</div>
);
}
}

View File

@@ -1,4 +1,4 @@
import { Slider, Typography, Box } from "@mui/material";
import { Slider, Typography, Box, useTheme } from "@mui/material";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
@@ -10,12 +10,13 @@ type PriceSliderProps = {
export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSliderProps) {
const { t } = useTranslation();
const theme = useTheme();
const [value, setValue] = useState<[number, number]>([min, max]);
// Synchronisiere den Zustand nur, wenn sich die Props ändern
useEffect(() => {
setValue([min, max]);
onChange?.([min, max]); // Initiale Werte an die übergeordnete Komponente übergeben
onChange?.([min, max]);
}, [min, max]);
const handleChange = (_: Event, newValue: number | number[]) => {
@@ -26,32 +27,55 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
const handleCommitted = (_: Event, newValue: number | number[]) => {
if (Array.isArray(newValue)) {
onChange?.([newValue[0], newValue[1]]); // Übergebe die neuen Werte an die übergeordnete Komponente
onChange?.([newValue[0], newValue[1]]);
}
};
const formatValueToEuro = (value: number) => {
return `${(value / 100).toFixed(2)}`; // Umrechnung von Cent in Euro
};
const formatValueToEuro = (val: number) => `${val.toFixed(2)}`;
return (
<div>
<h3>{t('price')}</h3>
<Box sx={{ pl: 1, pr: 1 }}>
<Box sx={{ mb: 4 }}>
<Typography
variant="h6"
sx={{
fontWeight: "bold",
color: theme.palette.text.primary,
mb: 1,
}}
>
{t("price")}
</Typography>
<Box sx={{ px: 1 }}>
<Slider
value={value}
onChange={handleChange}
onChangeCommitted={handleCommitted}
valueLabelDisplay="auto"
valueLabelFormat={formatValueToEuro} // Formatierung der Werte in Euro
valueLabelFormat={formatValueToEuro}
min={min}
max={max}
step={1}
sx={{
color: "#0fd13f",
'& .MuiSlider-valueLabel': {
color: theme.palette.text.primary,
},
}}
/>
<Typography>
{formatValueToEuro(value[0])} - {formatValueToEuro(value[1])}
<Typography
variant="body2"
sx={{
mt: 1,
color: theme.palette.text.primary,
fontSize: "0.9rem",
textAlign: "center",
}}
>
{formatValueToEuro(value[0])} {formatValueToEuro(value[1])}
</Typography>
</Box>
</div>
</Box>
);
}

View File

@@ -76,7 +76,7 @@ export default function NavBar() {
};
return (
<AppBar position="static" color="primary" elevation={4}>
<AppBar position="static" color="primary" elevation={4}>np
<Toolbar
disableGutters
sx={{

View File

@@ -1,25 +1,58 @@
import { Card, CardActionArea, CardContent, Paper, Rating, Typography } from "@mui/material";
import {
Card,
CardActionArea,
CardContent,
Paper,
Rating,
Typography,
useTheme
} from "@mui/material";
import RatingType from "../../components/Rating";
import { useTranslation } from 'react-i18next';
export default function RatingCard(ratingType: RatingType) {
const { t } = useTranslation();
const theme = useTheme(); // Zugriff auf Light/Dark-Mode
const handleClick = () => {
console.log(`Clicked on rating`);
}
};
return (
<Paper elevation={4}>
<Card>
<Paper
elevation={4}
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
}}
>
<Card
sx={{
backgroundColor: theme.palette.background.paper,
}}
>
<CardActionArea onClick={handleClick}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
<Typography
gutterBottom
variant="h6"
component="div"
sx={{ color: theme.palette.text.primary }}
>
{t('ratingFrom')} {new Date(ratingType.timestamp).toLocaleDateString('de-DE')}
</Typography>
<Rating name="half-rating" readOnly defaultValue={ratingType.rating / 2} precision={0.01} />
<Typography variant="body2" sx={{ color: 'text.secondary' }} className="item-description">
<Rating
name="half-rating"
readOnly
defaultValue={ratingType.rating / 2}
precision={0.01}
/>
<Typography
variant="body2"
sx={{ color: theme.palette.text.secondary, mt: 1 }}
>
{ratingType.content}
</Typography>
</CardContent>
@@ -27,4 +60,4 @@ export default function RatingCard(ratingType: RatingType) {
</Card>
</Paper>
);
}
}

View File

@@ -1,5 +1,16 @@
import {
Box,
Button,
Divider,
IconButton,
Rating,
Snackbar,
SnackbarCloseReason,
TextField,
Typography,
useTheme
} from "@mui/material";
import { Close } from "@mui/icons-material";
import { Box, Button, Divider, IconButton, Rating, Snackbar, SnackbarCloseReason, TextField, Typography } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import React, { useMemo, useState } from "react";
import { useTranslation } from 'react-i18next';
@@ -8,48 +19,46 @@ import { fetchRatingList, submitRating } from "../query/Queries";
import RatingCard from "./RatingCard";
import RatingSubmitType from "../../components/RatingSubmit";
export default function Ratings({itemId}: {itemId: string}) {
export default function Ratings({ itemId }: { itemId: string }) {
const { t } = useTranslation();
const theme = useTheme();
const [open, setOpen] = useState<boolean>(false);
const [ratingText, setRatingText] = useState<string>("");
const [ratingValue, setRatingValue] = useState<number | null>(2.5);
const handleClose = (
event: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason,
) => {
if (reason === 'clickaway') {
return;
}
setOpen(false);
};
const ratingData: RatingSubmitType = {
rating: ratingValue || 0,
content: ratingText || "",
articleId: itemId, // Konvertiert itemId in einen String,
articleId: itemId,
};
const { refetch } = useQuery({
queryKey: ["submitRating", ratingData], // Query-Key mit Daten
queryKey: ["submitRating", ratingData],
queryFn: () => submitRating(ratingData),
retry: 3, // Versucht es 3-mal erneut
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
enabled: false, // Deaktiviert die automatische Ausführung
retry: 3,
retryDelay: 1000,
enabled: false,
});
const handleRatingSubmit = () => {
setOpen(true);
refetch(); // Manuelles Auslösen der Abfrage
}
void refetch(); // bewusst ausgelöst, kein await notwendig
};
const handleClose = (
_: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason
) => {
if (reason === "clickaway") return;
setOpen(false);
};
const action = (
<React.Fragment>
<IconButton
size="small"
aria-label={t('close')}
aria-label={t("close")}
color="inherit"
onClick={handleClose}
>
@@ -59,57 +68,99 @@ export default function Ratings({itemId}: {itemId: string}) {
);
const { data = [] } = useQuery<RatingType[]>({
queryKey: ['fetchRatingList', itemId],
queryKey: ["fetchRatingList", itemId],
queryFn: () => fetchRatingList(itemId),
retry: 3, // Versucht es 3-mal erneut
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
retry: 3,
retryDelay: 1000,
});
const ratings:RatingType[] = useMemo(() => data || [], [data]);
const ratings: RatingType[] = useMemo(() => data || [], [data]);
const getRatings = () => {
if (ratings.length === 0)
return <Typography variant="body1" className="no-ratings">{t('noRatingsYet')}</Typography>;
if (ratings.length === 0) {
return (
<Typography variant="body1" sx={{ color: theme.palette.text.secondary }}>
{t("noRatingsYet")}
</Typography>
);
}
return ratings.map((ratingType: RatingType) => (
<RatingCard {...ratingType} />
))
<RatingCard key={ratingType.timestamp} {...ratingType} />
));
};
return (
<>
<div className='contact-divider-box'>
<Divider className='contact-divider' />
</div>
<div className="rating-card-body">
<Typography variant="h5">
{t('rateThisProduct')}:</Typography>
<Rating name="half-rating" value={ratingValue} onChange={(e, value) => setRatingValue(value)} precision={0.5} />
<Divider sx={{ backgroundColor: theme.palette.divider, my: 3 }} />
<Box sx={{ mb: 4 }}>
<Typography variant="h5" sx={{ color: theme.palette.text.primary, mb: 2 }}>
{t("rateThisProduct")}:
</Typography>
<Rating
name="half-rating"
value={ratingValue}
onChange={(_, value) => setRatingValue(value)}
precision={0.5}
/>
<TextField
label={t('review')}
label={t("review")}
multiline
minRows={4}
maxRows={16}
className="rating-text-field"
fullWidth
sx={{
mt: 2,
mb: 2,
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
'& .MuiInputBase-input': {
color: theme.palette.text.primary,
},
'& label': {
color: theme.palette.text.secondary,
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: theme.palette.divider,
},
'&:hover fieldset': {
borderColor: theme.palette.text.primary,
},
'&.Mui-focused fieldset': {
borderColor: theme.palette.primary.main,
},
},
}}
value={ratingText}
onChange={(e) => setRatingText(e.target.value)}
/>
<Button variant="contained" color="primary" className="rating-button" onClick={handleRatingSubmit}>
{t('submit')}
<Button
variant="contained"
color="primary"
onClick={handleRatingSubmit}
>
{t("submit")}
</Button>
</div>
<div className='contact-divider-box'>
<Divider className='contact-divider' />
</div>
<Box className="rating-card-box">
</Box>
<Divider sx={{ backgroundColor: theme.palette.divider, my: 3 }} />
<Box>
{getRatings()}
</Box>
<Snackbar
open={open}
autoHideDuration={3000}
onClose={handleClose}
message="Thanks for your rating!"
message={t("thanksForRating")}
action={action}
/>
</>
);
}
}

View File

@@ -1,5 +1,19 @@
import { AccountCircle, Category, QueryStats, ReceiptLong } from "@mui/icons-material";
import { Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography } from "@mui/material";
import {
AccountCircle,
Category,
QueryStats,
ReceiptLong,
} from "@mui/icons-material";
import {
Box,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
useTheme,
} from "@mui/material";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import AccountsInfo from "../helper/adminpanel/AccountsInfo";
@@ -9,15 +23,14 @@ import ItemInfo from "../helper/adminpanel/ItemsInfo";
export default function AdminPanel() {
const { t } = useTranslation();
const [infoStatus, setInfoStatus] = useState<string>("statistics"); // Standardseite
const theme = useTheme();
const [infoStatus, setInfoStatus] = useState<string>("statistics");
const handleInfoStatus = (path: string) => {
console.log("Button clicked:", path); // Debugging
setInfoStatus(path);
};
const renderContent = () => {
console.log("Rendering content for:", infoStatus); // Debugging
switch (infoStatus) {
case "statistics":
return <StatisticsInfo />;
@@ -28,66 +41,69 @@ export default function AdminPanel() {
case "items":
return <ItemInfo />;
default:
return <StatisticsInfo />; // Fallback, falls kein gültiger Status
return <StatisticsInfo />;
}
};
const menuItems = [
{ key: "statistics", icon: <QueryStats />, label: t("statistics") },
{ key: "orders", icon: <ReceiptLong />, label: t("orders") },
{ key: "accounts", icon: <AccountCircle />, label: t("accounts") },
{ key: "items", icon: <Category />, label: t("items") },
];
return (
<div className="page-container">
<Box className="page-container" sx={{ color: theme.palette.text.primary }}>
<Box>
<Typography variant="h3" align="center" gutterBottom sx={{ color: 'text.primary' }}>
<Typography variant="h3" align="center" gutterBottom>
Admin Panel
</Typography>
<Typography variant="subtitle1" align="center">
Manage your application settings and content here.
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 4 }}>
<Box sx={{ display: "flex", gap: 4, mt: 4 }}>
{/* Sidebar */}
<Box sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper', border: '1px solid red' }}>
<nav aria-label="main mailbox folders">
<Box
sx={{
width: "100%",
maxWidth: 280,
bgcolor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
boxShadow: 1,
}}
>
<nav aria-label="main admin menu">
<List>
<ListItem disablePadding>
<ListItemButton onClick={() => handleInfoStatus("statistics")}>
<ListItemIcon>
<QueryStats />
</ListItemIcon>
<ListItemText primary={t("statistics")} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton onClick={() => handleInfoStatus("orders")}>
<ListItemIcon>
<ReceiptLong />
</ListItemIcon>
<ListItemText primary={t("orders")} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton onClick={() => handleInfoStatus("accounts")}>
<ListItemIcon>
<AccountCircle />
</ListItemIcon>
<ListItemText primary={t("accounts")} />
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton onClick={() => handleInfoStatus("items")}>
<ListItemIcon>
<Category />
</ListItemIcon>
<ListItemText primary={t("items")} />
</ListItemButton>
</ListItem>
{menuItems.map((item) => (
<ListItem key={item.key} disablePadding>
<ListItemButton
selected={infoStatus === item.key}
onClick={() => handleInfoStatus(item.key)}
sx={{
"&.Mui-selected": {
bgcolor: theme.palette.action.selected,
color: theme.palette.primary.main,
},
"&:hover": {
bgcolor: theme.palette.action.hover,
},
}}
>
<ListItemIcon sx={{ color: "inherit" }}>{item.icon}</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</nav>
</Box>
{/* Content */}
<Box sx={{ flexGrow: 1 }}>
{renderContent()}
</Box>
<Box sx={{ flexGrow: 1 }}>{renderContent()}</Box>
</Box>
</div>
</Box>
);
}
}

View File

@@ -9,35 +9,47 @@ import { useLocation, useNavigate } from 'react-router-dom';
import Item from '../components/Item';
import ProductInfo from '../helper/productpage/ProductInfo';
import Ratings from '../helper/productpage/Ratings';
import {useTranslation} from "react-i18next";
import { useTranslation } from "react-i18next";
import { useTheme } from '@mui/material/styles';
export default function Product() {
const { t } = useTranslation();
const location = useLocation();
const item = location.state?.item as Item;
const navigate = useNavigate();
const theme = useTheme(); // 🌗 Zugriff auf das aktive Theme
const handleGoHome = () => {
navigate("/");
};
// Redirect immediately if item is not found
// Wenn kein Produkt vorhanden ist
if (!item) {
return (
<Box className="no-page-container">
<Typography variant="h1" className="no-page-title">
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
textAlign: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
gap: '2rem',
px: 2,
}}
>
<Typography variant="h1">
{t('productNotFound')}
</Typography>
<Typography variant="h5" className="no-page-description">
<Typography variant="h5">
{t('productDoesNotExist')}
</Typography>
<Button
variant="contained"
color="primary"
size="large"
className="no-page-button"
onClick={handleGoHome}
>
{t('backToHome')}
@@ -47,21 +59,37 @@ export default function Product() {
}
return (
<div className='product-page-background'>
<Container maxWidth="lg" sx={{ py: 4 }} >
<ProductInfo item={item}/>
<div className='contact-divider-box'>
<Divider className='contact-divider' />
</div>
<Typography variant="caption" gutterBottom>
<Box
sx={{
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
minHeight: '100vh',
py: 4,
}}
>
<Container maxWidth="lg">
<ProductInfo item={item} />
<Box sx={{ my: 4 }}>
<Divider sx={{ backgroundColor: theme.palette.text.secondary }} />
</Box>
<Typography
variant="body2"
sx={{ color: theme.palette.text.secondary, mb: 1 }}
>
{t('articleNumber')}: {item.uuid}
</Typography>
<Typography variant="body1">
<Typography
variant="body1"
sx={{ color: theme.palette.text.primary, mb: 3 }}
>
{item.description}
</Typography>
<Ratings itemId={item.uuid} />
</Container>
</div>
</Box>
);
};
};

View File

@@ -48,14 +48,13 @@
width: 90%;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
padding: 16px;
margin-right: auto;
margin-left: auto;
margin: 0 auto;
box-sizing: border-box;
}
.page-background {
background-color: var(--background-color);
height: calc(100vh);
height: 100vh;
min-height: 600px;
display: flex;
flex-direction: column;
@@ -130,13 +129,13 @@
scroll-behavior: smooth;
height: 100vh;
box-sizing: border-box;
color: grey;
color: var(--text-color);
}
.filter-container {
width: fit-content;
display: grid;
margin: 20px 30px 20px 30px;
margin: 20px 30px;
place-self: start;
white-space: nowrap;
}
@@ -152,6 +151,6 @@
text-align: center;
font-size: 1rem;
min-width: 600px;
background-color: grey;
color: primary;
background-color: var(--background-color);
color: var(--text-color);
}

View File

@@ -125,7 +125,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: mode === 'dark' ? '#065f24' : '#0fd13f',
backgroundColor: '#0fd13f',
color: '#ffffff',
},
},