From 559307675c86cc59a36968fe9037ccca81446925 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 27 Sep 2026 07:08:13 +0200 Subject: [PATCH] u --- package-lock.json | 7 + package.json | 1 + src/api/ApiClient.js | 13 + src/api/index.js | 8 + src/appRouter.jsx | 2 + src/automats/brandAutomat.js | 10 + .../article/boxes/ArticleViewEANs.jsx | 51 +- .../settings/PrinterSettingsPanel.jsx | 482 ++++++++++++++++++ src/components/settings/SettingsDialog.jsx | 4 + src/components/settings/SettingsKeysPanel.jsx | 4 - src/components/settings/index.js | 2 +- src/nav.js | 2 + src/pages/artikel/marken.jsx | 267 ++++++++++ 13 files changed, 845 insertions(+), 8 deletions(-) create mode 100644 src/components/settings/PrinterSettingsPanel.jsx create mode 100644 src/pages/artikel/marken.jsx diff --git a/package-lock.json b/package-lock.json index 0409aef..64463f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@shopify/draggable": "^1.2.1", "dashgui": "file:../dashgui", "eslint": "^10.11.0", + "jsbarcode": "^3.12.3", "notistack": "^3.0.2", "pdfconvert-wasm": "^0.3.0", "react": "^19.3.0", @@ -5415,6 +5416,12 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsbarcode": { + "version": "3.12.3", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.3.tgz", + "integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==", + "license": "MIT" + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", diff --git a/package.json b/package.json index 7adbca6..f437f7d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@shopify/draggable": "^1.2.1", "dashgui": "file:../dashgui", "eslint": "^10.11.0", + "jsbarcode": "^3.12.3", "notistack": "^3.0.2", "pdfconvert-wasm": "^0.3.0", "react": "^19.3.0", diff --git a/src/api/ApiClient.js b/src/api/ApiClient.js index 514fe8e..10954d1 100644 --- a/src/api/ApiClient.js +++ b/src/api/ApiClient.js @@ -442,6 +442,19 @@ export class ApiClient { update: (id, body) => this.#json('PATCH', `/api/number/${enc(id)}`, { body }), }; + this.printer = { + /** GET /api/printer → { printers: Printer[] } */ + list: () => this.#json('GET', '/api/printer'), + /** POST /api/printer → 201 { printer } */ + create: (body) => this.#json('POST', '/api/printer', { body }), + /** PATCH /api/printer/{id} → { printer } */ + update: (id, body) => this.#json('PATCH', `/api/printer/${enc(id)}`, { body }), + /** DELETE /api/printer/{id} → 204 */ + delete: (id) => this.#empty('DELETE', `/api/printer/${enc(id)}`), + /** PUT /api/printer/order body { printerIds } → { printerIds } */ + order: (body) => this.#json('PUT', '/api/printer/order', { body }), + }; + this.setting = { /** GET /api/company/setting?frontendOnly=1 → { settings: [{ key, value, frontend }] } */ list: (query) => this.#json('GET', '/api/company/setting', { query }), diff --git a/src/api/index.js b/src/api/index.js index 5b1d970..0da6f97 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -581,6 +581,14 @@ export const brand = { upsert(body) { return http('/api/brand', { method: 'PUT', body }); }, + + /** + * DELETE /api/brand/:id + * → 204 No Content · 404 Brand not found + */ + delete(id) { + return http(`/api/brand/${encodeURIComponent(id)}`, { method: 'DELETE' }); + }, }; /** diff --git a/src/appRouter.jsx b/src/appRouter.jsx index aed4c07..835b4cd 100644 --- a/src/appRouter.jsx +++ b/src/appRouter.jsx @@ -7,6 +7,7 @@ const ArtikelUebersichtPage = lazy(() => import('./pages/artikel/index')); const ArtikelKategorienPage = lazy(() => import('./pages/artikel/kategorien')); const RedirectArtikelKategorien = lazy(() => import('./pages/artikel/RedirectArtikelKategorien')); const ArtikelLagerPage = lazy(() => import('./pages/artikel/lager')); +const ArtikelMarkenPage = lazy(() => import('./pages/artikel/marken')); const ArtikelSkuPage = lazy(() => import('./pages/artikel/sku')); const LieferungListePage = lazy(() => import('./pages/lieferung/index')); const LieferungNeuPage = lazy(() => import('./pages/lieferung/neu')); @@ -53,6 +54,7 @@ const routes = [ { index: true, element: }, { path: 'kategorien/*', element: }, { path: 'lager', element: }, + { path: 'marken', element: }, { path: ':sku/:tab?', element: }, ], }, diff --git a/src/automats/brandAutomat.js b/src/automats/brandAutomat.js index a521635..b910b57 100644 --- a/src/automats/brandAutomat.js +++ b/src/automats/brandAutomat.js @@ -54,6 +54,16 @@ const actions = { return row; }, + /** + * Delete a brand by id. + * @param {number} id + */ + async remove(id) { + await brandApi.delete(id); + const brands = brandAutomat.state.brands.filter((b) => b.id !== id); + brandAutomat.setState({ brands }); + }, + /** Reset to idle state (called on logout / company switch). */ reset() { brandAutomat.setState(initialState); diff --git a/src/components/article/boxes/ArticleViewEANs.jsx b/src/components/article/boxes/ArticleViewEANs.jsx index e4a8f4b..f3f60dd 100644 --- a/src/components/article/boxes/ArticleViewEANs.jsx +++ b/src/components/article/boxes/ArticleViewEANs.jsx @@ -8,6 +8,7 @@ import { StorageManagerContext } from '../../../api/storageManagerContext'; import { requireObject } from '../../../utils/apiShape'; import { articleEans } from '../../../utils/articleDetail'; import { notifyError } from '../../../utils/notify'; +import JsBarcode from 'jsbarcode'; class PanelValue extends Component { render() { const { label, children } = this.props; @@ -41,20 +42,63 @@ function eansDraftValid(draft) { ); } +class SkuBarcode extends PureComponent { + componentDidMount() { + this.renderBarcode(); + } + + componentDidUpdate() { + this.renderBarcode(); + } + + renderBarcode() { + if (!this._svg || !this.props.value) return; + try { + JsBarcode(this._svg, String(this.props.value).trim(), { + format: 'CODE128', + width: 1.5, + height: 38, + displayValue: false, + margin: 0, + background: 'transparent', + lineColor: '#1e293b', + }); + } catch (e) { + console.warn('Barcode render error:', e); + } + } + + render() { + const { value } = this.props; + if (!value) return null; + return ( + + { + this._svg = el; + }} + style={{ maxWidth: '100%', height: 'auto' }} + /> + + ); + } +} + const SECOND_ROW_MIN_HEIGHT = 220; class EansDisplay extends Component { render() { const { sku, eans } = this.props; return ( - <> + {sku} + {(eans ?? []).map((ean) => ( {ean} ))} - + ); } } @@ -65,7 +109,8 @@ class EansEdit extends Component { return ( {sku} - + + ({ form: { ...prev.form, [field]: value }, error: null })); + } + + async handleSubmit() { + const { form } = this.state; + const validationError = validateForm(form); + if (validationError) { + this.setState({ error: validationError }); + return; + } + this.setState({ busy: true, error: null }); + const body = { + address: form.address.trim(), + type: form.type, + model: form.model.trim() || null, + serial: form.serial.trim() || null, + desc: form.desc.trim() || null, + }; + try { + await this.props.onSubmit(body); + } catch (err) { + if (!this._unmounted) { + this.setState({ busy: false, error: backendErrorFrom(err) }); + } + } + } + + componentWillUnmount() { + this._unmounted = true; + } + + render() { + const { open, onClose, title } = this.props; + const { form, busy, error } = this.state; + return ( + + {title} + + + this.handleChange('address', e.target.value)} + autoComplete="off" + placeholder="192.168.1.200" + /> + this.handleChange('type', e.target.value)} + > + {PRINTER_TYPES.map((t) => ( + {t} + ))} + + this.handleChange('model', e.target.value)} + autoComplete="off" + placeholder="z. B. TM-T88VI" + /> + this.handleChange('serial', e.target.value)} + autoComplete="off" + /> + this.handleChange('desc', e.target.value)} + autoComplete="off" + placeholder="z. B. Bondrucker Bar" + /> + {error ? ( + + {typeof error === 'string' ? error : error.message} + + ) : null} + + + + + + + + ); + } +} + +export default class PrinterSettingsPanel extends Component { + static contextType = StorageManagerContext; + + constructor(props) { + super(props); + this.state = { + status: 'loading', + error: null, + printers: [], + draggingId: null, + dragOverId: null, + addOpen: false, + editPrinter: null, + deleteId: null, + deleteBusy: false, + }; + this.handleRetry = this.handleRetry.bind(this); + this.handleAdd = this.handleAdd.bind(this); + this.handleAddSubmit = this.handleAddSubmit.bind(this); + this.handleEditSubmit = this.handleEditSubmit.bind(this); + this.handleCloseAdd = this.handleCloseAdd.bind(this); + this.handleCloseEdit = this.handleCloseEdit.bind(this); + } + + componentDidMount() { + void this.load(); + } + + async load() { + this.setState({ status: 'loading', error: null }); + try { + const data = await this.context.printer.list(); + requireObject(data, 'GET /api/printer'); + const printers = requireArray(data.printers, 'printers'); + this.setState({ status: 'ready', printers }); + } catch (err) { + notifyError(err); + this.setState({ status: 'failed', error: backendErrorFrom(err) }); + } + } + + handleRetry() { + void this.load(); + } + + handleAdd() { + this.setState({ addOpen: true }); + } + + handleCloseAdd() { + this.setState({ addOpen: false }); + } + + handleCloseEdit() { + this.setState({ editPrinter: null }); + } + + async handleAddSubmit(body) { + const data = await this.context.printer.create(body); + requireObject(data, 'POST /api/printer'); + const printer = data.printer; + this.setState((prev) => ({ + printers: [...prev.printers, printer], + addOpen: false, + })); + enqueueSnackbar('Drucker hinzugefügt.', { variant: 'success' }); + } + + async handleEditSubmit(body) { + const { editPrinter } = this.state; + const data = await this.context.printer.update(editPrinter.id, body); + requireObject(data, 'PATCH /api/printer/:id'); + const updated = data.printer; + this.setState((prev) => ({ + printers: prev.printers.map((p) => (p.id === updated.id ? updated : p)), + editPrinter: null, + })); + enqueueSnackbar('Drucker gespeichert.', { variant: 'success' }); + } + + async handleDelete(id) { + this.setState({ deleteId: id, deleteBusy: true }); + try { + await this.context.printer.delete(id); + this.setState((prev) => ({ + printers: prev.printers.filter((p) => p.id !== id), + deleteId: null, + deleteBusy: false, + })); + enqueueSnackbar('Drucker gelöscht.', { variant: 'success' }); + } catch (err) { + if (this._unmounted) return; + notifyError(err); + this.setState({ deleteId: null, deleteBusy: false }); + } + } + + handleDragStart(id) { + this.setState({ draggingId: id, dragOverId: null }); + } + + handleDragOver(e, id) { + e.preventDefault(); + if (id !== this.state.dragOverId) { + this.setState({ dragOverId: id }); + } + } + + handleDragLeave() { + this.setState({ dragOverId: null }); + } + + async handleDrop(targetId) { + const { draggingId, printers } = this.state; + this.setState({ draggingId: null, dragOverId: null }); + if (!draggingId || draggingId === targetId) return; + + const fromIdx = printers.findIndex((p) => p.id === draggingId); + const toIdx = printers.findIndex((p) => p.id === targetId); + if (fromIdx === -1 || toIdx === -1) return; + + const reordered = [...printers]; + const [item] = reordered.splice(fromIdx, 1); + reordered.splice(toIdx, 0, item); + const printerIds = reordered.map((p) => p.id); + + this.setState({ printers: reordered }); + + try { + await this.context.printer.order({ printerIds }); + } catch (err) { + if (this._unmounted) return; + notifyError(err); + this.setState({ printers }); + } + } + + handleDragEnd() { + this.setState({ draggingId: null, dragOverId: null }); + } + + componentWillUnmount() { + this._unmounted = true; + } + + render() { + const { + status, error, printers, + draggingId, dragOverId, + addOpen, editPrinter, + deleteId, deleteBusy, + } = this.state; + + if (status === 'loading') { + return ( + + + + ); + } + if (status === 'failed') { + return ; + } + + return ( + <> + + + + + {printers.length === 0 ? ( + + Noch keine Drucker konfiguriert. + + ) : ( + + + + + + Adresse + Typ + Modell + Beschreibung + + + + + {printers.map((printer) => { + const isDeleting = deleteId === printer.id && deleteBusy; + const isDragging = draggingId === printer.id; + const isDragOver = dragOverId === printer.id; + return ( + this.handleDragStart(printer.id)} + onDragOver={(e) => this.handleDragOver(e, printer.id)} + onDragLeave={() => this.handleDragLeave()} + onDrop={() => this.handleDrop(printer.id)} + onDragEnd={() => this.handleDragEnd()} + sx={{ + opacity: isDragging ? 0.4 : 1, + outline: isDragOver ? '2px solid' : 'none', + outlineColor: 'primary.main', + cursor: 'grab', + transition: 'opacity 0.15s', + }} + > + + + + + {printer.address} + + + + {printer.type} + + + + + {printer.model ?? '—'} + + + + + {printer.desc ?? '—'} + + + + + this.setState({ editPrinter: printer })} + > + + + void this.handleDelete(printer.id)} + > + {isDeleting ? ( + + ) : ( + + )} + + + + + ); + })} + +
+
+ )} + + + + Reihenfolge durch Ziehen ändern. Unterstützte Typen: esc/pos, zpl. + + + + + + + ); + } +} diff --git a/src/components/settings/SettingsDialog.jsx b/src/components/settings/SettingsDialog.jsx index e298172..5acddad 100644 --- a/src/components/settings/SettingsDialog.jsx +++ b/src/components/settings/SettingsDialog.jsx @@ -6,18 +6,22 @@ import DialogTitle from '@mui/material/DialogTitle'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; import NumbersIcon from '@mui/icons-material/Numbers'; +import PrintOutlinedIcon from '@mui/icons-material/PrintOutlined'; import TuneIcon from '@mui/icons-material/Tune'; import NumberSettingsPanel from './NumberSettingsPanel'; +import PrinterSettingsPanel from './PrinterSettingsPanel'; import SettingsKeysPanel from './SettingsKeysPanel'; const TAB_KEYS = { keys: 'keys', numbers: 'numbers', + printers: 'printers', }; const TABS = [ { id: TAB_KEYS.keys, label: 'Einstellungen', icon: , Panel: SettingsKeysPanel }, { id: TAB_KEYS.numbers, label: 'Nummernkreise', icon: , Panel: NumberSettingsPanel }, + { id: TAB_KEYS.printers, label: 'Drucker', icon: , Panel: PrinterSettingsPanel }, ]; export default class SettingsDialog extends Component { diff --git a/src/components/settings/SettingsKeysPanel.jsx b/src/components/settings/SettingsKeysPanel.jsx index db7bfec..ccad89f 100644 --- a/src/components/settings/SettingsKeysPanel.jsx +++ b/src/components/settings/SettingsKeysPanel.jsx @@ -27,16 +27,12 @@ const SETTABLE_KEYS = [ 'ui.theme', 'keys.tavily', 'keys.openrouter', - 'labelprinter.addr', - 'labelprinter.type', ]; const KEY_LABELS = { 'ui.theme': 'UI-Theme', 'keys.tavily': 'Tavily API-Key', 'keys.openrouter': 'OpenRouter API-Key', - 'labelprinter.addr': 'Labeldrucker-Adresse', - 'labelprinter.type': 'Labeldrucker-Typ', }; function keyLabel(key) { diff --git a/src/components/settings/index.js b/src/components/settings/index.js index df96d02..cc6f719 100644 --- a/src/components/settings/index.js +++ b/src/components/settings/index.js @@ -1,3 +1,3 @@ export { default as SettingsDialog } from './SettingsDialog'; export { default as SettingsKeysPanel } from './SettingsKeysPanel'; -export { default as NumberSettingsPanel } from './NumberSettingsPanel'; \ No newline at end of file +export { default as NumberSettingsPanel } from './NumberSettingsPanel';export { default as PrinterSettingsPanel } from './PrinterSettingsPanel'; diff --git a/src/nav.js b/src/nav.js index 500d312..af5063c 100644 --- a/src/nav.js +++ b/src/nav.js @@ -8,6 +8,7 @@ import InfoIcon from '@mui/icons-material/Info'; import ViewListIcon from '@mui/icons-material/ViewList'; import CategoryIcon from '@mui/icons-material/Category'; import WarehouseIcon from '@mui/icons-material/Warehouse'; +import CopyrightIcon from '@mui/icons-material/Copyright'; import ListAltIcon from '@mui/icons-material/ListAlt'; import GroupsIcon from '@mui/icons-material/Groups'; import AddBoxIcon from '@mui/icons-material/AddBox'; @@ -103,6 +104,7 @@ export const SUB_NAV = { { segment: '', label: 'Liste', title: 'Artikel', icon: ViewListIcon }, { segment: 'kategorien', label: 'Kategorieen', title: 'Kategorien', icon: CategoryIcon }, { segment: 'lager', label: 'Lager', icon: WarehouseIcon }, + { segment: 'marken', label: 'Marken', icon: CopyrightIcon }, ], '/lieferung': [ { segment: '', label: 'Liste', title: 'Lieferungen', icon: ListAltIcon }, diff --git a/src/pages/artikel/marken.jsx b/src/pages/artikel/marken.jsx new file mode 100644 index 0000000..68df76a --- /dev/null +++ b/src/pages/artikel/marken.jsx @@ -0,0 +1,267 @@ +import { PureComponent } from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import CircularProgress from '@mui/material/CircularProgress'; +import IconButton from '@mui/material/IconButton'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import Typography from '@mui/material/Typography'; +import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; +import CopyrightIcon from '@mui/icons-material/Copyright'; +import BackendError from '../../components/BackendError'; +import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import { PanelTile, PanelTileGrid } from 'dashgui/panels'; +import { backendErrorFrom } from '../../utils/backendError'; +import { notifyError } from '../../utils/notify'; +import { setLastArticleSku } from '../../automats'; +import brandAutomat from '../../automats/brandAutomat'; +import { enqueueSnackbar } from 'notistack'; + +function draftsFrom(brands) { + return Object.fromEntries(brands.map((b) => [b.id, b.name])); +} + +class ArtikelMarkenPage extends PureComponent { + constructor(props) { + super(props); + const initialBrands = brandAutomat.state.brands || []; + this.state = { + status: brandAutomat.state.status === 'succeeded' ? 'ready' : 'loading', + error: brandAutomat.state.error ? backendErrorFrom({ message: brandAutomat.state.error }) : null, + brands: initialBrands, + drafts: draftsFrom(initialBrands), + newName: '', + busyKey: null, + }; + this._cancelled = false; + this.handleCreate = this.handleCreate.bind(this); + this.handleRetry = this.handleRetry.bind(this); + this.loadBrands = this.loadBrands.bind(this); + } + + componentDidMount() { + setLastArticleSku(null); + this._cancelled = false; + + this.unsub = brandAutomat.subscribe((state) => { + if (this._cancelled) return; + if (state.status === 'succeeded') { + this.applyBrands(state.brands); + } else if (state.status === 'failed') { + this.setState({ status: 'failed', error: backendErrorFrom({ message: state.error }) }); + } + }); + + if (brandAutomat.state.status !== 'succeeded') { + void this.loadBrands(); + } + } + + componentWillUnmount() { + this._cancelled = true; + this.unsub?.(); + } + + applyBrands(brands) { + this.setState((prev) => { + const currentDrafts = prev.drafts || {}; + const nextDrafts = { ...draftsFrom(brands) }; + for (const b of brands) { + if (prev.busyKey !== b.id && currentDrafts[b.id] !== undefined) { + nextDrafts[b.id] = currentDrafts[b.id]; + } + } + return { + status: 'ready', + error: null, + brands, + drafts: nextDrafts, + }; + }); + } + + async loadBrands() { + if (brandAutomat.state.status !== 'succeeded') { + this.setState({ status: 'loading', error: null }); + } + try { + const list = await brandAutomat.actions.load(); + if (this._cancelled) return; + this.applyBrands(list); + } catch (err) { + if (this._cancelled) return; + this.setState({ status: 'failed', error: backendErrorFrom(err) }); + } + } + + handleRetry() { + void this.loadBrands(); + } + + async handleCreate() { + const name = this.state.newName.trim(); + if (!name || this.state.busyKey != null) return; + + this.setState({ busyKey: 'new' }); + try { + const row = await brandAutomat.actions.upsert({ name }); + if (this._cancelled) return; + this.setState({ newName: '', busyKey: null }); + enqueueSnackbar(`Marke „${row.name || name}" angelegt.`, { variant: 'success' }); + } catch (err) { + if (this._cancelled) return; + notifyError(err); + this.setState({ busyKey: null }); + } + } + + async handleRename(brand) { + const draft = String(this.state.drafts[brand.id] ?? '').trim(); + if (!draft) { + this.setState((prev) => ({ + drafts: { ...prev.drafts, [brand.id]: brand.name }, + })); + return; + } + if (draft === brand.name || this.state.busyKey != null) return; + + this.setState({ busyKey: brand.id }); + try { + await brandAutomat.actions.upsert({ name: draft }); + if (this._cancelled) return; + this.setState({ busyKey: null }); + enqueueSnackbar('Marke gespeichert.', { variant: 'success' }); + } catch (err) { + if (this._cancelled) return; + notifyError(err); + this.setState((prev) => ({ + busyKey: null, + drafts: { ...prev.drafts, [brand.id]: brand.name }, + })); + } + } + + async handleDelete(brand) { + if (this.state.busyKey != null) return; + if (!window.confirm(`Marke „${brand.name}" wirklich löschen?`)) return; + + this.setState({ busyKey: brand.id }); + try { + await brandAutomat.actions.remove(brand.id); + if (this._cancelled) return; + this.setState({ busyKey: null }); + enqueueSnackbar('Marke gelöscht.', { variant: 'success' }); + } catch (err) { + if (this._cancelled) return; + notifyError(err); + this.setState({ busyKey: null }); + } + } + + render() { + const { status, error, brands, drafts, newName, busyKey } = this.state; + const busy = busyKey != null; + + if (status === 'loading') { + return ( + + + + ); + } + + if (status === 'failed') { + return ; + } + + return ( + + + + Marken der ausgewählten Firma. + + + + {brands.length === 0 ? ( + + Noch keine Marken angelegt. + + ) : ( + brands.map((b) => ( + + + + this.setState((prev) => ({ + drafts: { ...prev.drafts, [b.id]: next }, + })) + } + onBlur={() => void this.handleRename(b)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + event.target.blur(); + } + if (event.key === 'Escape') { + event.preventDefault(); + this.setState((prev) => ({ + drafts: { ...prev.drafts, [b.id]: b.name }, + })); + event.target.blur(); + } + }} + sx={{ flex: 1 }} + /> + void this.handleDelete(b)} + > + + + + )) + )} + + + + this.setState({ newName: next })} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + void this.handleCreate(); + } + }} + sx={{ flex: 1 }} + /> + + + + + ); + } +} + +export default ArtikelMarkenPage;