777 lines
21 KiB
Vue
777 lines
21 KiB
Vue
<template>
|
|
<div class="loket-container">
|
|
<!-- Compact Header -->
|
|
<PageHeader
|
|
icon="mdi-view-dashboard"
|
|
title="Admin Loket"
|
|
:subtitle="currentDate"
|
|
:show-add-button="false"
|
|
theme="primary"
|
|
/>
|
|
|
|
<!-- Main Content Grid -->
|
|
<v-row class="content-grid" dense>
|
|
<!-- Left Column: Current Patient & Queue Actions -->
|
|
<v-col cols="12" md="5">
|
|
<div class="sticky-wrapper">
|
|
<!-- Current Patient Card -->
|
|
<CurrentPatientCard
|
|
:patient="currentProcessingPatient"
|
|
:has-next-queue="(diLoketPatients || []).length > 0"
|
|
:next-queue-info="nextQueueInfo"
|
|
@action="handlePatientAction"
|
|
@change-klinik="showChangeKlinikDialog = true"
|
|
@process-next="handleProcessNext"
|
|
@call="handleCallPatient"
|
|
@open-klinik-ruang="openKlinikRuangDialog"
|
|
@open-penunjang="openPenunjangDialog"
|
|
/>
|
|
|
|
<!-- Queue Actions Card -->
|
|
<QueueActionsCard
|
|
class="mt-3"
|
|
:total-quota="150"
|
|
:used-quota="quotaUsed"
|
|
:menunggu-count="menungguCount"
|
|
:has-next="!!nextPatient"
|
|
@call="handleCall"
|
|
/>
|
|
</div>
|
|
</v-col>
|
|
|
|
<!-- Right Column: Patient Table -->
|
|
<v-col cols="12" md="7">
|
|
<v-card class="patient-data-container" elevation="0">
|
|
<v-card-text class="pa-4">
|
|
<!-- Header with Filters -->
|
|
<div class="data-header mb-4">
|
|
<div class="section-label">CARI PASIEN</div>
|
|
|
|
<div class="filters">
|
|
<!-- Search Field -->
|
|
<v-text-field
|
|
v-model="searchQuery"
|
|
placeholder="Cari barcode, nomor antrian..."
|
|
density="compact"
|
|
hide-details
|
|
class="search-field"
|
|
prepend-inner-icon="mdi-magnify"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<PatientDataTable
|
|
:items="allPatientsForStage"
|
|
v-model:selected-status="selectedStatus"
|
|
v-model:search-query="searchQuery"
|
|
v-model:selected-fast-track="selectedFastTrack"
|
|
:di-loket-count="diLoketCount"
|
|
:diproses-count="currentProcessingPatient ? 1 : 0"
|
|
:waiting-count="waitingCount"
|
|
:terlambat-count="(terlambatPatients || []).length"
|
|
:pending-count="(pendingPatients || []).length"
|
|
:show-diproses="false"
|
|
:fast-track-options="fastTrackOptions"
|
|
@action="handleTableAction"
|
|
/>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Klinik Dialog -->
|
|
<SelectionDialog
|
|
v-model="showKlinikDialog"
|
|
title="Pilih Klinik"
|
|
:items="filteredKliniks"
|
|
v-model:search-query="klinikSearch"
|
|
search-placeholder="Cari klinik..."
|
|
@select="selectKlinik"
|
|
/>
|
|
|
|
<!-- Penunjang Dialog -->
|
|
<SelectionDialog
|
|
v-model="showPenunjangDialog"
|
|
title="Pilih Penunjang"
|
|
:items="filteredPenunjangs"
|
|
v-model:search-query="penunjangSearch"
|
|
search-placeholder="Cari penunjang..."
|
|
@select="selectPenunjang"
|
|
/>
|
|
|
|
<!-- Change Klinik Dialog -->
|
|
<SelectionDialog
|
|
v-model="showChangeKlinikDialog"
|
|
title="Ubah Klinik"
|
|
:items="filteredChangeKliniks"
|
|
v-model:search-query="changeKlinikSearch"
|
|
search-placeholder="Cari klinik..."
|
|
@select="changeKlinik"
|
|
/>
|
|
|
|
<!-- Dialog Klinik Ruang -->
|
|
<v-dialog v-model="showKlinikRuangDialog" max-width="900px" scrollable>
|
|
<v-card class="dialog-card">
|
|
<v-card-title class="dialog-header dialog-header-warning">
|
|
<span class="headline-4">Pilih Klinik Ruang</span>
|
|
<v-btn icon variant="text" size="small" class="btn-close" @click="closeKlinikRuangDialog">
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
|
|
<v-divider/>
|
|
|
|
<v-card-text class="dialog-content">
|
|
<div v-if="currentProcessingPatient" class="patient-card mb-4">
|
|
<h4 class="headline-5 mb-3">Informasi Pasien</h4>
|
|
<v-row dense>
|
|
<v-col cols="6">
|
|
<div class="detail-item">
|
|
<span class="caption-2 text-muted">Barcode</span>
|
|
<span class="body-2 text-semibold">{{ currentProcessingPatient.barcode }}</span>
|
|
</div>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<div class="detail-item">
|
|
<span class="caption-2 text-muted">No. Antrian</span>
|
|
<span class="body-2 text-semibold">{{ currentProcessingPatient.noAntrian }}</span>
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<v-text-field
|
|
v-model="klinikRuangSearch"
|
|
placeholder="Cari Klinik Ruang..."
|
|
prepend-inner-icon="mdi-magnify"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
class="mb-4"
|
|
/>
|
|
|
|
<v-expansion-panels class="expansion-panels">
|
|
<v-expansion-panel
|
|
v-for="klinikRuang in filteredKlinikRuang"
|
|
:key="klinikRuang.id"
|
|
class="expansion-panel"
|
|
>
|
|
<v-expansion-panel-title class="expansion-title">
|
|
<div class="expansion-header">
|
|
<v-chip size="small" color="warning" class="mr-2">
|
|
{{ klinikRuang.kodeKlinik }}
|
|
</v-chip>
|
|
<span class="body-2 text-semibold">{{ klinikRuang.namaKlinik }}</span>
|
|
</div>
|
|
<template #actions>
|
|
<v-chip size="x-small" variant="outlined">
|
|
{{ klinikRuang.ruangList.length }} Ruang
|
|
</v-chip>
|
|
</template>
|
|
</v-expansion-panel-title>
|
|
|
|
<v-expansion-panel-text>
|
|
<div class="ruang-list">
|
|
<div
|
|
v-for="ruang in klinikRuang.ruangList"
|
|
:key="ruang.nomorRuang"
|
|
class="ruang-item"
|
|
@click="buatAntreanKlinikRuang(klinikRuang, ruang)"
|
|
>
|
|
<v-icon color="warning" size="20">mdi-door</v-icon>
|
|
<div class="ruang-info">
|
|
<span class="body-3 text-semibold">Ruang {{ ruang.nomorRuang }} - {{ ruang.namaRuang }}</span>
|
|
<span class="caption-2 text-muted">Screen: {{ ruang.nomorScreen }}</span>
|
|
</div>
|
|
<v-icon size="20" color="grey">mdi-chevron-right</v-icon>
|
|
</div>
|
|
</div>
|
|
</v-expansion-panel-text>
|
|
</v-expansion-panel>
|
|
</v-expansion-panels>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Snackbar -->
|
|
<AppSnackbar
|
|
v-model="snackbar"
|
|
:message="snackbarText"
|
|
:color="snackbarColor"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from "vue";
|
|
import { useQueue } from "@/composables/useQueue";
|
|
import { useQueueStore } from "@/stores/queueStore";
|
|
import { useMasterStore } from "@/stores/masterStore";
|
|
import PageHeader from "@/components/common/PageHeader.vue";
|
|
import CurrentPatientCard from "@/components/features/queue/CurrentPatientCard.vue";
|
|
import QueueActionsCard from "@/components/features/queue/QueueActionsCard.vue";
|
|
import PatientDataTable from "@/components/features/queue/TabelPatientData.vue";
|
|
import SelectionDialog from "@/components/common/SelectionDialog.vue";
|
|
import AppSnackbar from "@/components/common/AppSnackbar.vue";
|
|
import { useThermalPrint } from "@/composables/useThermalPrint";
|
|
|
|
const masterStore = useMasterStore();
|
|
const queueStore = useQueueStore();
|
|
const { printTicketFromPatient } = useThermalPrint();
|
|
|
|
const {
|
|
snackbar,
|
|
snackbarText,
|
|
snackbarColor,
|
|
showKlinikDialog,
|
|
showPenunjangDialog,
|
|
showChangeKlinikDialog,
|
|
klinikSearch,
|
|
penunjangSearch,
|
|
changeKlinikSearch,
|
|
currentProcessingPatient,
|
|
diLoketPatients,
|
|
terlambatPatients,
|
|
pendingPatients,
|
|
waitingPatients,
|
|
menungguPatients,
|
|
nextPatient,
|
|
quotaUsed,
|
|
filteredKliniks,
|
|
filteredPenunjangs,
|
|
filteredChangeKliniks,
|
|
callNext,
|
|
callMultiplePatients,
|
|
processPatient,
|
|
selectKlinik,
|
|
selectPenunjang,
|
|
openPenunjangDialog,
|
|
changeKlinik,
|
|
processNextQueue,
|
|
} = useQueue("loket");
|
|
|
|
const currentDate = ref(
|
|
new Date().toLocaleDateString("id-ID", {
|
|
weekday: "long",
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
})
|
|
);
|
|
|
|
const selectedStatus = ref("all");
|
|
const searchQuery = ref("");
|
|
const selectedFastTrack = ref(null);
|
|
|
|
// Dialog Klinik Ruang
|
|
const showKlinikRuangDialog = ref(false);
|
|
const klinikRuangSearch = ref("");
|
|
|
|
// Watch dialog state to reset search when closed
|
|
watch(showKlinikRuangDialog, (newValue) => {
|
|
if (!newValue) {
|
|
klinikRuangSearch.value = "";
|
|
}
|
|
});
|
|
|
|
// Get klinik ruang data from masterStore
|
|
const klinikRuangList = computed(() => {
|
|
return masterStore.ruangData || [];
|
|
});
|
|
|
|
const filteredKlinikRuang = computed(() => {
|
|
if (!klinikRuangSearch.value) return klinikRuangList.value;
|
|
return klinikRuangList.value.filter(
|
|
(k) =>
|
|
k.namaKlinik.toLowerCase().includes(klinikRuangSearch.value.toLowerCase()) ||
|
|
k.kodeKlinik.toLowerCase().includes(klinikRuangSearch.value.toLowerCase())
|
|
);
|
|
});
|
|
|
|
// Fast Track options from all patients
|
|
const fastTrackOptions = computed(() => {
|
|
const normalizedValues = allPatientsForStage.value
|
|
.map((p) => (p.fastTrack ?? "").toString().trim().toUpperCase())
|
|
.filter((v) => v.length > 0);
|
|
const uniqueTracks = [...new Set(normalizedValues)];
|
|
return uniqueTracks.sort();
|
|
});
|
|
|
|
// Combine all patients with status - PRESERVE ALL PROPERTIES
|
|
// Hanya tampilkan pasien yang sudah dipanggil (bukan status "menunggu")
|
|
const allPatientsForStage = computed(() => {
|
|
const currentPatientNo = currentProcessingPatient.value?.no;
|
|
|
|
// Pasien yang sudah di loket (sudah check-in)
|
|
const diLoket = (diLoketPatients.value || []).map((p) => ({
|
|
...p, // Spread all properties first
|
|
// If this patient is currently being processed, mark as "diproses", otherwise "diloket"
|
|
status: p.no === currentPatientNo ? "diproses" : "diloket",
|
|
}));
|
|
|
|
// Pasien terlambat
|
|
const terlambat = (terlambatPatients.value || []).map((p) => ({
|
|
...p,
|
|
status: "terlambat",
|
|
}));
|
|
|
|
// Pasien pending
|
|
const pending = (pendingPatients.value || []).map((p) => ({
|
|
...p,
|
|
status: "pending",
|
|
}));
|
|
|
|
// Pasien dengan status "waiting" (sudah dipanggil tapi belum check-in)
|
|
// Ini adalah kategori baru "Menunggu" di tabel Admin Loket
|
|
const waiting = (waitingPatients.value || []).map((p) => ({
|
|
...p,
|
|
status: "waiting", // Tampilkan sebagai "waiting" atau "menunggu" di UI
|
|
}));
|
|
|
|
// Jangan tampilkan pasien dengan status "menunggu" (belum dipanggil) di tabel
|
|
// Hanya tampilkan pasien yang sudah dipanggil oleh admin loket
|
|
const combined = [...diLoket, ...waiting, ...terlambat, ...pending];
|
|
|
|
// Debug: check if fastTrack property exists
|
|
console.log("📊 AdminLoket - allPatientsForStage:", combined.length);
|
|
if (combined.length > 0) {
|
|
console.log("📊 First patient:", combined[0]);
|
|
console.log("📊 First patient has fastTrack?", "fastTrack" in combined[0]);
|
|
console.log("📊 First patient fastTrack value:", combined[0].fastTrack);
|
|
console.log(
|
|
"📊 All fastTrack values:",
|
|
combined.map((p) => p.fastTrack)
|
|
);
|
|
}
|
|
|
|
return combined;
|
|
});
|
|
|
|
// Count diLoket patients (excluding currently processing)
|
|
const diLoketCount = computed(() => {
|
|
const currentPatientNo = currentProcessingPatient.value?.no;
|
|
return (diLoketPatients.value || []).filter(p => p.no !== currentPatientNo).length;
|
|
});
|
|
|
|
// Count menunggu patients (belum dipanggil)
|
|
const menungguCount = computed(() => {
|
|
return (menungguPatients.value || []).length;
|
|
});
|
|
|
|
// Count waiting patients (sudah dipanggil tapi belum check-in)
|
|
const waitingCount = computed(() => {
|
|
return (waitingPatients.value || []).length;
|
|
});
|
|
|
|
// Next queue info for CurrentPatientCard
|
|
const nextQueueInfo = computed(() => {
|
|
const currentPatientNo = currentProcessingPatient.value?.no;
|
|
const nextPatient = (diLoketPatients.value || []).find(p => p.no !== currentPatientNo) || (diLoketPatients.value || [])[0];
|
|
if (nextPatient) {
|
|
return `Antrian berikutnya: ${nextPatient.noAntrian.split(" |")[0]}`;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const handlePatientAction = (action) => {
|
|
if (currentProcessingPatient.value) {
|
|
processPatient(currentProcessingPatient.value, action);
|
|
}
|
|
};
|
|
|
|
const handleCall = (count) => {
|
|
if (count === 1) {
|
|
callNext();
|
|
} else {
|
|
callMultiplePatients(count);
|
|
}
|
|
};
|
|
|
|
const handleTableAction = (item, action) => {
|
|
processPatient(item, action);
|
|
};
|
|
|
|
const handleProcessNext = () => {
|
|
processNextQueue();
|
|
};
|
|
|
|
const handleCallPatient = () => {
|
|
// TODO: Integrate text-to-speech library here
|
|
// Example: speak(`Nomor antrian ${currentProcessingPatient.value?.noAntrian.split(" |")[0]}, silakan menuju ke loket`)
|
|
if (currentProcessingPatient.value) {
|
|
console.log('Calling patient:', currentProcessingPatient.value);
|
|
// Placeholder for text-to-speech integration
|
|
}
|
|
};
|
|
|
|
const openKlinikRuangDialog = () => {
|
|
showKlinikRuangDialog.value = true;
|
|
};
|
|
|
|
const closeKlinikRuangDialog = () => {
|
|
showKlinikRuangDialog.value = false;
|
|
klinikRuangSearch.value = "";
|
|
};
|
|
|
|
const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
|
|
// Pastikan currentProcessingPatient valid, jika tidak, coba dapatkan dari store
|
|
let patient = currentProcessingPatient.value;
|
|
if (!patient) {
|
|
try {
|
|
// Coba dapatkan pasien yang sedang diproses dari store
|
|
const processingPatient = queueStore.currentProcessingPatient?.loket;
|
|
if (processingPatient) {
|
|
// Dapatkan data terbaru dari allPatients langsung (bukan dari getPatientsByStage yang sudah difilter)
|
|
// allPatients adalah ref yang di-export dari Pinia store, bisa diakses langsung atau dengan .value
|
|
let allPatients = [];
|
|
if (queueStore.allPatients) {
|
|
// Jika allPatients adalah ref (punya .value)
|
|
allPatients = Array.isArray(queueStore.allPatients)
|
|
? queueStore.allPatients
|
|
: (queueStore.allPatients.value || []);
|
|
}
|
|
|
|
const latestPatient = allPatients.find(
|
|
p => (p && p.no === processingPatient.no) ||
|
|
(p && p.barcode && p.barcode === processingPatient.barcode) ||
|
|
(p && p.noAntrian && p.noAntrian === processingPatient.noAntrian)
|
|
);
|
|
patient = latestPatient || processingPatient;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error getting patient from store:', error);
|
|
}
|
|
}
|
|
|
|
if (!patient) {
|
|
snackbarText.value = "Tidak ada pasien yang sedang diproses";
|
|
snackbarColor.value = "error";
|
|
snackbar.value = true;
|
|
closeKlinikRuangDialog();
|
|
return;
|
|
}
|
|
|
|
const result = queueStore.createAntreanKlinikRuang(
|
|
klinikRuang,
|
|
ruang,
|
|
patient,
|
|
"loket"
|
|
);
|
|
|
|
if (result.success && result.patient) {
|
|
// Print ticket dengan nomor antrian baru
|
|
try {
|
|
await printTicketFromPatient(result.patient);
|
|
} catch (error) {
|
|
console.error('Error printing ticket:', error);
|
|
snackbarText.value = `${result.message}. Gagal print tiket.`;
|
|
snackbarColor.value = "warning";
|
|
snackbar.value = true;
|
|
closeKlinikRuangDialog();
|
|
return;
|
|
}
|
|
}
|
|
|
|
snackbarText.value = result.message;
|
|
snackbarColor.value = result.success ? "success" : "error";
|
|
snackbar.value = true;
|
|
|
|
closeKlinikRuangDialog();
|
|
};
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.loket-container {
|
|
background: var(--color-neutral-300);
|
|
min-height: 100vh;
|
|
padding: 16px;
|
|
overflow-x: hidden; /* Prevent horizontal scroll */
|
|
width: 100%;
|
|
max-width: 100vw;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.content-grid {
|
|
margin: 0 -8px;
|
|
}
|
|
|
|
.content-grid > .v-col {
|
|
padding: 8px;
|
|
}
|
|
|
|
.content-grid > .v-col:first-child {
|
|
padding-left: 0;
|
|
}
|
|
|
|
.content-grid > .v-col:last-child {
|
|
padding-right: 0;
|
|
}
|
|
|
|
.sticky-wrapper {
|
|
position: sticky;
|
|
top: 16px;
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.filters {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.status-filter {
|
|
flex: 1;
|
|
min-width: 300px;
|
|
}
|
|
|
|
.status-filter .v-chip {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
height: 32px;
|
|
border: 1px solid var(--color-neutral-500);
|
|
background: var(--color-neutral-100);
|
|
color: var(--color-neutral-600);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.status-filter .v-chip:hover {
|
|
background: var(--color-neutral-300);
|
|
}
|
|
|
|
.status-filter .v-chip.active-chip {
|
|
background: var(--color-primary-600);
|
|
color: var(--color-neutral-100);
|
|
border-color: var(--color-primary-600);
|
|
}
|
|
|
|
.search-field {
|
|
max-width: 300px;
|
|
min-width: 250px;
|
|
}
|
|
|
|
.section-label {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
color: var(--color-neutral-600);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.data-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.patient-data-container {
|
|
border-radius: 12px;
|
|
border: 1px solid var(--color-neutral-500);
|
|
background: var(--color-neutral-100);
|
|
}
|
|
|
|
/* Dialog Klinik Ruang Styles */
|
|
.dialog-card {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.dialog-header {
|
|
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
|
|
color: var(--color-neutral-100);
|
|
padding: 20px 24px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.dialog-header-warning {
|
|
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
|
|
}
|
|
|
|
.headline-4 {
|
|
font-size: 20px;
|
|
line-height: 28px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.headline-5 {
|
|
font-size: 18px;
|
|
line-height: 24px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
color: var(--color-neutral-900);
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.btn-close {
|
|
color: var(--color-neutral-100) !important;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-close:hover {
|
|
background: rgba(255, 255, 255, 0.2) !important;
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.dialog-content {
|
|
padding: 24px !important;
|
|
background: var(--color-neutral-300);
|
|
}
|
|
|
|
.dialog-actions {
|
|
padding: 16px 24px;
|
|
background: var(--color-neutral-300);
|
|
}
|
|
|
|
.dialog-actions .v-btn {
|
|
text-transform: none;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
min-width: 100px;
|
|
}
|
|
|
|
.btn-cancel {
|
|
border-color: var(--color-neutral-600) !important;
|
|
color: var(--color-neutral-800) !important;
|
|
}
|
|
|
|
.btn-submit {
|
|
background-color: var(--color-secondary-600) !important;
|
|
color: var(--color-neutral-100) !important;
|
|
}
|
|
|
|
.patient-card {
|
|
background: var(--color-neutral-100);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
border: 1px solid var(--color-neutral-400);
|
|
}
|
|
|
|
.detail-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.caption-2 {
|
|
font-size: 12px;
|
|
line-height: 16px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.body-2 {
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.body-3 {
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.text-semibold {
|
|
font-weight: 600 !important;
|
|
}
|
|
|
|
.text-muted {
|
|
color: var(--color-neutral-700);
|
|
}
|
|
|
|
.expansion-panels {
|
|
background: transparent;
|
|
}
|
|
|
|
.expansion-panel {
|
|
background: var(--color-neutral-100);
|
|
border: 1px solid var(--color-neutral-400);
|
|
border-radius: 12px !important;
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.expansion-panel::before {
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.expansion-title {
|
|
background: var(--color-neutral-100);
|
|
min-height: 60px;
|
|
}
|
|
|
|
.expansion-header {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.ruang-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.ruang-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
background: var(--color-neutral-300);
|
|
border: 1px solid var(--color-neutral-400);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.ruang-item:hover {
|
|
border-color: var(--color-warning-600);
|
|
background: rgba(255, 185, 95, 0.15);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.ruang-item:active {
|
|
transform: translateY(0);
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.ruang-info {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.loket-container {
|
|
padding: 12px;
|
|
}
|
|
|
|
.sticky-wrapper {
|
|
position: relative;
|
|
top: 0;
|
|
max-height: none;
|
|
}
|
|
|
|
.filters {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.status-filter {
|
|
min-width: 100%;
|
|
}
|
|
|
|
.search-field {
|
|
max-width: 100%;
|
|
min-width: 100%;
|
|
}
|
|
}
|
|
</style>
|