242 lines
12 KiB
HTML
242 lines
12 KiB
HTML
{% 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 %}
|