292 lines
14 KiB
PHP
292 lines
14 KiB
PHP
@extends('store.layouts.app', ['pageTitle' => 'Record Sale', 'pageSubtitle' => 'Register retail QR code sales'])
|
|
|
|
@push('styles')
|
|
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<style>
|
|
.ts-control { border-radius: 0.75rem !important; padding: 0.75rem 1rem !important; font-size: 1.125rem !important; }
|
|
#resident-map { height: 250px; border-radius: 0.75rem; z-index: 1; }
|
|
.verde-receipt-card { background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%); }
|
|
</style>
|
|
@endpush
|
|
|
|
@section('page')
|
|
<div class="mx-auto max-w-4xl space-y-8">
|
|
<div class="rounded-2xl border border-neutral-200 bg-white p-8 shadow-sm">
|
|
<div class="mb-8">
|
|
<h2 class="text-2xl font-black text-neutral-900 tracking-tight">Record New Sale</h2>
|
|
<p class="mt-1 text-sm text-neutral-500 font-medium">Link QR stickers to a verified household in your LGU.</p>
|
|
</div>
|
|
|
|
<div class="space-y-6">
|
|
{{-- Search & Map --}}
|
|
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
|
<div class="space-y-6">
|
|
<div class="mb-4 flex rounded-xl bg-neutral-100 p-1">
|
|
<button type="button" id="toggle-household" class="flex-1 rounded-lg bg-white py-2 text-xs font-bold shadow-sm text-neutral-900 transition-all">Household</button>
|
|
<button type="button" id="toggle-resident" class="flex-1 rounded-lg py-2 text-xs font-bold text-neutral-500 hover:text-neutral-900 transition-all">Resident</button>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-widest text-neutral-400 mb-2" id="search-label">Search Household</label>
|
|
<select id="entity-select" placeholder="Start typing name or address..." class="w-full"></select>
|
|
</div>
|
|
|
|
<div id="selected-info" class="hidden animate-in fade-in slide-in-from-top-4">
|
|
<div class="rounded-xl bg-verde-50/50 p-4 ring-1 ring-verde-100">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-verde-600 text-white">
|
|
<i data-lucide="user" class="h-5 w-5"></i>
|
|
</div>
|
|
<div>
|
|
<h4 id="display-name" class="font-bold text-neutral-900">...</h4>
|
|
<p id="display-address" class="text-xs text-neutral-500">...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cash Only Badge -->
|
|
<div class="flex items-center gap-2 rounded-xl bg-amber-50 p-3 ring-1 ring-amber-200">
|
|
<i data-lucide="banknote" class="h-5 w-5 text-amber-600"></i>
|
|
<p class="text-xs font-bold text-amber-900 uppercase tracking-wide">Cash Payment Only</p>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-widest text-neutral-400 mb-2">Quantity</label>
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" onclick="adjustQty(-1)" class="flex h-12 w-12 items-center justify-center rounded-xl border border-neutral-200 bg-white hover:bg-neutral-50 active:scale-95 transition-all">
|
|
<i data-lucide="minus" class="h-4 w-4"></i>
|
|
</button>
|
|
<input type="number" id="sale-qty" value="1" min="1" class="h-12 w-20 rounded-xl border-neutral-200 text-center font-bold text-lg focus:ring-verde-500 focus:border-verde-500">
|
|
<button type="button" onclick="adjustQty(1)" class="flex h-12 w-12 items-center justify-center rounded-xl border border-neutral-200 bg-white hover:bg-neutral-50 active:scale-95 transition-all">
|
|
<i data-lucide="plus" class="h-4 w-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col justify-end items-end">
|
|
<p class="text-[10px] font-black uppercase tracking-tighter text-neutral-400">Total Amount</p>
|
|
<p id="total-price" class="text-3xl font-black text-neutral-900">₱0.00</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-xs font-bold uppercase tracking-widest text-neutral-400 mb-2">Manual Serial (Optional)</label>
|
|
<input type="text" id="serial-entry" placeholder="VERDE-XXXX-XXXX" class="w-full rounded-xl border-neutral-200 py-3 font-mono text-sm uppercase tracking-wider focus:ring-verde-500 focus:border-verde-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
<div id="resident-map" class="bg-neutral-100 flex items-center justify-center text-neutral-400 border border-neutral-200">
|
|
<div class="text-center p-8">
|
|
<i data-lucide="map-pin" class="h-8 w-8 mx-auto mb-2 opacity-20"></i>
|
|
<p class="text-xs">Select a resident to see their location</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-8">
|
|
<button type="button" id="btn-submit" class="w-full flex items-center justify-center gap-3 rounded-2xl bg-verde-600 py-5 text-lg font-black text-white shadow-2xl shadow-verde-600/30 hover:bg-verde-700 active:scale-[0.99] transition-all disabled:opacity-50">
|
|
<i data-lucide="credit-card" class="h-6 w-6"></i>
|
|
<span>CONFIRM SALE & PRINT RECEIPT</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Success Modal --}}
|
|
<div id="success-modal" class="fixed inset-0 z-[100] hidden items-center justify-center bg-neutral-900/80 backdrop-blur-md p-4">
|
|
<div class="w-full max-w-md rounded-3xl bg-white p-8 shadow-2xl animate-in zoom-in-95 duration-200">
|
|
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-verde-100 text-verde-600 mb-6">
|
|
<i data-lucide="check" class="h-10 w-10"></i>
|
|
</div>
|
|
<h3 class="text-2xl font-black text-neutral-900 text-center">Sale Recorded!</h3>
|
|
<p class="mt-2 text-center text-neutral-500">The transaction was successful. The resident has been notified and can view their receipt in their app.</p>
|
|
|
|
<div class="mt-8 space-y-3">
|
|
<a id="download-receipt" href="#" target="_blank" class="flex w-full items-center justify-center gap-3 rounded-2xl bg-neutral-900 py-4 font-bold text-white hover:bg-neutral-800 transition-colors">
|
|
<i data-lucide="file-text" class="h-5 w-5"></i>
|
|
Download PDF Receipt
|
|
</a>
|
|
<button onclick="location.reload()" class="flex w-full items-center justify-center gap-3 rounded-2xl border-2 border-neutral-200 py-4 font-bold text-neutral-600 hover:bg-neutral-50 transition-colors">
|
|
<i data-lucide="plus" class="h-5 w-5"></i>
|
|
Record Another Sale
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script type="module">
|
|
let map, marker;
|
|
let retailPrice = 0;
|
|
const token = window.Verde.getToken();
|
|
|
|
// Init Map
|
|
const initMap = () => {
|
|
map = L.map('resident-map').setView([14.5995, 120.9842], 13); // Manila default
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
};
|
|
|
|
// Fetch Settings
|
|
const fetchSettings = async () => {
|
|
const res = await window.Verde.apiFetch('/api/v1/store/settings');
|
|
const data = res.body?.data;
|
|
if (data) {
|
|
retailPrice = data.retail_price_centavos;
|
|
updateTotal();
|
|
}
|
|
};
|
|
|
|
const updateTotal = () => {
|
|
const qty = parseInt(document.getElementById('sale-qty').value) || 0;
|
|
const total = (qty * retailPrice) / 100;
|
|
document.getElementById('total-price').textContent = new Intl.NumberFormat('en-PH', {
|
|
style: 'currency', currency: 'PHP'
|
|
}).format(total);
|
|
};
|
|
|
|
window.adjustQty = (delta) => {
|
|
const input = document.getElementById('sale-qty');
|
|
input.value = Math.max(1, parseInt(input.value) + delta);
|
|
updateTotal();
|
|
};
|
|
|
|
document.getElementById('sale-qty').addEventListener('input', updateTotal);
|
|
|
|
let searchMode = 'household';
|
|
let tomSelectInstance = null;
|
|
|
|
// Toggle logic
|
|
const tHousehold = document.getElementById('toggle-household');
|
|
const tResident = document.getElementById('toggle-resident');
|
|
const searchLabel = document.getElementById('search-label');
|
|
|
|
const setMode = (mode) => {
|
|
searchMode = mode;
|
|
if (mode === 'household') {
|
|
tHousehold.className = 'flex-1 rounded-lg bg-white py-2 text-xs font-bold shadow-sm text-neutral-900 transition-all';
|
|
tResident.className = 'flex-1 rounded-lg py-2 text-xs font-bold text-neutral-500 hover:text-neutral-900 transition-all';
|
|
searchLabel.textContent = 'Search Household';
|
|
} else {
|
|
tResident.className = 'flex-1 rounded-lg bg-white py-2 text-xs font-bold shadow-sm text-neutral-900 transition-all';
|
|
tHousehold.className = 'flex-1 rounded-lg py-2 text-xs font-bold text-neutral-500 hover:text-neutral-900 transition-all';
|
|
searchLabel.textContent = 'Search Resident';
|
|
}
|
|
document.getElementById('selected-info').classList.add('hidden');
|
|
if (marker) map.removeLayer(marker);
|
|
initSearch();
|
|
};
|
|
|
|
tHousehold.addEventListener('click', () => setMode('household'));
|
|
tResident.addEventListener('click', () => setMode('resident'));
|
|
|
|
// Init Autocomplete
|
|
const initSearch = () => {
|
|
if (tomSelectInstance) {
|
|
tomSelectInstance.destroy();
|
|
document.getElementById('entity-select').value = '';
|
|
}
|
|
|
|
tomSelectInstance = new TomSelect('#entity-select', {
|
|
valueField: 'id',
|
|
labelField: 'head_name',
|
|
searchField: ['head_name', 'address'],
|
|
load: async (query, callback) => {
|
|
const endpoint = searchMode === 'household' ? 'households' : 'users';
|
|
const url = `/api/v1/store/${endpoint}?q=${encodeURIComponent(query)}`;
|
|
const res = await window.Verde.apiFetch(url);
|
|
callback(res.body?.data || []);
|
|
},
|
|
render: {
|
|
option: (data, escape) => `
|
|
<div class="px-3 py-2">
|
|
<div class="font-bold text-neutral-900">${escape(data.head_name)}</div>
|
|
<div class="text-[10px] text-neutral-500 uppercase tracking-wide">${escape(data.address)} · ${escape(data.barangay)}</div>
|
|
</div>
|
|
`,
|
|
item: (data, escape) => `<div>${escape(data.head_name)}</div>`
|
|
},
|
|
onChange: (id) => {
|
|
const data = tomSelectInstance.options[id];
|
|
if (!data) return;
|
|
|
|
document.getElementById('display-name').textContent = data.head_name;
|
|
document.getElementById('display-address').textContent = `${data.address}, ${data.barangay}`;
|
|
document.getElementById('selected-info').classList.remove('hidden');
|
|
|
|
if (data.lat && data.lng) {
|
|
if (marker) map.removeLayer(marker);
|
|
const pos = [data.lat, data.lng];
|
|
marker = L.marker(pos).addTo(map);
|
|
map.setView(pos, 16);
|
|
} else {
|
|
if (marker) map.removeLayer(marker);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// Form Submission
|
|
document.getElementById('btn-submit').addEventListener('click', async () => {
|
|
const id = document.getElementById('entity-select').value;
|
|
if (!id) return alert(`Please select a ${searchMode} first.`);
|
|
|
|
const btn = document.getElementById('btn-submit');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i data-lucide="loader-2" class="h-6 w-6 animate-spin"></i> PROCESSING...';
|
|
if (window.lucide) window.lucide.createIcons();
|
|
|
|
const payload = {
|
|
quantity: document.getElementById('sale-qty').value,
|
|
serial_number: document.getElementById('serial-entry').value || null
|
|
};
|
|
|
|
if (searchMode === 'household') {
|
|
payload.household_id = id;
|
|
} else {
|
|
payload.user_id = id;
|
|
}
|
|
|
|
try {
|
|
const res = await window.Verde.apiFetch('/api/v1/store/sales', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = res.body?.data;
|
|
document.getElementById('download-receipt').href = `/api/v1/store/sales/${data.sale_id}/receipt`;
|
|
document.getElementById('success-modal').classList.replace('hidden', 'flex');
|
|
} else {
|
|
alert(res.body?.message || 'Transaction failed.');
|
|
}
|
|
} catch (e) {
|
|
alert('A network error occurred.');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i data-lucide="credit-card" class="h-6 w-6"></i> CONFIRM SALE & PRINT RECEIPT';
|
|
if (window.lucide) window.lucide.createIcons();
|
|
}
|
|
});
|
|
|
|
// Lifecycle
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initMap();
|
|
initSearch();
|
|
fetchSettings();
|
|
});
|
|
</script>
|
|
@endpush
|
|
|