diff --git a/AGENTS.md b/AGENTS.md index 0f178a0..09eacf1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,7 +84,7 @@ flow: | **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 | | **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` | | **State Management** | `react-automat` | Single state layer for all domain & configuration data (articles, categories, company, brands, units, POS terminals, session) | | **Notifications** | `notistack` | `useSnackbar()` / `` in [src/main.jsx](file:///home/seb/src/dashfront/src/main.jsx) | diff --git a/index.html b/index.html index da05854..1eaa375 100644 --- a/index.html +++ b/index.html @@ -1,56 +1,59 @@ - - - - - - - Dashfront - - - -
- - - - + }).then(function (reload) { + if (reload && !sessionStorage.getItem('dashfront-sw-cleared')) { + sessionStorage.setItem('dashfront-sw-cleared', '1'); + location.reload(); + } + }); + })(); + + + + + \ No newline at end of file diff --git a/src/colors.js b/src/colors.js index c1471f4..41922b7 100644 --- a/src/colors.js +++ b/src/colors.js @@ -1,2 +1,2 @@ // Mirrored in index.html and index.css for paint before JS loads. -export const MAIN_BACKGROUND = '#c5ddd8'; +export const MAIN_BACKGROUND = '#85b7ad'; diff --git a/src/components/TextInput.jsx b/src/components/TextInput.jsx index 3dd1152..bc6e4a4 100644 --- a/src/components/TextInput.jsx +++ b/src/components/TextInput.jsx @@ -112,6 +112,7 @@ export default class TextInput extends Component { const mergedSlotProps = { ...restSlotProps, + ...(resolvedInputProps ? { input: resolvedInputProps } : {}), inputLabel: { ...(label ? { shrink: true } : {}), ...InputLabelProps, @@ -128,7 +129,6 @@ export default class TextInput extends Component { autoComplete="one-time-code" onFocus={this.handleFocus} onMouseDown={this.handleMouseDown} - InputProps={resolvedInputProps} slotProps={mergedSlotProps} sx={resolveSx(sx)} {...rest} diff --git a/src/components/article/tabs/ArticleAttributesTab.jsx b/src/components/article/tabs/ArticleAttributesTab.jsx index 303d81b..6d40777 100644 --- a/src/components/article/tabs/ArticleAttributesTab.jsx +++ b/src/components/article/tabs/ArticleAttributesTab.jsx @@ -246,20 +246,19 @@ export default class ArticleAttributesTab extends PureComponent { const isExclusive = Boolean(targetAttr.exclusive); const targetValId = Number(effectiveItemId); 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) { enqueueSnackbar('Bereits zugeordnet.', { variant: 'info' }); return; } - const nextValues = isExclusive - ? [ - ...assignedAttributes - .filter((a) => a.attributeId !== targetAttr.id) - .map((a) => a.attributeValueId), - targetValId, - ] - : [...assignedAttributes.map((a) => a.attributeValueId), targetValId]; + const nextValues = [...assignedAttributes.map((a) => a.attributeValueId), targetValId]; this.setState({ saving: true }); try { @@ -282,6 +281,13 @@ export default class ArticleAttributesTab extends PureComponent { if (!targetProp || !val) return; 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) { const isAlreadyAssigned = assignedProperties.some( (p) => p.propertyId === propId && p.value.trim().toLowerCase() === val.toLowerCase() @@ -291,11 +297,8 @@ export default class ArticleAttributesTab extends PureComponent { return; } } - const sourceProps = isExclusive - ? assignedProperties.filter((p) => p.propertyId !== propId) - : assignedProperties; 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 }, ]; @@ -470,6 +473,36 @@ export default class ArticleAttributesTab extends PureComponent { 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 ( {/* Unified Add Bar: [Merkmal ▼] [Wert ▼ / Input] [+] */} @@ -531,16 +564,11 @@ export default class ArticleAttributesTab extends PureComponent { /> )} - + void this.handleAddItem()} sx={{ bgcolor: 'primary.main', diff --git a/src/index.css b/src/index.css index 213716a..1e77cb9 100644 --- a/src/index.css +++ b/src/index.css @@ -5,7 +5,7 @@ html, body, #root { min-height: 100%; - background-color: #c5ddd8; + background-color: #85b7ad; } html, @@ -30,4 +30,4 @@ html { box-shadow: 0 8px 20px rgba(0, 0, 0, 0.28); cursor: grabbing !important; user-select: none !important; -} +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 20cc135..ade9593 100644 --- a/vite.config.js +++ b/vite.config.js @@ -288,45 +288,45 @@ export default defineConfig(({ command }) => ({ nginxHeadersPreviewPlugin(), ...(command === 'build' ? [ - VitePWA({ - registerType: 'autoUpdate', - injectRegister: 'auto', - manifest: { - id: '/', - name: 'Dashfront', - short_name: 'Dashfront', - start_url: '/', - scope: '/', - display: 'standalone', - background_color: '#c5ddd8', - theme_color: '#3b5bdb', - icons: [ - { - src: '/icons/icon-192.png', - sizes: '192x192', - type: 'image/png', - }, - { - src: '/icons/icon-512.png', - sizes: '512x512', - type: 'image/png', - }, - { - src: '/icons/icon-512.png', - sizes: '512x512', - type: 'image/png', - purpose: 'maskable', - }, - ], - }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'], - globIgnores: ['**/*.wasm'], - navigateFallback: 'index.html', - navigateFallbackDenylist: [/^\/api/], - }, - }), - ] + VitePWA({ + registerType: 'autoUpdate', + injectRegister: 'auto', + manifest: { + id: '/', + name: 'Dashfront', + short_name: 'Dashfront', + start_url: '/', + scope: '/', + display: 'standalone', + background_color: '#85b7ad', + theme_color: '#3b5bdb', + icons: [ + { + src: '/icons/icon-192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: '/icons/icon-512.png', + sizes: '512x512', + type: 'image/png', + }, + { + src: '/icons/icon-512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable', + }, + ], + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'], + globIgnores: ['**/*.wasm'], + navigateFallback: 'index.html', + navigateFallbackDenylist: [/^\/api/], + }, + }), + ] : []), ], server: sharedServer,