Files

242 lines
12 KiB
HTML
Raw Permalink Normal View History

{% extends "base.html" %}
2026-06-09 10:41:13 +00:00
{% block extra_head %}
<link rel="stylesheet" href="/static/css/clients-crm.css?v=1" />
{% endblock %}
{% block content %}
2026-06-09 10:41:13 +00:00
<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>
2026-06-09 10:41:13 +00:00
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() {
2026-06-09 10:41:13 +00:00
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 %}