u
This commit is contained in:
@@ -84,7 +84,7 @@ flow:
|
|||||||
| **Build & Dev** | Vite 8 (`type: module`) | Dev server on `4209`, HTTPS on `4308`, proxies `/api` → `http://localhost:4208` |
|
| **Build & Dev** | Vite 8 (`type: module`) | Dev server on `4209`, HTTPS on `4308`, proxies `/api` → `http://localhost:4208` |
|
||||||
| **Framework** | React 19 (`react`, `react-dom`) | Uses `PureComponent` (with Automats & dashgui) and modern function components |
|
| **Framework** | React 19 (`react`, `react-dom`) | Uses `PureComponent` (with Automats & dashgui) and modern function components |
|
||||||
| **Routing** | `react-router-dom` v7 | Configured in [src/appRouter.jsx](file:///home/seb/src/dashfront/src/appRouter.jsx) with HMR router preservation |
|
| **Routing** | `react-router-dom` v7 | Configured in [src/appRouter.jsx](file:///home/seb/src/dashfront/src/appRouter.jsx) with HMR router preservation |
|
||||||
| **UI & Theming** | MUI 9 (`@mui/material`, Emotion) | Custom theme in `src/theme/`, base background `#c5ddd8` in `src/colors.js` |
|
| **UI & Theming** | MUI 9 (`@mui/material`, Emotion) | Custom theme in `src/theme/`, base background `#85b7ad` in `src/colors.js` |
|
||||||
| **Shared GUI** | `dashgui` (`file:../dashgui`) | `PanelTile`, `RecordList`, `Tile`, `SingleLineTextEdit`, `ElevatedPill` |
|
| **Shared GUI** | `dashgui` (`file:../dashgui`) | `PanelTile`, `RecordList`, `Tile`, `SingleLineTextEdit`, `ElevatedPill` |
|
||||||
| **State Management** | `react-automat` | Single state layer for all domain & configuration data (articles, categories, company, brands, units, POS terminals, session) |
|
| **State Management** | `react-automat` | Single state layer for all domain & configuration data (articles, categories, company, brands, units, POS terminals, session) |
|
||||||
| **Notifications** | `notistack` | `useSnackbar()` / `<SnackbarProvider>` in [src/main.jsx](file:///home/seb/src/dashfront/src/main.jsx) |
|
| **Notifications** | `notistack` | `useSnackbar()` / `<SnackbarProvider>` in [src/main.jsx](file:///home/seb/src/dashfront/src/main.jsx) |
|
||||||
|
|||||||
105
index.html
105
index.html
@@ -1,56 +1,59 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<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="#3b5bdb" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
||||||
<title>Dashfront</title>
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#root {
|
|
||||||
min-height: 100%;
|
|
||||||
background-color: #c5ddd8;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
<head>
|
||||||
body {
|
<meta charset="UTF-8" />
|
||||||
overscroll-behavior-y: none;
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
touch-action: manipulation;
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
||||||
}
|
<meta name="theme-color" content="#3b5bdb" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>Dashfront</title>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
min-height: 100%;
|
||||||
|
background-color: #85b7ad;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html,
|
||||||
scrollbar-gutter: stable;
|
body {
|
||||||
}
|
overscroll-behavior-y: none;
|
||||||
</style>
|
touch-action: manipulation;
|
||||||
</head>
|
}
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
html {
|
||||||
<script>
|
scrollbar-gutter: stable;
|
||||||
(function () {
|
}
|
||||||
var host = location.hostname;
|
</style>
|
||||||
if (host !== 'localhost' && host !== '127.0.0.1') return;
|
</head>
|
||||||
if (!navigator.serviceWorker) return;
|
|
||||||
var hadSw = false;
|
<body>
|
||||||
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
<div id="root"></div>
|
||||||
hadSw = regs.length > 0;
|
<script>
|
||||||
return Promise.all(regs.map(function (r) { return r.unregister(); }));
|
(function () {
|
||||||
}).then(function () {
|
var host = location.hostname;
|
||||||
if (!window.caches) return hadSw;
|
if (host !== 'localhost' && host !== '127.0.0.1') return;
|
||||||
return caches.keys().then(function (keys) {
|
if (!navigator.serviceWorker) return;
|
||||||
return Promise.all(keys.map(function (k) { return caches.delete(k); }))
|
var hadSw = false;
|
||||||
.then(function () { return hadSw; });
|
navigator.serviceWorker.getRegistrations().then(function (regs) {
|
||||||
});
|
hadSw = regs.length > 0;
|
||||||
}).then(function (reload) {
|
return Promise.all(regs.map(function (r) { return r.unregister(); }));
|
||||||
if (reload && !sessionStorage.getItem('dashfront-sw-cleared')) {
|
}).then(function () {
|
||||||
sessionStorage.setItem('dashfront-sw-cleared', '1');
|
if (!window.caches) return hadSw;
|
||||||
location.reload();
|
return caches.keys().then(function (keys) {
|
||||||
}
|
return Promise.all(keys.map(function (k) { return caches.delete(k); }))
|
||||||
|
.then(function () { return hadSw; });
|
||||||
});
|
});
|
||||||
})();
|
}).then(function (reload) {
|
||||||
</script>
|
if (reload && !sessionStorage.getItem('dashfront-sw-cleared')) {
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
sessionStorage.setItem('dashfront-sw-cleared', '1');
|
||||||
</body>
|
location.reload();
|
||||||
</html>
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
// Mirrored in index.html and index.css for paint before JS loads.
|
// Mirrored in index.html and index.css for paint before JS loads.
|
||||||
export const MAIN_BACKGROUND = '#c5ddd8';
|
export const MAIN_BACKGROUND = '#85b7ad';
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export default class TextInput extends Component {
|
|||||||
|
|
||||||
const mergedSlotProps = {
|
const mergedSlotProps = {
|
||||||
...restSlotProps,
|
...restSlotProps,
|
||||||
|
...(resolvedInputProps ? { input: resolvedInputProps } : {}),
|
||||||
inputLabel: {
|
inputLabel: {
|
||||||
...(label ? { shrink: true } : {}),
|
...(label ? { shrink: true } : {}),
|
||||||
...InputLabelProps,
|
...InputLabelProps,
|
||||||
@@ -128,7 +129,6 @@ export default class TextInput extends Component {
|
|||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
onMouseDown={this.handleMouseDown}
|
onMouseDown={this.handleMouseDown}
|
||||||
InputProps={resolvedInputProps}
|
|
||||||
slotProps={mergedSlotProps}
|
slotProps={mergedSlotProps}
|
||||||
sx={resolveSx(sx)}
|
sx={resolveSx(sx)}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@@ -246,20 +246,19 @@ export default class ArticleAttributesTab extends PureComponent {
|
|||||||
const isExclusive = Boolean(targetAttr.exclusive);
|
const isExclusive = Boolean(targetAttr.exclusive);
|
||||||
const targetValId = Number(effectiveItemId);
|
const targetValId = Number(effectiveItemId);
|
||||||
const isAlreadyAssigned = assignedAttributes.some((a) => a.attributeValueId === targetValId);
|
const isAlreadyAssigned = assignedAttributes.some((a) => a.attributeValueId === targetValId);
|
||||||
|
const isMerkmalAlreadyAssigned = assignedAttributes.some((a) => a.attributeId === targetAttr.id);
|
||||||
|
|
||||||
|
if (isExclusive && isMerkmalAlreadyAssigned) {
|
||||||
|
enqueueSnackbar('Eindeutiges Merkmal ist bereits zugeordnet.', { variant: 'info' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isAlreadyAssigned) {
|
if (isAlreadyAssigned) {
|
||||||
enqueueSnackbar('Bereits zugeordnet.', { variant: 'info' });
|
enqueueSnackbar('Bereits zugeordnet.', { variant: 'info' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextValues = isExclusive
|
const nextValues = [...assignedAttributes.map((a) => a.attributeValueId), targetValId];
|
||||||
? [
|
|
||||||
...assignedAttributes
|
|
||||||
.filter((a) => a.attributeId !== targetAttr.id)
|
|
||||||
.map((a) => a.attributeValueId),
|
|
||||||
targetValId,
|
|
||||||
]
|
|
||||||
: [...assignedAttributes.map((a) => a.attributeValueId), targetValId];
|
|
||||||
|
|
||||||
this.setState({ saving: true });
|
this.setState({ saving: true });
|
||||||
try {
|
try {
|
||||||
@@ -282,6 +281,13 @@ export default class ArticleAttributesTab extends PureComponent {
|
|||||||
if (!targetProp || !val) return;
|
if (!targetProp || !val) return;
|
||||||
|
|
||||||
const isExclusive = Boolean(targetProp.exclusive);
|
const isExclusive = Boolean(targetProp.exclusive);
|
||||||
|
const isMerkmalAlreadyAssigned = assignedProperties.some((p) => p.propertyId === propId);
|
||||||
|
|
||||||
|
if (isExclusive && isMerkmalAlreadyAssigned) {
|
||||||
|
enqueueSnackbar('Eindeutiges Merkmal ist bereits zugeordnet.', { variant: 'info' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isExclusive) {
|
if (!isExclusive) {
|
||||||
const isAlreadyAssigned = assignedProperties.some(
|
const isAlreadyAssigned = assignedProperties.some(
|
||||||
(p) => p.propertyId === propId && p.value.trim().toLowerCase() === val.toLowerCase()
|
(p) => p.propertyId === propId && p.value.trim().toLowerCase() === val.toLowerCase()
|
||||||
@@ -291,11 +297,8 @@ export default class ArticleAttributesTab extends PureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sourceProps = isExclusive
|
|
||||||
? assignedProperties.filter((p) => p.propertyId !== propId)
|
|
||||||
: assignedProperties;
|
|
||||||
const nextProps = [
|
const nextProps = [
|
||||||
...sourceProps.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
|
...assignedProperties.map((p) => ({ propertyId: p.propertyId, value: p.value, sort: p.sort })),
|
||||||
{ propertyId: propId, value: val },
|
{ propertyId: propId, value: val },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -470,6 +473,36 @@ export default class ArticleAttributesTab extends PureComponent {
|
|||||||
|
|
||||||
assignedGroups.sort((a, b) => a.sort - b.sort || a.name.localeCompare(b.name));
|
assignedGroups.sort((a, b) => a.sort - b.sort || a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
const isExclusive = Boolean(selectedOption?.exclusive);
|
||||||
|
const isMerkmalAlreadyAssigned = isAttr
|
||||||
|
? assignedAttributes.some((a) => a.attributeId === selectedOption?.id)
|
||||||
|
: assignedProperties.some((p) => p.propertyId === selectedOption?.id);
|
||||||
|
|
||||||
|
const isValueAlreadyAssigned = isAttr
|
||||||
|
? assignedAttributes.some((a) => a.attributeValueId === Number(currentItemId))
|
||||||
|
: (Boolean(customValueText.trim()) && assignedProperties.some(
|
||||||
|
(p) => p.propertyId === selectedOption?.id && p.value.trim().toLowerCase() === customValueText.trim().toLowerCase()
|
||||||
|
));
|
||||||
|
|
||||||
|
const isUniqueAlreadyAdded = isExclusive && isMerkmalAlreadyAssigned;
|
||||||
|
|
||||||
|
const isAddDisabled =
|
||||||
|
saving ||
|
||||||
|
isVariantBlocked ||
|
||||||
|
!selectedMerkmalKey ||
|
||||||
|
(isAttr ? !currentItemId || availableItems.length === 0 : !customValueText.trim()) ||
|
||||||
|
isUniqueAlreadyAdded ||
|
||||||
|
isValueAlreadyAssigned;
|
||||||
|
|
||||||
|
let addTooltipTitle = 'Hinzufügen';
|
||||||
|
if (isUniqueAlreadyAdded) {
|
||||||
|
addTooltipTitle = 'Eindeutiges Merkmal ist bereits zugeordnet';
|
||||||
|
} else if (isValueAlreadyAssigned) {
|
||||||
|
addTooltipTitle = 'Bereits zugeordnet';
|
||||||
|
} else if (isVariantBlocked) {
|
||||||
|
addTooltipTitle = 'Varianten-Merkmal kann nicht zugeordnet werden';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
{/* Unified Add Bar: [Merkmal ▼] [Wert ▼ / Input] [+] */}
|
{/* Unified Add Bar: [Merkmal ▼] [Wert ▼ / Input] [+] */}
|
||||||
@@ -531,16 +564,11 @@ export default class ArticleAttributesTab extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip title="Hinzufügen">
|
<Tooltip title={addTooltipTitle}>
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={
|
disabled={isAddDisabled}
|
||||||
saving ||
|
|
||||||
isVariantBlocked ||
|
|
||||||
!selectedMerkmalKey ||
|
|
||||||
(isAttr ? !selectedItemId || availableItems.length === 0 : !customValueText.trim())
|
|
||||||
}
|
|
||||||
onClick={() => void this.handleAddItem()}
|
onClick={() => void this.handleAddItem()}
|
||||||
sx={{
|
sx={{
|
||||||
bgcolor: 'primary.main',
|
bgcolor: 'primary.main',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ html,
|
|||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background-color: #c5ddd8;
|
background-color: #85b7ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -30,4 +30,4 @@ html {
|
|||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28);
|
||||||
cursor: grabbing !important;
|
cursor: grabbing !important;
|
||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
}
|
}
|
||||||
@@ -288,45 +288,45 @@ export default defineConfig(({ command }) => ({
|
|||||||
nginxHeadersPreviewPlugin(),
|
nginxHeadersPreviewPlugin(),
|
||||||
...(command === 'build'
|
...(command === 'build'
|
||||||
? [
|
? [
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
injectRegister: 'auto',
|
injectRegister: 'auto',
|
||||||
manifest: {
|
manifest: {
|
||||||
id: '/',
|
id: '/',
|
||||||
name: 'Dashfront',
|
name: 'Dashfront',
|
||||||
short_name: 'Dashfront',
|
short_name: 'Dashfront',
|
||||||
start_url: '/',
|
start_url: '/',
|
||||||
scope: '/',
|
scope: '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#c5ddd8',
|
background_color: '#85b7ad',
|
||||||
theme_color: '#3b5bdb',
|
theme_color: '#3b5bdb',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/icons/icon-192.png',
|
src: '/icons/icon-192.png',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/icons/icon-512.png',
|
src: '/icons/icon-512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/icons/icon-512.png',
|
src: '/icons/icon-512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
purpose: 'maskable',
|
purpose: 'maskable',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
||||||
globIgnores: ['**/*.wasm'],
|
globIgnores: ['**/*.wasm'],
|
||||||
navigateFallback: 'index.html',
|
navigateFallback: 'index.html',
|
||||||
navigateFallbackDenylist: [/^\/api/],
|
navigateFallbackDenylist: [/^\/api/],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
server: sharedServer,
|
server: sharedServer,
|
||||||
|
|||||||
Reference in New Issue
Block a user