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:
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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.' });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user