Files
foodlinkk-command-center/cockpit/templates/suppliers.html
T
2026-06-09 10:41:13 +00:00

242 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="/static/css/clients-crm.css?v=1" />
{% endblock %}
{% block content %}
<div class="clients-shell" x-data="suppliersHub()" x-init="init()">
<header class="page-header">
<div>
<h1 x-text="$t('suppliers.title')">Leveranciers</h1>
<p class="subtitle page-tip" x-text="$t('suppliers.subtitle')"></p>
</div>
<div class="retail-actions">
<button class="btn btn-sm btn-pulse" @click="loadAll()" :disabled="busy" x-text="$t('common.refresh')"></button>
<a class="btn btn-sm" href="/products" x-text="$t('nav.products')"></a>
</div>
</header>
<div class="clients-kpi-row">
<div class="clients-kpi"><span x-text="$t('suppliers.total')"></span><strong x-text="stats.total||0"></strong></div>
<div class="clients-kpi"><span x-text="$t('suppliers.avg_rating')"></span><strong x-text="(stats.avg_rating||0)+'/5'"></strong></div>
<div class="clients-kpi"><span x-text="$t('suppliers.countries')"></span><strong x-text="stats.countries||0"></strong></div>
<div class="clients-kpi"><span x-text="$t('suppliers.avg_lead')"></span><strong x-text="(stats.avg_lead||0)+'d'"></strong></div>
</div>
<div class="clients-tabs">
<button class="clients-tab" :class="{active: tab==='grid'}" @click="tab='grid'" x-text="$t('suppliers.tab_grid')"></button>
<button class="clients-tab" :class="{active: tab==='list'}" @click="tab='list'" x-text="$t('suppliers.tab_list')"></button>
<div class="clients-tab-actions">
<button class="btn btn-primary btn-sm" @click="openModal()" x-text="'+ ' + $t('suppliers.new')"></button>
</div>
</div>
<div x-show="tab==='grid'" x-cloak>
<div class="agents-neo-grid">
<template x-for="s in filtered" :key="s.id">
<article class="agent-soul-card" @click="openDetail(s)">
<strong x-text="s.name"></strong>
<div class="clients-card-meta" x-text="(s.country||'') + (s.category ? ' · '+categoryLabel(s.category) : '')"></div>
<div class="clients-card-tags">
<span class="clients-tag mrr" x-show="s.rating" x-text="'★ '+(s.rating||0)"></span>
<span class="clients-tag" x-show="s.lead_time_days" x-text="s.lead_time_days+'d'"></span>
</div>
<div class="clients-card-meta" x-show="s.contact" x-text="s.contact"></div>
</article>
</template>
</div>
<p class="clients-empty" x-show="!filtered.length" x-text="$t('suppliers.no_suppliers')"></p>
</div>
<div x-show="tab==='list'" x-cloak>
<div class="clients-toolbar">
<input type="search" :placeholder="$t('common.search')" x-model="filterQ" @input="applyFilter()" />
<select x-model="filterCountry" @change="applyFilter()">
<option value="" x-text="$t('suppliers.filter_country') + ' — ' + $t('common.all')"></option>
<template x-for="c in countryOptions" :key="c"><option :value="c" x-text="c"></option></template>
</select>
<select x-model="filterCategory" @change="applyFilter()">
<option value="" x-text="$t('suppliers.filter_category') + ' — ' + $t('common.all')"></option>
<template x-for="cat in categories" :key="cat.id"><option :value="cat.id" x-text="cat.label"></option></template>
</select>
</div>
<table class="data-table">
<thead><tr>
<th x-text="$t('suppliers.name')"></th><th x-text="$t('suppliers.country')"></th>
<th x-text="$t('suppliers.category')"></th><th x-text="$t('suppliers.rating')"></th>
<th x-text="$t('suppliers.lead_time')"></th><th x-text="$t('suppliers.moq')"></th><th></th>
</tr></thead>
<tbody>
<template x-for="s in filtered" :key="'l'+s.id">
<tr class="clickable-row" @click="openDetail(s)">
<td><strong x-text="s.name"></strong></td>
<td x-text="s.country||'—'"></td>
<td x-text="categoryLabel(s.category)"></td>
<td x-text="s.rating!=null?s.rating:'—'"></td>
<td x-text="s.lead_time_days!=null?s.lead_time_days+'d':'—'"></td>
<td x-text="s.moq||'—'"></td>
<td>
<button class="btn btn-sm" @click.stop="editSupplier(s)" x-text="$t('common.edit')"></button>
<button class="btn btn-sm btn-reject" @click.stop="remove(s.id)">×</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="modal" :class="{ open: modal }" @click.self="modal=false">
<div class="modal-card" style="max-width:560px">
<h3 x-text="form.id ? $t('common.edit') : $t('suppliers.new')"></h3>
<div class="clients-form-grid">
<label class="full"><span class="filter-label" x-text="$t('suppliers.name')"></span>
<input class="form-input" x-model="form.name" /></label>
<label><span class="filter-label" x-text="$t('suppliers.country')"></span>
<input class="form-input" x-model="form.country" list="country-list" /></label>
<datalist id="country-list"><option>NL</option><option>BE</option><option>DE</option><option>TR</option><option>MA</option></datalist>
<label><span class="filter-label" x-text="$t('suppliers.category')"></span>
<select class="form-input" x-model="form.category">
<option value="" x-text="$t('common.none')"></option>
<template x-for="cat in categories" :key="'fc'+cat.id"><option :value="cat.id" x-text="cat.label"></option></template>
</select></label>
<label><span class="filter-label" x-text="$t('suppliers.rating')"></span>
<input type="number" class="form-input" x-model="form.rating" min="0" max="5" step="0.1" /></label>
<label><span class="filter-label" x-text="$t('suppliers.moq')"></span>
<input type="number" class="form-input" x-model="form.moq" /></label>
<label><span class="filter-label" x-text="$t('suppliers.lead_time')"></span>
<input type="number" class="form-input" x-model="form.lead_time_days" /></label>
<label class="full"><span class="filter-label" x-text="$t('suppliers.contact')"></span>
<textarea class="form-input" rows="3" x-model="form.contact" placeholder="naam@bedrijf.nl · +31…"></textarea></label>
</div>
<div class="btn-group" style="margin-top:1rem">
<button class="btn btn-primary" @click="save()" :disabled="!form.name.trim()" x-text="$t('common.save')"></button>
<button class="btn btn-secondary" @click="modal=false" x-text="$t('common.cancel')"></button>
<button class="btn btn-reject" x-show="form.id" @click="remove(form.id); modal=false" x-text="$t('common.delete')"></button>
</div>
</div>
</div>
<aside class="clients-drawer" :class="{ open: drawer }">
<div class="clients-drawer-head">
<div><h2 x-text="detail?.supplier?.name||''"></h2>
<p class="muted" x-text="(detail?.supplier?.country||'') + ' · ' + categoryLabel(detail?.supplier?.category)"></p></div>
<button class="drawer-close" @click="drawer=false">×</button>
</div>
<template x-if="detail?.supplier">
<div class="clients-card-meta">
<div x-show="detail.supplier.rating"><strong x-text="$t('suppliers.rating')"></strong>: <span x-text="detail.supplier.rating+'/5'"></span></div>
<div x-show="detail.supplier.moq"><strong x-text="$t('suppliers.moq')"></strong>: <span x-text="detail.supplier.moq"></span></div>
<div x-show="detail.supplier.lead_time_days"><strong x-text="$t('suppliers.lead_time')"></strong>: <span x-text="detail.supplier.lead_time_days+' dagen'"></span></div>
<div x-show="detail.supplier.contact" style="white-space:pre-wrap;margin-top:0.5rem" x-text="detail.supplier.contact"></div>
<button class="btn btn-sm" style="margin-top:0.75rem" @click="editSupplier(detail.supplier)" x-text="$t('common.edit')"></button>
</div>
</template>
</aside>
</div>
{% endblock %}
{% block scripts %}
<script>
function suppliersHub() {
return {
tab: 'grid', busy: false, modal: false, drawer: false,
stats: {}, suppliers: [], filtered: [], detail: null,
filterQ: '', filterCountry: '', filterCategory: '',
form: {}, categories: [], countryOptions: [],
buildCategories() {
this.categories = [
{ id: 'Meat', label: this.$t('suppliers.cat_meat') },
{ id: 'Dairy', label: this.$t('suppliers.cat_dairy') },
{ id: 'Spices', label: this.$t('suppliers.cat_spices') },
{ id: 'Packaging', label: this.$t('suppliers.cat_packaging') },
{ id: 'Logistics', label: this.$t('suppliers.cat_logistics') },
{ id: 'Ingredients', label: this.$t('suppliers.cat_ingredients') },
{ id: 'Other', label: this.$t('suppliers.cat_other') },
];
},
categoryLabel(id) {
if (!id) return '—';
return (this.categories.find(c => c.id === id) || {}).label || id;
},
async init() {
this.buildCategories();
document.addEventListener('foodlinkk:locale', () => this.buildCategories());
await this.loadAll();
},
async loadAll() {
this.busy = true;
try {
const [statsR, supR] = await Promise.all([
Cockpit.api('/suppliers/stats'),
Cockpit.api('/suppliers'),
]);
this.stats = statsR.stats || {};
this.suppliers = supR.items || [];
const countries = new Set();
this.suppliers.forEach(s => { if (s.country) countries.add(s.country); });
this.countryOptions = Array.from(countries).sort();
this.applyFilter();
} catch (e) { Cockpit.toast(e.message, 'error'); }
this.busy = false;
},
applyFilter() {
const q = (this.filterQ || '').toLowerCase();
this.filtered = this.suppliers.filter(s => {
if (this.filterCountry && s.country !== this.filterCountry) return false;
if (this.filterCategory && s.category !== this.filterCategory) return false;
if (!q) return true;
return [s.name, s.country, s.category, s.contact].some(
f => f && String(f).toLowerCase().includes(q)
);
});
},
openModal() {
this.form = { id: null, name: '', country: 'NL', category: '', rating: 4, moq: '', lead_time_days: '', contact: '' };
this.modal = true;
},
editSupplier(s) {
this.form = {
id: s.id, name: s.name || '', country: s.country || '', category: s.category || '',
rating: s.rating ?? 4, moq: s.moq ?? '', lead_time_days: s.lead_time_days ?? '', contact: s.contact || '',
};
this.modal = true; this.drawer = false;
},
async save() {
const body = {
name: this.form.name.trim(),
country: this.form.country || null,
category: this.form.category || null,
rating: this.form.rating !== '' ? parseFloat(this.form.rating) : null,
moq: this.form.moq !== '' ? parseInt(this.form.moq, 10) : null,
lead_time_days: this.form.lead_time_days !== '' ? parseInt(this.form.lead_time_days, 10) : null,
contact: this.form.contact || null,
};
try {
if (this.form.id) await Cockpit.api('/suppliers/' + this.form.id, { method: 'PUT', body: JSON.stringify(body) });
else await Cockpit.api('/suppliers', { method: 'POST', body: JSON.stringify(body) });
Cockpit.toast(window.I18n.t('common.saved'), 'success');
this.modal = false;
await this.loadAll();
} catch (e) { Cockpit.toast(e.message, 'error'); }
},
async remove(id) {
if (!Cockpit.confirmDelete()) return;
await Cockpit.api('/suppliers/' + id, { method: 'DELETE' });
await this.loadAll();
},
async openDetail(s) {
const d = await Cockpit.api('/suppliers/' + s.id + '/detail');
this.detail = d;
this.drawer = true;
},
};
}
</script>
{% endblock %}