Improve mobile UI and add infinite scroll

This commit is contained in:
sas.fajri
2026-05-04 10:44:38 +07:00
parent b07f04217e
commit 7c378f78bf
17 changed files with 729 additions and 115 deletions

View File

@@ -39,19 +39,19 @@
</section>
<section class="grid gap-4 sm:grid-cols-3">
<article class="card p-5">
<article class="card border-l-4 border-l-brand-300 p-4">
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Checked In</p>
<p class="num mt-2 text-3xl font-semibold text-slate-900">{{.Summary.CheckedIn}}</p>
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.Summary.CheckedIn}}</p>
<p class="mt-1 text-xs text-slate-400">Sudah check-in pada tanggal ini</p>
</article>
<article class="card p-5">
<article class="card border-l-4 border-l-brand-500 p-4">
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Not Check-in Yet</p>
<p class="num mt-2 text-3xl font-semibold text-slate-900">{{.Summary.Pending}}</p>
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.Summary.Pending}}</p>
<p class="mt-1 text-xs text-slate-400">Belum masuk ke area MCU</p>
</article>
<article class="card p-5">
<article class="card border-l-4 border-l-emerald-500 p-4">
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Total Schedule</p>
<p class="num mt-2 text-3xl font-semibold text-slate-900">{{.Summary.Total}}</p>
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.Summary.Total}}</p>
<p class="mt-1 text-xs text-slate-400">Peserta yang dijadwalkan hari ini</p>
</article>
</section>
@@ -62,7 +62,8 @@
<p class="text-sm font-semibold text-slate-700">Check-in Overview</p>
<p class="text-xs text-slate-400">Inner ring: checked-in summary, outer ring: distribution by department / posisi</p>
</div>
<div id="arrival-overview-chart" class="h-72 w-full"></div>
<div id="arrival-overview-chart" class="h-72 w-full sm:h-80"></div>
<div id="arrival-overview-legend-mobile" class="mt-3 hidden grid-cols-2 gap-x-4 gap-y-2 text-xs text-slate-600 sm:hidden"></div>
</article>
<article class="card p-5">
<div class="mb-3">
@@ -105,7 +106,7 @@
<h2 class="text-base font-semibold text-slate-700">Live Arrival List</h2>
<p class="text-xs text-slate-400">Tanggal: {{.Date | fmtDate}}</p>
</div>
<span class="text-xs font-medium text-slate-400">{{len .FilteredRows}} ditampilkan</span>
<span class="text-xs font-medium text-slate-400">{{.VisibleCount}} / {{.TotalFiltered}} ditampilkan</span>
</div>
<div class="border-b border-slate-100 px-5 py-3">
@@ -128,7 +129,7 @@
<th class="px-4 py-3 font-medium">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<tbody id="arrival-desktop-rows" class="divide-y divide-slate-100">
{{range .FilteredRows}}
<tr class="hover:bg-slate-50">
<td class="px-4 py-3 num">{{if .InTime}}{{.InTime}}{{else}}-{{end}}</td>
@@ -166,44 +167,91 @@
</table>
</div>
<div class="grid gap-3 p-4 md:hidden">
<div id="arrival-mobile-rows" class="grid gap-3 p-4 md:hidden">
{{range .FilteredRows}}
<article class="rounded-xl border border-slate-200 p-3">
<p class="font-semibold text-slate-700">{{.Name}}</p>
<p class="mt-1 text-xs text-slate-400">{{if .InTime}}{{.InTime}}{{else}}-{{end}} • {{.NIP}} • {{.Department}}</p>
<div class="mt-2 flex flex-wrap gap-1.5">
<div class="mt-3 flex flex-col gap-1.5">
{{if .Stations}}
{{range .Stations}}
{{if eq .Tone "success"}}
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">{{.Name}}</span>
<span class="flex w-full items-center gap-2 border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-medium text-emerald-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
<span class="truncate">{{.Name}}</span>
</span>
{{else if eq .Tone "warning"}}
<span class="rounded-full border border-amber-200 bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700">{{.Name}}</span>
<span class="flex w-full items-center gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-medium text-amber-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="truncate">{{.Name}}</span>
</span>
{{else if eq .Tone "danger"}}
<span class="rounded-full border border-rose-200 bg-rose-50 px-2 py-1 text-xs font-medium text-rose-700">{{.Name}}</span>
<span class="flex w-full items-center gap-2 border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-medium text-rose-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="truncate">{{.Name}}</span>
</span>
{{else}}
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">{{.Name}}</span>
<span class="flex w-full items-center gap-2 border border-slate-200 bg-slate-100 px-3 py-2 text-xs font-medium text-slate-600">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="truncate">{{.Name}}</span>
</span>
{{end}}
{{end}}
{{else}}
{{if eq .StatusTone "success"}}
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">{{.Status}}</span>
<span class="flex w-full items-center gap-2 border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-medium text-emerald-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
</svg>
<span class="truncate">{{.Status}}</span>
</span>
{{else if eq .StatusTone "warning"}}
<span class="rounded-full border border-amber-200 bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700">{{.Status}}</span>
<span class="flex w-full items-center gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-medium text-amber-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="truncate">{{.Status}}</span>
</span>
{{else}}
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">{{.Status}}</span>
<span class="flex w-full items-center gap-2 border border-slate-200 bg-slate-100 px-3 py-2 text-xs font-medium text-slate-600">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
</svg>
<span class="truncate">{{.Status}}</span>
</span>
{{end}}
{{end}}
</div>
</article>
{{end}}
</div>
<div id="arrival-load-more" class="px-4 pb-4">
{{if .HasMore}}
<div
hx-get='{{b "/arrival/list"}}?date={{.Date}}&search={{.Search}}&dept={{.Department}}&page={{.NextPage}}'
hx-trigger="revealed"
hx-swap="outerHTML"
class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-center text-xs font-medium text-slate-500">
Memuat data berikutnya...
</div>
{{else}}
<div class="text-center text-xs text-slate-400">Semua data sudah ditampilkan</div>
{{end}}
</div>
{{else}}
<div class="px-5 py-10 text-center text-sm text-slate-400">
Belum ada data arrival pada tanggal ini.
</div>
{{end}}
</section>
<script>
(function() {
const arrivalFilterForm = document.getElementById('arrival-filter-form');
@@ -229,20 +277,41 @@
const deptColors = ['#f59e0b', '#8b5cf6', '#f97316', '#06b6d4', '#ec4899', '#84cc16', '#14b8a6'];
const overviewEl = document.getElementById('arrival-overview-chart');
const overviewLegendMobileEl = document.getElementById('arrival-overview-legend-mobile');
if (overviewEl && overviewData && typeof echarts !== 'undefined') {
const overviewChart = echarts.init(overviewEl);
const outerData = (overviewData.depts || []).map(function(d) {
return { value: d.value, name: d.name };
});
const isMobile = window.matchMedia('(max-width: 639px)').matches;
if (isMobile && overviewLegendMobileEl) {
const legendItems = [
{ name: 'Checked In', color: '#3b50a0' },
{ name: 'Not Check-in Yet', color: '#cbd5e1' }
].concat((overviewData.depts || []).map(function(d, idx) {
return { name: d.name, color: deptColors[idx % deptColors.length] };
}));
overviewLegendMobileEl.innerHTML = legendItems.map(function(item) {
return '<div class="flex items-center gap-2 min-w-0">' +
'<span class="h-2.5 w-2.5 shrink-0 rounded-sm" style="background:' + item.color + '"></span>' +
'<span class="truncate">' + item.name + '</span>' +
'</div>';
}).join('');
overviewLegendMobileEl.classList.remove('hidden');
overviewLegendMobileEl.classList.add('grid');
}
overviewChart.setOption({
color: ['#3b50a0', '#cbd5e1'].concat(deptColors),
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: {
show: !isMobile,
type: 'scroll',
orient: 'vertical',
left: '58%',
right: 0,
top: 'middle',
textStyle: { color: '#64748b', fontSize: 11 },
itemGap: 8,
selectedMode: false,
pageIconColor: '#3b50a0',
pageTextStyle: { color: '#64748b' }
@@ -252,7 +321,7 @@
name: 'Check-in Summary',
type: 'pie',
radius: ['28%', '45%'],
center: ['38%', '48%'],
center: isMobile ? ['50%', '50%'] : ['38%', '48%'],
label: { show: false },
labelLine: { show: false },
data: [
@@ -264,7 +333,7 @@
name: 'Dept Detail',
type: 'pie',
radius: ['55%', '72%'],
center: ['38%', '48%'],
center: isMobile ? ['50%', '50%'] : ['38%', '48%'],
label: { show: false },
labelLine: { show: false },
data: outerData
@@ -304,3 +373,108 @@
})();
</script>
{{end}}
{{define "arrival-list-chunk"}}
<tbody id="arrival-desktop-rows" hx-swap-oob="beforeend">
{{range .FilteredRows}}
<tr class="hover:bg-slate-50">
<td class="px-4 py-3 num">{{if .InTime}}{{.InTime}}{{else}}-{{end}}</td>
<td class="px-4 py-3 num">{{.NIP}}</td>
<td class="px-4 py-3 font-medium text-slate-700">{{.Name}}</td>
<td class="px-4 py-3 text-slate-500">{{.Department}}</td>
<td class="px-4 py-3">
{{if .Stations}}
<div class="flex flex-wrap gap-1.5">
{{range .Stations}}
{{if eq .Tone "success"}}
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">{{.Name}}</span>
{{else if eq .Tone "warning"}}
<span class="rounded-full border border-amber-200 bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700">{{.Name}}</span>
{{else if eq .Tone "danger"}}
<span class="rounded-full border border-rose-200 bg-rose-50 px-2 py-1 text-xs font-medium text-rose-700">{{.Name}}</span>
{{else}}
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">{{.Name}}</span>
{{end}}
{{end}}
</div>
{{else}}
{{if eq .StatusTone "success"}}
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">{{.Status}}</span>
{{else if eq .StatusTone "warning"}}
<span class="rounded-full border border-amber-200 bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700">{{.Status}}</span>
{{else}}
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">{{.Status}}</span>
{{end}}
{{end}}
</td>
</tr>
{{end}}
</tbody>
<div id="arrival-mobile-rows" hx-swap-oob="beforeend">
{{range .FilteredRows}}
<article class="rounded-xl border border-slate-200 p-3">
<p class="font-semibold text-slate-700">{{.Name}}</p>
<p class="mt-1 text-xs text-slate-400">{{if .InTime}}{{.InTime}}{{else}}-{{end}} • {{.NIP}} • {{.Department}}</p>
<div class="mt-3 flex flex-col gap-1.5">
{{if .Stations}}
{{range .Stations}}
{{if eq .Tone "success"}}
<span class="flex w-full items-center gap-2 border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-medium text-emerald-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span class="truncate">{{.Name}}</span>
</span>
{{else if eq .Tone "warning"}}
<span class="flex w-full items-center gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-medium text-amber-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="truncate">{{.Name}}</span>
</span>
{{else if eq .Tone "danger"}}
<span class="flex w-full items-center gap-2 border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-medium text-rose-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="truncate">{{.Name}}</span>
</span>
{{else}}
<span class="flex w-full items-center gap-2 border border-slate-200 bg-slate-100 px-3 py-2 text-xs font-medium text-slate-600">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="truncate">{{.Name}}</span>
</span>
{{end}}
{{end}}
{{else}}
{{if eq .StatusTone "success"}}
<span class="flex w-full items-center gap-2 border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-medium text-emerald-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
<span class="truncate">{{.Status}}</span>
</span>
{{else if eq .StatusTone "warning"}}
<span class="flex w-full items-center gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-medium text-amber-700">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="truncate">{{.Status}}</span>
</span>
{{else}}
<span class="flex w-full items-center gap-2 border border-slate-200 bg-slate-100 px-3 py-2 text-xs font-medium text-slate-600">
<svg class="h-3.5 w-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
<span class="truncate">{{.Status}}</span>
</span>
{{end}}
{{end}}
</div>
</article>
{{end}}
</div>
<div id="arrival-load-more" hx-swap-oob="outerHTML">
{{if .HasMore}}
<div
hx-get='{{b "/arrival/list"}}?date={{.Date}}&search={{.Search}}&dept={{.Department}}&page={{.NextPage}}'
hx-trigger="revealed"
hx-swap="outerHTML"
class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-center text-xs font-medium text-slate-500">
Memuat data berikutnya...
</div>
{{else}}
<div class="text-center text-xs text-slate-400">Semua data sudah ditampilkan</div>
{{end}}
</div>
{{end}}