u
This commit is contained in:
@@ -125,6 +125,21 @@ export const category = {
|
||||
},
|
||||
// #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)
|
||||
* `kategorien.zip` = kategorien.csv (semicolon-separated, Windows-1252,
|
||||
|
||||
@@ -401,6 +401,28 @@ const actions = {
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
/** Remove an article from all live category view lists (used after article deletion). */
|
||||
|
||||
@@ -3,6 +3,8 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
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 AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
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.
|
||||
*/
|
||||
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 fillSx = {
|
||||
width: '100%',
|
||||
@@ -162,22 +174,131 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds,
|
||||
|
||||
// Show photo upload UI when on photo tab
|
||||
if (settingsTab === -2) {
|
||||
if (fullHash) {
|
||||
return (
|
||||
<Box sx={fillSx}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<PhotoCameraIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
||||
<Typography
|
||||
variant="caption"
|
||||
noWrap
|
||||
sx={{ maxWidth: 100, color: 'text.secondary' }}
|
||||
>
|
||||
Bild löschen
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={fillSx}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<PhotoCameraIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
|
||||
<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={{ maxWidth: 100, color: 'text.secondary' }}
|
||||
sx={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 600,
|
||||
color: 'text.secondary',
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{fullHash ? 'Bild löschen' : 'Hochladen'}
|
||||
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
|
||||
if (settingsTab === -3) {
|
||||
return (
|
||||
@@ -303,7 +424,15 @@ class CategoryBackdrop extends PureComponent {
|
||||
* lower half = gray band with chevrons + delete
|
||||
*/
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
const { settingsTab } = this.state;
|
||||
const { onPhotoUpload, onPhotoDelete, fullHash, onDelete, onDeleteRecursive } = this.props;
|
||||
const { onPhotoDelete, fullHash, onDelete, onDeleteRecursive } = this.props;
|
||||
if (settingsTab === -2) {
|
||||
if (fullHash) {
|
||||
onPhotoDelete?.();
|
||||
} else {
|
||||
onPhotoUpload?.();
|
||||
}
|
||||
} else if (settingsTab === -3) {
|
||||
onDelete?.();
|
||||
@@ -400,13 +542,15 @@ export class CategoryTile extends Component {
|
||||
onTerminalToggle,
|
||||
onPhotoUpload,
|
||||
onPhotoDelete,
|
||||
onPhotoMatrix,
|
||||
onPhotoGenerate,
|
||||
onPlacementClick,
|
||||
onOpen,
|
||||
onDelete,
|
||||
onDeleteRecursive,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { settingsTab } = this.state;
|
||||
const { settingsTab, generatingMatrix } = this.state;
|
||||
|
||||
// Shared by both modes: the thumbnail backdrop.
|
||||
const backdrop = (
|
||||
@@ -431,7 +575,7 @@ export class CategoryTile extends Component {
|
||||
left: 0,
|
||||
right: 0,
|
||||
cursor:
|
||||
settingsTab === -2 || settingsTab === -3 || settingsTab === -4
|
||||
(settingsTab === -2 && fullHash) || settingsTab === -3 || settingsTab === -4
|
||||
? 'pointer'
|
||||
: 'default',
|
||||
}}
|
||||
@@ -443,6 +587,9 @@ export class CategoryTile extends Component {
|
||||
categoryPosterminalIds={categoryPosterminalIds}
|
||||
onTerminalToggle={onTerminalToggle}
|
||||
fullHash={fullHash}
|
||||
onPhotoUpload={onPhotoUpload}
|
||||
onPhotoMatrix={this.handlePhotoMatrix}
|
||||
generatingMatrix={generatingMatrix}
|
||||
/>
|
||||
</Box>
|
||||
{/* 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() {
|
||||
const {
|
||||
companyId,
|
||||
@@ -500,6 +509,7 @@ class ArtikelKategorienPage extends PureComponent {
|
||||
void categoryAutomat.actions.deleteCategoryImage(item.id)
|
||||
.catch(notifyError);
|
||||
},
|
||||
onPhotoMatrix: () => this.handleGenerateCategoryMatrix(item),
|
||||
onDelete: () => this.handleDeleteCategory(item),
|
||||
onDeleteRecursive: () => this.handleDeleteCategoryRecursive(item),
|
||||
nameInput: (
|
||||
|
||||
Reference in New Issue
Block a user