diff --git a/src/api/printerjob.js b/src/api/printerjob.js index 44d25a7..a1d9059 100644 --- a/src/api/printerjob.js +++ b/src/api/printerjob.js @@ -87,6 +87,37 @@ export const printerjob = { }); }, + /** + * POST /api/printerjob/cancel-all + * Cancels all pending/processing jobs, optionally scoped to an agentprinter. + * Falls back to listing and cancelling individual jobs if needed. + * @param {{ agentprinterId?: number }} [payload] + * → { success: true, cancelledCount: number, jobs: PrinterJob[] } + */ + async cancelAll({ agentprinterId } = {}) { + try { + return await http('/api/printerjob/cancel-all', { + method: 'POST', + body: agentprinterId != null ? { agentprinterId: Number(agentprinterId) } : {}, + }); + } catch (err) { + if (err?.status === 404) { + const { printerjobs = [] } = await this.list({ agentprinterId }); + const cancellable = printerjobs.filter( + (j) => j.state === 'pending' || j.state === 'processing', + ); + if (cancellable.length === 0) return { success: true, cancelledCount: 0, jobs: [] }; + const results = await Promise.all(cancellable.map((j) => this.cancel(j.id))); + return { + success: true, + cancelledCount: results.length, + jobs: results.map((r) => r.printerjob), + }; + } + throw err; + } + }, + /** * DELETE /api/printerjob/:id * → { success: true, deletedJob: PrinterJob } diff --git a/src/automats/companyAutomat.js b/src/automats/companyAutomat.js index bfdb529..181dc85 100644 --- a/src/automats/companyAutomat.js +++ b/src/automats/companyAutomat.js @@ -881,6 +881,10 @@ const actions = { return printerjobApi.sendTestZpl(agentprinterId, options); }, + async cancelAllPrinterJobs(agentprinterId) { + return printerjobApi.cancelAll({ agentprinterId }); + }, + async createZplJob(payload) { return printerjobApi.createZplJob(payload); }, diff --git a/src/components/AppShell.jsx b/src/components/AppShell.jsx index 79c424c..5113eab 100644 --- a/src/components/AppShell.jsx +++ b/src/components/AppShell.jsx @@ -212,6 +212,7 @@ class AppSidebar extends PureComponent { > + ); @@ -430,7 +431,6 @@ export default function AppShell() { - diff --git a/src/components/OnlinePrinterIcons.jsx b/src/components/OnlinePrinterIcons.jsx index d5b9ded..83ff379 100644 --- a/src/components/OnlinePrinterIcons.jsx +++ b/src/components/OnlinePrinterIcons.jsx @@ -10,6 +10,7 @@ import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Typography from '@mui/material/Typography'; import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined'; +import CancelScheduleSendOutlinedIcon from '@mui/icons-material/CancelScheduleSendOutlined'; import SettingsIcon from '@mui/icons-material/Settings'; import { enqueueSnackbar } from 'notistack'; import companyAutomat from '../automats/companyAutomat'; @@ -27,10 +28,12 @@ export default class OnlinePrinterIcons extends PureComponent { : [], menuAnchor: null, testingId: null, + cancellingId: null, }; this.handleOpenMenu = this.handleOpenMenu.bind(this); this.handleCloseMenu = this.handleCloseMenu.bind(this); this.handleTestPrint = this.handleTestPrint.bind(this); + this.handleCancelAllJobs = this.handleCancelAllJobs.bind(this); this.handleOpenPrinterSettings = this.handleOpenPrinterSettings.bind(this); } @@ -125,6 +128,29 @@ export default class OnlinePrinterIcons extends PureComponent { } } + async handleCancelAllJobs(printer) { + if (!printer?.id) return; + this.setState({ cancellingId: printer.id }); + try { + const res = await companyAutomat.actions.cancelAllPrinterJobs(printer.id); + const count = res?.cancelledCount ?? 0; + if (count > 0) { + enqueueSnackbar( + `${count} Druckauftrag/Druckaufträge für ${printer.model || 'Drucker'} abgebrochen.`, + { variant: 'info' }, + ); + } else { + enqueueSnackbar(`Keine offenen Aufträge für ${printer.model || 'Drucker'}.`, { + variant: 'info', + }); + } + } catch (err) { + notifyError(err); + } finally { + this.setState({ cancellingId: null, menuAnchor: null }); + } + } + handleOpenPrinterSettings() { this.handleCloseMenu(); window.dispatchEvent( @@ -133,7 +159,7 @@ export default class OnlinePrinterIcons extends PureComponent { } render() { - const { menuAnchor, testingId } = this.state; + const { menuAnchor, testingId, cancellingId } = this.state; const onlinePrinters = this.getOnlinePrinters(); if (onlinePrinters.length === 0) { @@ -266,7 +292,7 @@ export default class OnlinePrinterIcons extends PureComponent { void this.handleTestPrint(activePrinter)} - disabled={testingId === activePrinter?.id} + disabled={testingId === activePrinter?.id || cancellingId === activePrinter?.id} > @@ -277,6 +303,19 @@ export default class OnlinePrinterIcons extends PureComponent { /> + void this.handleCancelAllJobs(activePrinter)} + disabled={cancellingId === activePrinter?.id || testingId === activePrinter?.id} + > + + + + + + diff --git a/src/components/chat/ChatFab.jsx b/src/components/chat/ChatFab.jsx index 539ad3a..42d77e8 100644 --- a/src/components/chat/ChatFab.jsx +++ b/src/components/chat/ChatFab.jsx @@ -1,10 +1,9 @@ import React, { PureComponent } from 'react'; import Box from '@mui/material/Box'; -import Fab from '@mui/material/Fab'; +import Avatar from '@mui/material/Avatar'; +import IconButton from '@mui/material/IconButton'; import Badge from '@mui/material/Badge'; -import Tooltip from '@mui/material/Tooltip'; import CircularProgress from '@mui/material/CircularProgress'; -import ChatRoundedIcon from '@mui/icons-material/ChatRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded'; @@ -70,144 +69,161 @@ export default class ChatFab extends PureComponent { return ( ({ - position: 'fixed', - bottom: { xs: 20, sm: 28 }, - left: { xs: 20, lg: 164 }, - zIndex: theme.zIndex.speedDial || 1050, - })} + sx={{ + px: { xs: 0.5, sm: 1, lg: 1.5 }, + py: { xs: 0.5, lg: 1 }, + alignSelf: { xs: 'center', lg: 'flex-start' }, + display: 'flex', + alignItems: 'center', + }} > - - {/* Animated pulsing halo when awaiting user answer */} - {hasPendingAsk && !isOpen && ( - - )} - - {/* Animated pulsating halo when model is busy thinking/working */} - {!hasPendingAsk && isBusy && !isOpen && ( - - )} - - {/* Circular spinner ring when model is busy */} - {!hasPendingAsk && isBusy && !isOpen && ( - - )} - - + {/* Animated pulsing halo when awaiting user answer */} + {hasPendingAsk && !isOpen && ( + - + )} + + {/* Animated pulsating halo when model is busy thinking/working */} + {!hasPendingAsk && isBusy && !isOpen && ( + + )} + + {/* Circular spinner ring when model is busy */} + {!hasPendingAsk && isBusy && !isOpen && ( + + )} + + ({ - width: 54, - height: 54, - boxShadow: hasPendingAsk - ? CHAT_FAB_SHADOW_PENDING - : isBusy - ? CHAT_FAB_SHADOW_BUSY - : CHAT_FAB_SHADOW_IDLE, - background: hasPendingAsk - ? CHAT_FAB_PENDING - : isBusy - ? CHAT_FAB_BUSY - : `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`, - })} + aria-label={ariaLabel} + sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }} > - {isOpen ? ( - - ) : hasPendingAsk ? ( - - ) : isBusy ? ( - - ) : ( - - )} - - + + ({ + width: 32, + height: 32, + bgcolor: hasPendingAsk + ? CHAT_FAB_PENDING + : isBusy + ? CHAT_FAB_BUSY + : isOpen + ? theme.palette.primary.main + : `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`, + background: hasPendingAsk + ? CHAT_FAB_PENDING + : isBusy + ? CHAT_FAB_BUSY + : isOpen + ? theme.palette.primary.main + : `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`, + color: WHITE, + transition: 'all 0.2s ease', + boxShadow: hasPendingAsk + ? CHAT_FAB_SHADOW_PENDING + : isBusy + ? CHAT_FAB_SHADOW_BUSY + : isOpen + ? `0 2px 8px rgba(0,0,0,0.25)` + : 'none', + })} + > + {isOpen ? ( + + ) : hasPendingAsk ? ( + + ) : isBusy ? ( + + ) : ( + + )} + + + + - - ); + ); } }