Compare commits

..

2 Commits

Author SHA1 Message Date
seb
bac2d9a202 u 2026-10-05 15:56:41 +02:00
seb
81620521e7 u 2026-10-05 15:11:49 +02:00
6 changed files with 253 additions and 208 deletions

View File

@@ -3,11 +3,87 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>QuixPOS * Management Interface</title>
<!-- Primary Meta Tags -->
<meta name="title" content="QuixPOS * Management Interface" />
<meta name="description"
content="QuixPOS * Point-of-Sale- und Warenwirtschaftssystem. Schnelle Kassenabwicklung, intelligente Artikelverwaltung, Aufträge und Bestandsführung." />
<meta name="keywords"
content="QuixPOS, POS, Kassensystem, Warenwirtschaft, ERP, Artikelverwaltung, Lagerverwaltung, Kasse, Point of Sale" />
<meta name="author" content="QuixPOS" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#1d4ed8" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
<meta name="theme-color" content="#1d4ed8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Dashfront</title>
<!-- Open Graph / Facebook / LinkedIn / WhatsApp -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="QuixPOS" />
<meta property="og:title" content="QuixPOS * Management Interface" />
<meta property="og:description"
content="Webbasiertes Point-of-Sale- und Warenwirtschaftssystem. Schnelle Kassenabwicklung, intelligente Artikelverwaltung und Bestandsführung." />
<meta property="og:image" content="/og-image.png" />
<meta property="og:image:secure_url" content="/og-image.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="QuixPOS * Point-of-Sale & Warenwirtschaftssystem" />
<meta property="og:locale" content="de_DE" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="QuixPOS * Management Interface" />
<meta name="twitter:description"
content="Webbasiertes Point-of-Sale- und Warenwirtschaftssystem. Schnelle Kassenabwicklung, intelligente Artikelverwaltung und Bestandsführung." />
<meta name="twitter:image" content="/og-image.png" />
<meta name="twitter:image:alt" content="QuixPOS * Point-of-Sale & Warenwirtschaftssystem" />
<!-- Structured Data: JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
"@id": "#webapp",
"name": "QuixPOS",
"alternateName": "QuixPOS Management Interface",
"applicationCategory": "BusinessApplication",
"operatingSystem": "All",
"browserRequirements": "Requires modern JavaScript browser (Chrome, Firefox, Safari, Edge)",
"description": "Webbasiertes Point-of-Sale- und Warenwirtschaftssystem. Schnelle Kassenabwicklung, intelligente Artikelverwaltung, Aufträge und Bestandsführung.",
"image": "/og-image.png",
"screenshot": "/og-image.png",
"softwareVersion": "1.0",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR"
},
"featureList": [
"Point of Sale Kassenbetrieb",
"Artikel- & Barcodeverwaltung",
"Lager- & Lieferantenmanagement",
"Angebote & Bestellwesen",
"Kundenverwaltung & Preisgruppen"
]
},
{
"@type": "Organization",
"@id": "#organization",
"name": "QuixPOS",
"logo": "/quixpos-logo.png",
"image": "/og-image.png",
"description": "Point-of-Sale & Warenwirtschaftssystem"
}
]
}
</script>
<style>
/* First-paint colors. Mirrored from src/colors.js (MAIN_BACKGROUND /
THEME_COLOR) because this file must paint before the JS bundle loads.
@@ -32,6 +108,17 @@
</head>
<body>
<noscript>
<div
style="font-family:system-ui,-apple-system,sans-serif;max-width:440px;margin:60px auto;padding:32px 24px;text-align:center;background:#ffffff;border-radius:16px;box-shadow:0 8px 30px rgba(0,0,0,0.12);color:#1e293b;">
<img src="/quixpos-logo.png" alt="QuixPOS Logo" width="96" height="96"
style="border-radius:16px;margin-bottom:16px;box-shadow:0 4px 12px rgba(0,0,0,0.08);" />
<h1 style="margin:0 0 6px;font-size:26px;font-weight:700;color:#0f2f35;">QuixPOS</h1>
<p style="margin:0 0 12px;font-size:16px;font-weight:600;color:#0f766e;">Management Interface</p>
<p style="margin:0;font-size:13px;color:#94a3b8;">Bitte aktivieren Sie JavaScript, um das Management Interface zu
nutzen.</p>
</div>
</noscript>
<div id="root"></div>
<script>
(function () {

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
public/quixpos-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -29,6 +29,7 @@ class LoginPage extends PureComponent {
}
componentDidMount() {
document.title = 'QuixPOS * Management Interface Login';
this.unsubscribe = authAutomat.subscribe(this, ['status', 'googleClientId']);
}
@@ -65,9 +66,9 @@ class LoginPage extends PureComponent {
href: window.location.href,
dashHost: window.__dashHost
? {
isDashNative: window.__dashHost.isDashNative,
googleSignIn: window.__dashHost.googleSignIn,
}
isDashNative: window.__dashHost.isDashNative,
googleSignIn: window.__dashHost.googleSignIn,
}
: null,
error: err,
});

View File

@@ -254,20 +254,11 @@ export default class ArticleSupplierTab extends PureComponent {
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
justifyContent: 'flex-end',
alignItems: 'center',
mb: 2,
}}
>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Zugeordnete Lieferanten ({assignedSuppliers.length})
</Typography>
<Typography variant="caption" color="text.secondary">
Bezugsquellen, Lieferanten-Artikelnr. und Konditionen für diesen Artikel
</Typography>
</Box>
<Button
size="small"
variant="contained"
@@ -279,200 +270,166 @@ export default class ArticleSupplierTab extends PureComponent {
</Button>
</Box>
{/* Content Table or Empty State */}
{assignedSuppliers.length === 0 ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
py: 6,
px: 2,
borderRadius: 2,
border: `1px dashed ${SORT_GHOST_BG}`,
bgcolor: TINT_FAINT,
textAlign: 'center',
}}
>
<LocalShippingOutlinedIcon sx={{ fontSize: 48, mb: 1.5, color: 'text.secondary', opacity: 0.6 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5 }}>
Noch keine Lieferanten zugewiesen
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 380, mb: 2 }}>
Weise diesem Artikel Lieferanten zu, um Einkaufspreise, VPE und Artikelnummern für Wareneingänge zu hinterlegen.
</Typography>
<Button
variant="outlined"
startIcon={<AddIcon />}
onClick={this.handleOpenAdd}
size="small"
sx={{ textTransform: 'none' }}
>
Jetzt Lieferant zuweisen
</Button>
</Box>
) : (
<Table
size="small"
sx={{
'& td': { verticalAlign: 'middle' },
'& th': { fontWeight: 600, color: 'text.secondary', fontSize: '0.8rem' },
}}
>
<TableHead>
<TableRow>
<TableCell sx={{ width: 60, pl: 1 }}>Reihe</TableCell>
<TableCell>Lieferant</TableCell>
<TableCell>Lief.-Art.-Nr.</TableCell>
<TableCell align="right">VPE (PU)</TableCell>
<TableCell align="right">Lagerbestand</TableCell>
<TableCell align="right">Einkaufspreis</TableCell>
<TableCell align="right" sx={{ width: 100, pr: 1 }}>
Aktionen
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{assignedSuppliers.map((row, index) => {
const isBusy = busyId === row.id;
const netPrice = row.price?.net;
const taxRate = row.price?.taxRate;
{/* Content Table */}
<Table
size="small"
sx={{
'& td': { verticalAlign: 'middle' },
'& th': { fontWeight: 600, color: 'text.secondary', fontSize: '0.8rem' },
}}
>
<TableHead>
<TableRow>
<TableCell sx={{ width: 60, pl: 1 }}>Reihe</TableCell>
<TableCell>Lieferant</TableCell>
<TableCell>Lief.-Art.-Nr.</TableCell>
<TableCell align="right">VPE (PU)</TableCell>
<TableCell align="right">Lagerbestand</TableCell>
<TableCell align="right">Einkaufspreis</TableCell>
<TableCell align="right" sx={{ width: 100, pr: 1 }}>
Aktionen
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{assignedSuppliers.map((row, index) => {
const isBusy = busyId === row.id;
const netPrice = row.price?.net;
const taxRate = row.price?.taxRate;
return (
<TableRow
key={row.id}
hover
sx={{
opacity: isBusy ? 0.5 : 1,
transition: 'opacity 0.2s',
}}
>
{/* Sort Order & Reorder Controls */}
<TableCell sx={{ pl: 1 }}>
<Stack direction="row" spacing={0.5} alignItems="center">
<Typography variant="caption" sx={{ minWidth: 16, fontWeight: 700, color: 'text.secondary' }}>
{index + 1}
return (
<TableRow
key={row.id}
hover
sx={{
opacity: isBusy ? 0.5 : 1,
transition: 'opacity 0.2s',
}}
>
{/* Sort Order & Reorder Controls */}
<TableCell sx={{ pl: 1 }}>
<Stack direction="row" spacing={0.5} alignItems="center">
<Typography variant="caption" sx={{ minWidth: 16, fontWeight: 700, color: 'text.secondary' }}>
{index + 1}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Tooltip title="Nach oben">
<span>
<IconButton
size="small"
disabled={index === 0 || isBusy}
onClick={() => this.handleMove(index, -1)}
sx={{ p: 0.2 }}
>
<ArrowUpwardIcon sx={{ fontSize: 13 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten">
<span>
<IconButton
size="small"
disabled={index === assignedSuppliers.length - 1 || isBusy}
onClick={() => this.handleMove(index, 1)}
sx={{ p: 0.2 }}
>
<ArrowDownwardIcon sx={{ fontSize: 13 }} />
</IconButton>
</span>
</Tooltip>
</Box>
</Stack>
</TableCell>
{/* Supplier Name */}
<TableCell sx={{ fontWeight: 600 }}>
{row.supplierName ?? `Lieferant #${row.entityId}`}
</TableCell>
{/* Supplier SKU */}
<TableCell>
{row.sku ? (
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 500 }}>
{row.sku}
</Typography>
) : (
<Typography variant="body2" color="text.secondary">
—
</Typography>
)}
</TableCell>
{/* PU / VPE */}
<TableCell align="right">
<Typography variant="body2">{row.pu ?? 1}</Typography>
</TableCell>
{/* Stock */}
<TableCell align="right">
{row.stock != null ? (
<Chip
size="small"
label={`${row.stock} Stk`}
color={row.stock > 0 ? 'success' : 'default'}
variant="outlined"
sx={{ fontWeight: 500, height: 22, fontSize: '0.75rem' }}
/>
) : (
<Typography variant="body2" color="text.secondary">
—
</Typography>
)}
</TableCell>
{/* Purchase Price Net & Tax */}
<TableCell align="right">
{netPrice != null ? (
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{formatPrice(netPrice)}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Tooltip title="Nach oben">
<span>
<IconButton
size="small"
disabled={index === 0 || isBusy}
onClick={() => this.handleMove(index, -1)}
sx={{ p: 0.2 }}
>
<ArrowUpwardIcon sx={{ fontSize: 13 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten">
<span>
<IconButton
size="small"
disabled={index === assignedSuppliers.length - 1 || isBusy}
onClick={() => this.handleMove(index, 1)}
sx={{ p: 0.2 }}
>
<ArrowDownwardIcon sx={{ fontSize: 13 }} />
</IconButton>
</span>
</Tooltip>
</Box>
</Stack>
</TableCell>
{/* Supplier Name */}
<TableCell sx={{ fontWeight: 600 }}>
{row.supplierName ?? `Lieferant #${row.entityId}`}
</TableCell>
{/* Supplier SKU */}
<TableCell>
{row.sku ? (
<Typography variant="body2" sx={{ fontFamily: 'monospace', fontWeight: 500 }}>
{row.sku}
</Typography>
) : (
<Typography variant="body2" color="text.secondary">
—
</Typography>
)}
</TableCell>
{/* PU / VPE */}
<TableCell align="right">
<Typography variant="body2">{row.pu ?? 1}</Typography>
</TableCell>
{/* Stock */}
<TableCell align="right">
{row.stock != null ? (
<Chip
size="small"
label={`${row.stock} Stk`}
color={row.stock > 0 ? 'success' : 'default'}
variant="outlined"
sx={{ fontWeight: 500, height: 22, fontSize: '0.75rem' }}
/>
) : (
<Typography variant="body2" color="text.secondary">
—
</Typography>
)}
</TableCell>
{/* Purchase Price Net & Tax */}
<TableCell align="right">
{netPrice != null ? (
<Box>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{formatPrice(netPrice)}
{taxRate != null ? (
<Typography variant="caption" color="text.secondary">
zzgl. {taxRate} % MwSt.
</Typography>
{taxRate != null ? (
<Typography variant="caption" color="text.secondary">
zzgl. {taxRate} % MwSt.
</Typography>
) : null}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
—
</Typography>
)}
</TableCell>
) : null}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
—
</Typography>
)}
</TableCell>
{/* Actions */}
<TableCell align="right" sx={{ pr: 1 }}>
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
<Tooltip title="Bearbeiten">
<IconButton
size="small"
onClick={() => this.handleOpenEdit(row)}
disabled={isBusy}
>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Entfernen">
<IconButton
size="small"
color="error"
onClick={() => this.handleDelete(row)}
disabled={isBusy}
>
<DeleteOutlineOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
)}
{/* Actions */}
<TableCell align="right" sx={{ pr: 1 }}>
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
<Tooltip title="Bearbeiten">
<IconButton
size="small"
onClick={() => this.handleOpenEdit(row)}
disabled={isBusy}
>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Entfernen">
<IconButton
size="small"
color="error"
onClick={() => this.handleDelete(row)}
disabled={isBusy}
>
<DeleteOutlineOutlinedIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{/* Add / Edit Dialog */}
<ArticleSupplierDialog

View File

@@ -184,8 +184,8 @@ function nginxHeadersPreviewPlugin() {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
} else if (/^\/(manifest\.webmanifest|favicon\.svg|icons\/.*)$/.test(url)) {
// 1 hour for web manifest & icons
} else if (/^\/(manifest\.webmanifest|favicon\.svg|icons\/.*|og-image\.png|quixpos-logo\.png)$/.test(url)) {
// 1 hour for web manifest & icons / social share assets
res.setHeader('Cache-Control', 'public, max-age=3600');
res.setHeader('Vary', 'Accept-Encoding');
} else if (url.startsWith('/assets/')) {