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
+119
View File
@@ -0,0 +1,119 @@
import { API_PATHS } from '../app/config.js';
function normalizeBaseUrl(baseUrl) {
return baseUrl.trim().replace(/\/+$/, '');
}
function buildUrl({ baseUrl, database, kitchenId, path, query = {}, includeKitchen = true }) {
const cleanBaseUrl = normalizeBaseUrl(baseUrl);
const encodedDatabase = encodeURIComponent(database);
const encodedPath = path.replace(/^\/+/, '');
const kitchenSegment =
includeKitchen && kitchenId
? `/kitchen/${encodeURIComponent(String(kitchenId))}`
: '';
const url = new URL(
`${cleanBaseUrl}/${encodedDatabase}${kitchenSegment}/${encodedPath}`,
);
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
url.searchParams.set(key, String(value));
}
});
return url.toString();
}
async function parseResponse(response) {
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
return response.json();
}
if (contentType.includes('image/')) {
return response.blob();
}
if (response.status === 204) {
return null;
}
return response.text();
}
function normalizeError(response, payload) {
const message =
payload?.message ||
payload?.error ||
`Request failed with status ${response.status}.`;
return new Error(message, {
cause: {
status: response.status,
details: payload?.errors || payload?.details || null,
},
});
}
export async function apiRequest(store, path, options = {}) {
const { config, session, activeKitchen } = store;
if (!config.baseUrl || !config.database) {
throw new Error('Server URL and database name are required.');
}
const headers = new Headers(options.headers || {});
headers.set('Accept', options.accept || 'application/json');
if (options.body && !options.isFormData) {
headers.set('Content-Type', 'application/json');
}
if (session?.applicationKey) {
headers.set('Authorization', `Bearer ${session.applicationKey}`);
}
const response = await fetch(
buildUrl({
baseUrl: config.baseUrl,
database: config.database,
kitchenId: activeKitchen?.id,
path,
query: options.query,
includeKitchen: options.includeKitchen !== false,
}),
{
method: options.method || 'GET',
headers,
body:
options.body && !options.isFormData
? JSON.stringify(options.body)
: options.body || undefined,
},
);
const payload = await parseResponse(response);
if (!response.ok) {
throw normalizeError(response, payload);
}
return payload;
}
export function getPath(key) {
return API_PATHS[key];
}
export function buildKitchenApiUrl(store, path, query = {}) {
return buildUrl({
baseUrl: store.config.baseUrl,
database: store.config.database,
kitchenId: store.activeKitchen?.id,
path,
query,
includeKitchen: true,
});
}