import { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import FormControlLabel from '@mui/material/FormControlLabel';
import IconButton from '@mui/material/IconButton';
import Switch from '@mui/material/Switch';
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';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import CategoryPickerDialog from '../article/CategoryPickerDialog';
import authAutomat from '../../automats/authAutomat';
import companyAutomat from '../../automats/companyAutomat';
import categoryAutomat, { whenCategoryTreeReady } from '../../automats/categoryAutomat';
import { categoryDisplayName } from '../../utils/categoryUtils';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
function parseVhostSettings(str) {
if (!str || typeof str !== 'string') return {};
const result = {};
for (const part of str.split(';')) {
const trimmed = part.trim();
if (!trimmed) continue;
const colonIdx = trimmed.indexOf(':');
if (colonIdx !== -1) {
const key = trimmed.slice(0, colonIdx).trim().toLowerCase();
const val = trimmed.slice(colonIdx + 1).trim();
result[key] = val;
}
}
return result;
}
function formatDateTime(val) {
if (!val) return null;
try {
const d = new Date(val);
if (isNaN(d.getTime())) return null;
return d.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return null;
}
}
export default class PosTerminalSettingsPanel extends Component {
constructor(props) {
super(props);
const terminals = companyAutomat.state.terminals ?? [];
this.state = {
isAdmin: companyAutomat.state.current?.role === 'admin',
status: companyAutomat.state.terminalsStatus === 'succeeded' ? 'ready' : 'loading',
error: null,
terminals,
categories: categoryAutomat.state.categories,
rootCategoryId: categoryAutomat.state.rootCategoryId,
vhostSettings: authAutomat.state.vhostSettings,
newTerminalName: '',
creatingTerminal: false,
enableAllForNew: true,
deletingId: null,
pickerTerminal: null,
patchingTerminalId: null,
sortDrafts: {},
};
this.handleRetry = this.handleRetry.bind(this);
this.loadTerminals = this.loadTerminals.bind(this);
this.handleCreateTerminal = this.handleCreateTerminal.bind(this);
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);
}
componentDidMount() {
this.unsub = companyAutomat.subscribe(this, (state) => ({
isAdmin: state.current?.role === 'admin',
terminals: Array.isArray(state.terminals) ? state.terminals : [],
status: state.terminalsStatus === 'succeeded' ? 'ready' : state.terminalsStatus === 'failed' ? 'failed' : this.state.status,
}));
this.unsubCategory = categoryAutomat.subscribe(this, (state) => ({
categories: state.categories,
rootCategoryId: state.rootCategoryId,
}));
this.unsubAuth = authAutomat.subscribe(this, (state) => ({
vhostSettings: state.vhostSettings,
}));
if (categoryAutomat.state.treeStatus === 'idle') {
void categoryAutomat.actions.loadTree();
}
}
componentWillUnmount() {
this.unsub?.();
this.unsubCategory?.();
this.unsubAuth?.();
}
async loadTerminals() {
this.setState({ error: null });
try {
const terminals = await companyAutomat.actions.loadTerminals();
this.setState({
status: 'ready',
terminals: Array.isArray(terminals) ? terminals : [],
});
} catch (err) {
notifyError(err);
this.setState({ status: 'failed', error: backendErrorFrom(err) });
}
}
handleRetry() {
void this.loadTerminals();
}
async handleCreateTerminal() {
if (!this.state.isAdmin) return;
const { newTerminalName, enableAllForNew, terminals } = this.state;
const trimmed = newTerminalName.trim();
if (!trimmed) return;
this.setState({ creatingTerminal: true });
try {
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 });
await whenCategoryTreeReady();
}
this.setState({ newTerminalName: '' });
enqueueSnackbar('Terminal hinzugefügt.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ creatingTerminal: false });
}
}
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 {
await companyAutomat.actions.deleteTerminal(terminal.id);
enqueueSnackbar('Terminal gelöscht.', { variant: 'info' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ deletingId: null });
}
}
async handleSetRootCategory(terminal, categoryId) {
this.setState({ patchingTerminalId: terminal.id });
try {
await companyAutomat.actions.updateTerminal(terminal.id, {
rootCategoryId: categoryId != null ? Number(categoryId) : null,
});
enqueueSnackbar('Root-Kategorie aktualisiert.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingTerminalId: null });
}
}
async handleToggleSyncRootArticles(terminal, nextValue) {
this.setState({ patchingTerminalId: terminal.id });
try {
await companyAutomat.actions.updateTerminal(terminal.id, {
syncRootArticles: Boolean(nextValue),
});
enqueueSnackbar('Synchronisierungseinstellung gespeichert.', { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
this.setState({ patchingTerminalId: null });
}
}
getCategoryLabel(terminal) {
const { categories, rootCategoryId } = this.state;
if (terminal.rootCategoryId == null) {
return 'Standard (Home)';
}
const node = categories?.[String(terminal.rootCategoryId)];
if (!node) {
return `Kategorie #${terminal.rootCategoryId}`;
}
return categoryDisplayName(node.name, node.id, rootCategoryId) || `Kategorie #${terminal.rootCategoryId}`;
}
render() {
const {
isAdmin,
status,
error,
terminals,
vhostSettings,
newTerminalName,
creatingTerminal,
enableAllForNew,
deletingId,
pickerTerminal,
patchingTerminalId,
rootCategoryId,
} = this.state;
const parsedVhost = parseVhostSettings(vhostSettings);
const posIp = parsedVhost.posip || parsedVhost.ip || null;
const posPort = parsedVhost.posport || parsedVhost.port || null;
const hasVhostSettings = Boolean(posIp || posPort || vhostSettings);
if (status === 'loading' && terminals.length === 0) {
return (
);
}
if (status === 'failed' && terminals.length === 0) {
return ;
}
return (
{hasVhostSettings && (
Pos Sync IP:{' '}
{posIp ?? '—'}
Pos Sync Port:{' '}
{posPort ?? '—'}
)}
this.setState({ newTerminalName: e.target.value })}
disabled={!isAdmin || creatingTerminal}
autoComplete="off"
slotProps={{
htmlInput: {
autoComplete: 'off',
autoCorrect: 'off',
autoCapitalize: 'off',
spellCheck: false,
},
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void this.handleCreateTerminal();
}
}}
sx={{ flex: 1 }}
/>
this.setState({ enableAllForNew: e.target.checked })}
disabled={!isAdmin || creatingTerminal}
/>
: }
onClick={this.handleCreateTerminal}
disabled={!isAdmin || creatingTerminal || !newTerminalName.trim()}
>
Hinzufügen
{terminals.length === 0 ? (
Keine POS Terminals angelegt.
) : (
{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) ?? '—';
return (
{/* Header: Icon, Name, PIN, Version, Status Switch, Delete action */}
{terminal.name}
{terminal.pairingpin ? (
) : (
)}
{terminal.clientserial ? (
({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={!isAdmin || isDeleting || creatingTerminal || isPatching}
color="error"
>
{isDeleting ? (
) : (
)}
{/* Settings row: Root category picker, sort order controls, and syncRootArticles toggle */}
Root-Kategorie:
}
label={this.getCategoryLabel(terminal)}
onClick={isAdmin ? () => this.setState({ pickerTerminal: terminal }) : undefined}
onDelete={
isAdmin && terminal.rootCategoryId != null
? () => void this.handleSetRootCategory(terminal, null)
: undefined
}
clickable={isAdmin}
disabled={!isAdmin || isPatching || isDeleting}
variant={terminal.rootCategoryId != null ? 'filled' : 'outlined'}
color={terminal.rootCategoryId != null ? 'primary' : 'default'}
size="small"
/>
Reihenfolge:
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 */}
{pairedDateText && (
Gekoppelt: {pairedDateText}
)}
Zuletzt online: {lastseenText}
{terminal.clientversion && (
POS-Software: {terminal.clientversion}
)}
Aktualisiert: {updatedText}
);
})}
)}
{pickerTerminal && (
this.setState({ pickerTerminal: null })}
mode="single"
title={`Root-Kategorie für „${pickerTerminal.name}“`}
confirmLabel="Übernehmen"
selectedId={pickerTerminal.rootCategoryId ?? rootCategoryId}
onSelectCategory={(targetId) => {
const term = pickerTerminal;
this.setState({ pickerTerminal: null }, () => {
void this.handleSetRootCategory(term, targetId);
});
}}
/>
)}
);
}
}