Introduce initial version of the Lonc app with core features, styling, and configurations.

- Add base app structure, including Bootstrap setup and Alpine.js integration.
- Implement authentication flow with session handling.
- Integrate stock management and label creation functionalities.
- Include responsive styling and theme using CSS variables and custom components.
- Add API clients for Tryton-based backend.
- Set up kitchen and dashboard navigation workflows.
- Configure service worker for PWA support.
This commit is contained in:
2026-04-06 09:24:22 +02:00
commit 929ee6557a
48 changed files with 4879 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
import { login, verifyConnection } from '../../api/auth.js';
import { CONNECTION_STATES } from '../../app/config.js';
import { navigate } from '../../app/router.js';
import { createAsyncState, runAsyncState } from '../shared/ui-state.js';
export function renderLoginPage() {
return `
<section class="container-xxl py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-12 col-md-10 col-lg-7 col-xl-5">
<div class="card border-0 shadow-lg">
<div class="card-body p-4 p-lg-5" x-data="loginPage()" x-init="init()">
<div class="mb-4">
<p class="eyebrow mb-2">Kitchen User Application</p>
<h1 class="h3 mb-2">Connect Lonc to Tryton</h1>
<p class="text-body-secondary mb-0">
Lonc uses a Tryton user application key for <code>kitchen</code>, not a normal session login.
</p>
</div>
<form class="vstack gap-3" @submit.prevent="submit()">
<div>
<label class="form-label" for="base-url">Tryton server URL</label>
<input id="base-url" class="form-control" type="url" x-model="form.baseUrl" placeholder="https://tryton.example.com" required />
</div>
<div>
<label class="form-label" for="database">Database name</label>
<input id="database" class="form-control" type="text" x-model="form.database" placeholder="kitchen" required />
</div>
<div>
<label class="form-label" for="user-login">User login</label>
<input id="user-login" class="form-control" type="text" x-model="form.userLogin" autocomplete="username" required />
<div class="form-text">
This requests a pending application key that must be approved in Tryton client preferences.
</div>
</div>
<template x-if="state.error">
<div class="alert alert-danger mb-0" x-text="state.error"></div>
</template>
<template x-if="!hasStoredKey">
<button class="btn btn-primary btn-lg" type="submit" :disabled="state.isLoading">
<span x-show="!state.isLoading">Create application key</span>
<span x-show="state.isLoading">Creating key...</span>
</button>
</template>
</form>
<template x-if="hasStoredKey">
<div class="mt-4 pt-4 border-top">
<div class="alert mb-3" :class="statusAlertClass()">
<div class="fw-semibold mb-1" x-text="statusTitle()"></div>
<div x-text="statusMessage()"></div>
</div>
<div class="card bg-body-tertiary border-0 mb-3">
<div class="card-body">
<div class="fw-semibold mb-2">Application key to approve in Tryton</div>
<div class="small text-body-secondary mb-2">
Match this key with the pending key shown in Tryton client preferences.
</div>
<input
class="form-control font-monospace small"
type="text"
readonly
:value="maskedApplicationKey()"
/>
</div>
</div>
<ol class="small text-body-secondary ps-3 mb-4">
<li>Create the key from this screen.</li>
<li>Open Tryton client preferences.</li>
<li>Validate the pending key for the <code>kitchen</code> application.</li>
<li>Return here and verify the connection.</li>
</ol>
<template x-if="verifyState.error">
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<span x-text="verifyState.error"></span>
<button
type="button"
class="btn-close"
aria-label="Close"
@click="verifyState.error = ''"
></button>
</div>
</template>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-primary" type="button" @click="checkConnection()" :disabled="verifyState.isLoading">
<span x-show="!verifyState.isLoading">Verify connection</span>
<span x-show="verifyState.isLoading">Checking...</span>
</button>
<button class="btn btn-outline-secondary" type="button" @click="disconnect()">
Disconnect
</button>
</div>
</div>
</template>
</div>
</div>
</div>
</div>
</section>
`;
}
export function loginPageData(store) {
return {
store,
state: createAsyncState(),
verifyState: createAsyncState(),
sessionState: store.session?.state || CONNECTION_STATES.notConnected,
applicationKey: store.session?.applicationKey || '',
form: {
baseUrl: store.config.baseUrl || '',
database: store.config.database || '',
userLogin: store.session?.userLogin || '',
},
get hasStoredKey() {
return Boolean(this.applicationKey);
},
init() {
this.syncFromStore();
if (store.isConnected) {
navigate('/');
return;
}
if (this.hasStoredKey) {
this.tryAutoVerify();
}
},
async submit() {
await runAsyncState(this.state, async () => {
this.verifyState.error = '';
store.setConfig({
baseUrl: this.form.baseUrl.trim(),
database: this.form.database.trim(),
});
await login(store, {
userLogin: this.form.userLogin.trim(),
});
this.syncFromStore();
store.addAlert({
type: 'info',
message: `Got secret key starting ${store.session.applicationKey.slice(0, 8)}.... Approve this key in Tryton preferences.`,
});
});
},
async checkConnection() {
await runAsyncState(this.verifyState, async () => {
await window.__loncApp.verifyConnection();
this.syncFromStore();
if (store.isConnected) {
store.addAlert({ type: 'success', message: 'Kitchen application key is active.' });
navigate('/');
}
}).catch(() => {
this.verifyState.error =
'Failed to verify connection. Please verify the application key in Tryton first.';
});
},
async tryAutoVerify() {
await runAsyncState(this.verifyState, async () => {
try {
await verifyConnection(store);
} catch {
this.syncFromStore();
return;
}
this.syncFromStore();
if (store.isConnected) {
await window.__loncApp.refreshKitchens();
navigate('/');
}
}).catch(() => {});
},
async disconnect() {
await window.__loncApp.logout();
this.syncFromStore();
this.sessionState = CONNECTION_STATES.notConnected;
this.applicationKey = '';
this.verifyState.error = '';
},
statusTitle() {
return {
[CONNECTION_STATES.pendingValidation]: 'Validation still pending',
[CONNECTION_STATES.connected]: 'Connected',
[CONNECTION_STATES.invalidKey]: 'Stored key is invalid',
}[this.sessionState] || 'Not connected';
},
statusMessage() {
return {
[CONNECTION_STATES.pendingValidation]:
'The application key was created successfully. Copy or compare it with the pending key in Tryton client preferences, approve it there, then verify the connection here.',
[CONNECTION_STATES.connected]:
'The kitchen user application key is active and ready for protected requests.',
[CONNECTION_STATES.invalidKey]:
'This key worked before but is no longer accepted. Disconnect and create a new one.',
}[this.sessionState] || 'Create a new application key to connect.';
},
statusAlertClass() {
return {
[CONNECTION_STATES.pendingValidation]: 'alert-warning',
[CONNECTION_STATES.connected]: 'alert-success',
[CONNECTION_STATES.invalidKey]: 'alert-danger',
}[this.sessionState] || 'alert-secondary';
},
maskedApplicationKey() {
const key = this.applicationKey;
return key ? `${key.slice(0, 16)}...` : '';
},
syncFromStore() {
this.sessionState = store.session?.state || CONNECTION_STATES.notConnected;
this.applicationKey = store.session?.applicationKey || '';
this.form.userLogin = store.session?.userLogin || this.form.userLogin;
},
};
}
+65
View File
@@ -0,0 +1,65 @@
export function renderSettingsPage() {
return `
<section class="container-xxl py-4 py-lg-5">
<div class="row g-4">
<div class="col-12 col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-body p-4" x-data="settingsPage()">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<p class="eyebrow mb-2">Client Settings</p>
<h1 class="h3 mb-1">Connection & workspace</h1>
<p class="text-body-secondary mb-0">
These values are stored locally and reused to start the Tryton user application flow.
</p>
</div>
</div>
<form class="vstack gap-3" @submit.prevent="save()">
<div>
<label class="form-label">Tryton server URL</label>
<input class="form-control" type="url" x-model="form.baseUrl" required />
</div>
<div>
<label class="form-label">Database name</label>
<input class="form-control" type="text" x-model="form.database" required />
</div>
<button class="btn btn-primary align-self-start" type="submit">Save settings</button>
</form>
</div>
</div>
</div>
<div class="col-12 col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h2 class="h5">Integration notes</h2>
<ul class="text-body-secondary small ps-3 mb-0">
<li>Connection uses Tryton user application keys for the <code>kitchen</code> application.</li>
<li>Kitchen-scoped requests are built as <code>/{database}/kitchen/{kitchenId}/...</code>.</li>
<li>Label preview accepts image blobs, image URLs, or SVG payloads.</li>
</ul>
</div>
</div>
</div>
</div>
</section>
`;
}
export function settingsPageData(store) {
return {
form: {
baseUrl: store.config.baseUrl || '',
database: store.config.database || '',
},
save() {
store.setConfig({
baseUrl: this.form.baseUrl.trim(),
database: this.form.database.trim(),
});
store.addAlert({ type: 'success', message: 'Settings saved locally.' });
},
};
}