Compare commits
5 Commits
2f946442c8
...
13e1821355
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e1821355 | ||
|
|
eeb4b92a79 | ||
|
|
3801ac4e52 | ||
|
|
31fdc30c5a | ||
|
|
bf39f4d017 |
@@ -131,7 +131,7 @@ Strip supplierName?: str from T11 (resolve via entityId in cached supplier list)
|
|||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 268 API endpoints across all modules.
|
Overview of all 270 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -460,6 +460,9 @@ _none_
|
|||||||
#### Add category image — `POST /category/{id}/image`
|
#### Add category image — `POST /category/{id}/image`
|
||||||
- Params: `id: int, image/avif`
|
- Params: `id: int, image/avif`
|
||||||
- Returns: `201 { category: { id: int, thumbW: int, thumbH: int, thumbHash: str } }`
|
- Returns: `201 { category: { id: int, thumbW: int, thumbH: int, thumbHash: str } }`
|
||||||
|
#### Generate category mosaic image from contents — `POST /category/{id}/image/generate`
|
||||||
|
- Params: `id: int, body?: { size?: int }`
|
||||||
|
- Returns: `200 { category: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, size: int, grid: str, articleCount: int } }`
|
||||||
#### List category properties — `GET /category/{id}/property`
|
#### List category properties — `GET /category/{id}/property`
|
||||||
- Params: `id: int`
|
- Params: `id: int`
|
||||||
- Returns: `200 { properties: { id: int, propertyId: int, name: str, exclusive: bool, value: str, sort: int, categoryId?: int }[] }`
|
- Returns: `200 { properties: { id: int, propertyId: int, name: str, exclusive: bool, value: str, sort: int, categoryId?: int }[] }`
|
||||||
@@ -1055,6 +1058,9 @@ _none_
|
|||||||
#### JTL-POS category synchronization — `GET /v1/category`
|
#### JTL-POS category synchronization — `GET /v1/category`
|
||||||
- Params: `query: lastChangedCategory?: str, limit?: int`
|
- Params: `query: lastChangedCategory?: str, limit?: int`
|
||||||
- Returns: `200 {}[]`
|
- Returns: `200 {}[]`
|
||||||
|
#### POS category image delivery — `GET /v1/cimage`
|
||||||
|
- Params: `query: path: str, size?: int, w?: int, h?: int, format?: str`
|
||||||
|
- Returns: `200 jpeg`
|
||||||
#### POS discovery and pairing — `GET /v1/client`
|
#### POS discovery and pairing — `GET /v1/client`
|
||||||
- Params: `query: authCode: str, name?: str`
|
- Params: `query: authCode: str, name?: str`
|
||||||
- Returns: `200 { authCode: str|null, authToken: str|null, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str|null, mandantDatabase: str|null, serverFingerprint: str|null, name: str|null, serverTimestamp: str }`
|
- Returns: `200 { authCode: str|null, authToken: str|null, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str|null, mandantDatabase: str|null, serverFingerprint: str|null, name: str|null, serverTimestamp: str }`
|
||||||
@@ -1074,7 +1080,7 @@ _none_
|
|||||||
- Params: `query: mandantId?: str, body: { orders?: {}[] }`
|
- Params: `query: mandantId?: str, body: { orders?: {}[] }`
|
||||||
- Returns: `200 { status: str, externalOrderId: str, message: str }[]`
|
- Returns: `200 { status: str, externalOrderId: str, message: str }[]`
|
||||||
#### POS product image delivery — `GET /v1/pimage`
|
#### POS product image delivery — `GET /v1/pimage`
|
||||||
- Params: `query: path: str, w?: int, h?: int, format?: str`
|
- Params: `query: path: str, size?: int, w?: int, h?: int, format?: str`
|
||||||
- Returns: `200 jpeg`
|
- Returns: `200 jpeg`
|
||||||
#### JTL-POS product catalog synchronization — `GET /v1/product`
|
#### JTL-POS product catalog synchronization — `GET /v1/product`
|
||||||
- Params: `query: lastChangedProduct?: str, limit?: int`
|
- Params: `query: lastChangedProduct?: str, limit?: int`
|
||||||
|
|||||||
@@ -125,6 +125,21 @@ export const category = {
|
|||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/category/:id/image/generate
|
||||||
|
* Body (optional): { size?: 2 | 3 | 4 }
|
||||||
|
* → 200 { category: { id, fullHash, thumbW, thumbH, thumbHash, size, grid, articleCount } }
|
||||||
|
*/
|
||||||
|
// #region POST /api/category/:id/image/generate — generateImage
|
||||||
|
generateImage(categoryId, { size } = {}) {
|
||||||
|
return http(`/api/category/${categoryId}/image/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: size != null ? { size } : undefined,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/category/export.zip → application/zip (binary download)
|
* GET /api/category/export.zip → application/zip (binary download)
|
||||||
* `kategorien.zip` = kategorien.csv (semicolon-separated, Windows-1252,
|
* `kategorien.zip` = kategorien.csv (semicolon-separated, Windows-1252,
|
||||||
|
|||||||
@@ -401,6 +401,28 @@ const actions = {
|
|||||||
return res.category;
|
return res.category;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a mosaic image for a category from its articles and subcategories.
|
||||||
|
* @param {{ categoryId: number|string, size?: number }} param0
|
||||||
|
*/
|
||||||
|
async generateCategoryImage({ categoryId, size } = {}) {
|
||||||
|
const res = await categoryApi.generateImage(categoryId, { size });
|
||||||
|
const { id, fullHash, thumbW, thumbH, thumbHash } = res.category;
|
||||||
|
const { categories } = categoryAutomat.state;
|
||||||
|
if (!categories) return res.category;
|
||||||
|
const key = String(id);
|
||||||
|
const nextCategories = {
|
||||||
|
...categories,
|
||||||
|
[key]: { ...categories[key], fullHash, thumbW, thumbH, thumbHash },
|
||||||
|
};
|
||||||
|
categoryAutomat.setState({
|
||||||
|
categories: nextCategories,
|
||||||
|
});
|
||||||
|
refreshCategoryViewChildren(nextCategories);
|
||||||
|
return res.category;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// ─── Local state patches ───────────────────────────────────────────────────
|
// ─── Local state patches ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Remove an article from all live category view lists (used after article deletion). */
|
/** Remove an article from all live category view lists (used after article deletion). */
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Button from '@mui/material/Button';
|
|||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
|
||||||
import Skeleton from '@mui/material/Skeleton';
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||||
@@ -125,13 +124,23 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
renderLink(label, onClick) {
|
renderLink(label, onClick) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
component="button"
|
component="span"
|
||||||
type="button"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
variant="body2"
|
variant="body2"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
const selection = window.getSelection()?.toString();
|
||||||
|
if (selection && selection.length > 0) return;
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClick?.(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
verticalAlign: 'baseline',
|
verticalAlign: 'baseline',
|
||||||
@@ -141,6 +150,7 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
fontSize: 'inherit',
|
fontSize: 'inherit',
|
||||||
|
userSelect: 'text',
|
||||||
p: 0,
|
p: 0,
|
||||||
m: 0,
|
m: 0,
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@@ -321,6 +331,7 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
lineHeight: 0,
|
lineHeight: 0,
|
||||||
|
cursor: 'default',
|
||||||
m: 0,
|
m: 0,
|
||||||
my: 0,
|
my: 0,
|
||||||
p: 0,
|
p: 0,
|
||||||
@@ -347,9 +358,7 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
if (!item) {
|
if (!item) {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<Box
|
||||||
disabled
|
|
||||||
divider
|
|
||||||
sx={{
|
sx={{
|
||||||
...rowGridSx,
|
...rowGridSx,
|
||||||
height: 32,
|
height: 32,
|
||||||
@@ -357,19 +366,28 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
maxHeight: 32,
|
maxHeight: 32,
|
||||||
px: 1.5,
|
px: 1.5,
|
||||||
py: 0,
|
py: 0,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}> </Typography>
|
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}> </Typography>
|
||||||
</ListItemButton>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<Box
|
||||||
disabled
|
sx={{
|
||||||
divider
|
...rowGridSx,
|
||||||
sx={{ ...rowGridSx, height: 32, minHeight: 32, maxHeight: 32, px: 1.5, py: 0 }}
|
height: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
maxHeight: 32,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderInvisibleDummyRow(dummy)}
|
{renderInvisibleDummyRow(dummy)}
|
||||||
<Box sx={{ gridColumn: 1, gridRow: 2 }}>
|
<Box sx={{ gridColumn: 1, gridRow: 2 }}>
|
||||||
@@ -378,25 +396,37 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
<Box sx={{ gridColumn: 2, gridRow: 2 }}>
|
<Box sx={{ gridColumn: 2, gridRow: 2 }}>
|
||||||
<Skeleton variant="text" width="95%" height={16} />
|
<Skeleton variant="text" width="95%" height={16} />
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemButton>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = decodeMeta(item.meta);
|
const meta = decodeMeta(item.meta);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton
|
<Box
|
||||||
divider
|
sx={{
|
||||||
disabled={disabled}
|
...rowGridSx,
|
||||||
sx={{ ...rowGridSx, height: 32, minHeight: 32, maxHeight: 32, px: 1.5, py: 0 }}
|
height: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
maxHeight: 32,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
userSelect: 'text',
|
||||||
|
cursor: 'text',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{renderInvisibleDummyRow(dummy)}
|
{renderInvisibleDummyRow(dummy)}
|
||||||
|
|
||||||
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}>
|
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0, userSelect: 'text', cursor: 'text' }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ fontVariantNumeric: 'tabular-nums', fontSize: '0.8125rem', ...ellipsisSx }}
|
sx={{ fontVariantNumeric: 'tabular-nums', fontSize: '0.8125rem', userSelect: 'text', cursor: 'text', ...ellipsisSx }}
|
||||||
>
|
>
|
||||||
{formatDateTime(item.date || item.createdAt)}
|
{formatDateTime(item.date || item.createdAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -410,18 +440,20 @@ class ArticleHistoryRow extends PureComponent {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
userSelect: 'text',
|
||||||
|
cursor: 'text',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{this.renderSkelehashThumb(meta)}
|
{this.renderSkelehashThumb(meta)}
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
component="div"
|
component="div"
|
||||||
sx={{ ...ellipsisSx, display: 'inline', width: '100%', fontSize: '0.8125rem' }}
|
sx={{ ...ellipsisSx, display: 'inline', width: '100%', fontSize: '0.8125rem', userSelect: 'text', cursor: 'text' }}
|
||||||
>
|
>
|
||||||
{this.renderEntrySpans(item.text, meta)}
|
{this.renderEntrySpans(item.text, meta)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemButton>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -836,8 +868,11 @@ class ArticleHistoryTab extends PureComponent {
|
|||||||
|
|
||||||
handleOpenBrand(brandName) {
|
handleOpenBrand(brandName) {
|
||||||
if (!brandName) return;
|
if (!brandName) return;
|
||||||
brandAutomat.actions.filter(brandName);
|
const match = (brandAutomat.state.brands || []).find(
|
||||||
this.props.navigate('/artikel/marken');
|
(b) => b && (b.name?.toLowerCase() === brandName.toLowerCase() || String(b.id) === String(brandName)),
|
||||||
|
);
|
||||||
|
const brandParam = match?.id != null ? String(match.id) : brandName;
|
||||||
|
this.props.navigate(`/artikel?brand=${encodeURIComponent(brandParam)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -865,8 +900,8 @@ class ArticleHistoryTab extends PureComponent {
|
|||||||
<Box sx={{ width: '100%', position: 'relative' }}>
|
<Box sx={{ width: '100%', position: 'relative' }}>
|
||||||
<FillSpace
|
<FillSpace
|
||||||
aboveRef={[this.searchRef]}
|
aboveRef={[this.searchRef]}
|
||||||
reserveBottom={16}
|
reserveBottom={48}
|
||||||
minPageSize={20}
|
minPageSize={8}
|
||||||
minHeight={0}
|
minHeight={0}
|
||||||
resetKey={filterKey}
|
resetKey={filterKey}
|
||||||
items={items || []}
|
items={items || []}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import Box from '@mui/material/Box';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import GridViewIcon from '@mui/icons-material/GridView';
|
||||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
@@ -116,7 +118,17 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick
|
|||||||
/**
|
/**
|
||||||
* Middle row (1/3): gray band — TextField or terminal toggle.
|
* Middle row (1/3): gray band — TextField or terminal toggle.
|
||||||
*/
|
*/
|
||||||
function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, fullHash }) {
|
function MiddleBand({
|
||||||
|
settingsTab,
|
||||||
|
nameInput,
|
||||||
|
terminals,
|
||||||
|
categoryPosterminalIds,
|
||||||
|
onTerminalToggle,
|
||||||
|
fullHash,
|
||||||
|
onPhotoUpload,
|
||||||
|
onPhotoMatrix,
|
||||||
|
generatingMatrix,
|
||||||
|
}) {
|
||||||
const isActionTab = settingsTab === -2 || settingsTab === -3 || settingsTab === -4;
|
const isActionTab = settingsTab === -2 || settingsTab === -3 || settingsTab === -4;
|
||||||
const fillSx = {
|
const fillSx = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -162,6 +174,7 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
|
|||||||
|
|
||||||
// Show photo upload UI when on photo tab
|
// Show photo upload UI when on photo tab
|
||||||
if (settingsTab === -2) {
|
if (settingsTab === -2) {
|
||||||
|
if (fullHash) {
|
||||||
return (
|
return (
|
||||||
<Box sx={fillSx}>
|
<Box sx={fillSx}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
@@ -171,13 +184,121 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
|
|||||||
noWrap
|
noWrap
|
||||||
sx={{ maxWidth: 100, color: 'text.secondary' }}
|
sx={{ maxWidth: 100, color: 'text.secondary' }}
|
||||||
>
|
>
|
||||||
{fullHash ? 'Bild löschen' : 'Hochladen'}
|
Bild löschen
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
bgcolor: 'grey.300',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Hochladen"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPhotoUpload?.();
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onPhotoUpload?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 0.25,
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRight: '1px solid',
|
||||||
|
borderColor: 'grey.400',
|
||||||
|
userSelect: 'none',
|
||||||
|
transition: 'background-color 0.15s',
|
||||||
|
'&:hover': { bgcolor: 'grey.400' },
|
||||||
|
'&:active': { bgcolor: 'grey.500' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PhotoCameraIcon sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.68rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'text.secondary',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hochladen
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Matrix"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPhotoMatrix?.();
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onPhotoMatrix?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 0.25,
|
||||||
|
cursor: generatingMatrix ? 'wait' : 'pointer',
|
||||||
|
userSelect: 'none',
|
||||||
|
transition: 'background-color 0.15s',
|
||||||
|
'&:hover': { bgcolor: generatingMatrix ? undefined : 'grey.400' },
|
||||||
|
'&:active': { bgcolor: generatingMatrix ? undefined : 'grey.500' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{generatingMatrix ? (
|
||||||
|
<CircularProgress size={16} sx={{ color: 'text.secondary' }} />
|
||||||
|
) : (
|
||||||
|
<GridViewIcon sx={{ fontSize: 18, color: 'text.secondary' }} />
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
noWrap
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.68rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'text.secondary',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Matrix
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Show category deletion UI when on delete tab
|
// Show category deletion UI when on delete tab
|
||||||
if (settingsTab === -3) {
|
if (settingsTab === -3) {
|
||||||
return (
|
return (
|
||||||
@@ -303,7 +424,15 @@ class CategoryBackdrop extends PureComponent {
|
|||||||
* lower half = gray band with chevrons + delete
|
* lower half = gray band with chevrons + delete
|
||||||
*/
|
*/
|
||||||
export class CategoryTile extends Component {
|
export class CategoryTile extends Component {
|
||||||
state = { settingsTab: -1 }; // -1 = rename, -2 = photo, -3 = delete, -4 = delete recursive, 0..n-1 = terminal index
|
state = { settingsTab: -1, generatingMatrix: false }; // -1 = rename, -2 = photo, -3 = delete, -4 = delete recursive, 0..n-1 = terminal index
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._isMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._isMounted = false;
|
||||||
|
}
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
this.props.onClick?.();
|
this.props.onClick?.();
|
||||||
@@ -338,14 +467,27 @@ export class CategoryTile extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handlePhotoMatrix = async (e) => {
|
||||||
|
e?.stopPropagation?.();
|
||||||
|
if (this.state.generatingMatrix) return;
|
||||||
|
const fn = this.props.onPhotoMatrix || this.props.onPhotoGenerate;
|
||||||
|
if (!fn) return;
|
||||||
|
this.setState({ generatingMatrix: true });
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} finally {
|
||||||
|
if (this._isMounted) {
|
||||||
|
this.setState({ generatingMatrix: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleMiddleBandClick = () => {
|
handleMiddleBandClick = () => {
|
||||||
const { settingsTab } = this.state;
|
const { settingsTab } = this.state;
|
||||||
const { onPhotoUpload, onPhotoDelete, fullHash, onDelete, onDeleteRecursive } = this.props;
|
const { onPhotoDelete, fullHash, onDelete, onDeleteRecursive } = this.props;
|
||||||
if (settingsTab === -2) {
|
if (settingsTab === -2) {
|
||||||
if (fullHash) {
|
if (fullHash) {
|
||||||
onPhotoDelete?.();
|
onPhotoDelete?.();
|
||||||
} else {
|
|
||||||
onPhotoUpload?.();
|
|
||||||
}
|
}
|
||||||
} else if (settingsTab === -3) {
|
} else if (settingsTab === -3) {
|
||||||
onDelete?.();
|
onDelete?.();
|
||||||
@@ -400,13 +542,15 @@ export class CategoryTile extends Component {
|
|||||||
onTerminalToggle,
|
onTerminalToggle,
|
||||||
onPhotoUpload,
|
onPhotoUpload,
|
||||||
onPhotoDelete,
|
onPhotoDelete,
|
||||||
|
onPhotoMatrix,
|
||||||
|
onPhotoGenerate,
|
||||||
onPlacementClick,
|
onPlacementClick,
|
||||||
onOpen,
|
onOpen,
|
||||||
onDelete,
|
onDelete,
|
||||||
onDeleteRecursive,
|
onDeleteRecursive,
|
||||||
...rest
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { settingsTab } = this.state;
|
const { settingsTab, generatingMatrix } = this.state;
|
||||||
|
|
||||||
// Shared by both modes: the thumbnail backdrop.
|
// Shared by both modes: the thumbnail backdrop.
|
||||||
const backdrop = (
|
const backdrop = (
|
||||||
@@ -431,7 +575,7 @@ export class CategoryTile extends Component {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
cursor:
|
cursor:
|
||||||
settingsTab === -2 || settingsTab === -3 || settingsTab === -4
|
(settingsTab === -2 && fullHash) || settingsTab === -3 || settingsTab === -4
|
||||||
? 'pointer'
|
? 'pointer'
|
||||||
: 'default',
|
: 'default',
|
||||||
}}
|
}}
|
||||||
@@ -443,6 +587,9 @@ export class CategoryTile extends Component {
|
|||||||
categoryPosterminalIds={categoryPosterminalIds}
|
categoryPosterminalIds={categoryPosterminalIds}
|
||||||
onTerminalToggle={onTerminalToggle}
|
onTerminalToggle={onTerminalToggle}
|
||||||
fullHash={fullHash}
|
fullHash={fullHash}
|
||||||
|
onPhotoUpload={onPhotoUpload}
|
||||||
|
onPhotoMatrix={this.handlePhotoMatrix}
|
||||||
|
generatingMatrix={generatingMatrix}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Lower third: clickable in edit mode to open/navigate into category */}
|
{/* Lower third: clickable in edit mode to open/navigate into category */}
|
||||||
|
|||||||
@@ -363,6 +363,15 @@ class ArtikelKategorienPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleGenerateCategoryMatrix = async (cat) => {
|
||||||
|
try {
|
||||||
|
await categoryAutomat.actions.generateCategoryImage({ categoryId: cat.id });
|
||||||
|
enqueueSnackbar('Matrix-Bild erstellt.', { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
companyId,
|
companyId,
|
||||||
@@ -500,6 +509,7 @@ class ArtikelKategorienPage extends PureComponent {
|
|||||||
void categoryAutomat.actions.deleteCategoryImage(item.id)
|
void categoryAutomat.actions.deleteCategoryImage(item.id)
|
||||||
.catch(notifyError);
|
.catch(notifyError);
|
||||||
},
|
},
|
||||||
|
onPhotoMatrix: () => this.handleGenerateCategoryMatrix(item),
|
||||||
onDelete: () => this.handleDeleteCategory(item),
|
onDelete: () => this.handleDeleteCategory(item),
|
||||||
onDeleteRecursive: () => this.handleDeleteCategoryRecursive(item),
|
onDeleteRecursive: () => this.handleDeleteCategoryRecursive(item),
|
||||||
nameInput: (
|
nameInput: (
|
||||||
|
|||||||
Reference in New Issue
Block a user