Initial commit
This commit is contained in:
306
cpone-dashboard/templates/dashboard/index.html
Normal file
306
cpone-dashboard/templates/dashboard/index.html
Normal file
@@ -0,0 +1,306 @@
|
||||
{{define "title"}}Dashboard — CpOne{{end}}
|
||||
{{define "header-title"}}MCU Live Dashboard{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
{{$proj := .Project}}
|
||||
<style>
|
||||
@keyframes sseFlash {
|
||||
0% { box-shadow: 0 0 0 0 rgba(59, 80, 160, 0.45); background-color: #eef2ff; }
|
||||
100% { box-shadow: 0 0 0 0 rgba(59, 80, 160, 0); background-color: transparent; }
|
||||
}
|
||||
|
||||
.sse-updated {
|
||||
animation: sseFlash 3.8s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- SSE wrapper — satu koneksi, semua section dapat update otomatis -->
|
||||
<div hx-ext="sse" sse-connect="{{b "/dashboard/stream"}}?mode=daily&date={{.DateFrom}}">
|
||||
|
||||
<!-- Project Banner -->
|
||||
<section class="card p-5">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-brand-500">Ongoing Project</p>
|
||||
{{if .IsLive}}
|
||||
<span class="flex items-center gap-1 rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-600">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-red-500 animate-pulse"></span> LIVE
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<h2 class="mt-1 text-lg font-semibold text-slate-900">{{$proj.Label}}</h2>
|
||||
<p class="mt-0.5 text-sm text-slate-500">
|
||||
{{$proj.Number}} • {{$proj.CorporateName}} •
|
||||
<span class="num">{{$proj.StartDate | fmtDate}}</span> – <span class="num">{{$proj.EndDate | fmtDate}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<a href="{{b "/projects"}}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-600 transition hover:border-brand-400 hover:text-brand-600">
|
||||
Ganti project
|
||||
</a>
|
||||
<form method="get" action="{{b "/dashboard"}}" class="flex flex-wrap items-center gap-2" id="dashboard-filter-form">
|
||||
<input type="hidden" name="mode" value="daily"/>
|
||||
<label class="text-xs font-medium text-slate-500">Tanggal Check-in</label>
|
||||
<select name="date"
|
||||
id="dashboard-date"
|
||||
onchange="this.form.submit()"
|
||||
class="num rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm text-slate-700
|
||||
focus:border-brand-400 focus:outline-none focus:ring-2 focus:ring-brand-200">
|
||||
{{if .AvailableDates}}
|
||||
{{range .AvailableDates}}
|
||||
<option value="{{.}}" {{if eq . $.DateFrom}}selected{{end}}>{{. | fmtDate}}</option>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<option value="{{.DateFrom}}" selected>{{.DateFrom | fmtDate}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit"
|
||||
class="rounded-lg bg-brand-500 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-brand-600">
|
||||
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-swap="kpi" hx-swap="innerHTML">
|
||||
{{range $i := seq 3}}
|
||||
<div class="card h-28 animate-pulse bg-slate-50"></div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<!-- TAT + TAT Chart -->
|
||||
<section class="grid gap-5 xl:grid-cols-[1fr_2fr]">
|
||||
|
||||
<article class="card border-l-4 border-l-brand-500 p-5">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-brand-500">Avg TAT by Hour</p>
|
||||
<p class="mt-0.5 text-sm font-medium text-slate-600">Check-in → Check-out</p>
|
||||
</div>
|
||||
<span class="num rounded-full bg-brand-50 px-3 py-1 text-xs font-semibold text-brand-600">
|
||||
{{.DateFrom | fmtDate}}
|
||||
</span>
|
||||
</div>
|
||||
{{if gt .TAT.CheckedOut 0}}
|
||||
<p class="num mt-5 text-4xl font-semibold text-slate-900">
|
||||
{{div .TAT.AvgMinutes 60}}<span class="text-xl text-slate-400">h</span>
|
||||
{{mod .TAT.AvgMinutes 60}}<span class="text-xl text-slate-400">m</span>
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Average turnaround untuk pasien yang sudah selesai</p>
|
||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||
<div class="rounded-xl bg-slate-50 px-2 py-2.5 text-center">
|
||||
<p class="text-xs text-slate-400">Fastest</p>
|
||||
<p class="num mt-1 text-sm font-semibold text-slate-700">
|
||||
{{div .TAT.Fastest 60}}h {{mod .TAT.Fastest 60}}m
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-slate-50 px-2 py-2.5 text-center">
|
||||
<p class="text-xs text-slate-400">Median</p>
|
||||
<p class="num mt-1 text-sm font-semibold text-slate-700">
|
||||
{{div .TAT.Median 60}}h {{mod .TAT.Median 60}}m
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="mt-6 text-sm text-slate-400">Belum ada data checkout pada tanggal ini.</p>
|
||||
{{end}}
|
||||
</article>
|
||||
|
||||
<article class="card p-5">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Average TAT by Hour</h2>
|
||||
<span class="text-xs text-slate-400">Hourly average across selected date(s)</span>
|
||||
</div>
|
||||
<div id="tat-chart" class="h-52 w-full"></div>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Station Status + Arrival List — SSE swap -->
|
||||
<section class="grid gap-5 xl:grid-cols-3">
|
||||
|
||||
<article id="sse-stations" class="card xl:col-span-2 overflow-hidden"
|
||||
sse-swap="stations" hx-swap="innerHTML">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Station Status</h2>
|
||||
<span class="flex items-center gap-1.5 text-xs font-medium text-slate-400 animate-pulse">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-slate-300"></span> Connecting...
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-5 text-sm text-slate-400">Memuat data...</div>
|
||||
</article>
|
||||
|
||||
<article id="sse-arrivals" class="card overflow-hidden"
|
||||
sse-swap="arrivals" hx-swap="innerHTML">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Arrival List</h2>
|
||||
<a href="{{b "/arrival"}}" class="text-xs font-medium text-brand-500 hover:text-brand-700">View all</a>
|
||||
</div>
|
||||
<div class="p-5 text-sm text-slate-400">Memuat data...</div>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Trend Chart -->
|
||||
<section>
|
||||
<article class="card p-5">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Arrival to Verification Trend by Hour</h2>
|
||||
<span class="num text-xs text-slate-400">
|
||||
{{.DateFrom | fmtDate}}
|
||||
</span>
|
||||
</div>
|
||||
<div id="trend-chart" class="h-64 w-full"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
</div><!-- end SSE wrapper -->
|
||||
|
||||
<!-- Modal: Semua Pasien -->
|
||||
<dialog id="patients-modal"
|
||||
class="w-full max-w-6xl rounded-2xl border border-slate-200 bg-white shadow-2xl p-0 backdrop:bg-slate-900/50"
|
||||
onclick="if(event.target===this)this.close()">
|
||||
<div class="flex flex-col max-h-[85vh]">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-6 py-4 flex-shrink-0">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-slate-900">Semua Pasien</h2>
|
||||
<p id="patients-modal-subtitle" class="mt-0.5 text-xs text-slate-400"></p>
|
||||
</div>
|
||||
<button onclick="document.getElementById('patients-modal').close()"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-colors">
|
||||
<svg class="h-4 w-4" 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>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div id="patients-modal-body" class="overflow-auto flex-1 min-h-0">
|
||||
<div class="flex items-center justify-center py-16 text-slate-400">
|
||||
<svg class="h-5 w-5 animate-spin mr-2 text-brand-400" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path>
|
||||
</svg>
|
||||
Memuat data...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
function fmtDate(s) {
|
||||
if (!s) return '';
|
||||
const [y, m, d] = s.split('-');
|
||||
return d + '/' + m + '/' + y;
|
||||
}
|
||||
|
||||
function openPatientsModal() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const modal = document.getElementById('patients-modal');
|
||||
const body = document.getElementById('patients-modal-body');
|
||||
const subtitle = document.getElementById('patients-modal-subtitle');
|
||||
|
||||
const mode = params.get('mode') || 'daily';
|
||||
const date = params.get('date') || '';
|
||||
const dateEnd = params.get('date_end') || '';
|
||||
|
||||
if (mode === 'daily') {
|
||||
subtitle.textContent = date ? 'Tanggal: ' + fmtDate(date) : 'Hari ini';
|
||||
} else {
|
||||
subtitle.textContent = 'Periode: ' + fmtDate(date) + (dateEnd ? ' s/d ' + fmtDate(dateEnd) : '');
|
||||
}
|
||||
|
||||
body.innerHTML = '<div class="flex items-center justify-center py-16 text-slate-400"><svg class="h-5 w-5 animate-spin mr-2 text-brand-400" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path></svg>Memuat data...</div>';
|
||||
modal.showModal();
|
||||
|
||||
fetch('{{b "/dashboard/patients"}}?' + params.toString())
|
||||
.then(r => r.text())
|
||||
.then(html => { body.innerHTML = html; })
|
||||
.catch(() => { body.innerHTML = '<p class="p-6 text-sm text-red-500">Gagal memuat data.</p>'; });
|
||||
}
|
||||
|
||||
(function() {
|
||||
const sseTargets = new Set(['sse-kpi', 'sse-stations', 'sse-arrivals']);
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
||||
const target = evt.detail && evt.detail.target ? evt.detail.target : null;
|
||||
if (!target || !target.id || !sseTargets.has(target.id)) return;
|
||||
|
||||
// Skip first hydration so highlight means "new/update", not initial render.
|
||||
if (!target.dataset.sseHydrated) {
|
||||
target.dataset.sseHydrated = '1';
|
||||
return;
|
||||
}
|
||||
|
||||
target.classList.remove('sse-updated');
|
||||
void target.offsetWidth;
|
||||
target.classList.add('sse-updated');
|
||||
});
|
||||
|
||||
const palette = ['#3b50a0', '#6677d6', '#10b981', '#f59e0b'];
|
||||
const tatData = {{.TATChart}};
|
||||
const trendData = {{.TrendChart}};
|
||||
|
||||
const tatEl = document.getElementById('tat-chart');
|
||||
if (tatEl && tatData.labels && tatData.labels.length) {
|
||||
const tatChart = echarts.init(tatEl);
|
||||
tatChart.setOption({
|
||||
color: palette,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: p => `${p[0].axisValue}<br/>Avg TAT: <b>${Math.round(p[0].data)} mnt</b>`
|
||||
},
|
||||
grid: { left: 50, right: 20, top: 16, bottom: 28 },
|
||||
xAxis: {
|
||||
type: 'category', data: tatData.labels,
|
||||
axisLine: { lineStyle: { color: '#e2e8f0' } }, axisTick: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value', name: 'Mnt',
|
||||
nameTextStyle: { color: '#94a3b8', fontSize: 11 },
|
||||
splitLine: { lineStyle: { color: '#f1f5f9' } }
|
||||
},
|
||||
series: [{
|
||||
name: 'Avg TAT', type: 'bar', barWidth: 24, data: tatData.values,
|
||||
itemStyle: { borderRadius: [6, 6, 0, 0], color: '#3b50a0' }
|
||||
}]
|
||||
});
|
||||
window.addEventListener('resize', () => tatChart.resize());
|
||||
}
|
||||
|
||||
const trendEl = document.getElementById('trend-chart');
|
||||
if (trendEl && trendData.labels && trendData.labels.length) {
|
||||
const trendChart = echarts.init(trendEl);
|
||||
trendChart.setOption({
|
||||
color: palette,
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: {
|
||||
data: ['Checked In', 'Checked Out'],
|
||||
textStyle: { fontSize: 12, color: '#64748b' }, top: 0
|
||||
},
|
||||
grid: { left: 40, right: 20, top: 36, bottom: 28 },
|
||||
xAxis: {
|
||||
type: 'category', data: trendData.labels,
|
||||
axisLine: { lineStyle: { color: '#e2e8f0' } }, axisTick: { show: false }
|
||||
},
|
||||
yAxis: { type: 'value', splitLine: { lineStyle: { color: '#f1f5f9' } } },
|
||||
series: [
|
||||
{ name: 'Checked In', type: 'line', smooth: true, data: trendData.checkedIn, symbolSize: 5 },
|
||||
{ name: 'Checked Out', type: 'line', smooth: true, data: trendData.checkedOut, symbolSize: 5, lineStyle: { type: 'dashed' } }
|
||||
]
|
||||
});
|
||||
window.addEventListener('resize', () => trendChart.resize());
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
37
cpone-dashboard/templates/dashboard/partials/arrivals.html
Normal file
37
cpone-dashboard/templates/dashboard/partials/arrivals.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{{define "arrivals"}}
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Arrival List</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
{{if .IsLive}}
|
||||
<span class="flex items-center gap-1.5 text-xs font-medium text-emerald-600">
|
||||
<span class="live-dot h-1.5 w-1.5 rounded-full bg-emerald-500"></span> Live
|
||||
</span>
|
||||
{{end}}
|
||||
<button onclick="openPatientsModal()"
|
||||
class="text-xs font-medium text-brand-500 hover:text-brand-700 transition-colors">
|
||||
Lihat selengkapnya
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Rows}}
|
||||
<ul class="divide-y divide-slate-50 px-3 py-2">
|
||||
{{range .Rows}}
|
||||
<li class="flex items-center gap-3 rounded-xl px-2 py-2.5 transition-colors hover:bg-slate-50">
|
||||
<div class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-brand-50 text-xs font-bold text-brand-600">
|
||||
{{.Name | initials}}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-slate-800">{{.Name}}</p>
|
||||
<p class="text-xs text-slate-400">
|
||||
<span class="num">{{fmtDateTime .Date .InTime}}</span> • {{.Station | stationShort}}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="px-5 py-8 text-center text-sm text-slate-400">
|
||||
Belum ada arrival pada tanggal ini.
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
43
cpone-dashboard/templates/dashboard/partials/kpi.html
Normal file
43
cpone-dashboard/templates/dashboard/partials/kpi.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{{define "kpi"}}
|
||||
<!-- Total Staff -->
|
||||
<article class="card border-l-4 border-l-brand-300 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Total 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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.TotalStaff}}</p>
|
||||
{{if gt .InvitedStaff 0}}
|
||||
<p class="mt-1 text-xs font-medium text-emerald-600">{{printf "%.1f%%" (pct .TotalStaff .InvitedStaff)}} dari invited</p>
|
||||
{{else}}
|
||||
<p class="mt-1 text-xs text-slate-400">Yang benar-benar datang</p>
|
||||
{{end}}
|
||||
</article>
|
||||
|
||||
<!-- Checked In -->
|
||||
<article class="card border-l-4 border-l-brand-500 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">In Progress</p>
|
||||
<svg class="h-4 w-4 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.CheckedIn}}</p>
|
||||
<p class="mt-1 text-xs text-slate-400">Masih dalam proses</p>
|
||||
</article>
|
||||
|
||||
<!-- Checked Out -->
|
||||
<article class="card border-l-4 border-l-emerald-500 p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<p class="text-xs font-semibold uppercase tracking-widest text-slate-400">Checked Out</p>
|
||||
<svg class="h-4 w-4 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="num mt-3 text-3xl font-semibold text-slate-900">{{.CheckedOut}}</p>
|
||||
{{if gt .CheckedIn 0}}
|
||||
<p class="mt-1 text-xs text-slate-400">{{printf "%.1f%%" (pct .CheckedOut .CheckedIn)}} selesai</p>
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
80
cpone-dashboard/templates/dashboard/partials/patients.html
Normal file
80
cpone-dashboard/templates/dashboard/partials/patients.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{{define "patients"}}
|
||||
{{if not .Patients}}
|
||||
<div class="flex flex-col items-center justify-center py-16 text-slate-400">
|
||||
<svg class="mb-3 h-10 w-10 text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/>
|
||||
</svg>
|
||||
<p class="text-sm font-medium">Belum ada data pasien pada tanggal ini.</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="divide-y divide-slate-100">
|
||||
{{range .Patients}}
|
||||
{{if and (gt .DoneCount 0) (eq .DoneCount (len .Stations))}}
|
||||
<div class="px-5 py-4 bg-emerald-50/60 transition-colors hover:bg-emerald-50">
|
||||
{{else}}
|
||||
<div class="px-5 py-4 hover:bg-slate-50 transition-colors">
|
||||
{{end}}
|
||||
<!-- Row atas: nama + waktu -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-brand-50 text-xs font-bold text-brand-600">
|
||||
{{.Name | initials}}
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-slate-800">{{.Name}}</p>
|
||||
{{if $.IsRange}}<p class="text-xs text-slate-400 num">{{.Date | fmtDate}}</p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<span class="num text-slate-500">
|
||||
Masuk: <span class="font-semibold text-slate-700">{{.InTime}}</span>
|
||||
</span>
|
||||
{{if .HasOut}}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2.5 py-1 font-semibold text-emerald-700 num">
|
||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||
Keluar: {{.OutTime}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 font-semibold text-amber-600">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse"></span>
|
||||
Dalam proses
|
||||
</span>
|
||||
{{end}}
|
||||
<span class="text-slate-400">
|
||||
<span class="font-semibold text-brand-600">{{.DoneCount}}</span>/{{len .Stations}} station selesai
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Row bawah: station badges -->
|
||||
{{if .Stations}}
|
||||
<div class="mt-2.5 flex flex-wrap gap-1.5 pl-10">
|
||||
{{range .Stations}}
|
||||
{{if .Done}}
|
||||
<span class="inline-flex max-w-full flex-col gap-0.5 rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-800 cursor-default">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<svg class="h-3 w-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||
{{.Station}}
|
||||
</span>
|
||||
<span class="num pl-4 text-[10px] font-semibold text-emerald-700/80">
|
||||
Proses: {{if .ProcessAt}}{{.ProcessAt}}{{else}}-{{end}} | Selesai: {{if .DoneAt}}{{.DoneAt}}{{else}}-{{end}}
|
||||
</span>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="inline-flex max-w-full flex-col gap-0.5 rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-500 cursor-default">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<svg class="h-3 w-3 flex-shrink-0 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{{.Station}}
|
||||
</span>
|
||||
<span class="num pl-4 text-[10px] font-semibold text-slate-400">
|
||||
Proses: {{if .ProcessAt}}{{.ProcessAt}}{{else}}-{{end}} | Selesai: {{if .DoneAt}}{{.DoneAt}}{{else}}-{{end}}
|
||||
</span>
|
||||
</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
53
cpone-dashboard/templates/dashboard/partials/stations.html
Normal file
53
cpone-dashboard/templates/dashboard/partials/stations.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{{define "stations"}}
|
||||
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
|
||||
<h2 class="text-sm font-semibold text-slate-700">Station Status</h2>
|
||||
{{if .IsLive}}
|
||||
<span class="flex items-center gap-1.5 text-xs font-medium text-emerald-600">
|
||||
<span class="live-dot h-1.5 w-1.5 rounded-full bg-emerald-500"></span> Live
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Rows}}
|
||||
<div class="p-3">
|
||||
<table class="min-w-full text-sm">
|
||||
<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="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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
{{range .Rows}}
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<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 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">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-slate-100">
|
||||
<div class="h-full rounded-full transition-all"
|
||||
style="width: {{printf "%.1f" .Pct}}%; background: {{if ge .Pct 90.0}}#10b981{{else if ge .Pct 60.0}}#3b50a0{{else}}#f59e0b{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
<span class="num text-xs font-semibold {{if ge .Pct 90.0}}text-emerald-600{{else if ge .Pct 60.0}}text-brand-600{{else}}text-amber-600{{end}}">
|
||||
{{printf "%.0f%%" .Pct}}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="px-5 py-8 text-center text-sm text-slate-400">
|
||||
Belum ada data station pada tanggal ini.
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user