diff --git a/src/api/posterminal.js b/src/api/posterminal.js index 0aa4f00..a30bda9 100644 --- a/src/api/posterminal.js +++ b/src/api/posterminal.js @@ -2,8 +2,8 @@ import { http } from './http.js'; export const posterminal = { /** - * GET /api/posterminal → { posterminals: [{ id, name, token, pairingpin }] } - * List all terminals for the selected company. + * GET /api/posterminal → { posterminals: [{ id, name, token, pairingpin, sort, enabled, clientversion, ... }] } + * List all terminals for the selected company, sorted by sort ASC, id ASC. */ // #region GET /api/posterminal — list list() { @@ -13,24 +13,29 @@ export const posterminal = { /** * PUT /api/posterminal - * Body: { name: string } + * Body: { name: string, sort?: number, enabled?: boolean, rootCategoryId?: number|null, syncRootArticles?: boolean } * Creates a new terminal (auto-generates 6-digit pin). - * → 200 { posterminal: { id, name, token, pairingpin } } + * → 200 { posterminal: { id, name, token, pairingpin, sort, enabled, clientversion, ... } } */ // #region PUT /api/posterminal — create - create({ name }) { + create({ name, sort, enabled, rootCategoryId, syncRootArticles } = {}) { + const body = { name }; + if (sort !== undefined) body.sort = Number(sort); + if (enabled !== undefined) body.enabled = Boolean(enabled); + if (rootCategoryId !== undefined) body.rootCategoryId = rootCategoryId != null ? Number(rootCategoryId) : null; + if (syncRootArticles !== undefined) body.syncRootArticles = Boolean(syncRootArticles); return http('/api/posterminal', { method: 'PUT', - body: { name }, + body, }); }, // #endregion /** * PATCH /api/posterminal/:id - * Body: { name?: string, rootCategoryId?: number|null, syncRootArticles?: boolean, pairingpin?: string } + * Body: { name?: string, rootCategoryId?: number|null, syncRootArticles?: boolean, pairingpin?: string, sort?: number, enabled?: boolean } * Update posterminal configuration. - * → 200 { posterminal } + * → 200 { posterminal: { id, name, token, pairingpin, sort, enabled, clientversion, ... } } */ // #region PATCH /api/posterminal/:id — update update(terminalId, patch) { diff --git a/src/automats/companyAutomat.js b/src/automats/companyAutomat.js index 657b328..eb2b243 100644 --- a/src/automats/companyAutomat.js +++ b/src/automats/companyAutomat.js @@ -159,6 +159,15 @@ const sortBrands = (brands) => const sortGroups = (items) => [...items].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0)); +const sortTerminals = (terminals) => { + if (!Array.isArray(terminals)) return []; + return [...terminals].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) => [...items].sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de')); @@ -578,7 +587,7 @@ const actions = { inflightTerminals = (async () => { try { const res = await posterminalApi.list(); - const terminals = Array.isArray(res.posterminals) ? res.posterminals : []; + const terminals = sortTerminals(Array.isArray(res.posterminals) ? res.posterminals : []); companyAutomat.setState({ terminals, terminalsCompanyId: companyId, @@ -603,11 +612,15 @@ const actions = { return actions.loadTerminals(); }, - async createTerminal({ name }) { - const res = await posterminalApi.create({ name }); + reloadTerminals() { + return actions.loadTerminals(); + }, + + async createTerminal(payload) { + const res = await posterminalApi.create(payload); const terminal = res.posterminal; companyAutomat.setState({ - terminals: [...companyAutomat.state.terminals, terminal], + terminals: sortTerminals([...companyAutomat.state.terminals, terminal]), }); return terminal; }, @@ -620,8 +633,10 @@ const actions = { const res = await posterminalApi.update(terminalId, patch); const updated = res.posterminal; companyAutomat.setState({ - terminals: companyAutomat.state.terminals.map((t) => - t.id === terminalId ? { ...t, ...updated } : t, + terminals: sortTerminals( + companyAutomat.state.terminals.map((t) => + t.id === terminalId ? { ...t, ...updated } : t, + ), ), }); return updated; diff --git a/src/components/settings/PosTerminalSettingsPanel.jsx b/src/components/settings/PosTerminalSettingsPanel.jsx index 2bb648c..2fb5ed8 100644 --- a/src/components/settings/PosTerminalSettingsPanel.jsx +++ b/src/components/settings/PosTerminalSettingsPanel.jsx @@ -11,6 +11,8 @@ import TextField from '@mui/material/TextField'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import AddIcon from '@mui/icons-material/Add'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import FolderIcon from '@mui/icons-material/Folder'; import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; @@ -56,6 +58,7 @@ export default class PosTerminalSettingsPanel extends Component { deletingId: null, pickerTerminal: null, patchingTerminalId: null, + sortDrafts: {}, }; this.handleRetry = this.handleRetry.bind(this); this.loadTerminals = this.loadTerminals.bind(this); @@ -63,6 +66,10 @@ export default class PosTerminalSettingsPanel extends Component { this.handleDeleteTerminal = this.handleDeleteTerminal.bind(this); this.handleSetRootCategory = this.handleSetRootCategory.bind(this); this.handleToggleSyncRootArticles = this.handleToggleSyncRootArticles.bind(this); + this.handleToggleEnabled = this.handleToggleEnabled.bind(this); + this.handleSortDraftChange = this.handleSortDraftChange.bind(this); + this.handleSortSubmit = this.handleSortSubmit.bind(this); + this.handleMoveTerminal = this.handleMoveTerminal.bind(this); this.getCategoryLabel = this.getCategoryLabel.bind(this); } @@ -104,13 +111,20 @@ export default class PosTerminalSettingsPanel extends Component { } async handleCreateTerminal() { - const { newTerminalName, enableAllForNew } = this.state; + const { newTerminalName, enableAllForNew, terminals } = this.state; const trimmed = newTerminalName.trim(); if (!trimmed) return; this.setState({ creatingTerminal: true }); try { - const created = await companyAutomat.actions.createTerminal({ name: trimmed }); + const nextSort = terminals.length > 0 + ? Math.max(...terminals.map((t) => Number(t.sort) || 0)) + 1 + : 0; + const created = await companyAutomat.actions.createTerminal({ + name: trimmed, + sort: nextSort, + enabled: true, + }); if (enableAllForNew && created?.id != null) { await companyAutomat.actions.enableAllTerminalCategories({ terminalId: created.id }); @@ -126,6 +140,84 @@ export default class PosTerminalSettingsPanel extends Component { } } + async handleToggleEnabled(terminal, nextValue) { + this.setState({ patchingTerminalId: terminal.id }); + try { + await companyAutomat.actions.updateTerminal(terminal.id, { + enabled: Boolean(nextValue), + }); + enqueueSnackbar(nextValue ? 'Terminal aktiviert.' : 'Terminal deaktiviert.', { + variant: 'success', + }); + } catch (err) { + notifyError(err); + } finally { + this.setState({ patchingTerminalId: null }); + } + } + + handleSortDraftChange(terminalId, value) { + this.setState((prev) => ({ + sortDrafts: { ...prev.sortDrafts, [terminalId]: value }, + })); + } + + async handleSortSubmit(terminal) { + const draft = this.state.sortDrafts[terminal.id]; + if (draft === undefined) return; + const nextSort = Number(draft); + if (isNaN(nextSort) || nextSort === (terminal.sort ?? 0)) { + this.setState((prev) => { + const next = { ...prev.sortDrafts }; + delete next[terminal.id]; + return { sortDrafts: next }; + }); + return; + } + this.setState({ patchingTerminalId: terminal.id }); + try { + await companyAutomat.actions.updateTerminal(terminal.id, { sort: nextSort }); + enqueueSnackbar('Reihenfolge aktualisiert.', { variant: 'success' }); + } catch (err) { + notifyError(err); + } finally { + this.setState((prev) => { + const next = { ...prev.sortDrafts }; + delete next[terminal.id]; + return { sortDrafts: next, patchingTerminalId: null }; + }); + } + } + + async handleMoveTerminal(terminal, direction) { + const { terminals } = this.state; + const currentIndex = terminals.findIndex((t) => t.id === terminal.id); + if (currentIndex === -1) return; + const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1; + if (targetIndex < 0 || targetIndex >= terminals.length) return; + + const other = terminals[targetIndex]; + const currentSort = terminal.sort ?? 0; + const otherSort = other.sort ?? 0; + + let newSort; + if (direction === 'up') { + newSort = otherSort <= currentSort ? otherSort - 1 : otherSort; + } else { + newSort = otherSort >= currentSort ? otherSort + 1 : otherSort; + } + + this.setState({ patchingTerminalId: terminal.id }); + try { + await companyAutomat.actions.updateTerminal(terminal.id, { sort: newSort }); + enqueueSnackbar('Reihenfolge aktualisiert.', { variant: 'success' }); + } catch (err) { + notifyError(err); + } finally { + this.setState({ patchingTerminalId: null }); + } + } + async handleDeleteTerminal(terminal) { this.setState({ deletingId: terminal.id }); try { @@ -261,9 +353,10 @@ export default class PosTerminalSettingsPanel extends Component { ) : ( - {terminals.map((terminal) => { + {terminals.map((terminal, index) => { const isDeleting = deletingId === terminal.id; const isPatching = patchingTerminalId === terminal.id; + const isEnabled = terminal.enabled !== false && terminal.enabled !== 0 && terminal.enabled != null; const pairedDateText = formatDateTime(terminal.pairedAt); const lastseenText = formatDateTime(terminal.lastseenAt) ?? 'Nie'; const updatedText = formatDateTime(terminal.updatedAt) ?? '—'; @@ -275,18 +368,24 @@ export default class PosTerminalSettingsPanel extends Component { p: 2, borderRadius: 2, border: '1px solid', - borderColor: 'divider', + borderColor: isEnabled ? 'divider' : 'action.disabledBackground', bgcolor: 'background.paper', display: 'flex', flexDirection: 'column', gap: 1.5, + opacity: isEnabled ? 1 : 0.72, + transition: 'opacity 0.2s ease, border-color 0.2s ease', }} > - {/* Header: Icon, Name, PIN, Delete action */} + {/* Header: Icon, Name, PIN, Version, Status Switch, Delete action */} - - + + {terminal.name} {terminal.pairingpin ? ( @@ -310,23 +409,68 @@ export default class PosTerminalSettingsPanel extends Component { ({terminal.clientserial}) ) : null} + {terminal.clientversion ? ( + + + + ) : null} + + + + void this.handleToggleEnabled(terminal, e.target.checked)} + color="success" + /> + } + label={ + + {isEnabled ? 'Aktiv' : 'Inaktiv'} + + } + sx={{ m: 0 }} + /> + + void this.handleDeleteTerminal(terminal)} + disabled={isDeleting || creatingTerminal || isPatching} + color="error" + > + {isDeleting ? ( + + ) : ( + + )} + - void this.handleDeleteTerminal(terminal)} - disabled={isDeleting || creatingTerminal || isPatching} - color="error" - > - {isDeleting ? ( - - ) : ( - - )} - - {/* Settings row: Root category picker and syncRootArticles toggle */} + {/* Settings row: Root category picker, sort order controls, and syncRootArticles toggle */} - void this.handleToggleSyncRootArticles(terminal, e.target.checked)} - /> - } - label={ - - Root-Artikel synchronisieren + + + + Reihenfolge: - } - sx={{ m: 0 }} - /> + + + void this.handleMoveTerminal(terminal, 'up')} + sx={{ p: 0.25 }} + > + + + + + + + void this.handleMoveTerminal(terminal, 'down')} + sx={{ p: 0.25 }} + > + + + + + this.handleSortDraftChange(terminal.id, e.target.value)} + onBlur={() => void this.handleSortSubmit(terminal)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + e.target.blur(); + } + }} + slotProps={{ + htmlInput: { + step: 1, + style: { width: 44, padding: '3px 6px', textAlign: 'center', fontSize: '0.8125rem' }, + }, + }} + sx={{ width: 56 }} + /> + + + void this.handleToggleSyncRootArticles(terminal, e.target.checked)} + /> + } + label={ + + Root-Artikel synchronisieren + + } + sx={{ m: 0 }} + /> + {/* Dates row */} @@ -402,6 +599,11 @@ export default class PosTerminalSettingsPanel extends Component { Zuletzt online: {lastseenText} + {terminal.clientversion && ( + + POS-Software: {terminal.clientversion} + + )} Aktualisiert: {updatedText}