import { Component, PureComponent } from 'react'; 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 DragHandleIcon from '@mui/icons-material/DragHandle'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; import SettingsIcon from '@mui/icons-material/Settings'; import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; import PhotoCameraIcon from '@mui/icons-material/PhotoCamera'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import { getThumbHashDataUrl, isImageLoaded, markImageLoaded } from '../article/ThumbHashImage'; import { thumb } from '../../api'; import { tileBaseSx } from 'dashgui/tiles'; import { RED_TINT, SCRIM_BADGE, SCRIM_MEDIUM, VEIL_HOVER, } from '../../colors'; /** * Top row (1/3): sort handle | settings toggle | move handle. * Icons are pinned to exact thirds (left / center / right) so they stay * evenly spread regardless of icon size or button padding. */ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick }) { const isTerminalMode = settingsTab >= 0 && settingsTab < terminalCount; const isPhotoMode = settingsTab === -2; const isDeleteMode = settingsTab === -3; const handleSx = { position: 'absolute', top: '50%', transform: 'translateY(-50%)', color: 'common.white', '&:hover': { bgcolor: VEIL_HOVER }, }; return ( {isTerminalMode ? ( ) : isPhotoMode ? ( ) : isDeleteMode ? ( ) : ( )} { e.stopPropagation(); onPlacementClick?.(); }} sx={{ ...handleSx, right: 0, cursor: 'pointer', }} > ); } /** * Middle row (1/3): gray band — TextField or terminal toggle. */ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, fullHash }) { const isActionTab = settingsTab === -2 || settingsTab === -3; const fillSx = { width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', bgcolor: 'grey.300', px: 1, ...(isActionTab ? { '&:hover': { bgcolor: settingsTab === -3 ? RED_TINT : 'grey.400', }, } : {}), }; // Show terminal-specific toggle when on a terminal tab if (settingsTab >= 0 && settingsTab < terminals.length) { const terminal = terminals[settingsTab]; const isEnabled = categoryPosterminalIds.includes(terminal.id); return ( {terminal.name} onTerminalToggle?.(terminal.id, e.target.checked)} size="small" /> ); } // Show photo upload UI when on photo tab if (settingsTab === -2) { return ( {fullHash ? 'Bild löschen' : 'Hochladen'} ); } // Show category deletion UI when on delete tab if (settingsTab === -3) { return ( Kategorie löschen ); } return ( {nameInput} ); } /** * Renders a ThumbHash preview and the full thumbnail as backdrop for a category tile. * ThumbHash is shown immediately as a placeholder; the thumb fades in once loaded. */ class CategoryBackdrop extends PureComponent { constructor(props) { super(props); this.thumbUrl = getThumbHashDataUrl(props.thumbHash); this.isCached = isImageLoaded(props.fullHash); } componentDidUpdate(prevProps) { if (prevProps.thumbHash !== this.props.thumbHash) { this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash); } if (prevProps.fullHash !== this.props.fullHash) { this.isCached = isImageLoaded(this.props.fullHash); this.forceUpdate(); } } handleLoad = (e) => { if (this.props.fullHash && e.target) { markImageLoaded(this.props.fullHash, e.target); } }; render() { const { fullHash } = this.props; if (!this.thumbUrl && !fullHash) return null; const isCached = this.isCached || isImageLoaded(fullHash); return ( {!isCached && this.thumbUrl ? ( ) : null} {fullHash ? ( ) : null} ); } } /** * Category tile. * * Normal mode (edit=false): clickable tile with label overlay; click navigates. * Edit mode (edit=true): three rows of 1/3 each — * Top row: sort-handle | settings toggle | drop-handle * Middle row: gray band — TextField, POS toggle, or photo upload * Bottom row: upper half = open-category click zone * lower half = gray band with chevrons + delete */ export class CategoryTile extends Component { state = { settingsTab: -1 }; // -1 = rename, -2 = photo, -3 = delete, 0..n-1 = terminal index handleClick = () => { this.props.onClick?.(); }; handleKeyDown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.handleClick(); } }; handleSettingsToggle = () => { const { terminals } = this.props; const terminalCount = terminals?.length || 0; this.setState((prev) => { let nextTab; if (prev.settingsTab === -1) { nextTab = terminalCount > 0 ? 0 : -2; } else if (prev.settingsTab >= 0 && prev.settingsTab < terminalCount - 1) { nextTab = prev.settingsTab + 1; } else if (prev.settingsTab === terminalCount - 1 || prev.settingsTab >= 0) { nextTab = -2; } else if (prev.settingsTab === -2) { nextTab = -3; } else { nextTab = -1; } return { settingsTab: nextTab }; }); }; handleMiddleBandClick = () => { const { settingsTab } = this.state; const { onPhotoUpload, onPhotoDelete, fullHash, onDelete } = this.props; if (settingsTab === -2) { if (fullHash) { onPhotoDelete?.(); } else { onPhotoUpload?.(); } } else if (settingsTab === -3) { onDelete?.(); } }; renderLabel() { const { label, nameInput } = this.props; if (label == null) return null; return ( {nameInput ?? ( {label} )} ); } render() { const { sx, edit, label, thumbHash, fullHash, onClick, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, onPhotoUpload, onPhotoDelete, onPlacementClick, onOpen, onDelete, ...rest } = this.props; const { settingsTab } = this.state; // Shared by both modes: the thumbnail backdrop. const backdrop = ( ); // Only the overlay differs between normal and edit mode. const overlay = edit ? ( <> {/* Lower third: clickable in edit mode to open/navigate into category */} { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); (onOpen || onClick)?.(); } }} sx={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '33.33%', cursor: 'pointer', '&:hover': { bgcolor: 'action.hover', }, }} /> ) : ( this.renderLabel() ); return ( {backdrop} {overlay} ); } }