This commit is contained in:
seb
2026-10-31 20:37:21 +01:00
parent cdb031921b
commit 6719cf9d51
7 changed files with 733 additions and 89 deletions

66
src/api/agentprinter.js Normal file
View File

@@ -0,0 +1,66 @@
import { http } from './http.js';
export const agentprinter = {
/**
* GET /api/agentprinter
* Query: ?printeragentId=<id>
* → { agentprinters: Agentprinter[] }
*/
list(printeragentId) {
const url =
printeragentId != null
? `/api/agentprinter?printeragentId=${encodeURIComponent(printeragentId)}`
: '/api/agentprinter';
return http(url);
},
/**
* GET /api/agentprinter/:id
* → { agentprinter: Agentprinter }
*/
get(id) {
return http(`/api/agentprinter/${id}`);
},
/**
* PATCH /api/agentprinter/:id
* Body: { enabled?: boolean, sort?: number }
* → { agentprinter: Agentprinter }
*/
update(id, patch = {}) {
const body = {};
if (patch.enabled !== undefined) body.enabled = Boolean(patch.enabled);
if (patch.sort !== undefined && patch.sort !== null && patch.sort !== '') {
body.sort = Number(patch.sort);
}
return http(`/api/agentprinter/${id}`, {
method: 'PATCH',
body,
});
},
/**
* PUT /api/agentprinter/order
* Body: { printeragentId: number, agentprinterIds: number[] }
* → { agentprinterIds: number[] }
*/
order({ printeragentId, agentprinterIds }) {
return http('/api/agentprinter/order', {
method: 'PUT',
body: {
printeragentId: Number(printeragentId),
agentprinterIds: Array.isArray(agentprinterIds) ? agentprinterIds.map(Number) : [],
},
});
},
/**
* DELETE /api/agentprinter/:id → 204
*/
delete(id) {
return http(`/api/agentprinter/${id}`, {
method: 'DELETE',
});
},
};

View File

@@ -16,6 +16,7 @@ export { order } from './order.js';
export { posterminal } from './posterminal.js'; export { posterminal } from './posterminal.js';
export { printer } from './printer.js'; export { printer } from './printer.js';
export { printeragent } from './printeragent.js'; export { printeragent } from './printeragent.js';
export { agentprinter } from './agentprinter.js';
export { propertyname } from './propertyname.js'; export { propertyname } from './propertyname.js';
export { quote } from './quote.js'; export { quote } from './quote.js';
export { retoure } from './retoure.js'; export { retoure } from './retoure.js';

View File

@@ -51,4 +51,29 @@ export const printer = {
return http('/api/printer/order', { method: 'PUT', body: { printerIds } }); return http('/api/printer/order', { method: 'PUT', body: { printerIds } });
}, },
// #endregion // #endregion
/**
* GET /api/printer/state
* Query: ?serial=, ?agentId=
* → { printers: LivePrinter[] }
*/
// #region GET /api/printer/state — live state
state({ serial, agentId } = {}) {
const params = new URLSearchParams();
if (serial) params.set('serial', serial);
if (agentId != null) params.set('agentId', agentId);
const query = params.toString();
return http(`/api/printer/state${query ? `?${query}` : ''}`);
},
// #endregion
/**
* GET /api/printer/:id/state
* → { printer: LivePrinter }
*/
// #region GET /api/printer/:id/state — single printer state
printerState(id) {
return http(`/api/printer/${id}/state`);
},
// #endregion
}; };

View File

@@ -28,8 +28,8 @@ export const printeragent = {
/** /**
* PATCH /api/printeragent/:id * PATCH /api/printeragent/:id
* Body: { name?: string, sort?: number } * Body: { name?: string, sort?: number, enabled?: boolean }
* Updates name, sort, or both. * Updates name, sort, enabled, or any combination.
* → 200 { printeragent: Printeragent } * → 200 { printeragent: Printeragent }
*/ */
update(id, patch = {}) { update(id, patch = {}) {
@@ -38,6 +38,7 @@ export const printeragent = {
if (patch.sort !== undefined && patch.sort !== null && patch.sort !== '') { if (patch.sort !== undefined && patch.sort !== null && patch.sort !== '') {
body.sort = Number(patch.sort); body.sort = Number(patch.sort);
} }
if (patch.enabled !== undefined) body.enabled = Boolean(patch.enabled);
return http(`/api/printeragent/${id}`, { return http(`/api/printeragent/${id}`, {
method: 'PATCH', method: 'PATCH',
body, body,

View File

@@ -25,7 +25,9 @@ import {
company as companyApi, company as companyApi,
number as numberApi, number as numberApi,
posterminal as posterminalApi, posterminal as posterminalApi,
printer as printerApi,
printeragent as printeragentApi, printeragent as printeragentApi,
agentprinter as agentprinterApi,
supplier as supplierApi, supplier as supplierApi,
unit as unitApi, unit as unitApi,
warehouse as warehouseApi, warehouse as warehouseApi,
@@ -178,6 +180,15 @@ const sortPrinterAgents = (agents) => {
}); });
}; };
const sortAgentPrinters = (printers) => {
if (!Array.isArray(printers)) return [];
return [...printers].sort((a, b) => {
const sDiff = (Number(a.sort) || 0) - (Number(b.sort) || 0);
if (sDiff !== 0) return sDiff;
return (Number(a.id) || 0) - (Number(b.id) || 0);
});
};
const sortAccounts = (items) => const sortAccounts = (items) =>
[...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de')); [...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
@@ -247,12 +258,19 @@ const initialState = {
terminalsStatus: 'idle', terminalsStatus: 'idle',
terminalsError: null, terminalsError: null,
// Printer agents // Printer agents & agent printers
printeragents: [], printeragents: [],
printeragentsCompanyId: null, printeragentsCompanyId: null,
printeragentsStatus: 'idle', printeragentsStatus: 'idle',
printeragentsError: null, printeragentsError: null,
agentprinters: [],
agentprintersCompanyId: null,
agentprintersStatus: 'idle',
agentprintersError: null,
livePrinters: [], // LivePrinter[] from /api/printer/state or SSE printer/printerstate
// 4. Suppliers // 4. Suppliers
suppliers: [], suppliers: [],
articlesBySupplier: {}, articlesBySupplier: {},
@@ -320,6 +338,8 @@ let inflightCompany = null;
let inflightNumbers = null; let inflightNumbers = null;
let inflightTerminals = null; let inflightTerminals = null;
let inflightPrinterAgents = null; let inflightPrinterAgents = null;
let inflightAgentPrinters = null;
let inflightLivePrinters = null;
let inflightSuppliers = null; let inflightSuppliers = null;
let inflightUnits = null; let inflightUnits = null;
let inflightWarehouses = null; let inflightWarehouses = null;
@@ -514,6 +534,8 @@ const actions = {
actions.loadNumbers(), actions.loadNumbers(),
actions.loadTerminals(), actions.loadTerminals(),
actions.loadPrinterAgents(), actions.loadPrinterAgents(),
actions.loadAgentPrinters(),
actions.loadLivePrinters(),
actions.loadSuppliers(), actions.loadSuppliers(),
actions.loadUnits(), actions.loadUnits(),
actions.loadWarehouses(), actions.loadWarehouses(),
@@ -531,6 +553,8 @@ const actions = {
inflightNumbers = null; inflightNumbers = null;
inflightTerminals = null; inflightTerminals = null;
inflightPrinterAgents = null; inflightPrinterAgents = null;
inflightAgentPrinters = null;
inflightLivePrinters = null;
inflightSuppliers = null; inflightSuppliers = null;
inflightUnits = null; inflightUnits = null;
inflightWarehouses = null; inflightWarehouses = null;
@@ -779,6 +803,102 @@ const actions = {
await printeragentApi.delete(agentId); await printeragentApi.delete(agentId);
companyAutomat.setState({ companyAutomat.setState({
printeragents: companyAutomat.state.printeragents.filter((a) => a.id !== agentId), printeragents: companyAutomat.state.printeragents.filter((a) => a.id !== agentId),
agentprinters: companyAutomat.state.agentprinters.filter((p) => p.printeragentId !== agentId),
});
},
// #endregion
// #region Agent printers (agentprinter) & live telemetry
// MARK: calls agentprinter.list from ../api/index.js
async loadAgentPrinters(printeragentId) {
const companyId =
authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
if (!companyId) return [];
if (inflightAgentPrinters) return inflightAgentPrinters;
companyAutomat.setState({ agentprintersStatus: 'loading', agentprintersError: null });
inflightAgentPrinters = (async () => {
try {
const res = await agentprinterApi.list(printeragentId);
const agentprinters = sortAgentPrinters(Array.isArray(res.agentprinters) ? res.agentprinters : []);
companyAutomat.setState({
agentprinters,
agentprintersCompanyId: companyId,
agentprintersStatus: 'succeeded',
agentprintersError: null,
});
return agentprinters;
} catch (err) {
companyAutomat.setState({
agentprintersStatus: 'failed',
agentprintersError: err?.message ?? String(err),
});
throw err;
} finally {
inflightAgentPrinters = null;
}
})();
return inflightAgentPrinters;
},
reloadAgentPrinters(printeragentId) {
return actions.loadAgentPrinters(printeragentId);
},
async updateAgentPrinter(id, patch) {
const res = await agentprinterApi.update(id, patch);
const updated = res.agentprinter;
companyAutomat.setState({
agentprinters: sortAgentPrinters(
companyAutomat.state.agentprinters.map((p) =>
p.id === id ? { ...p, ...updated } : p,
),
),
});
return updated;
},
async reorderAgentPrinters({ printeragentId, agentprinterIds }) {
await agentprinterApi.order({ printeragentId, agentprinterIds });
const idOrder = new Map(agentprinterIds.map((id, index) => [id, index]));
companyAutomat.setState({
agentprinters: sortAgentPrinters(
companyAutomat.state.agentprinters.map((p) =>
idOrder.has(p.id) ? { ...p, sort: idOrder.get(p.id) } : p,
),
),
});
},
async deleteAgentPrinter(id) {
await agentprinterApi.delete(id);
companyAutomat.setState({
agentprinters: companyAutomat.state.agentprinters.filter((p) => p.id !== id),
});
},
async loadLivePrinters() {
const companyId =
authAutomat.state.user?.selectedCompanyId ?? companyAutomat.state.current?.id;
if (!companyId) return [];
if (inflightLivePrinters) return inflightLivePrinters;
inflightLivePrinters = (async () => {
try {
const res = await printerApi.state();
const livePrinters = Array.isArray(res.printers) ? res.printers : [];
companyAutomat.setState({ livePrinters });
return livePrinters;
} catch (err) {
return [];
} finally {
inflightLivePrinters = null;
}
})();
return inflightLivePrinters;
},
setLivePrinters(printers) {
companyAutomat.setState({
livePrinters: Array.isArray(printers) ? printers : [],
}); });
}, },
// #endregion // #endregion
@@ -1641,6 +1761,27 @@ sseAutomat.actions.on('posterminal', () => {
sseAutomat.actions.on('printeragent', () => { sseAutomat.actions.on('printeragent', () => {
void companyAutomat.actions.loadPrinterAgents().catch(() => {}); void companyAutomat.actions.loadPrinterAgents().catch(() => {});
}); });
sseAutomat.actions.on('agentprinter', () => {
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
});
sseAutomat.actions.on('printerstate', (ev) => {
try {
const payload = typeof ev?.data === 'string' ? JSON.parse(ev.data || '{}') : (ev?.data || {});
if (Array.isArray(payload.printers)) {
companyAutomat.actions.setLivePrinters(payload.printers);
return;
}
} catch {}
void companyAutomat.actions.loadLivePrinters().catch(() => {});
});
sseAutomat.actions.on('printer', (ev) => {
try {
const payload = typeof ev?.data === 'string' ? JSON.parse(ev.data || '{}') : (ev?.data || {});
if (Array.isArray(payload.printers)) {
companyAutomat.actions.setLivePrinters(payload.printers);
}
} catch {}
});
sseAutomat.actions.on('customergroup', () => { sseAutomat.actions.on('customergroup', () => {
void companyAutomat.actions.reloadCustomergroups().catch(() => {}); void companyAutomat.actions.reloadCustomergroups().catch(() => {});
}); });
@@ -1686,6 +1827,11 @@ export const selectPrinterAgents = () => companyAutomat.state.printeragents;
export const selectPrinterAgentStatus = () => companyAutomat.state.printeragentsStatus; export const selectPrinterAgentStatus = () => companyAutomat.state.printeragentsStatus;
export const selectHasPrinterAgents = () => companyAutomat.state.printeragents.length > 0; export const selectHasPrinterAgents = () => companyAutomat.state.printeragents.length > 0;
// Agent printers selectors
export const selectAgentPrinters = () => companyAutomat.state.agentprinters;
export const selectAgentPrinterStatus = () => companyAutomat.state.agentprintersStatus;
export const selectLivePrinters = () => companyAutomat.state.livePrinters;
// Suppliers selectors // Suppliers selectors
export const selectSuppliers = () => companyAutomat.state.suppliers; export const selectSuppliers = () => companyAutomat.state.suppliers;
export const selectSuppliersStatus = () => companyAutomat.state.suppliersStatus; export const selectSuppliersStatus = () => companyAutomat.state.suppliersStatus;

View File

@@ -154,6 +154,9 @@ export {
selectPrinterAgents, selectPrinterAgents,
selectPrinterAgentStatus, selectPrinterAgentStatus,
selectHasPrinterAgents, selectHasPrinterAgents,
selectAgentPrinters,
selectAgentPrinterStatus,
selectLivePrinters,
selectSuppliers, selectSuppliers,
selectSuppliersStatus, selectSuppliersStatus,
selectUnits, selectUnits,

View File

@@ -3,21 +3,26 @@ import Box from '@mui/material/Box';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import CheckIcon from '@mui/icons-material/Check'; import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import LocalPrintshopOutlinedIcon from '@mui/icons-material/LocalPrintshopOutlined'; import LocalPrintshopOutlinedIcon from '@mui/icons-material/LocalPrintshopOutlined';
import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined';
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined'; import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import QRCode from 'qrcode';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import QRCode from 'qrcode';
import BackendError from '../BackendError'; import BackendError from '../BackendError';
import companyAutomat from '../../automats/companyAutomat'; import companyAutomat from '../../automats/companyAutomat';
import { backendErrorFrom } from '../../utils/backendError'; import { backendErrorFrom } from '../../utils/backendError';
@@ -114,6 +119,91 @@ class AgentQrCode extends PureComponent {
} }
} }
function renderPrinterDiagnostics(printer, live) {
if (!live) {
return (
<Chip
label="Offline"
size="small"
variant="outlined"
color="default"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
);
}
const errors = live.errors || {};
const errorChips = [];
if (errors.headOpen) {
errorChips.push(
<Chip
key="headOpen"
label="Druckkopf offen"
size="small"
color="error"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errors.pause) {
errorChips.push(
<Chip
key="pause"
label="Pausiert"
size="small"
color="warning"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errors.unreachable) {
errorChips.push(
<Chip
key="unreachable"
label="Nicht erreichbar"
size="small"
color="error"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errors.other) {
errorChips.push(
<Chip
key="other"
label="Fehler"
size="small"
color="error"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>,
);
}
if (errorChips.length > 0) {
return <Box sx={{ display: 'inline-flex', gap: 0.5, flexWrap: 'wrap' }}>{errorChips}</Box>;
}
return (
<Box sx={{ display: 'inline-flex', gap: 0.5, alignItems: 'center', flexWrap: 'wrap' }}>
<Chip
label="Bereit"
size="small"
color="success"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20, fontWeight: 600 }}
/>
{live.labelLength != null ? (
<Chip
label={`Etikett: ${live.labelLength} px`}
size="small"
variant="outlined"
sx={{ fontSize: '0.7rem', height: 20 }}
/>
) : null}
</Box>
);
}
export default class PrinterAgentSettingsPanel extends PureComponent { export default class PrinterAgentSettingsPanel extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@@ -121,6 +211,12 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
printeragents: Array.isArray(companyAutomat.state.printeragents) printeragents: Array.isArray(companyAutomat.state.printeragents)
? companyAutomat.state.printeragents ? companyAutomat.state.printeragents
: [], : [],
agentprinters: Array.isArray(companyAutomat.state.agentprinters)
? companyAutomat.state.agentprinters
: [],
livePrinters: Array.isArray(companyAutomat.state.livePrinters)
? companyAutomat.state.livePrinters
: [],
error: companyAutomat.state.printeragentsError error: companyAutomat.state.printeragentsError
? backendErrorFrom(companyAutomat.state.printeragentsError) ? backendErrorFrom(companyAutomat.state.printeragentsError)
: null, : null,
@@ -135,22 +231,32 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
this.handleRetry = this.handleRetry.bind(this); this.handleRetry = this.handleRetry.bind(this);
this.handleCreateAgent = this.handleCreateAgent.bind(this); this.handleCreateAgent = this.handleCreateAgent.bind(this);
this.handleDeleteAgent = this.handleDeleteAgent.bind(this); this.handleDeleteAgent = this.handleDeleteAgent.bind(this);
this.handleToggleAgentEnabled = this.handleToggleAgentEnabled.bind(this);
this.handleStartRename = this.handleStartRename.bind(this); this.handleStartRename = this.handleStartRename.bind(this);
this.handleCancelRename = this.handleCancelRename.bind(this); this.handleCancelRename = this.handleCancelRename.bind(this);
this.handleSaveRename = this.handleSaveRename.bind(this); this.handleSaveRename = this.handleSaveRename.bind(this);
this.handleSortDraftChange = this.handleSortDraftChange.bind(this); this.handleSortDraftChange = this.handleSortDraftChange.bind(this);
this.handleSortSubmit = this.handleSortSubmit.bind(this); this.handleSortSubmit = this.handleSortSubmit.bind(this);
this.handleMoveAgent = this.handleMoveAgent.bind(this); this.handleMoveAgent = this.handleMoveAgent.bind(this);
this.handleTogglePrinterEnabled = this.handleTogglePrinterEnabled.bind(this);
this.handleMovePrinter = this.handleMovePrinter.bind(this);
this.handleDeletePrinter = this.handleDeletePrinter.bind(this);
} }
componentDidMount() { componentDidMount() {
this.unsub = companyAutomat.subscribe(this, (state) => ({ this.unsub = companyAutomat.subscribe(this, (state) => ({
printeragents: Array.isArray(state.printeragents) ? state.printeragents : [], printeragents: Array.isArray(state.printeragents) ? state.printeragents : [],
agentprinters: Array.isArray(state.agentprinters) ? state.agentprinters : [],
livePrinters: Array.isArray(state.livePrinters) ? state.livePrinters : [],
error: state.printeragentsError ? backendErrorFrom(state.printeragentsError) : null, error: state.printeragentsError ? backendErrorFrom(state.printeragentsError) : null,
})); }));
if (companyAutomat.state.printeragentsStatus === 'idle') { if (companyAutomat.state.printeragentsStatus === 'idle') {
void companyAutomat.actions.loadPrinterAgents().catch(() => { }); void companyAutomat.actions.loadPrinterAgents().catch(() => {});
} }
if (companyAutomat.state.agentprintersStatus === 'idle') {
void companyAutomat.actions.loadAgentPrinters().catch(() => {});
}
void companyAutomat.actions.loadLivePrinters().catch(() => {});
} }
componentWillUnmount() { componentWillUnmount() {
@@ -159,6 +265,8 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
handleRetry() { handleRetry() {
void companyAutomat.actions.loadPrinterAgents().catch(notifyError); void companyAutomat.actions.loadPrinterAgents().catch(notifyError);
void companyAutomat.actions.loadAgentPrinters().catch(notifyError);
void companyAutomat.actions.loadLivePrinters().catch(() => {});
} }
async handleCreateAgent() { async handleCreateAgent() {
@@ -200,6 +308,23 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
} }
} }
async handleToggleAgentEnabled(agent, nextValue) {
this.setState({ patchingAgentId: agent.id });
try {
await companyAutomat.actions.updatePrinterAgent(agent.id, {
enabled: Boolean(nextValue),
});
enqueueSnackbar(
nextValue ? 'Drucker-Agent aktiviert.' : 'Drucker-Agent deaktiviert.',
{ variant: 'success' },
);
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingAgentId: null });
}
}
handleStartRename(agent) { handleStartRename(agent) {
this.setState({ editingId: agent.id, editNameDraft: agent.name || '' }); this.setState({ editingId: agent.id, editNameDraft: agent.name || '' });
} }
@@ -291,10 +416,63 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
} }
} }
async handleTogglePrinterEnabled(printer, nextValue) {
try {
await companyAutomat.actions.updateAgentPrinter(printer.id, {
enabled: Boolean(nextValue),
});
enqueueSnackbar(
nextValue ? 'Drucker aktiviert.' : 'Drucker deaktiviert.',
{ variant: 'success' },
);
} catch (err) {
notifyError(err);
}
}
async handleMovePrinter(printer, direction) {
const { agentprinters } = this.state;
const printersForAgent = agentprinters.filter(
(p) => p.printeragentId === printer.printeragentId,
);
const currentIndex = printersForAgent.findIndex((p) => p.id === printer.id);
if (currentIndex === -1) return;
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (targetIndex < 0 || targetIndex >= printersForAgent.length) return;
const reordered = [...printersForAgent];
const [moved] = reordered.splice(currentIndex, 1);
reordered.splice(targetIndex, 0, moved);
try {
await companyAutomat.actions.reorderAgentPrinters({
printeragentId: printer.printeragentId,
agentprinterIds: reordered.map((p) => p.id),
});
} catch (err) {
notifyError(err);
}
}
async handleDeletePrinter(printer) {
const label = printer.model || printer.serial || `#${printer.id}`;
if (!window.confirm(`Möchten Sie den Drucker „${label}“ wirklich entfernen?`)) {
return;
}
try {
await companyAutomat.actions.deleteAgentPrinter(printer.id);
enqueueSnackbar('Drucker entfernt.', { variant: 'success' });
} catch (err) {
notifyError(err);
}
}
render() { render() {
const { const {
error, error,
printeragents, printeragents,
agentprinters,
livePrinters,
newAgentName, newAgentName,
creatingAgent, creatingAgent,
deletingId, deletingId,
@@ -311,6 +489,12 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
); );
} }
// Build lookup maps for fast access
const livePrintersBySerial = new Map();
for (const lp of livePrinters) {
if (lp.serial) livePrintersBySerial.set(lp.serial, lp);
}
return ( return (
<Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}> <Box sx={{ p: 3, maxWidth: 900, mx: 'auto' }}>
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
@@ -318,8 +502,9 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
Drucker-Agenten Drucker-Agenten
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Verwalten Sie externe Drucker-Agenten für Druckaufträge. Nach Erstellung eines Agenten Verwalten Sie externe Drucker-Agenten und die daran angeschlossenen Drucker. Nach
geben Sie die 6-stellige PIN auf dem Agenten-Gerät ein, um die Kopplung abzuschließen. Erstellung eines Agenten geben Sie die 6-stellige PIN auf dem Gerät ein oder scannen
Sie den QR-Code, um die Kopplung abzuschließen.
</Typography> </Typography>
</Box> </Box>
@@ -327,7 +512,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<Box sx={{ display: 'flex', gap: 1.5, mb: 3, alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: 1.5, mb: 3, alignItems: 'center' }}>
<TextField <TextField
size="small" size="small"
placeholder="Neuer Drucker-Agent Name (z.B. Bar Receipt Printer)" placeholder="Neuer Drucker-Agent Name (z.B. Bar, Küche)"
value={newAgentName} value={newAgentName}
onChange={(e) => this.setState({ newAgentName: e.target.value })} onChange={(e) => this.setState({ newAgentName: e.target.value })}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -346,7 +531,9 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
spellCheck: false, spellCheck: false,
}, },
input: { input: {
startAdornment: <LocalPrintshopOutlinedIcon sx={{ mr: 1, color: 'text.secondary' }} />, startAdornment: (
<LocalPrintshopOutlinedIcon sx={{ mr: 1, color: 'text.secondary' }} />
),
}, },
}} }}
sx={{ flex: 1 }} sx={{ flex: 1 }}
@@ -370,16 +557,23 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
<Typography variant="body2">Keine Drucker-Agenten angelegt.</Typography> <Typography variant="body2">Keine Drucker-Agenten angelegt.</Typography>
</Box> </Box>
) : ( ) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2.5 }}>
{printeragents.map((agent, index) => { {printeragents.map((agent, index) => {
const isDeleting = deletingId === agent.id; const isDeleting = deletingId === agent.id;
const isPatching = patchingAgentId === agent.id; const isPatching = patchingAgentId === agent.id;
const isEditing = editingId === agent.id; const isEditing = editingId === agent.id;
const isPaired = agent.pairedAt != null; const isPaired = agent.pairedAt != null;
const isEnabled = agent.enabled !== false;
const clientState = agent.clientstate;
const isOnline = clientState === 'online';
const pairedDateText = formatDateTime(agent.pairedAt); const pairedDateText = formatDateTime(agent.pairedAt);
const lastseenText = formatDateTime(agent.lastseenAt) ?? 'Nie'; const lastseenText = formatDateTime(agent.lastseenAt) ?? 'Nie';
const updatedText = formatDateTime(agent.updatedAt) ?? '—'; const updatedText = formatDateTime(agent.updatedAt) ?? '—';
const printersForAgent = agentprinters.filter(
(p) => p.printeragentId === agent.id,
);
return ( return (
<Box <Box
key={agent.id} key={agent.id}
@@ -387,15 +581,20 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
p: 2, p: 2,
borderRadius: 2, borderRadius: 2,
border: '1px solid', border: '1px solid',
borderColor: isPaired ? 'divider' : 'warning.light', borderColor: !isEnabled
? 'action.disabledBackground'
: isPaired
? 'divider'
: 'warning.light',
bgcolor: 'background.paper', bgcolor: 'background.paper',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 1.5, gap: 1.5,
transition: 'border-color 0.2s ease', opacity: isEnabled ? 1 : 0.72,
transition: 'opacity 0.2s ease, border-color 0.2s ease',
}} }}
> >
{/* Header: Icon, Name (or Edit input), Status chip, Actions */} {/* Header: Icon, Name (or Edit input), Status chips, Enabled switch, Delete */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -407,7 +606,14 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 260 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, minWidth: 260 }}>
<LocalPrintshopOutlinedIcon <LocalPrintshopOutlinedIcon
sx={{ color: isPaired ? 'primary.main' : 'warning.main', fontSize: 24 }} sx={{
color: !isEnabled
? 'text.disabled'
: isPaired
? 'primary.main'
: 'warning.main',
fontSize: 24,
}}
/> />
{isEditing ? ( {isEditing ? (
@@ -455,7 +661,11 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box> </Box>
) : ( ) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle1" fontWeight={600}> <Typography
variant="subtitle1"
fontWeight={600}
sx={{ color: isEnabled ? 'text.primary' : 'text.disabled' }}
>
{agent.name} {agent.name}
</Typography> </Typography>
<IconButton <IconButton
@@ -469,6 +679,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box> </Box>
)} )}
{/* Pairing status badge */}
{isPaired ? ( {isPaired ? (
<Chip <Chip
icon={<CheckCircleOutlinedIcon />} icon={<CheckCircleOutlinedIcon />}
@@ -488,10 +699,59 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
sx={{ fontWeight: 600, fontSize: '0.75rem' }} sx={{ fontWeight: 600, fontSize: '0.75rem' }}
/> />
)} )}
{/* Live connection state */}
{clientState ? (
<Chip
label={isOnline ? 'Online' : 'Offline'}
size="small"
variant="outlined"
color={isOnline ? 'success' : 'default'}
sx={{ fontSize: '0.75rem', fontWeight: 600 }}
/>
) : null}
{/* Agent version */}
{agent.agentversion ? (
<Tooltip title="Agent Software-Version">
<Chip
label={`v${agent.agentversion}`}
size="small"
variant="outlined"
color="info"
sx={{ fontFamily: 'monospace', fontWeight: 600, fontSize: '0.75rem' }}
/>
</Tooltip>
) : null}
</Box> </Box>
{/* Delete control */} {/* Enabled Switch and Delete action */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControlLabel
control={
<Switch
size="small"
checked={isEnabled}
disabled={isPatching || isDeleting}
onChange={(e) => void this.handleToggleAgentEnabled(agent, e.target.checked)}
color="success"
/>
}
label={
<Typography
variant="body2"
sx={{
fontSize: '0.8125rem',
fontWeight: 500,
color: isEnabled ? 'text.primary' : 'text.disabled',
}}
>
{isEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
<IconButton <IconButton
size="small" size="small"
aria-label={`Drucker-Agent ${agent.name} löschen`} aria-label={`Drucker-Agent ${agent.name} löschen`}
@@ -504,7 +764,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
</Box> </Box>
</Box> </Box>
{/* Body: Pairing details with QR code or Paired status info */} {/* Body: Pairing details with QR code or Discovered Printers */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -534,27 +794,39 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
borderColor: 'warning.light', borderColor: 'warning.light',
}} }}
> >
<Box sx={{ display: 'inline-flex' }}> <Tooltip title="QR-Code zum Koppeln mit dem Drucker-Agenten">
<AgentQrCode value={qrPayload} size={110} /> <Box sx={{ display: 'inline-flex' }}>
</Box> <AgentQrCode value={qrPayload} size={110} />
</Box>
</Tooltip>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 220 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minWidth: 220 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}> <Typography variant="body2" color="text.secondary" sx={{ fontWeight: 500 }}>
Kopplungs-PIN: Kopplungs-PIN:
</Typography> </Typography>
<Chip <Tooltip title="PIN in die Zwischenablage kopieren">
label={agent.pairingpin || '—'} <Chip
color="warning" icon={<ContentCopyIcon sx={{ fontSize: 14 }} />}
variant="filled" label={agent.pairingpin || '—'}
sx={{ onClick={() => {
fontFamily: 'monospace', if (agent.pairingpin) {
fontWeight: 700, navigator.clipboard?.writeText(agent.pairingpin);
fontSize: '0.9rem', enqueueSnackbar('PIN in Zwischenablage kopiert.', { variant: 'info' });
letterSpacing: '0.1em', }
px: 0.5, }}
}} clickable
/> color="warning"
variant="filled"
sx={{
fontFamily: 'monospace',
fontWeight: 700,
fontSize: '0.9rem',
letterSpacing: '0.1em',
px: 0.5,
}}
/>
</Tooltip>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
@@ -567,75 +839,205 @@ export default class PrinterAgentSettingsPanel extends PureComponent {
variant="outlined" variant="outlined"
sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }} sx={{ fontFamily: 'monospace', fontSize: '0.8rem' }}
/> />
<Tooltip title="QR-Daten (JSON) kopieren">
<IconButton
size="small"
onClick={() => {
navigator.clipboard?.writeText(qrPayload);
enqueueSnackbar('QR-Daten kopiert.', { variant: 'info' });
}}
sx={{ p: 0.5 }}
>
<ContentCopyIcon sx={{ fontSize: 14 }} />
</IconButton>
</Tooltip>
</Box> </Box>
<Typography variant="caption" color="text.secondary">
Scannen Sie den QR-Code mit dem Drucker-Agenten oder geben Sie Domain und PIN manuell ein.
</Typography>
</Box> </Box>
</Box> </Box>
); );
})() })()
) : ( ) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}> /* Discovered printers section */
{agent.clientserial ? ( <Box sx={{ p: 1.5, borderRadius: 1.5, bgcolor: 'action.hover' }}>
<Chip <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
label={`S/N: ${agent.clientserial}`} <Typography variant="subtitle2" fontWeight={600} color="text.secondary">
size="small" Erkannte Drucker ({printersForAgent.length})
variant="outlined" </Typography>
sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }} </Box>
/> {printersForAgent.length === 0 ? (
) : null} <Typography
{agent.clientmodel ? ( variant="body2"
<Chip color="text.secondary"
label={`Modell: ${agent.clientmodel}`} sx={{ fontStyle: 'italic', py: 0.5 }}
size="small" >
variant="outlined" Keine Drucker erkannt. Schließen Sie einen Drucker per USB an den Agenten an.
sx={{ fontSize: '0.75rem' }} </Typography>
/> ) : (
) : null} <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{agent.agentversion ? ( {printersForAgent.map((p, pIdx) => {
<Chip const pEnabled = p.enabled !== false;
label={`v${agent.agentversion}`} const live =
size="small" (p.serial ? livePrintersBySerial.get(p.serial) : null) ??
variant="outlined" livePrinters.find(
color="info" (lp) => lp.agentId === agent.id && lp.serial === p.serial,
sx={{ fontFamily: 'monospace', fontWeight: 600, fontSize: '0.75rem' }} );
/>
) : null} return (
{agent.clientstate ? ( <Box
<Chip key={p.id}
label={`Status: ${agent.clientstate}`} sx={{
size="small" p: 1.25,
variant="outlined" borderRadius: 1,
sx={{ fontSize: '0.75rem' }} border: '1px solid',
/> borderColor: 'divider',
) : null} bgcolor: 'background.paper',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 1.5,
flexWrap: 'wrap',
opacity: pEnabled ? 1 : 0.65,
transition: 'opacity 0.2s ease',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<PrintOutlinedIcon sx={{ fontSize: 20, color: 'text.secondary' }} />
<Typography variant="body2" fontWeight={600}>
{p.model || 'Drucker'}
</Typography>
{p.type ? (
<Chip
label={p.type.toUpperCase()}
size="small"
color="primary"
variant="outlined"
sx={{ fontWeight: 600, fontSize: '0.7rem', height: 20 }}
/>
) : null}
{p.serial ? (
<Typography
variant="caption"
color="text.secondary"
sx={{ fontFamily: 'monospace' }}
>
({p.serial})
</Typography>
) : null}
{renderPrinterDiagnostics(p, live)}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Enabled toggle switch */}
<FormControlLabel
control={
<Switch
size="small"
checked={pEnabled}
onChange={(e) =>
void this.handleTogglePrinterEnabled(p, e.target.checked)
}
/>
}
label={
<Typography
variant="caption"
color={pEnabled ? 'text.primary' : 'text.disabled'}
>
{pEnabled ? 'Aktiv' : 'Inaktiv'}
</Typography>
}
sx={{ m: 0 }}
/>
{/* Reorder buttons */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title="Nach oben verschieben">
<span>
<IconButton
size="small"
disabled={pIdx === 0}
onClick={() => void this.handleMovePrinter(p, 'up')}
sx={{ p: 0.25 }}
>
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Nach unten verschieben">
<span>
<IconButton
size="small"
disabled={pIdx === printersForAgent.length - 1}
onClick={() => void this.handleMovePrinter(p, 'down')}
sx={{ p: 0.25 }}
>
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
</Box>
{/* Delete printer button */}
<Tooltip title="Drucker entfernen">
<IconButton
size="small"
color="error"
onClick={() => void this.handleDeletePrinter(p)}
sx={{ p: 0.5 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
</IconButton>
</Tooltip>
</Box>
</Box>
);
})}
</Box>
)}
</Box> </Box>
)} )}
{/* Row with Sort Order Controls */} {/* Agent sort order controls */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5, pt: 0.5 }}> <Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: 0.5,
pt: 0.5,
}}
>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Reihenfolge: Reihenfolge:
</Typography> </Typography>
<span> <Tooltip title="Nach oben verschieben">
<IconButton <span>
size="small" <IconButton
disabled={index === 0 || isPatching || isDeleting} size="small"
onClick={() => void this.handleMoveAgent(agent, 'up')} disabled={index === 0 || isPatching || isDeleting}
sx={{ p: 0.25 }} onClick={() => void this.handleMoveAgent(agent, 'up')}
> sx={{ p: 0.25 }}
<ArrowUpwardIcon sx={{ fontSize: 16 }} /> >
</IconButton> <ArrowUpwardIcon sx={{ fontSize: 16 }} />
</span> </IconButton>
</span>
<span> </Tooltip>
<IconButton <Tooltip title="Nach unten verschieben">
size="small" <span>
disabled={index === printeragents.length - 1 || isPatching || isDeleting} <IconButton
onClick={() => void this.handleMoveAgent(agent, 'down')} size="small"
sx={{ p: 0.25 }} disabled={index === printeragents.length - 1 || isPatching || isDeleting}
> onClick={() => void this.handleMoveAgent(agent, 'down')}
<ArrowDownwardIcon sx={{ fontSize: 16 }} /> sx={{ p: 0.25 }}
</IconButton> >
</span> <ArrowDownwardIcon sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
<TextField <TextField
type="number" type="number"
size="small" size="small"