541 lines
26 KiB
HTML
541 lines
26 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="clientsHub()" x-init="init()">
|
||
<header class="page-header">
|
||
<div>
|
||
<h1 x-text="$t('clients.title')">CRM · Klanten</h1>
|
||
<p class="subtitle page-tip" x-text="$t('clients.subtitle')"></p>
|
||
</div>
|
||
<div class="retail-actions">
|
||
<button class="btn btn-sm btn-pulse" @click="loadAll()" :disabled="busy">↻ Verversen</button>
|
||
<a class="btn btn-sm btn-pulse-green" href="/retail">Retail 360 →</a>
|
||
<a class="btn btn-sm" href="/deals">Deals →</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="clients-kpi-row">
|
||
<div class="clients-kpi"><span>Totaal klanten</span><strong x-text="stats.total||0"></strong></div>
|
||
<div class="clients-kpi"><span>Actief</span><strong x-text="stats.active||0"></strong></div>
|
||
<div class="clients-kpi"><span>MRR schatting</span><strong x-text="fmtEur(stats.mrr_total)"></strong></div>
|
||
<div class="clients-kpi"><span>Pipeline</span><strong x-text="fmtEur(stats.pipeline_value)"></strong></div>
|
||
<div class="clients-kpi"><span>Filiaal-koppelingen</span><strong x-text="stats.store_links||0"></strong></div>
|
||
</div>
|
||
|
||
<div class="clients-tabs">
|
||
<button class="clients-tab" :class="{active: tab==='kanban'}" @click="tab='kanban'" x-text="$t('clients.tab_kanban')">Kanban</button>
|
||
<button class="clients-tab" :class="{active: tab==='list'}" @click="tab='list'" x-text="$t('clients.tab_list')">Lijst</button>
|
||
<button class="clients-tab" :class="{active: tab==='stores'}" @click="tab='stores'; searchStores()" x-text="$t('clients.tab_stores')">Supermarkten</button>
|
||
<div class="clients-tab-actions">
|
||
<button class="btn btn-primary btn-sm" @click="openClientModal()" x-text="'+ ' + $t('clients.new')">+ Nieuwe klant</button>
|
||
<button class="btn btn-sm" @click="openStoreCreate()" x-show="tab==='stores'">+ Nieuw filiaal</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Kanban -->
|
||
<div x-show="tab==='kanban'" x-cloak>
|
||
<div class="clients-kanban">
|
||
<template x-for="st in stages" :key="st.id">
|
||
<div class="clients-kanban-col">
|
||
<h3><span x-text="st.label"></span> <span class="count" x-text="stageCount(st.id)"></span></h3>
|
||
<template x-for="c in clientsByStage(st.id)" :key="c.id">
|
||
<div class="clients-card" :class="'stage-'+c.stage" @click="openDetail(c)">
|
||
<strong x-text="c.name"></strong>
|
||
<div class="clients-card-meta">
|
||
<span x-show="c.contact" x-text="c.contact"></span>
|
||
<span x-show="c.sector"> · <span x-text="c.sector"></span></span>
|
||
</div>
|
||
<div class="clients-card-meta" x-show="c.email" x-text="c.email"></div>
|
||
<div class="clients-card-tags">
|
||
<span class="clients-tag mrr" x-show="c.mrr_estimate" x-text="'€'+Number(c.mrr_estimate).toLocaleString('nl-NL')+'/mnd'"></span>
|
||
<span class="clients-tag" x-show="c.store_count" x-text="c.store_count+' filialen'"></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- List -->
|
||
<div x-show="tab==='list'" x-cloak>
|
||
<div class="clients-toolbar">
|
||
<input type="search" placeholder="Zoek naam, contact, e-mail…" x-model="filterQ" @input="applyFilter()" />
|
||
<select x-model="filterStage" @change="applyFilter()">
|
||
<option value="">Alle stages</option>
|
||
<template x-for="st in stages" :key="'f'+st.id"><option :value="st.id" x-text="st.label"></option></template>
|
||
</select>
|
||
<select x-model="filterSector" @change="applyFilter()">
|
||
<option value="">Alle sectoren</option>
|
||
<template x-for="s in sectorOptions" :key="s"><option :value="s" x-text="s"></option></template>
|
||
</select>
|
||
</div>
|
||
<table class="data-table">
|
||
<thead><tr>
|
||
<th>Naam</th><th>Contact</th><th>E-mail</th><th>Sector</th><th>Stage</th><th>MRR</th><th>Filialen</th><th></th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<template x-for="c in filtered" :key="'l'+c.id">
|
||
<tr class="clickable-row" @click="openDetail(c)">
|
||
<td><strong x-text="c.name"></strong></td>
|
||
<td x-text="c.contact||'—'"></td>
|
||
<td x-text="c.email||'—'"></td>
|
||
<td x-text="c.sector||'—'"></td>
|
||
<td><span class="badge" x-text="stageLabel(c.stage)"></span></td>
|
||
<td x-text="c.mrr_estimate ? fmtEur(c.mrr_estimate) : '—'"></td>
|
||
<td x-text="c.store_count||0"></td>
|
||
<td>
|
||
<button class="btn btn-sm" @click.stop="editClient(c)">Bewerk</button>
|
||
<button class="btn btn-sm btn-reject" @click.stop="removeClient(c.id)">×</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
<p class="clients-empty" x-show="!filtered.length">Geen klanten gevonden.</p>
|
||
</div>
|
||
|
||
<!-- Supermarkten -->
|
||
<div x-show="tab==='stores'" x-cloak>
|
||
<p class="page-tip" style="margin-bottom:0.75rem">Zoek een filiaal, vul ontbrekende data aan (contact, halal, manager) en koppel aan een klant.</p>
|
||
<div class="clients-toolbar">
|
||
<input type="search" placeholder="Zoek filiaal, keten, stad…" x-model="storeQ" @keyup.enter="searchStores()" />
|
||
<select x-model="storeChain" @change="searchStores()">
|
||
<option value="">Alle ketens</option>
|
||
<template x-for="ch in chains" :key="ch"><option :value="ch" x-text="ch"></option></template>
|
||
</select>
|
||
<select x-model="storeProvince" @change="searchStores()">
|
||
<option value="">Alle provincies</option>
|
||
<template x-for="p in provinces" :key="p"><option :value="p" x-text="p"></option></template>
|
||
</select>
|
||
<button class="btn btn-sm btn-pulse" @click="searchStores()" :disabled="storeBusy">Zoeken</button>
|
||
</div>
|
||
<div class="store-search-panel">
|
||
<div>
|
||
<div class="store-list">
|
||
<template x-for="s in storeResults" :key="s.id">
|
||
<div class="store-list-item" :class="{selected: selectedStore?.id===s.id}" @click="selectStore(s.id)">
|
||
<strong x-text="s.chain+' · '+s.name"></strong>
|
||
<div class="clients-card-meta" x-text="(s.city||'') + (s.province ? ', '+s.province : '')"></div>
|
||
<div class="clients-card-tags">
|
||
<span class="clients-tag" x-show="s.partnership_status && s.partnership_status!=='none'" x-text="s.partnership_status"></span>
|
||
<span class="clients-tag" x-show="s.halal_certified">Halal ✓</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<p class="clients-empty" x-show="!storeResults.length && !storeBusy">Geen resultaten — pas filters aan.</p>
|
||
</div>
|
||
<p class="muted" style="font-size:0.72rem;margin-top:0.35rem" x-text="storeResults.length+' filialen · max 80'"></p>
|
||
</div>
|
||
<div class="store-edit-panel" x-show="selectedStore">
|
||
<h3 x-text="(selectedStore?.chain||'') + ' · ' + (selectedStore?.name||'')"></h3>
|
||
<div class="clients-form-grid">
|
||
<label class="full"><span class="filter-label">Adres</span>
|
||
<input class="form-input" x-model="storeForm.address" /></label>
|
||
<label><span class="filter-label">Postcode</span>
|
||
<input class="form-input" x-model="storeForm.postcode" /></label>
|
||
<label><span class="filter-label">Stad</span>
|
||
<input class="form-input" x-model="storeForm.city" /></label>
|
||
<label><span class="filter-label">Provincie</span>
|
||
<input class="form-input" x-model="storeForm.province" /></label>
|
||
<label><span class="filter-label">Telefoon</span>
|
||
<input class="form-input" x-model="storeForm.phone" /></label>
|
||
<label><span class="filter-label">E-mail</span>
|
||
<input class="form-input" x-model="storeForm.email" /></label>
|
||
<label><span class="filter-label">Website</span>
|
||
<input class="form-input" x-model="storeForm.website" /></label>
|
||
<label><span class="filter-label">Manager</span>
|
||
<input class="form-input" x-model="storeForm.manager_name" /></label>
|
||
<label><span class="filter-label">Partnership</span>
|
||
<select class="form-input" x-model="storeForm.partnership_status">
|
||
<option value="none">Geen</option><option value="prospect">Prospect</option>
|
||
<option value="active">Actief</option><option value="paused">Gepauzeerd</option>
|
||
</select></label>
|
||
<label><span class="filter-label">Halal certificerder</span>
|
||
<input class="form-input" x-model="storeForm.halal_certifier" placeholder="HQC, OSM…" /></label>
|
||
</div>
|
||
<div class="store-checks">
|
||
<label><input type="checkbox" x-model="storeForm.halal_certified" /> Halal gecertificeerd</label>
|
||
<label><input type="checkbox" x-model="storeForm.has_halal_section" /> Halal schap</label>
|
||
</div>
|
||
<div class="clients-form-grid" style="margin-top:0.75rem">
|
||
<label class="full"><span class="filter-label">Koppel aan klant</span>
|
||
<select class="form-input" x-model="linkForm.client_id">
|
||
<option value="">— Selecteer klant —</option>
|
||
<template x-for="c in clients" :key="'lc'+c.id">
|
||
<option :value="c.id" x-text="c.name + ' ('+stageLabel(c.stage)+')'"></option>
|
||
</template>
|
||
</select></label>
|
||
<label><span class="filter-label">Relatie</span>
|
||
<select class="form-input" x-model="linkForm.relationship_type">
|
||
<option value="prospect">Prospect</option><option value="partner">Partner</option>
|
||
<option value="supplier">Leverancier</option><option value="competitor">Concurrent</option>
|
||
</select></label>
|
||
<label><span class="filter-label">Notities koppeling</span>
|
||
<input class="form-input" x-model="linkForm.notes" placeholder="Optioneel" /></label>
|
||
</div>
|
||
<div class="btn-group" style="margin-top:1rem">
|
||
<button class="btn btn-primary btn-sm" @click="saveStore()" :disabled="storeBusy">Opslaan filiaal</button>
|
||
<button class="btn btn-sm btn-pulse-green" @click="linkStore()" :disabled="!linkForm.client_id">Koppel klant</button>
|
||
<a class="btn btn-sm" :href="'/retail?store='+selectedStore?.id" target="_blank">Retail 360 →</a>
|
||
</div>
|
||
</div>
|
||
<div class="store-edit-panel clients-empty" x-show="!selectedStore && !storeBusy">
|
||
Selecteer een filiaal links om data aan te vullen.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Client modal -->
|
||
<div class="modal" :class="{ open: clientModal }" @click.self="clientModal=false">
|
||
<div class="modal-card" style="max-width:560px">
|
||
<h3 x-text="clientForm.id ? 'Klant bewerken' : 'Nieuwe klant'"></h3>
|
||
<div class="clients-form-grid">
|
||
<label class="full"><span class="filter-label">Bedrijfsnaam *</span>
|
||
<input class="form-input" x-model="clientForm.name" placeholder="Chai N Masala B.V." /></label>
|
||
<label><span class="filter-label">Contactpersoon</span>
|
||
<input class="form-input" x-model="clientForm.contact" /></label>
|
||
<label><span class="filter-label">E-mail</span>
|
||
<input class="form-input" type="email" x-model="clientForm.email" /></label>
|
||
<label><span class="filter-label">Sector</span>
|
||
<select class="form-input" x-model="clientForm.sector">
|
||
<option value="">— Kies sector —</option>
|
||
<template x-for="s in sectorOptions" :key="'s'+s"><option :value="s" x-text="s"></option></template>
|
||
</select></label>
|
||
<label><span class="filter-label">Stage</span>
|
||
<select class="form-input" x-model="clientForm.stage">
|
||
<template x-for="st in stages" :key="'cs'+st.id"><option :value="st.id" x-text="st.label"></option></template>
|
||
</select></label>
|
||
<label><span class="filter-label">MRR schatting (€/mnd)</span>
|
||
<input class="form-input" type="number" min="0" step="100" x-model="clientForm.mrr_estimate" /></label>
|
||
<label class="full"><span class="filter-label">Notities</span>
|
||
<textarea class="form-input" rows="4" x-model="clientForm.notes" placeholder="Context, afspraken, next steps…"></textarea></label>
|
||
</div>
|
||
<div class="btn-group" style="margin-top:1rem">
|
||
<button class="btn btn-primary" @click="saveClient()" :disabled="!clientForm.name.trim()">Opslaan</button>
|
||
<button class="btn btn-secondary" @click="clientModal=false">Annuleren</button>
|
||
<button class="btn btn-reject" x-show="clientForm.id" @click="removeClient(clientForm.id); clientModal=false">Verwijderen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Store create modal -->
|
||
<div class="modal" :class="{ open: storeCreateModal }" @click.self="storeCreateModal=false">
|
||
<div class="modal-card" style="max-width:560px">
|
||
<h3>Nieuw filiaal toevoegen</h3>
|
||
<div class="clients-form-grid">
|
||
<label><span class="filter-label">Keten *</span>
|
||
<input class="form-input" x-model="newStore.chain" list="chain-list" placeholder="Jumbo, AH, Lidl…" /></label>
|
||
<datalist id="chain-list"><template x-for="ch in chains" :key="'dl'+ch"><option :value="ch"></option></template></datalist>
|
||
<label><span class="filter-label">Naam *</span>
|
||
<input class="form-input" x-model="newStore.name" placeholder="Jumbo Bos en Lommer" /></label>
|
||
<label class="full"><span class="filter-label">Adres *</span>
|
||
<input class="form-input" x-model="newStore.address" /></label>
|
||
<label><span class="filter-label">Postcode</span>
|
||
<input class="form-input" x-model="newStore.postcode" /></label>
|
||
<label><span class="filter-label">Stad *</span>
|
||
<input class="form-input" x-model="newStore.city" /></label>
|
||
<label><span class="filter-label">Provincie</span>
|
||
<input class="form-input" x-model="newStore.province" /></label>
|
||
<label><span class="filter-label">Telefoon</span>
|
||
<input class="form-input" x-model="newStore.phone" /></label>
|
||
<label><span class="filter-label">E-mail</span>
|
||
<input class="form-input" x-model="newStore.email" /></label>
|
||
</div>
|
||
<div class="btn-group" style="margin-top:1rem">
|
||
<button class="btn btn-primary" @click="createStore()" :disabled="!newStore.name||!newStore.chain||!newStore.city">Toevoegen</button>
|
||
<button class="btn btn-secondary" @click="storeCreateModal=false">Annuleren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail drawer -->
|
||
<aside class="clients-drawer" :class="{ open: drawer }">
|
||
<div class="clients-drawer-head">
|
||
<div>
|
||
<h2 x-text="detail?.client?.name||''"></h2>
|
||
<p class="muted" x-text="stageLabel(detail?.client?.stage)+' · '+(detail?.client?.sector||'Geen sector')"></p>
|
||
</div>
|
||
<button class="drawer-close" @click="drawer=false">×</button>
|
||
</div>
|
||
<template x-if="detail?.client">
|
||
<div>
|
||
<div class="clients-card-meta">
|
||
<div x-show="detail.client.contact"><strong>Contact:</strong> <span x-text="detail.client.contact"></span></div>
|
||
<div x-show="detail.client.email"><strong>E-mail:</strong> <span x-text="detail.client.email"></span></div>
|
||
<div x-show="detail.client.mrr_estimate"><strong>MRR:</strong> <span x-text="fmtEur(detail.client.mrr_estimate)"></span></div>
|
||
<div x-show="detail.client.notes" style="margin-top:0.5rem;white-space:pre-wrap" x-text="detail.client.notes"></div>
|
||
</div>
|
||
<div class="btn-group" style="margin-top:0.75rem">
|
||
<button class="btn btn-sm" @click="editClient(detail.client)">Bewerken</button>
|
||
<select class="form-input" style="width:auto" x-model="detail.client.stage" @change="quickStage(detail.client)">
|
||
<template x-for="st in stages" :key="'ds'+st.id"><option :value="st.id" x-text="st.label"></option></template>
|
||
</select>
|
||
</div>
|
||
<div class="clients-drawer-section">
|
||
<h4>Deals (<span x-text="(detail.deals||[]).length"></span>)</h4>
|
||
<template x-for="d in detail.deals||[]" :key="d.id">
|
||
<div class="clients-card" style="cursor:default">
|
||
<strong x-text="d.title"></strong>
|
||
<div class="clients-card-meta" x-text="fmtEur(d.value)+' · '+d.stage"></div>
|
||
</div>
|
||
</template>
|
||
<p class="muted" x-show="!(detail.deals||[]).length">Nog geen deals — <a href="/deals">voeg toe</a></p>
|
||
</div>
|
||
<div class="clients-drawer-section">
|
||
<h4>Gekoppelde filialen (<span x-text="(detail.stores||[]).length"></span>)</h4>
|
||
<template x-for="s in detail.stores||[]" :key="s.id">
|
||
<div class="clients-card" style="cursor:default">
|
||
<strong x-text="s.chain+' · '+s.name"></strong>
|
||
<div class="clients-card-meta" x-text="s.city+(s.province?', '+s.province:'')"></div>
|
||
<span class="clients-tag" x-text="s.relationship_type||s.partnership_status"></span>
|
||
</div>
|
||
</template>
|
||
<button class="btn btn-sm" style="margin-top:0.5rem" @click="tab='stores'; drawer=false">+ Filiaal koppelen</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</aside>
|
||
</div>
|
||
{% endblock %}
|
||
{% block scripts %}
|
||
<script>
|
||
function clientsHub() {
|
||
const STAGES = [
|
||
{ id: 'intake', label: 'Intake' },
|
||
{ id: 'discovery', label: 'Verkenning' },
|
||
{ id: 'proposal', label: 'Voorstel' },
|
||
{ id: 'active', label: 'Actief' },
|
||
{ id: 'churned', label: 'Verloren' },
|
||
];
|
||
const SECTORS = [
|
||
'Retail', 'Supermarkt', 'Groothandel', 'Horeca', 'Food service',
|
||
'Spices & Tea', 'Kant-en-klaar', 'Import/Export', 'Private label', 'Overig',
|
||
];
|
||
return {
|
||
tab: 'kanban',
|
||
busy: false,
|
||
storeBusy: false,
|
||
clientModal: false,
|
||
storeCreateModal: false,
|
||
drawer: false,
|
||
stages: STAGES,
|
||
sectorOptions: SECTORS,
|
||
stats: {},
|
||
clients: [],
|
||
filtered: [],
|
||
filterQ: '',
|
||
filterStage: '',
|
||
filterSector: '',
|
||
detail: null,
|
||
clientForm: {},
|
||
storeResults: [],
|
||
selectedStore: null,
|
||
storeForm: {},
|
||
linkForm: { client_id: '', relationship_type: 'prospect', notes: '' },
|
||
storeQ: '',
|
||
storeChain: '',
|
||
storeProvince: '',
|
||
chains: [],
|
||
provinces: [],
|
||
newStore: { chain: '', name: '', address: '', postcode: '0000AA', city: '', province: '', phone: '', email: '' },
|
||
|
||
fmtEur(v) {
|
||
if (v == null || v === '') return '—';
|
||
return '€' + Number(v).toLocaleString('nl-NL', { maximumFractionDigits: 0 });
|
||
},
|
||
stageLabel(id) {
|
||
return (STAGES.find(s => s.id === id) || {}).label || id || '—';
|
||
},
|
||
stageCount(id) { return this.clients.filter(c => c.stage === id).length; },
|
||
clientsByStage(id) { return this.clients.filter(c => c.stage === id); },
|
||
|
||
async init() { await this.loadAll(); },
|
||
|
||
async loadAll() {
|
||
this.busy = true;
|
||
try {
|
||
const [statsR, clientsR, filtersR] = await Promise.all([
|
||
Cockpit.api('/clients/stats'),
|
||
Cockpit.api('/clients'),
|
||
fetch('/api/retail/filters').then(r => r.json()).catch(() => ({})),
|
||
]);
|
||
this.stats = statsR.stats || {};
|
||
this.clients = clientsR.items || [];
|
||
this.chains = (filtersR.chains || []).map(x => x.chain || x).filter(Boolean);
|
||
this.provinces = (filtersR.provinces || []).map(x => x.province || x).filter(Boolean);
|
||
this.applyFilter();
|
||
} catch (e) {
|
||
Cockpit.toast(e.message, 'error');
|
||
}
|
||
this.busy = false;
|
||
},
|
||
|
||
applyFilter() {
|
||
const q = (this.filterQ || '').toLowerCase();
|
||
this.filtered = this.clients.filter(c => {
|
||
if (this.filterStage && c.stage !== this.filterStage) return false;
|
||
if (this.filterSector && c.sector !== this.filterSector) return false;
|
||
if (!q) return true;
|
||
return [c.name, c.contact, c.email, c.sector, c.notes].some(
|
||
f => f && String(f).toLowerCase().includes(q)
|
||
);
|
||
});
|
||
},
|
||
|
||
openClientModal() {
|
||
this.clientForm = { id: null, name: '', contact: '', email: '', stage: 'intake', sector: '', mrr_estimate: '', notes: '' };
|
||
this.clientModal = true;
|
||
},
|
||
editClient(c) {
|
||
this.clientForm = {
|
||
id: c.id, name: c.name || '', contact: c.contact || '', email: c.email || '',
|
||
stage: c.stage || 'intake', sector: c.sector || '', mrr_estimate: c.mrr_estimate || '',
|
||
notes: c.notes || '',
|
||
};
|
||
this.clientModal = true;
|
||
this.drawer = false;
|
||
},
|
||
|
||
async saveClient() {
|
||
const body = {
|
||
name: this.clientForm.name.trim(),
|
||
contact: this.clientForm.contact || null,
|
||
email: this.clientForm.email || null,
|
||
stage: this.clientForm.stage,
|
||
sector: this.clientForm.sector || null,
|
||
mrr_estimate: this.clientForm.mrr_estimate ? parseFloat(this.clientForm.mrr_estimate) : null,
|
||
notes: this.clientForm.notes || null,
|
||
};
|
||
try {
|
||
if (this.clientForm.id) {
|
||
await Cockpit.api('/clients/' + this.clientForm.id, { method: 'PUT', body: JSON.stringify(body) });
|
||
} else {
|
||
await Cockpit.api('/clients', { method: 'POST', body: JSON.stringify(body) });
|
||
}
|
||
Cockpit.toast('Klant opgeslagen', 'success');
|
||
this.clientModal = false;
|
||
await this.loadAll();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async removeClient(id) {
|
||
if (!Cockpit.confirmDelete('Klant definitief verwijderen?')) return;
|
||
try {
|
||
await Cockpit.api('/clients/' + id, { method: 'DELETE' });
|
||
Cockpit.toast('Klant verwijderd', 'info');
|
||
this.drawer = false;
|
||
await this.loadAll();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async openDetail(c) {
|
||
try {
|
||
const d = await Cockpit.api('/clients/' + c.id + '/detail');
|
||
this.detail = d;
|
||
this.drawer = true;
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async quickStage(c) {
|
||
const body = {
|
||
name: c.name, contact: c.contact, email: c.email, stage: c.stage,
|
||
sector: c.sector, mrr_estimate: c.mrr_estimate, notes: c.notes,
|
||
};
|
||
try {
|
||
await Cockpit.api('/clients/' + c.id, { method: 'PUT', body: JSON.stringify(body) });
|
||
Cockpit.toast('Stage bijgewerkt', 'success');
|
||
await this.loadAll();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async searchStores() {
|
||
this.storeBusy = true;
|
||
try {
|
||
const params = new URLSearchParams({ limit: '80', sort: 'name' });
|
||
if (this.storeQ) params.set('q', this.storeQ);
|
||
if (this.storeChain) params.set('chain', this.storeChain);
|
||
if (this.storeProvince) params.set('province', this.storeProvince);
|
||
const r = await fetch('/api/retail/list?' + params).then(x => x.json());
|
||
this.storeResults = r.items || [];
|
||
if (this.storeResults.length && !this.selectedStore) {
|
||
await this.selectStore(this.storeResults[0].id);
|
||
}
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
this.storeBusy = false;
|
||
},
|
||
|
||
async selectStore(id) {
|
||
try {
|
||
const r = await fetch('/api/retail/supermarkets/' + id).then(x => x.json());
|
||
this.selectedStore = r;
|
||
this.storeForm = {
|
||
address: r.address || '', postcode: r.postcode || '', city: r.city || '',
|
||
province: r.province || '', phone: r.phone || '', email: r.email || '',
|
||
website: r.website || '', manager_name: r.manager_name || r.profile_manager || '',
|
||
partnership_status: r.partnership_status || 'none',
|
||
halal_certified: !!r.halal_certified, has_halal_section: !!r.has_halal_section,
|
||
halal_certifier: r.halal_certifier || '',
|
||
};
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
async saveStore() {
|
||
if (!this.selectedStore) return;
|
||
this.storeBusy = true;
|
||
try {
|
||
await fetch('/api/retail/supermarkets/' + this.selectedStore.id, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(this.storeForm),
|
||
}).then(r => { if (!r.ok) throw new Error('Opslaan mislukt'); return r.json(); });
|
||
Cockpit.toast('Filiaaldata opgeslagen', 'success');
|
||
await this.selectStore(this.selectedStore.id);
|
||
await this.searchStores();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
this.storeBusy = false;
|
||
},
|
||
|
||
async linkStore() {
|
||
if (!this.selectedStore || !this.linkForm.client_id) return;
|
||
try {
|
||
await fetch('/api/retail/supermarkets/' + this.selectedStore.id + '/link', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
client_id: parseInt(this.linkForm.client_id, 10),
|
||
relationship_type: this.linkForm.relationship_type,
|
||
partnership_status: this.storeForm.partnership_status !== 'none' ? this.storeForm.partnership_status : 'prospect',
|
||
notes: this.linkForm.notes || null,
|
||
}),
|
||
}).then(r => { if (!r.ok) throw new Error('Koppelen mislukt'); return r.json(); });
|
||
Cockpit.toast('Filiaal gekoppeld aan klant', 'success');
|
||
await this.loadAll();
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
|
||
openStoreCreate() {
|
||
this.newStore = { chain: '', name: '', address: '', postcode: '0000AA', city: '', province: '', phone: '', email: '' };
|
||
this.storeCreateModal = true;
|
||
},
|
||
|
||
async createStore() {
|
||
try {
|
||
const r = await fetch('/api/retail/supermarkets', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(this.newStore),
|
||
}).then(x => { if (!x.ok) throw new Error('Toevoegen mislukt'); return x.json(); });
|
||
Cockpit.toast('Filiaal toegevoegd', 'success');
|
||
this.storeCreateModal = false;
|
||
await this.searchStores();
|
||
if (r.item?.id) await this.selectStore(r.item.id);
|
||
} catch (e) { Cockpit.toast(e.message, 'error'); }
|
||
},
|
||
};
|
||
}
|
||
</script>
|
||
{% endblock %}
|