Improve mobile UI and add infinite scroll
This commit is contained in:
@@ -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}}
|
||||
|
||||
@@ -207,22 +207,16 @@
|
||||
Lihat
|
||||
</button>
|
||||
</form>
|
||||
{{if gt .KPI.InvitedStaff 0}}
|
||||
<div class="rounded-xl bg-brand-50 px-3 py-2 text-center">
|
||||
<p class="text-xs text-slate-500">Invited Staff</p>
|
||||
<p class="num text-sm font-semibold text-brand-600">{{.KPI.InvitedStaff}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- KPI Cards — SSE swap -->
|
||||
<section id="sse-kpi" class="grid gap-4 sm:grid-cols-3 sse-anim-target"
|
||||
<section id="sse-kpi" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4 sse-anim-target"
|
||||
sse-swap="kpi" hx-swap="innerHTML">
|
||||
<span class="sse-running-bar" aria-hidden="true"></span>
|
||||
<span class="sse-updating-label" aria-hidden="true">Updating...</span>
|
||||
{{range $i := seq 3}}
|
||||
{{range $i := seq 4}}
|
||||
<div class="card h-28 animate-pulse bg-slate-50"></div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
{{define "kpi"}}
|
||||
<!-- Invited Staff -->
|
||||
<article class="card border-l-4 border-l-brand-200 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Invited Staff</p>
|
||||
<svg class="h-4 w-4 text-brand-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A9.956 9.956 0 0112 15c2.2 0 4.236.711 5.879 1.915M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.InvitedStaff}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Total staff undangan</p>
|
||||
</article>
|
||||
|
||||
<!-- Total Staff -->
|
||||
<article class="card border-l-4 border-l-brand-300 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Rows}}
|
||||
<div class="p-3">
|
||||
<table class="min-w-full text-sm">
|
||||
<div class="overflow-x-auto p-3">
|
||||
<table class="min-w-full text-sm md:table-fixed">
|
||||
<thead>
|
||||
<tr class="text-left text-xs font-semibold uppercase tracking-wide text-slate-400">
|
||||
<th class="px-3 py-2">Station</th>
|
||||
<th class="px-3 py-2 text-right">Sudah</th>
|
||||
<th class="hidden px-3 py-2 text-right sm:table-cell">Sudah</th>
|
||||
<th class="px-3 py-2 text-right">Belum</th>
|
||||
<th class="px-3 py-2 text-right">Total</th>
|
||||
<th class="px-3 py-2 w-40">Progress</th>
|
||||
<th class="px-3 py-2 sm:w-40">Progress</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@@ -29,7 +29,7 @@
|
||||
<td class="px-3 py-2.5 font-medium text-slate-700">
|
||||
{{.Station | stationShort}}
|
||||
</td>
|
||||
<td class="num px-3 py-2.5 text-right font-semibold text-slate-900">{{.Processed}}</td>
|
||||
<td class="num hidden px-3 py-2.5 text-right font-semibold text-slate-900 sm:table-cell">{{.Processed}}</td>
|
||||
<td class="num px-3 py-2.5 text-right text-amber-600">{{.Pending}}</td>
|
||||
<td class="num px-3 py-2.5 text-right text-slate-400">{{.Total}}</td>
|
||||
<td class="px-3 py-2.5">
|
||||
|
||||
@@ -53,16 +53,27 @@
|
||||
<body class="min-h-screen bg-slate-100 text-slate-800">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="bg-brand-500 text-white">
|
||||
<div class="mx-auto flex w-full max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
||||
<header class="w-full bg-brand-500 text-white">
|
||||
<div class="mx-auto flex w-full items-center justify-between px-4 py-3 sm:px-6 lg:max-w-7xl lg:px-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{b "/dashboard"}}" class="shrink-0 rounded-lg bg-white px-3 py-1.5">
|
||||
<img src="{{b "/static/img/logo.png"}}" alt="Logo" class="h-8 w-auto">
|
||||
</a>
|
||||
<div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold leading-tight">{{block "header-title" .}}Dashboard{{end}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
id="mobile-menu-toggle"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-lg p-2 text-white transition hover:bg-white/15 sm:hidden"
|
||||
aria-label="Buka menu navigasi"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false">
|
||||
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="hidden items-center gap-1 text-sm sm:flex">
|
||||
<nav class="flex items-center gap-1">
|
||||
<a href="{{b "/dashboard"}}" class="rounded-lg px-3 py-1.5 font-medium transition hover:bg-white/15">Dashboard</a>
|
||||
@@ -79,12 +90,39 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden border-t border-white/20 px-4 py-3 sm:hidden">
|
||||
<nav class="flex flex-col gap-1 text-sm">
|
||||
<a href="{{b "/dashboard"}}" class="rounded-lg px-3 py-2 font-medium transition hover:bg-white/15">Dashboard</a>
|
||||
<a href="{{b "/arrival"}}" class="rounded-lg px-3 py-2 font-medium transition hover:bg-white/15">Arrival</a>
|
||||
<a href="{{b "/progress"}}" class="rounded-lg px-3 py-2 font-medium transition hover:bg-white/15">Progress</a>
|
||||
<a href="{{b "/abnormal"}}" class="rounded-lg px-3 py-2 font-medium transition hover:bg-white/15">Abnormal</a>
|
||||
<a href="{{b "/result"}}" class="rounded-lg px-3 py-2 font-medium transition hover:bg-white/15">Result</a>
|
||||
</nav>
|
||||
{{if .Username}}
|
||||
<div class="mt-3 flex items-center justify-between gap-2 border-t border-white/20 pt-3">
|
||||
<a href="{{b "/password"}}" class="rounded-full bg-white/15 px-3 py-1 text-xs font-semibold tracking-wide transition hover:bg-white/25">{{.Username}}</a>
|
||||
<a href="{{b "/logout"}}" class="rounded-lg px-3 py-1.5 text-sm font-medium opacity-85 transition hover:bg-white/15 hover:opacity-100">Logout</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto w-full max-w-7xl space-y-5 px-4 py-5 sm:px-6 lg:px-8">
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const btn = document.getElementById('mobile-menu-toggle');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (!btn || !menu) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const expanded = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', expanded ? 'false' : 'true');
|
||||
menu.classList.toggle('hidden');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
@@ -23,19 +23,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">Total Patients</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 dalam project ini</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">Validated</p>
|
||||
<p class="num mt-2 text-3xl font-semibold text-emerald-600">{{.Summary.Validated}}</p>
|
||||
<p class="num mt-3 text-3xl font-semibold text-emerald-600">{{.Summary.Validated}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Resume sudah divalidasi dokter</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">Published</p>
|
||||
<p class="num mt-2 text-3xl font-semibold text-brand-500">{{.Summary.Published}}</p>
|
||||
<p class="num mt-3 text-3xl font-semibold text-brand-500">{{.Summary.Published}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Hasil sudah diupload</p>
|
||||
</article>
|
||||
</section>
|
||||
@@ -102,7 +102,7 @@
|
||||
<h2 class="text-base font-semibold text-slate-700">Patient Resume List</h2>
|
||||
<p class="text-xs text-slate-400">Data dari mcu_patient_resume_status</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">
|
||||
@@ -127,7 +127,7 @@
|
||||
<th class="px-4 py-3 font-medium">Published</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tbody id="progress-desktop-rows" class="divide-y divide-slate-100">
|
||||
{{range .FilteredRows}}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-4 py-3 font-medium text-slate-700">{{.Name}}</td>
|
||||
@@ -160,7 +160,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 p-4 md:hidden">
|
||||
<div id="progress-mobile-rows" class="grid gap-3 p-4 md:hidden">
|
||||
{{range .FilteredRows}}
|
||||
<article class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
@@ -187,6 +187,19 @@
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="progress-load-more" class="px-4 pb-4">
|
||||
{{if .HasMore}}
|
||||
<div
|
||||
hx-get='{{b "/progress/list"}}?search={{.Search}}&status={{.Status}}&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 resume untuk project ini.
|
||||
@@ -215,3 +228,78 @@
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{define "progress-list-chunk"}}
|
||||
<tbody id="progress-desktop-rows" hx-swap-oob="beforeend">
|
||||
{{range .FilteredRows}}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="px-4 py-3 font-medium text-slate-700">{{.Name}}</td>
|
||||
<td class="px-4 py-3 num text-slate-500">{{.NIP}}</td>
|
||||
<td class="px-4 py-3 text-slate-500">{{.Posisi}}</td>
|
||||
<td class="px-4 py-3">
|
||||
{{if .ResumeStatus}}
|
||||
<span class="rounded-full border border-slate-200 bg-slate-50 px-2 py-1 text-xs font-medium text-slate-600">{{.ResumeStatus}}</span>
|
||||
{{else}}
|
||||
<span class="text-xs text-slate-400">—</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{if eq .Validated "Y"}}
|
||||
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">Y</span>
|
||||
{{else}}
|
||||
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-500">N</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{if eq .Published "Y"}}
|
||||
<span class="rounded-full border border-brand-400/40 bg-brand-50 px-2 py-1 text-xs font-medium text-brand-500">Y</span>
|
||||
{{else}}
|
||||
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-500">N</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
<div id="progress-mobile-rows" hx-swap-oob="beforeend">
|
||||
{{range .FilteredRows}}
|
||||
<article class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-700">{{.Name}}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">{{.NIP}} • {{.Posisi}}</p>
|
||||
</div>
|
||||
{{if .ResumeStatus}}
|
||||
<span class="rounded-full border border-slate-200 bg-slate-50 px-2 py-1 text-xs font-medium text-slate-600">{{.ResumeStatus}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
{{if eq .Validated "Y"}}
|
||||
<span class="rounded-full border border-emerald-200 bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700">Validated</span>
|
||||
{{else}}
|
||||
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-500">Not Validated</span>
|
||||
{{end}}
|
||||
{{if eq .Published "Y"}}
|
||||
<span class="rounded-full border border-brand-400/40 bg-brand-50 px-2 py-1 text-xs font-medium text-brand-500">Published</span>
|
||||
{{else}}
|
||||
<span class="rounded-full border border-slate-200 bg-slate-100 px-2 py-1 text-xs font-medium text-slate-500">Not Published</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div id="progress-load-more" hx-swap-oob="outerHTML">
|
||||
{{if .HasMore}}
|
||||
<div
|
||||
hx-get='{{b "/progress/list"}}?search={{.Search}}&status={{.Status}}&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}}
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
|
||||
{{/* Section 2: Summary cards */}}
|
||||
<section class="grid gap-4 sm:grid-cols-2">
|
||||
<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">Total Patients</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 dalam project ini</p>
|
||||
</article>
|
||||
<article class="card p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Has PDF</p>
|
||||
<p class="num mt-2 text-3xl font-semibold text-brand-500">{{.Summary.HasPDF}}</p>
|
||||
<article class="card border-l-4 border-l-brand-500 p-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Sudah Ada Report Hasil</p>
|
||||
<p class="num mt-3 text-3xl font-semibold text-brand-500">{{.Summary.HasPDF}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Laporan hasil sudah tersedia</p>
|
||||
</article>
|
||||
</section>
|
||||
@@ -69,7 +69,7 @@
|
||||
<h2 class="text-base font-semibold text-slate-700">Patient Result List</h2>
|
||||
<p class="text-xs text-slate-400">Data dari published_mcu_dashboard_sync</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>
|
||||
|
||||
{{if .FilteredRows}}
|
||||
@@ -84,7 +84,7 @@
|
||||
<th class="px-4 py-3 font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<tbody id="result-desktop-rows" class="divide-y divide-slate-100">
|
||||
{{range .FilteredRows}}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="num px-4 py-3 text-slate-500">{{.NIP}}</td>
|
||||
@@ -109,7 +109,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 p-4 md:hidden">
|
||||
<div id="result-mobile-rows" class="grid gap-3 p-4 md:hidden">
|
||||
{{range .FilteredRows}}
|
||||
<article class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
@@ -130,6 +130,19 @@
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="result-load-more" class="px-4 pb-4">
|
||||
{{if .HasMore}}
|
||||
<div
|
||||
hx-get='{{b "/result/list"}}?search={{.Search}}&filter={{.Filter}}&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 untuk project ini.
|
||||
@@ -196,3 +209,64 @@ document.addEventListener('keydown', function(e) {
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{define "result-list-chunk"}}
|
||||
<tbody id="result-desktop-rows" hx-swap-oob="beforeend">
|
||||
{{range .FilteredRows}}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="num px-4 py-3 text-slate-500">{{.NIP}}</td>
|
||||
<td class="px-4 py-3 font-medium text-slate-700">{{.Name}}</td>
|
||||
<td class="px-4 py-3 text-slate-500">{{.Posisi}}</td>
|
||||
<td class="num px-4 py-3 text-slate-500">
|
||||
{{if .ReportDate}}{{.ReportDate | fmtDate}}{{else}}<span class="text-slate-300">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{if .FileUrl}}
|
||||
<button onclick="openPDFModal('{{$.PDFBaseURL}}{{.FileUrl}}', '{{.Name}}')"
|
||||
class="inline-flex items-center gap-1.5 rounded-lg bg-brand-500 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-brand-600">
|
||||
View PDF
|
||||
</button>
|
||||
{{else}}
|
||||
<span class="text-xs text-slate-300">—</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
<div id="result-mobile-rows" hx-swap-oob="beforeend">
|
||||
{{range .FilteredRows}}
|
||||
<article class="rounded-xl border border-slate-200 p-3">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-700">{{.Name}}</p>
|
||||
<p class="mt-0.5 text-xs text-slate-400">{{.NIP}} • {{.Posisi}}</p>
|
||||
{{if .ReportDate}}<p class="mt-0.5 num text-xs text-slate-400">{{.ReportDate | fmtDate}}</p>{{end}}
|
||||
</div>
|
||||
{{if .FileUrl}}
|
||||
<button onclick="openPDFModal('{{$.PDFBaseURL}}{{.FileUrl}}', '{{.Name}}')"
|
||||
class="shrink-0 rounded-lg bg-brand-500 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-brand-600">
|
||||
View PDF
|
||||
</button>
|
||||
{{else}}
|
||||
<span class="text-xs text-slate-300">No PDF</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</article>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div id="result-load-more" hx-swap-oob="outerHTML">
|
||||
{{if .HasMore}}
|
||||
<div
|
||||
hx-get='{{b "/result/list"}}?search={{.Search}}&filter={{.Filter}}&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}}
|
||||
|
||||
Reference in New Issue
Block a user