update API dan header

This commit is contained in:
bagus-arie05
2026-01-21 08:59:13 +07:00
parent 9d45071a36
commit 08750087b1
29 changed files with 2095 additions and 1174 deletions

No files matched your search

+11 -38
View File
@@ -9304,17 +9304,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/comment-parser": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
@@ -9521,22 +9510,6 @@
"node": ">= 8"
}
},
"node_modules/crossws": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/crossws/-/crossws-0.4.1.tgz",
"integrity": "sha512-E7WKBcHVhAVrY6JYD5kteNqVq1GSZxqGrdSiwXR9at+XHi43HJoCQKXcCczR5LBnBquFZPsB3o7HklulKoBU5w==",
"license": "MIT",
"optional": true,
"peer": true,
"peerDependencies": {
"srvx": ">=0.7.1"
},
"peerDependenciesMeta": {
"srvx": {
"optional": true
}
}
},
"node_modules/css-declaration-sorter": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.1.tgz",
@@ -10457,9 +10430,9 @@
}
},
"node_modules/devalue": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz",
"integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==",
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
"license": "MIT"
},
"node_modules/devlop": {
@@ -12193,9 +12166,9 @@
}
},
"node_modules/h3": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz",
"integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz",
"integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==",
"license": "MIT",
"dependencies": {
"cookie-es": "^1.2.2",
@@ -12203,9 +12176,9 @@
"defu": "^6.1.4",
"destr": "^2.0.5",
"iron-webcrypto": "^1.2.1",
"node-mock-http": "^1.0.2",
"node-mock-http": "^1.0.4",
"radix3": "^1.1.2",
"ufo": "^1.6.1",
"ufo": "^1.6.3",
"uncrypto": "^0.1.3"
}
},
@@ -18879,9 +18852,9 @@
}
},
"node_modules/tar": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.5.tgz",
"integrity": "sha512-ocjwSUBo2V8ExKxy9FH6iROIsK60OCW//h14MFYpivNSYIj7ntgm35ijGT82Jl//xwbxBhIYXkfAovjtVm9nrA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
+11 -3
View File
@@ -1,5 +1,5 @@
<template>
<div class="loket-container">
<div>
<!-- Compact Header -->
<PageHeader
icon="mdi-hospital-building"
@@ -9,8 +9,9 @@
theme="primary"
/>
<!-- Main Content Grid -->
<v-row class="content-grid" dense>
<div class="loket-container">
<!-- 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">
@@ -227,6 +228,7 @@
:message="snackbarText"
:color="snackbarColor"
/>
</div>
</div>
</template>
@@ -416,6 +418,12 @@ const buatAntreanKlinikRuang = (klinikRuang, ruang) => {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 16px;
border-radius: 0 !important;
}
/* Remove rounded edges from PageHeader component */
:deep(.page-header) {
border-radius: 0 !important;
}
.content-grid {
+12 -4
View File
@@ -1,5 +1,5 @@
<template>
<div class="admin-klinik-ruang-container">
<div>
<!-- Header -->
<PageHeader
icon="mdi-door-open"
@@ -9,7 +9,8 @@
theme="primary"
/>
<!-- Global Search and Actions -->
<div class="admin-klinik-ruang-container">
<!-- Global Search and Actions -->
<div class="global-search-section sticky-search-section">
<div class="search-actions-row">
<v-text-field
@@ -793,6 +794,7 @@
:message="snackbarText"
:color="snackbarColor"
/>
</div>
</div>
</template>
@@ -1770,6 +1772,12 @@ onMounted(() => {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 16px;
border-radius: 0 !important;
}
/* Remove rounded edges from PageHeader component */
:deep(.page-header) {
border-radius: 0 !important;
}
.generate-ticket-card {
@@ -2095,7 +2103,7 @@ onMounted(() => {
}
.dialog-header {
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
color: var(--color-neutral-100);
padding: 20px 24px;
display: flex;
@@ -2104,7 +2112,7 @@ onMounted(() => {
}
.dialog-header-warning {
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
}
.headline-4 {
+10 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="admin-klinik-ruang-index">
<div>
<PageHeader
icon="mdi-door-open"
title="Admin Klinik Ruang"
@@ -8,7 +8,8 @@
theme="primary"
/>
<v-row class="mt-4">
<div class="admin-klinik-ruang-index">
<v-row class="mt-4">
<v-col
v-for="klinikRuang in klinikRuangList"
:key="klinikRuang.id"
@@ -41,6 +42,7 @@
<v-icon size="64" color="grey-lighten-2">mdi-hospital-building-outline</v-icon>
<div class="empty-text">Tidak ada klinik ruang yang tersedia</div>
</div>
</div>
</div>
</template>
@@ -67,6 +69,12 @@ const navigateToKlinik = (kodeKlinik) => {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 16px;
border-radius: 0 !important;
}
/* Remove rounded edges from PageHeader component */
:deep(.page-header) {
border-radius: 0 !important;
}
.klinik-card {
+13 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="loket-container">
<div>
<!-- Compact Header -->
<PageHeader
icon="mdi-view-dashboard"
@@ -9,8 +9,9 @@
theme="primary"
/>
<!-- Main Content Grid -->
<v-row class="content-grid" dense>
<div class="loket-container">
<!-- Main Content Grid -->
<v-row class="content-grid" dense>
<!-- Left Column: Current Patient & Queue Actions -->
<v-col cols="12" md="5" class="sticky-column">
<div class="sticky-wrapper">
@@ -199,6 +200,7 @@
:message="snackbarText"
:color="snackbarColor"
/>
</div>
</div>
</template>
@@ -518,6 +520,12 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
width: 100%;
max-width: 100vw;
box-sizing: border-box;
border-radius: 0 !important;
}
/* Remove rounded edges from PageHeader component */
:deep(.page-header) {
border-radius: 0 !important;
}
.content-grid {
@@ -606,7 +614,7 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
}
.dialog-header {
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
color: var(--color-neutral-100);
padding: 20px 24px;
display: flex;
@@ -615,7 +623,7 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
}
.dialog-header-warning {
background: linear-gradient(135deg, var(--color-secondary-600) 0%, var(--color-secondary-700) 100%);
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
}
.headline-4 {
+8 -5
View File
@@ -1,5 +1,5 @@
<template>
<v-container fluid class="bg-grey-lighten-4 pa-4">
<div>
<div class="page-header">
<div class="header-content">
<div class="header-left">
@@ -17,7 +17,9 @@
</div>
</div>
</div>
<v-card flat>
<v-container fluid class="bg-grey-lighten-4 pa-4">
<v-card flat>
<v-card-text>
<v-row align="center">
<v-col cols="12" md="4">
@@ -82,7 +84,8 @@
</template>
</TabelData>
</v-card>
</v-container>
</v-container>
</div>
</template>
<script setup lang="ts">
@@ -200,8 +203,8 @@ const items = ref([
<style scoped>
.page-header {
background: linear-gradient(135deg, #3A61C9 0%, #3556AE 100%);
border-radius: 16px;
margin-bottom: 24px;
border-radius: 0 !important;
margin-bottom: 0;
box-shadow: 0 8px 32px rgba(58, 97, 201, 0.3);
}
+152 -188
View File
@@ -742,151 +742,60 @@ const anjunganData = computed(() => {
return list.find((a) => Number(a.id) === Number(id)) || null;
});
//kode mapping table
const KODE_MAPPING = {
AN: "ANA", // Anak
AS: "ANESTESI", // Anestesi
BD: "BEDAH", // Bedah
GR: "GERIATRI", // Geriatri
GI: "GIGI", // Gigi dan Mulut
GZ: "GIZI", // Gizi
HO: "HEMATO", // Hemato Onkologi
IP: "INTERNA", // IPD / Penyakit Dalam
JT: "JANTUNG", // Jantung / Cardiologi
JW: "JIWA", // Jiwa / Psikiatri
OB: "OBGYN", // Kandungan
KH: "KEMOTERAPI", // Kemoterapi
KN: "NYERI", // Komplementer Nyeri
KK: "KULIT", // Kulit Kelamin
MT: "MATA", // Mata
MC: "MCU", // MCU
ON: "ONKOLOGI", // Onkologi
PR: "PARU", // Paru
TD: "TINDAKAN", // R. Tindakan
RT: "RADIOTERAPI", // Radioterapi
RM: "REHAB", // Rehab Medik
SR: "SARAF", // Saraf / Neurologi
TH: "THT", // THT
};
const todayString = new Date().toISOString().substring(0, 10);
const spesialisList = ref([]);
const doctorsByKode = ref({});
const doctorsByKlinikId = ref({});
const loadingDoctors = ref({});
// load list spesialis dari API
const loadSpesialisForDate = async (tanggal) => {
try {
const data = await $fetch(
`http://10.10.150.131:8088/api/jadwaldokter/tanggal/${tanggal}`
);
spesialisList.value = Array.isArray(data) ? data : [];
console.log(
`✅ Loaded ${spesialisList.value.length} spesialis for ${tanggal}`
);
} catch (error) {
console.error("❌ Gagal mengambil data spesialis dari API", error);
spesialisList.value = [];
}
};
//get spesialis ID
const getSpesialisIdForKode = (kodeKlinik) => {
if (!kodeKlinik) return null;
// 1. normalize kode menggunakan mapping table
const kodeUpper = kodeKlinik.toUpperCase();
const normalizedKode = KODE_MAPPING[kodeUpper] || kodeUpper;
console.log(`🔄 Mapping: "${kodeKlinik}" → "${normalizedKode}"`);
// 2. cari exact match dulu
let entry = spesialisList.value.find((item) => {
const apiKode = (item.Kode || item.kode || "").toUpperCase();
return apiKode === normalizedKode;
});
// 3. jika tidak ada exact match, coba prefix match
if (!entry) {
entry = spesialisList.value.find((item) => {
const apiKode = (item.Kode || item.kode || "").toUpperCase();
return apiKode.startsWith(normalizedKode);
});
if (entry) {
console.log(
`⚠️ Using prefix match: "${normalizedKode}" → "${
entry.Kode || entry.kode
}"`
);
}
}
const spesialisId = entry
? entry.id || entry.ID || entry.FK_daftar_spesialis_ID
: null;
if (!spesialisId) {
console.warn(
`❌ No match for: "${kodeKlinik}" (normalized: "${normalizedKode}")`
);
console.log(
"Available API kodes:",
spesialisList.value.map((s) => s.Kode || s.kode)
);
} else {
console.log(`✅ Found spesialisId: ${spesialisId} for "${kodeKlinik}"`);
}
return spesialisId;
};
const loadDoctorsForKode = async (kodeKlinik) => {
if (!kodeKlinik) return;
// Load doctors dari API berdasarkan idklinik
const loadDoctorsForClinic = async (clinic) => {
if (!clinic || !clinic.id) return;
const idklinik = clinic.id;
// cek apakah sudah ada data atau sedang loading
if (doctorsByKode.value[kodeKlinik] || loadingDoctors.value[kodeKlinik]) {
if (doctorsByKlinikId.value[idklinik] || loadingDoctors.value[idklinik]) {
return;
}
loadingDoctors.value[kodeKlinik] = true;
const spesialisId = getSpesialisIdForKode(kodeKlinik);
if (!spesialisId) {
console.warn(
`⚠️ Skip loading doctors untuk kode: ${kodeKlinik} (spesialisId not found)`
);
loadingDoctors.value[kodeKlinik] = false;
// set empty array agar tidak retry terus-menerus
doctorsByKode.value[kodeKlinik] = [];
return;
}
const tanggal = todayString;
loadingDoctors.value[idklinik] = true;
try {
console.log(`🔄 Fetching doctors for klinik ID: ${idklinik} (${clinic.name})`);
const data = await $fetch(
`http://10.10.150.131:8088/api/jadwaldokter/tanggal/${tanggal}/spesialis/${spesialisId}`
`http://10.10.150.131:8089/api/v1/dokter/${idklinik}`
);
const list = Array.isArray(data) ? data : [];
const doctorNames = list
.map((d) => d.nama_lengkap || d.Nama_dokter || d.name)
console.log(`📦 Raw doctor data for ${idklinik}:`, data);
// Handle different response formats
let doctorList = [];
if (Array.isArray(data)) {
doctorList = data;
} else if (data && typeof data === 'object') {
if (Array.isArray(data.data)) {
doctorList = data.data;
} else if (Array.isArray(data.result)) {
doctorList = data.result;
}
}
// Extract doctor names
const doctorNames = doctorList
.map((d) => d.nama_dokter || d.nama_lengkap || d.Nama_dokter || d.name || d.nama)
.filter(Boolean);
doctorsByKode.value[kodeKlinik] = doctorNames;
doctorsByKlinikId.value[idklinik] = doctorNames;
console.log(
`✅ Loaded ${doctorNames.length} doctors untuk ${kodeKlinik}:`,
`✅ Loaded ${doctorNames.length} doctors untuk klinik ${clinic.name} (ID: ${idklinik}):`,
doctorNames
);
} catch (error) {
console.error(`❌ Gagal mengambil dokter untuk kode ${kodeKlinik}:`, error);
console.error(`❌ Gagal mengambil dokter untuk klinik ID ${idklinik}:`, error);
// set empty array agar tidak retry
doctorsByKode.value[kodeKlinik] = [];
doctorsByKlinikId.value[idklinik] = [];
} finally {
loadingDoctors.value[kodeKlinik] = false;
loadingDoctors.value[idklinik] = false;
}
};
@@ -896,22 +805,27 @@ const prefetchDoctorsForCurrentAnjungan = async () => {
return;
}
const uniqueKodes = Array.from(new Set(anjunganData.value.klinik));
// Get all clinics for this anjungan
const allClinics = clinicStore.getAllClinics;
const clinicsForAnjungan = anjunganData.value.klinik
.map((kode) => allClinics.find((c) => c.kode === kode))
.filter(Boolean);
console.log(
`🔄 Prefetching doctors for ${uniqueKodes.length} klinik:`,
uniqueKodes
`🔄 Prefetching doctors for ${clinicsForAnjungan.length} klinik`
);
await Promise.all(uniqueKodes.map((kode) => loadDoctorsForKode(kode)));
await Promise.all(clinicsForAnjungan.map((clinic) => loadDoctorsForClinic(clinic)));
console.log("✅ Prefetch complete. Doctors by kode:", doctorsByKode.value);
console.log("✅ Prefetch complete. Doctors by klinik ID:", doctorsByKlinikId.value);
};
const dialogDoctors = computed(() => {
const kode = selectedClinic.value?.kode;
if (!kode) return [];
const clinic = selectedClinic.value;
if (!clinic || !clinic.id) return [];
const apiDoctors = doctorsByKode.value[kode];
const idklinik = clinic.id;
const apiDoctors = doctorsByKlinikId.value[idklinik];
// prioritas: API > Static
if (apiDoctors && apiDoctors.length > 0) {
@@ -919,10 +833,10 @@ const dialogDoctors = computed(() => {
}
// fallback ke static data
const staticDoctors = selectedClinic.value?.doctors || [];
const staticDoctors = clinic.doctors || [];
if (staticDoctors.length > 0) {
console.log(`️ Using static doctors for ${kode} (API data not available)`);
console.log(`️ Using static doctors for ${clinic.name} (API data not available)`);
}
return staticDoctors;
@@ -931,40 +845,10 @@ const dialogDoctors = computed(() => {
onMounted(async () => {
console.log("🚀 Component mounted, starting data load...");
// Step 1: Load spesialis list
await loadSpesialisForDate(todayString);
// Step 2: Wait for computed values to be ready
// Wait for computed values to be ready
await nextTick();
if (anjunganData.value?.klinik) {
console.group("📊 Kode Mapping Comparison");
console.table(
anjunganData.value.klinik.map((storeKode) => {
const clinic = clinicStore.getClinicByKode(storeKode);
const normalizedKode =
KODE_MAPPING[storeKode.toUpperCase()] || storeKode;
const spesialisId = getSpesialisIdForKode(storeKode);
const apiMatch = spesialisList.value.find(
(s) => (s.id || s.ID || s.FK_daftar_spesialis_ID) === spesialisId
);
return {
"Store Kode": storeKode,
"Clinic Name": clinic?.name || "❌ Not found",
Normalized: normalizedKode,
"API Kode": apiMatch
? apiMatch.Kode || apiMatch.kode
: "❌ Not matched",
"Spesialis ID": spesialisId || "❌",
Status: spesialisId ? "✅" : "❌",
};
})
);
console.groupEnd();
}
// Step 3: Prefetch doctors
// Prefetch doctors for all clinics in this anjungan
await prefetchDoctorsForCurrentAnjungan();
console.log("✅ Initial data load complete");
@@ -1019,8 +903,8 @@ const isEksekutif = computed(() => {
// tampilkan doctor info dengan prioritas API
const getDisplayDoctorInfo = (clinic) => {
const kode = clinic.kode;
const apiDoctors = kode ? doctorsByKode.value[kode] : null;
const idklinik = clinic.id;
const apiDoctors = idklinik ? doctorsByKlinikId.value[idklinik] : null;
const doctors =
apiDoctors && apiDoctors.length > 0 ? apiDoctors : clinic.doctors || [];
@@ -1039,43 +923,123 @@ const getDisplayDoctorInfo = (clinic) => {
}
};
// Helper to get current day name in Indonesian
const getTodayName = () => {
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'];
return days[new Date().getDay()];
};
// Helper to get current hour
const getCurrentHour = () => {
return new Date().getHours();
};
// Get current shift based on time and day
const getCurrentShift = (clinic) => {
if (!clinic || !clinic.jamShiftPerHari) return null;
const today = getTodayName();
const shiftsToday = clinic.jamShiftPerHari[today];
if (!shiftsToday || shiftsToday.length === 0) return null;
const currentHour = getCurrentHour();
const currentMinutes = new Date().getMinutes();
const currentTime = currentHour * 60 + currentMinutes;
// Find active shift
for (const shift of shiftsToday) {
const [startH, startM] = shift.dari.split(':').map(Number);
const [endH, endM] = shift.sampai.split(':').map(Number);
const startTime = startH * 60 + startM;
const endTime = endH * 60 + endM;
if (currentTime >= startTime && currentTime < endTime) {
return shift;
}
}
// If no active shift, return first shift
return shiftsToday[0];
};
const isShift1Full = (clinic) => {
if (!clinic || !clinic.shifts || clinic.shifts.length === 0) return true;
const shift1 = clinic.shifts.find((s) => s.name === "Shift 1");
return shift1 ? shift1.quota === 0 : true;
if (!clinic || !clinic.jamShiftPerHari) return true;
const today = getTodayName();
const shiftsToday = clinic.jamShiftPerHari[today];
if (!shiftsToday || shiftsToday.length === 0) return true;
// Check first shift
const firstShift = shiftsToday[0];
return firstShift ? firstShift.kuota === 0 : true;
};
const getCurrentShiftNumber = () => {
return 1;
if (!selectedClinic.value) return 1;
const today = getTodayName();
const shiftsToday = selectedClinic.value.jamShiftPerHari?.[today];
if (!shiftsToday || shiftsToday.length === 0) return 1;
const currentShift = getCurrentShift(selectedClinic.value);
if (!currentShift) return 1;
return shiftsToday.indexOf(currentShift) + 1;
};
const getCurrentShiftQuota = () => {
if (!selectedClinic.value || !selectedClinic.value.shifts) return 0;
const currentShift = selectedClinic.value.shifts.find(
(s) => s.name === "Shift 1"
);
return currentShift ? currentShift.quota : 0;
if (!selectedClinic.value || !selectedClinic.value.jamShiftPerHari) return 0;
const today = getTodayName();
const shiftsToday = selectedClinic.value.jamShiftPerHari[today];
if (!shiftsToday || shiftsToday.length === 0) return 0;
const currentShift = getCurrentShift(selectedClinic.value);
return currentShift ? currentShift.kuota : 0;
};
const getAvailableShiftsForBooking = () => {
if (!selectedClinic.value || !selectedClinic.value.shifts) return [];
if (!selectedClinic.value || !selectedClinic.value.jamShiftPerHari) return [];
const selectedDate = bookingForm.value.date;
const today = new Date().toISOString().substring(0, 10);
const isToday = selectedDate === today;
return selectedClinic.value.shifts
.filter((shift) => {
if (isToday) {
return shift.name !== "Shift 1" && shift.quota > 0;
} else {
return shift.quota > 0;
// Get day name from selected date
const date = new Date(selectedDate);
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'];
const dayName = days[date.getDay()];
const shiftsForDay = selectedClinic.value.jamShiftPerHari[dayName];
if (!shiftsForDay || shiftsForDay.length === 0) {
return [];
}
return shiftsForDay
.map((shift, index) => {
const shiftName = `Shift ${index + 1}`;
const shiftTime = `${shift.dari} - ${shift.sampai}`;
// If today, skip shift 1
if (isToday && index === 0) {
return null;
}
if (shift.kuota === 0) {
return null;
}
return {
label: `${shiftName} (${shiftTime}) - Kuota: ${shift.kuota} pasien`,
value: shiftName,
};
})
.map((shift) => ({
label: `${shift.name} (Kuota: ${shift.quota} pasien)`,
value: shift.name,
}));
.filter(Boolean);
};
const getMinDate = () => {
+48 -33
View File
@@ -1,6 +1,6 @@
<!-- pages/Anjungan/Anjungan/index.vue -->
<template>
<div class="anjungan-selection-container">
<div>
<div class="selection-header">
<div class="header-icon">
<img
@@ -15,7 +15,8 @@
</div>
</div>
<div class="anjungan-grid">
<div class="anjungan-selection-container">
<div class="anjungan-grid">
<div
v-for="anj in paginatedAnjungan"
:key="anj.id"
@@ -84,10 +85,11 @@
</v-btn>
</div>
<div v-else-if="totalPages > 1" class="pagination">
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
<div v-else-if="totalPages > 1" class="pagination">
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
</div>
</div>
</div>
</template>
@@ -152,38 +154,37 @@ const navigateToSettings = () => {
<style scoped lang="scss">
.anjungan-selection-container {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 40px;
min-height: calc(100vh - 80px);
padding: 16px;
font-family: 'Inter', 'Roboto', sans-serif;
}
.selection-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
border-radius: 0 !important;
padding: 16px 28px;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 8px 24px rgba(255, 155, 27, 0.3);
gap: 16px;
height: 80px;
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.2);
}
.header-icon {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 24px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-logo {
width: 80px;
height: 80px;
width: 52px;
height: 52px;
object-fit: contain;
padding: 4px;
}
.header-content {
@@ -192,19 +193,21 @@ const navigateToSettings = () => {
}
.main-title {
font-size: 40px;
font-weight: 800;
font-size: 32px;
font-weight: 600;
margin: 0;
color: var(--color-neutral-100);
line-height: 1.2;
line-height: 40px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 18px;
font-weight: 600;
margin: 8px 0 0 0;
font-size: 15px;
font-weight: 400;
margin: 2px 0 0 0;
color: var(--color-neutral-100);
opacity: 0.95;
opacity: 0.9;
line-height: 22px;
}
.anjungan-grid {
@@ -338,21 +341,33 @@ const navigateToSettings = () => {
@media (max-width: 768px) {
.anjungan-selection-container {
padding: 20px;
padding: 12px;
}
.selection-header {
flex-direction: column;
text-align: center;
padding: 32px 24px;
flex-direction: row;
padding: 12px 16px;
height: auto;
min-height: 64px;
}
.header-icon {
padding: 8px;
}
.header-logo {
width: 40px;
height: 40px;
}
.main-title {
font-size: 32px;
font-size: 20px;
line-height: 28px;
}
.subtitle {
font-size: 16px;
font-size: 13px;
line-height: 18px;
}
.anjungan-grid {
+41 -31
View File
@@ -1,6 +1,6 @@
<!-- pages/Anjungan/AntreanMasuk/index.vue -->
<template>
<div class="screen-selection-container">
<div>
<div class="selection-header">
<div class="header-icon">
<v-icon size="64" color="white">mdi-ticket-account</v-icon>
@@ -11,7 +11,8 @@
</div>
</div>
<div v-if="paginatedScreens.length > 0" class="screens-grid">
<div class="screen-selection-container">
<div v-if="paginatedScreens.length > 0" class="screens-grid">
<div
v-for="screen in paginatedScreens"
:key="screen.id"
@@ -80,10 +81,11 @@
</v-btn>
</div>
<div v-if="paginatedScreens.length > 0 && totalPages > 1" class="pagination">
<v-btn variant="outlined" :disabled="page <= 1" @click="goPrev">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" :disabled="page >= totalPages" @click="goNext">Next</v-btn>
<div v-if="paginatedScreens.length > 0 && totalPages > 1" class="pagination">
<v-btn variant="outlined" :disabled="page <= 1" @click="goPrev">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" :disabled="page >= totalPages" @click="goNext">Next</v-btn>
</div>
</div>
</div>
</template>
@@ -147,26 +149,27 @@ const navigateToSettings = () => {
<style scoped lang="scss">
.screen-selection-container {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 40px;
min-height: calc(100vh - 80px);
padding: 16px;
font-family: 'Inter', 'Roboto', sans-serif;
}
.selection-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
border-radius: 0 !important;
padding: 16px 28px;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 8px 24px rgba(25, 118, 210, 0.3);
gap: 16px;
height: 80px;
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.2);
}
.header-icon {
background: rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 24px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
@@ -179,21 +182,21 @@ const navigateToSettings = () => {
}
.main-title {
font-size: 48px;
font-weight: 800;
font-size: 32px;
font-weight: 600;
margin: 0;
letter-spacing: 2px;
color: var(--color-neutral-100);
line-height: 1.2;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
line-height: 40px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 20px;
font-weight: 600;
margin: 8px 0 0 0;
font-size: 15px;
font-weight: 400;
margin: 2px 0 0 0;
color: var(--color-neutral-100);
opacity: 0.95;
opacity: 0.9;
line-height: 22px;
}
.screens-grid {
@@ -327,21 +330,28 @@ const navigateToSettings = () => {
@media (max-width: 768px) {
.screen-selection-container {
padding: 20px;
padding: 12px;
}
.selection-header {
flex-direction: column;
text-align: center;
padding: 32px 24px;
flex-direction: row;
padding: 12px 16px;
height: auto;
min-height: 64px;
}
.header-icon {
padding: 8px;
}
.main-title {
font-size: 32px;
font-size: 20px;
line-height: 28px;
}
.subtitle {
font-size: 16px;
font-size: 13px;
line-height: 18px;
}
.screens-grid {
+41 -31
View File
@@ -1,6 +1,6 @@
<!-- pages/Anjungan/AntrianKlinik/index.vue -->
<template>
<div class="screen-selection-container">
<div>
<div class="selection-header">
<div class="header-icon">
<v-icon size="64" color="white">mdi-monitor-multiple</v-icon>
@@ -11,7 +11,8 @@
</div>
</div>
<div class="screens-grid">
<div class="screen-selection-container">
<div class="screens-grid">
<div
v-for="screen in paginatedScreens"
:key="screen.id"
@@ -80,10 +81,11 @@
</v-btn>
</div>
<div v-else-if="totalPages > 1" class="pagination">
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
<div v-else-if="totalPages > 1" class="pagination">
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
</div>
</div>
</div>
</template>
@@ -150,26 +152,27 @@ const navigateToSettings = () => {
<style scoped lang="scss">
.screen-selection-container {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 40px;
min-height: calc(100vh - 80px);
padding: 16px;
font-family: 'Inter', 'Roboto', sans-serif;
}
.selection-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
border-radius: 0 !important;
padding: 16px 28px;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 8px 24px rgba(255, 155, 27, 0.3);
gap: 16px;
height: 80px;
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.2);
}
.header-icon {
background: rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 24px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
@@ -182,21 +185,21 @@ const navigateToSettings = () => {
}
.main-title {
font-size: 48px;
font-weight: 800;
font-size: 32px;
font-weight: 600;
margin: 0;
letter-spacing: 2px;
color: var(--color-neutral-100);
line-height: 1.2;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
line-height: 40px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 20px;
font-weight: 600;
margin: 8px 0 0 0;
font-size: 15px;
font-weight: 400;
margin: 2px 0 0 0;
color: var(--color-neutral-100);
opacity: 0.95;
opacity: 0.9;
line-height: 22px;
}
.screens-grid {
@@ -330,21 +333,28 @@ const navigateToSettings = () => {
@media (max-width: 768px) {
.screen-selection-container {
padding: 20px;
padding: 12px;
}
.selection-header {
flex-direction: column;
text-align: center;
padding: 32px 24px;
flex-direction: row;
padding: 12px 16px;
height: auto;
min-height: 64px;
}
.header-icon {
padding: 8px;
}
.main-title {
font-size: 32px;
font-size: 20px;
line-height: 28px;
}
.subtitle {
font-size: 16px;
font-size: 13px;
line-height: 18px;
}
.screens-grid {
+48 -33
View File
@@ -1,6 +1,6 @@
<!-- pages/Anjungan/AntrianKlinikRuang/index.vue -->
<template>
<div class="klinik-selection-container">
<div>
<div class="selection-header">
<div class="header-icon">
<img
@@ -15,7 +15,8 @@
</div>
</div>
<div class="kliniks-grid" v-if="paginatedKliniks.length > 0">
<div class="klinik-selection-container">
<div class="kliniks-grid" v-if="paginatedKliniks.length > 0">
<div
v-for="klinik in paginatedKliniks"
:key="klinik.kodeKlinik"
@@ -84,10 +85,11 @@
</v-btn>
</div>
<div v-if="paginatedKliniks.length > 0 && totalPages > 1" class="pagination">
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
<div v-if="paginatedKliniks.length > 0 && totalPages > 1" class="pagination">
<v-btn variant="outlined" @click="goPrev" :disabled="page <= 1">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" @click="goNext" :disabled="page >= totalPages">Next</v-btn>
</div>
</div>
</div>
</template>
@@ -179,38 +181,37 @@ const navigateToSettings = () => {
<style scoped lang="scss">
.klinik-selection-container {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 40px;
min-height: calc(100vh - 80px);
padding: 16px;
font-family: 'Inter', 'Roboto', sans-serif;
}
.selection-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
border-radius: 0 !important;
padding: 16px 28px;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 8px 24px rgba(33, 150, 243, 0.3);
gap: 16px;
height: 80px;
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.2);
}
.header-icon {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 24px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-logo {
width: 80px;
height: 80px;
width: 52px;
height: 52px;
object-fit: contain;
padding: 4px;
}
.header-content {
@@ -219,19 +220,21 @@ const navigateToSettings = () => {
}
.main-title {
font-size: 40px;
font-weight: 800;
font-size: 32px;
font-weight: 600;
margin: 0;
color: var(--color-neutral-100);
line-height: 1.2;
line-height: 40px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 18px;
font-weight: 600;
margin: 8px 0 0 0;
font-size: 15px;
font-weight: 400;
margin: 2px 0 0 0;
color: var(--color-neutral-100);
opacity: 0.95;
opacity: 0.9;
line-height: 22px;
}
.kliniks-grid {
@@ -365,21 +368,33 @@ const navigateToSettings = () => {
@media (max-width: 768px) {
.klinik-selection-container {
padding: 20px;
padding: 12px;
}
.selection-header {
flex-direction: column;
text-align: center;
padding: 32px 24px;
flex-direction: row;
padding: 12px 16px;
height: auto;
min-height: 64px;
}
.header-icon {
padding: 8px;
}
.header-logo {
width: 40px;
height: 40px;
}
.main-title {
font-size: 32px;
font-size: 20px;
line-height: 28px;
}
.subtitle {
font-size: 16px;
font-size: 13px;
line-height: 18px;
}
.kliniks-grid {
+48 -33
View File
@@ -1,6 +1,6 @@
<!-- pages/Anjungan/AntrianLoket/index.vue -->
<template>
<div class="loket-selection-container">
<div>
<div class="selection-header">
<div class="header-icon">
<img
@@ -15,7 +15,8 @@
</div>
</div>
<div v-if="paginatedLokets.length > 0" class="lokets-grid">
<div class="loket-selection-container">
<div v-if="paginatedLokets.length > 0" class="lokets-grid">
<div
v-for="loket in paginatedLokets"
:key="loket.id"
@@ -84,10 +85,11 @@
</v-btn>
</div>
<div v-if="paginatedLokets.length > 0 && totalPages > 1" class="pagination">
<v-btn variant="outlined" :disabled="page <= 1" @click="goPrev">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" :disabled="page >= totalPages" @click="goNext">Next</v-btn>
<div v-if="paginatedLokets.length > 0 && totalPages > 1" class="pagination">
<v-btn variant="outlined" :disabled="page <= 1" @click="goPrev">Prev</v-btn>
<span class="page-info">Page {{ page }} / {{ totalPages }}</span>
<v-btn variant="outlined" :disabled="page >= totalPages" @click="goNext">Next</v-btn>
</div>
</div>
</div>
</template>
@@ -161,38 +163,37 @@ const getKlinikName = (kode) => {
<style scoped lang="scss">
.loket-selection-container {
background: var(--color-neutral-300);
min-height: 100vh;
padding: 40px;
min-height: calc(100vh - 80px);
padding: 16px;
font-family: 'Inter', 'Roboto', sans-serif;
}
.selection-header {
background: linear-gradient(135deg, var(--color-primary-600) 0%, var(--color-primary-700) 100%);
border-radius: 20px;
padding: 40px;
margin-bottom: 40px;
border-radius: 0 !important;
padding: 16px 28px;
margin-bottom: 0;
display: flex;
align-items: center;
gap: 32px;
box-shadow: 0 8px 24px rgba(33, 150, 243, 0.3);
gap: 16px;
height: 80px;
box-shadow: 0 4px 16px rgba(33, 150, 243, 0.2);
}
.header-icon {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 24px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-logo {
width: 80px;
height: 80px;
width: 52px;
height: 52px;
object-fit: contain;
padding: 4px;
}
.header-content {
@@ -201,19 +202,21 @@ const getKlinikName = (kode) => {
}
.main-title {
font-size: 40px;
font-weight: 800;
font-size: 32px;
font-weight: 600;
margin: 0;
color: var(--color-neutral-100);
line-height: 1.2;
line-height: 40px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
font-size: 18px;
font-weight: 600;
margin: 8px 0 0 0;
font-size: 15px;
font-weight: 400;
margin: 2px 0 0 0;
color: var(--color-neutral-100);
opacity: 0.95;
opacity: 0.9;
line-height: 22px;
}
.lokets-grid {
@@ -347,21 +350,33 @@ const getKlinikName = (kode) => {
@media (max-width: 768px) {
.loket-selection-container {
padding: 20px;
padding: 12px;
}
.selection-header {
flex-direction: column;
text-align: center;
padding: 32px 24px;
flex-direction: row;
padding: 12px 16px;
height: auto;
min-height: 64px;
}
.header-icon {
padding: 8px;
}
.header-logo {
width: 40px;
height: 40px;
}
.main-title {
font-size: 32px;
font-size: 20px;
line-height: 28px;
}
.subtitle {
font-size: 16px;
font-size: 13px;
line-height: 18px;
}
.lokets-grid {
+30 -28
View File
@@ -1,27 +1,28 @@
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-clipboard-plus</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Buat Antrean</h2>
<p class="page-subtitle">{{ currentDate }} - Manajemen Pembuatan Antrean</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-clipboard-plus</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Buat Antrean</h2>
<p class="page-subtitle">{{ currentDate }} - Manajemen Pembuatan Antrean</p>
</div>
<v-chip color="white" variant="flat" class="stat-chip">
<v-icon start size="20" color="primary">mdi-account-multiple</v-icon>
<span class="text-semibold">{{ totalPasien }} Pasien</span>
</v-chip>
</div>
<v-chip color="white" variant="flat" class="stat-chip">
<v-icon start size="20" color="primary">mdi-account-multiple</v-icon>
<span class="text-semibold">{{ totalPasien }} Pasien</span>
</v-chip>
</div>
</div>
<!-- Filters & Search -->
<v-card-text class="pa-4">
<v-container>
<v-card>
<!-- Filters & Search -->
<v-card-text class="pa-4">
<v-row dense class="mb-4">
<v-col cols="12" md="8">
<v-text-field
@@ -327,14 +328,15 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar" :color="snackbarColor" :timeout="3000" location="top right">
<span class="body-3">{{ snackbarText }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar" :color="snackbarColor" :timeout="3000" location="top right">
<span class="body-3">{{ snackbarText }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
@@ -574,7 +576,7 @@ $font-weight-bold: 700;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+5 -3
View File
@@ -1,5 +1,5 @@
<template>
<div class="checkin-page-container">
<div>
<!-- Page Header Component -->
<PageHeader
logo="/Rumah_Sakit_Umum_Daerah_Dr._Saiful_Anwar.webp"
@@ -11,8 +11,9 @@
@add-click="showGenerateQRDialog = true"
/>
<!-- Main Content Grid - Same pattern as AdminLoket.vue -->
<v-row class="content-grid" dense>
<div class="checkin-page-container">
<!-- Main Content Grid - Same pattern as AdminLoket.vue -->
<v-row class="content-grid" dense>
<!-- Left Column: QR Scan Card -->
<v-col cols="12" md="6" lg="5" class="sticky-column">
<div class="sticky-wrapper">
@@ -1292,6 +1293,7 @@
</v-card>
</v-dialog>
</div>
</div>
</template>
+32 -30
View File
@@ -1,37 +1,38 @@
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-heart-pulse</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Monitoring Pasien</h2>
<p class="page-subtitle">{{ currentDate }} - Manajemen Data Pasien</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-heart-pulse</v-icon>
</div>
<div class="header-stats">
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-account-group</v-icon>
{{ totalCount }} Total
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-clock-outline</v-icon>
{{ waitingCount }} Menunggu
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip">
<v-icon start size="16">mdi-check-circle</v-icon>
{{ doneCount }} Selesai
</v-chip>
<div class="header-text">
<h2 class="page-title">Monitoring Pasien</h2>
<p class="page-subtitle">{{ currentDate }} - Manajemen Data Pasien</p>
</div>
</div>
<div class="header-stats">
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-account-group</v-icon>
{{ totalCount }} Total
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-clock-outline</v-icon>
{{ waitingCount }} Menunggu
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip">
<v-icon start size="16">mdi-check-circle</v-icon>
{{ doneCount }} Selesai
</v-chip>
</div>
</div>
</div>
<!-- Status Filter Section -->
<v-card-text class="filter-section-top">
<v-container>
<v-card>
<!-- Status Filter Section -->
<v-card-text class="filter-section-top">
<div class="section-label">DATA PASIEN</div>
<v-chip-group
@@ -192,7 +193,8 @@
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</v-container>
</div>
</template>
<script setup>
@@ -400,7 +402,7 @@ $font-weight-semibold: 600;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+7 -7
View File
@@ -13,7 +13,7 @@
<div class="dialog-header-content">
<div class="dialog-header-left">
<v-avatar
:color="viewMode === 'add' ? 'green-darken-2' : 'orange-darken-2'"
color="primary-darken-1"
size="56"
class="dialog-avatar elevation-4"
>
@@ -287,7 +287,7 @@
<div class="dialog-header-content">
<div class="dialog-header-left">
<v-avatar
color="green-darken-2"
color="primary-darken-1"
size="56"
class="dialog-avatar elevation-4"
>
@@ -332,7 +332,7 @@
<div class="dialog-header-content">
<div class="dialog-header-left">
<v-avatar
color="blue-darken-2"
color="primary-darken-1"
size="56"
class="dialog-avatar elevation-4"
>
@@ -1911,19 +1911,19 @@ $font-weight-semibold: 600;
}
.header-add {
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
}
.header-edit {
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
}
.header-edit-access {
background: linear-gradient(135deg, #009262 0%, #1B6E53 100%);
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
}
.header-view {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
}
.dialog-content {
+56 -33
View File
@@ -1,30 +1,31 @@
<template>
<v-container>
<v-card>
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-monitor-dashboard</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Anjungan</h2>
<p class="page-subtitle">Kelola anjungan dan klinik yang ditampilkan</p>
</div>
<div>
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-monitor-dashboard</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Anjungan</h2>
<p class="page-subtitle">Kelola anjungan dan klinik yang ditampilkan</p>
</div>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Anjungan
</v-btn>
</div>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Anjungan
</v-btn>
</div>
</div>
<v-card-text>
<v-container>
<v-card>
<v-card-text>
<v-data-table
:headers="headers"
:items="anjunganItems"
@@ -137,7 +138,7 @@
<v-select
label="Pilih Klinik"
:items="clinicStore.getClinicsForDropdown(masterStore)"
:items="availableClinics"
v-model="formData.klinik"
item-title="name"
item-value="kode"
@@ -149,6 +150,7 @@
:rules="[v => v.length > 0 || 'Pilih minimal 1 klinik']"
hide-details="auto"
class="input-field"
:disabled="!formData.jenisPasien"
>
<template v-slot:chip="{ props, item }">
<v-chip
@@ -223,17 +225,18 @@
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
import { useMasterStore } from '@/stores/masterStore';
import { useAnjunganStore } from '@/stores/anjunganStore';
import { useClinicStore } from '@/stores/clinicStore';
@@ -264,7 +267,7 @@ const headers = ref([
{ title: 'Actions', value: 'actions', sortable: false, width: 'auto' },
]);
const jenisPasienOptions = ['Reguler', 'BPJS', 'Eksekutif'];
const jenisPasienOptions = ['Reguler', 'Eksekutif'];
const anjunganItems = computed(() => anjunganStore.getAllAnjungan);
@@ -275,6 +278,21 @@ const formData = ref({
klinik: [],
});
// Computed property to get filtered clinics based on selected jenisPasien
const availableClinics = computed(() => {
if (!formData.value.jenisPasien) {
return clinicStore.getClinicsForDropdown();
}
return clinicStore.getClinicsForDropdown(formData.value.jenisPasien);
});
// Watch jenisPasien changes and clear clinic selection
watch(() => formData.value.jenisPasien, (newVal, oldVal) => {
if (newVal !== oldVal && oldVal !== '') {
formData.value.klinik = [];
}
});
const openTambahDialog = () => {
isEdit.value = false;
resetForm();
@@ -354,6 +372,11 @@ const closePreviewDialog = () => {
previewItem.value = null;
previewUrl.value = '';
};
// Fetch Reguler clinics from API on mount
onMounted(async () => {
await clinicStore.fetchRegulerClinics();
});
</script>
<style scoped lang="scss">
@@ -390,7 +413,7 @@ $font-weight-semibold: 600;
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+546 -157
View File
@@ -1,39 +1,49 @@
<!-- eslint-disable vue/valid-v-slot -->
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-hospital-building</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Klinik</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Klinik</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-hospital-building</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Klinik</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Klinik</p>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Klinik
</v-btn>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Klinik
</v-btn>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-container>
<v-card>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="masterStore.klinikData"
:items-per-page="10"
class="elevation-0 data-table"
>
<template #item.jenisLayanan="{ item }">
<v-chip
size="small"
:class="item.jenisLayanan === 'Reguler' ? 'chip-reguler' : 'chip-eksekutif'"
>
{{ item.jenisLayanan }}
</v-chip>
</template>
<template #item.shift="{ item }">
<v-chip size="small" class="chip-secondary">
{{ item.shift }}
@@ -89,7 +99,7 @@
</div>
<v-row dense>
<v-col cols="4">
<v-col cols="6">
<v-text-field
v-model="formData.kode"
label="Kode"
@@ -102,7 +112,7 @@
/>
<small class="caption-2">2 Huruf</small>
</v-col>
<v-col cols="4">
<v-col cols="6">
<v-text-field
v-model="formData.nama"
label="Nama"
@@ -114,19 +124,6 @@
class="mb-3 input-field"
/>
</v-col>
<v-col cols="4">
<v-text-field
v-model.number="formData.shift"
label="Jumlah Shift"
type="number"
variant="outlined"
density="compact"
:rules="[v => !!v || 'Shift harus diisi', v => v > 0 || 'Minimal 1']"
hide-details="auto"
class="mb-3 input-field"
@update:model-value="updateShiftCount"
/>
</v-col>
</v-row>
<v-checkbox
@@ -141,87 +138,11 @@
<v-divider class="my-4 divider-section"/>
<!-- Konfigurasi Shift & Kuota -->
<div class="field-group">
<div class="group-label">
<v-icon size="18" class="icon-label">mdi-clock-outline</v-icon>
<span>Konfigurasi Shift & Kuota</span>
</div>
<div v-for="(shift, index) in formData.jamShiftList" :key="index" class="shift-item">
<v-row dense align="center">
<v-col cols="2">
<div class="shift-badge body-3">Shift {{ index + 1 }}</div>
</v-col>
<v-col cols="3">
<v-text-field
v-model="shift.dari"
label="Mulai"
type="time"
variant="outlined"
density="compact"
hide-details
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model="shift.sampai"
label="Selesai"
type="time"
variant="outlined"
density="compact"
hide-details
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="shift.kuota"
label="Kuota"
type="number"
variant="outlined"
density="compact"
hide-details
placeholder="0"
class="input-field"
/>
</v-col>
<v-col cols="1">
<v-btn
v-if="formData.jamShiftList.length > 1"
icon
size="small"
variant="text"
class="btn-delete-shift"
@click="removeShift(index)"
>
<v-icon size="18">mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
<div class="total-badge">
<v-icon size="18" class="icon-success">mdi-sigma</v-icon>
<span class="body-3 text-medium">Total Kuota:</span>
<v-chip size="small" variant="flat" class="chip-success">
{{ calculateTotalQuota() }}
</v-chip>
</div>
</div>
<v-divider class="my-4 divider-section"/>
<!-- Jadwal Klinik -->
<div class="field-group">
<div class="group-label">
<v-icon size="18" class="icon-label">mdi-calendar-check</v-icon>
<span>Jadwal Klinik</span>
<span>Pilih Hari Operasional</span>
</div>
<div class="day-selector">
@@ -238,6 +159,192 @@
</v-chip>
</div>
</div>
<v-divider class="my-4 divider-section"/>
<!-- Konfigurasi Shift Per Hari -->
<div v-if="formData.jadwalKlinik.length > 0" class="field-group">
<div class="group-label">
<v-icon size="18" class="icon-label">mdi-clock-outline</v-icon>
<span>Konfigurasi Shift & Kuota Per Hari</span>
</div>
<!-- Day Tabs -->
<div class="day-tabs">
<v-chip
v-for="hari in formData.jadwalKlinik"
:key="hari"
class="day-tab"
:class="{ 'day-tab-active': activeDay === hari }"
@click="activeDay = hari"
>
{{ hari }}
</v-chip>
</div>
<!-- Shift Configuration for Active Day -->
<div class="shifts-container">
<div v-for="(shift, index) in getShiftsForDay(activeDay)" :key="index" class="shift-item">
<v-row dense align="center">
<v-col cols="2">
<div class="shift-badge body-3">Shift {{ index + 1 }}</div>
</v-col>
<v-col cols="3">
<v-text-field
v-model="shift.dari"
label="Mulai"
type="time"
variant="outlined"
density="compact"
hide-details
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model="shift.sampai"
label="Selesai"
type="time"
variant="outlined"
density="compact"
hide-details
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="shift.kuota"
label="Kuota"
type="number"
variant="outlined"
density="compact"
hide-details
placeholder="0"
class="input-field"
/>
</v-col>
<v-col cols="1">
<v-btn
v-if="getShiftsForDay(activeDay).length > 1"
icon
size="small"
variant="text"
class="btn-delete-shift"
@click="removeShiftFromDay(activeDay, index)"
>
<v-icon size="18">mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
<v-btn
size="small"
variant="outlined"
class="btn-add-shift mt-2"
@click="addShiftToDay(activeDay)"
>
<v-icon left size="16">mdi-plus</v-icon>
Tambah Shift untuk {{ activeDay }}
</v-btn>
</div>
<div class="total-badge mt-3">
<v-icon size="18" class="icon-success">mdi-sigma</v-icon>
<span class="body-3 text-medium">Total Kuota Semua Hari:</span>
<v-chip size="small" variant="flat" class="chip-success">
{{ calculateTotalQuota() }}
</v-chip>
</div>
</div>
<v-divider class="my-4 divider-section"/>
<!-- Tanggal Tutup Klinik -->
<div class="field-group">
<div class="group-label">
<v-icon size="18" class="icon-label">mdi-calendar-remove</v-icon>
<span>Jadwal Tutup Klinik</span>
</div>
<p class="helper-text mb-3">Tambahkan tanggal ketika klinik ini tutup (tidak beroperasi)</p>
<!-- Input Tanggal Tutup -->
<v-row dense>
<v-col cols="9">
<v-text-field
v-model="newClosedDate"
type="date"
label="Pilih Tanggal Tutup"
variant="outlined"
density="compact"
hide-details
:min="new Date().toISOString().split('T')[0]"
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-btn
color="danger-600"
class="text-white btn-add-closed-date"
size="large"
block
@click="addClosedDate"
:disabled="!newClosedDate"
>
<v-icon left size="18">mdi-plus</v-icon>
Tambah
</v-btn>
</v-col>
</v-row>
<!-- List Tanggal Tutup -->
<div v-if="formData.tanggalTutup.length > 0" class="closed-dates-list mt-4">
<div class="list-header">
<span class="body-3 text-medium">{{ formData.tanggalTutup.length }} Hari Tutup</span>
<v-btn
size="x-small"
variant="text"
color="danger-600"
@click="clearAllClosedDates"
>
<v-icon size="14">mdi-delete-sweep</v-icon>
Hapus Semua
</v-btn>
</div>
<div class="closed-dates-items">
<div
v-for="(date, idx) in sortedClosedDates"
:key="idx"
class="closed-date-item"
>
<div class="date-info">
<v-icon size="16" color="danger-600">mdi-calendar-remove</v-icon>
<span class="date-text">{{ formatClosedDate(date) }}</span>
</div>
<v-btn
icon
size="x-small"
variant="text"
color="danger-600"
@click="removeClosedDate(date)"
>
<v-icon size="16">mdi-close</v-icon>
</v-btn>
</div>
</div>
</div>
<div v-else class="empty-closed-dates mt-3">
<v-icon size="24" color="neutral-600">mdi-calendar-check</v-icon>
<p class="empty-text">Belum ada tanggal tutup</p>
</div>
</div>
</v-form>
</v-card-text>
@@ -265,21 +372,24 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useMasterStore } from '@/stores/masterStore';
import { useClinicStore } from '@/stores/clinicStore';
const masterStore = useMasterStore();
const clinicStore = useClinicStore();
const dialog = ref(false);
const isEdit = ref(false);
const formRef = ref(null);
@@ -294,6 +404,7 @@ const headers = ref([
{ title: 'No', value: 'no', sortable: true },
{ title: 'Kode', value: 'kode', sortable: true },
{ title: 'Nama Klinik', value: 'nama', sortable: true },
{ title: 'Jenis Layanan', value: 'jenisLayanan', sortable: true },
{ title: 'Shift', value: 'shift', sortable: true },
{ title: 'Total Kuota', value: 'totalQuota', sortable: true },
{ title: 'Aksi', value: 'aksi', sortable: false },
@@ -312,54 +423,133 @@ const formData = ref({
kode: '',
nama: '',
shift: 1,
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 0 }],
jamShiftPerHari: {}, // { 'Senin': [{ dari, sampai, kuota }], ... }
jamShiftList: [], // Legacy, kept for compatibility
autoShift: false,
jadwalKlinik: [],
tanggalTutup: [], // Array of closed dates
});
const calculateTotalQuota = () => {
return formData.value.jamShiftList.reduce((total, shift) => {
return total + (parseInt(shift.kuota) || 0);
}, 0);
const activeDay = ref('Senin'); // Currently selected day for shift configuration
const newClosedDate = ref(''); // For adding new closed date
// Computed property for sorted closed dates
const sortedClosedDates = computed(() => {
return [...formData.value.tanggalTutup].sort((a, b) => new Date(a) - new Date(b));
});
// Format closed date for display
const formatClosedDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
});
};
const updateShiftCount = (newShiftCount) => {
const currentCount = formData.value.jamShiftList.length;
// Add new closed date
const addClosedDate = () => {
if (!newClosedDate.value) return;
if (newShiftCount > currentCount) {
for (let i = currentCount; i < newShiftCount; i++) {
let defaultDari = '07:00', defaultSampai = '11:00';
if (i === 1) { defaultDari = '13:00'; defaultSampai = '16:00'; }
else if (i === 2) { defaultDari = '18:00'; defaultSampai = '20:00'; }
else if (i > 2) {
const prevShift = formData.value.jamShiftList[i - 1];
const [prevHour] = prevShift.sampai.split(':');
const nextHour = (parseInt(prevHour) + 1) % 24;
defaultDari = `${String(nextHour).padStart(2, '0')}:00`;
defaultSampai = `${String((nextHour + 4) % 24).padStart(2, '0')}:00`;
}
formData.value.jamShiftList.push({ dari: defaultDari, sampai: defaultSampai, kuota: 0 });
}
} else if (newShiftCount < currentCount && newShiftCount > 0) {
formData.value.jamShiftList = formData.value.jamShiftList.slice(0, newShiftCount);
// Check if date already exists
if (formData.value.tanggalTutup.includes(newClosedDate.value)) {
snackbar.value = {
show: true,
message: 'Tanggal sudah ada dalam daftar',
color: 'warning'
};
return;
}
formData.value.tanggalTutup.push(newClosedDate.value);
newClosedDate.value = ''; // Reset input
};
// Remove a specific closed date
const removeClosedDate = (dateToRemove) => {
const index = formData.value.tanggalTutup.findIndex(d => d === dateToRemove);
if (index > -1) {
formData.value.tanggalTutup.splice(index, 1);
}
};
// Clear all closed dates
const clearAllClosedDates = () => {
if (confirm('Hapus semua tanggal tutup?')) {
formData.value.tanggalTutup = [];
}
};
const calculateTotalQuota = () => {
let total = 0;
Object.values(formData.value.jamShiftPerHari || {}).forEach(shifts => {
if (Array.isArray(shifts)) {
shifts.forEach(shift => {
total += parseInt(shift.kuota) || 0;
});
}
});
return total;
};
// Add shift for specific day
const addShiftToDay = (hari) => {
if (!formData.value.jamShiftPerHari[hari]) {
formData.value.jamShiftPerHari[hari] = [];
}
const shifts = formData.value.jamShiftPerHari[hari];
let defaultDari = '07:00', defaultSampai = '11:00';
if (shifts.length === 1) { defaultDari = '13:00'; defaultSampai = '16:00'; }
else if (shifts.length === 2) { defaultDari = '18:00'; defaultSampai = '20:00'; }
else if (shifts.length > 2) {
const prevShift = shifts[shifts.length - 1];
const [prevHour]= prevShift.sampai.split(':');
const nextHour = (parseInt(prevHour) + 1) % 24;
defaultDari = `${String(nextHour).padStart(2, '0')}:00`;
defaultSampai = `${String((nextHour + 4) % 24).padStart(2, '0')}:00`;
}
shifts.push({ dari: defaultDari, sampai: defaultSampai, kuota: 0 });
};
// Remove shift from specific day
const removeShiftFromDay = (hari, index) => {
if (formData.value.jamShiftPerHari[hari] && formData.value.jamShiftPerHari[hari].length > 1) {
formData.value.jamShiftPerHari[hari].splice(index, 1);
}
};
// Get shifts for a specific day
const getShiftsForDay = (hari) => {
return formData.value.jamShiftPerHari[hari] || [];
};
// Legacy compatibility methods
const updateShiftCount = (newShiftCount) => {
// This is kept for compatibility but not used with new per-day structure
formData.value.shift = newShiftCount;
};
const removeShift = (index) => {
if (formData.value.jamShiftList.length > 1) {
formData.value.jamShiftList.splice(index, 1);
formData.value.shift = formData.value.jamShiftList.length;
}
// Legacy method, kept for compatibility
};
const toggleHari = (hari) => {
const index = formData.value.jadwalKlinik.indexOf(hari);
if (index > -1) {
// Removing day - also remove its shifts
formData.value.jadwalKlinik.splice(index, 1);
delete formData.value.jamShiftPerHari[hari];
} else {
// Adding day - initialize with one shift
formData.value.jadwalKlinik.push(hari);
if (!formData.value.jamShiftPerHari[hari]) {
formData.value.jamShiftPerHari[hari] = [{ dari: '07:00', sampai: '11:00', kuota: 100 }];
}
}
};
@@ -376,10 +566,13 @@ const openEditDialog = (item) => {
kode: item.kode,
nama: item.nama,
shift: item.shift,
jamShiftList: JSON.parse(JSON.stringify(item.jamShiftList)),
jamShiftPerHari: JSON.parse(JSON.stringify(item.jamShiftPerHari || {})),
jamShiftList: [], // Legacy
autoShift: item.autoShift || false,
jadwalKlinik: [...(item.jadwalKlinik || [])],
tanggalTutup: [...(item.tanggalTutup || [])],
};
activeDay.value = formData.value.jadwalKlinik[0] || 'Senin';
dialog.value = true;
};
@@ -394,10 +587,14 @@ const resetForm = () => {
kode: '',
nama: '',
shift: 1,
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 0 }],
jamShiftPerHari: {},
jamShiftList: [],
autoShift: false,
jadwalKlinik: [],
tanggalTutup: [],
};
activeDay.value = 'Senin';
newClosedDate.value = '';
if (formRef.value) {
formRef.value.reset();
}
@@ -433,6 +630,42 @@ const handleDelete = (item) => {
};
}
};
// Fetch Reguler clinics from API on mount
onMounted(async () => {
console.log('🚀 MasterKlinik mounted, fetching clinics...');
console.log('📊 Current clinic count before fetch:', masterStore.klinikData.length);
try {
const result = await clinicStore.fetchRegulerClinics();
console.log('✅ Fetch result:', result);
console.log('📊 Current clinic count after fetch:', masterStore.klinikData.length);
if (!result.success) {
console.error('❌ Fetch failed:', result.message);
snackbar.value = {
show: true,
message: result.message,
color: 'warning'
};
} else {
console.log('🎉 Fetch successful:', result.message);
// Show success message
snackbar.value = {
show: true,
message: result.message,
color: 'success'
};
}
} catch (error) {
console.error('❌ Error in onMounted:', error);
snackbar.value = {
show: true,
message: `Error: ${error.message}`,
color: 'error'
};
}
});
</script>
<style scoped lang="scss">
@@ -481,7 +714,7 @@ $font-weight-bold: 700;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
@@ -546,6 +779,18 @@ $font-weight-bold: 700;
font-weight: $font-weight-medium;
}
.chip-reguler {
background-color: $success-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
}
.chip-eksekutif {
background-color: $secondary-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
}
.btn-edit {
background-color: $primary-600 !important;
color: $neutral-100 !important;
@@ -744,6 +989,150 @@ $font-weight-bold: 700;
border-color: $secondary-600 !important;
}
// ============================================
// DAY TABS (for shift configuration)
// ============================================
.day-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
padding: 12px;
background: $neutral-300;
border-radius: 8px;
}
.day-tab {
cursor: pointer;
font-size: 14px;
line-height: 20px;
font-weight: $font-weight-medium;
border: 1px solid $neutral-500;
background-color: $neutral-100;
color: $neutral-800;
&:hover {
background-color: $primary-600;
color: $neutral-100;
}
}
.day-tab-active {
background-color: $primary-600 !important;
color: $neutral-100 !important;
border-color: $primary-600 !important;
}
.shifts-container {
padding: 12px;
background: $neutral-300;
border-radius: 8px;
}
.btn-add-shift {
border-color: $primary-600 !important;
color: $primary-600 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
// ============================================
// CLOSED DATES
// ============================================
.helper-text {
font-size: 13px;
line-height: 18px;
font-weight: $font-weight-regular;
color: $neutral-700;
margin: 0;
}
.btn-add-closed-date {
height: 40px !important;
font-weight: $font-weight-semibold;
text-transform: none;
font-size: 14px;
line-height: 20px;
}
.closed-dates-list {
background: $neutral-300;
border-radius: 8px;
border: 1px solid $neutral-400;
padding: 12px;
}
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
margin-bottom: 8px;
border-bottom: 1px solid $neutral-400;
span {
color: $neutral-800;
}
}
.closed-dates-items {
display: flex;
flex-direction: column;
gap: 6px;
}
.closed-date-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: $neutral-100;
border-radius: 6px;
border: 1px solid $neutral-400;
transition: all 0.2s ease;
&:hover {
border-color: $danger-400;
background: rgba($danger-600, 0.02);
}
}
.date-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.date-text {
font-size: 14px;
line-height: 20px;
font-weight: $font-weight-medium;
color: $neutral-800;
}
.empty-closed-dates {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: $neutral-300;
border-radius: 8px;
border: 1px dashed $neutral-500;
gap: 8px;
}
.empty-text {
font-size: 13px;
line-height: 18px;
font-weight: $font-weight-medium;
color: $neutral-700;
margin: 0;
}
// ============================================
// BUTTONS
// ============================================
+45 -43
View File
@@ -1,42 +1,43 @@
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-door-open</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Klinik Ruang</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Ruangan</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-door-open</v-icon>
</div>
<div class="header-stats">
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-hospital-building</v-icon>
{{ masterStore.totalKlinikRuang }} Klinik
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-door</v-icon>
{{ masterStore.totalRuangan }} Ruang
</v-chip>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Ruang
</v-btn>
<div class="header-text">
<h2 class="page-title">Master Klinik Ruang</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Ruangan</p>
</div>
</div>
<div class="header-stats">
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-hospital-building</v-icon>
{{ masterStore.totalKlinikRuang }} Klinik
</v-chip>
<v-chip color="white" variant="flat" class="stat-chip mr-2">
<v-icon start size="16">mdi-door</v-icon>
{{ masterStore.totalRuangan }} Ruang
</v-chip>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Ruang
</v-btn>
</div>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-container>
<v-card>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="masterStore.ruangData"
@@ -264,14 +265,15 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
@@ -459,7 +461,7 @@ $font-weight-semibold: 600;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
@@ -729,7 +731,7 @@ $font-weight-semibold: 600;
}
.preview-dialog-header {
background: linear-gradient(135deg, $success-600 0%, $success-700 100%);
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
color: $neutral-100;
padding: 16px 24px;
display: flex;
+35 -33
View File
@@ -1,33 +1,34 @@
<!-- eslint-disable vue/valid-v-slot -->
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-view-dashboard</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Loket</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-view-dashboard</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Loket</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Loket
</v-btn>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Loket
</v-btn>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-container>
<v-card>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="loketHeaders"
:items="masterStore.loketData"
@@ -240,14 +241,15 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
@@ -396,7 +398,7 @@ $font-weight-semibold: 600;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+35 -33
View File
@@ -1,33 +1,34 @@
<!-- pages/MasterPenunjang.vue -->
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-clipboard-pulse</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Penunjang</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Layanan Penunjang</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-clipboard-pulse</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Penunjang</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Layanan Penunjang</p>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Penunjang
</v-btn>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Penunjang
</v-btn>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-container>
<v-card>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="masterStore.penunjangData"
@@ -334,14 +335,15 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
@@ -559,7 +561,7 @@ $font-weight-bold: 700;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+35 -33
View File
@@ -1,32 +1,33 @@
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-monitor</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Screen Configuration</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Konfigurasi Layar Antrian</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-monitor</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Screen Configuration</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Konfigurasi Layar Antrian</p>
</div>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Screen
</v-btn>
</div>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Screen
</v-btn>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-container>
<v-card>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="screenItems"
@@ -255,14 +256,15 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
@@ -428,7 +430,7 @@ $font-weight-semibold: 600;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+7 -4
View File
@@ -1,5 +1,5 @@
<template>
<div class="screen-list">
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
@@ -22,7 +22,9 @@
</div>
</div>
</div>
<!-- <div class="d-flex justify-space-between align-center mb-4">
<div class="screen-list">
<!-- <div class="d-flex justify-space-between align-center mb-4">
<h2>Screen</h2>
<div class="d-flex gap-2">
<v-btn color="primary" variant="outlined" prepend-icon="mdi-eye">
@@ -113,6 +115,7 @@
:total-visible="5"
></v-pagination>
</div>
</div>
</div>
</template>
@@ -196,8 +199,8 @@ const editScreen = (item) => {
<style scoped>
.page-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
border-radius: 16px;
margin-bottom: 24px;
border-radius: 0 !important;
margin-bottom: 0;
box-shadow: 0 8px 32px rgba(25, 118, 210, 0.3);
}
+35 -33
View File
@@ -1,32 +1,33 @@
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-ticket-account</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Screen Antrean Masuk</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Konfigurasi Layar Antrean Masuk</p>
</div>
<div>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-ticket-account</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Screen Antrean Masuk</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Konfigurasi Layar Antrean Masuk</p>
</div>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Screen
</v-btn>
</div>
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Screen
</v-btn>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-container>
<v-card>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="antreanMasukScreenItems"
@@ -255,14 +256,15 @@
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false" size="small">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</div>
</template>
<script setup>
@@ -463,7 +465,7 @@ $font-weight-semibold: 600;
// ============================================
.page-header {
background: linear-gradient(135deg, $primary-600 0%, $primary-700 100%);
border-radius: 16px 16px 0 0;
border-radius: 0 !important;
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
}
+25 -22
View File
@@ -1,29 +1,30 @@
<template>
<v-container>
<v-card class="form-card" elevation="0">
<!-- Header -->
<div class="form-header">
<div class="header-content">
<div class="header-left">
<v-btn
icon
variant="text"
color="white"
@click="cancelAdd"
class="back-btn"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<div class="header-text">
<h1 class="page-title">Tambah Loket Baru</h1>
<p class="page-subtitle">Isi form untuk menambahkan loket</p>
</div>
<div>
<!-- Header -->
<div class="form-header">
<div class="header-content">
<div class="header-left">
<v-btn
icon
variant="text"
color="white"
@click="cancelAdd"
class="back-btn"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<div class="header-text">
<h1 class="page-title">Tambah Loket Baru</h1>
<p class="page-subtitle">Isi form untuk menambahkan loket</p>
</div>
</div>
</div>
</div>
<!-- Form Content -->
<v-card-text class="form-content">
<v-container>
<v-card class="form-card" elevation="0">
<!-- Form Content -->
<v-card-text class="form-content">
<v-form @submit.prevent="simpanLoket" ref="formRef">
<!-- Section: Informasi Loket -->
<div class="form-section">
@@ -139,7 +140,8 @@
</v-form>
</v-card-text>
</v-card>
</v-container>
</v-container>
</div>
</template>
<script setup>
@@ -220,6 +222,7 @@ const cancelAdd = () => {
.form-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.2);
border-radius: 0 !important;
}
.header-content {
+4 -4
View File
@@ -18,7 +18,7 @@
:class="readOnly ? 'bg-blue-darken-3' : isEditMode ? 'bg-orange-darken-2' : 'bg-green-darken-2'"
>
<v-avatar
:color="readOnly ? 'blue-darken-4' : isEditMode ? 'orange-darken-3' : 'green-darken-3'"
color="primary-darken-1"
size="48"
class="mr-3 elevation-2"
>
@@ -1205,15 +1205,15 @@ $font-weight-semibold: 600;
}
.dialog-header.bg-blue-darken-3 {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
background: linear-gradient(135deg, var(--v-theme-primary) 0%, var(--v-theme-primary-darken-1) 100%);
}
.dialog-header.bg-orange-darken-2 {
background: linear-gradient(135deg, #f57c00 0%, #e65100 100%);
background: linear-gradient(135deg, var(--v-theme-primary) 0%, var(--v-theme-primary-darken-1) 100%);
}
.dialog-header.bg-green-darken-2 {
background: linear-gradient(135deg, #388e3c 0%, #2e7d32 100%);
background: linear-gradient(135deg, var(--v-theme-primary) 0%, var(--v-theme-primary-darken-1) 100%);
}
// ============================================
+5 -5
View File
@@ -7,15 +7,15 @@ export const useAnjunganStore = defineStore('anjungan', () => {
const anjunganItems = ref([
{
id: 1,
namaAnjungan: 'Anjungan',
namaAnjungan: 'Anjungan Reguler',
jenisPasien: 'Reguler',
klinik: ['AN', 'AS', 'BD', 'GR', 'HO','GI', 'GZ','IP', 'JT', 'JW', 'KK', 'MT', 'OB', 'PR', 'RT', 'RM', 'SR'],
klinik: ['AK', 'AN', 'BD', 'GR', 'GM', 'GZ', 'HO', 'IP', 'JT', 'JW', 'KD', 'KK', 'KM', 'KO', 'MC', 'MT', 'ON', 'PR', 'RD', 'RM', 'RT', 'SR', 'TH'],
},
{
id: 2,
namaAnjungan: 'Anjungan BPJS',
jenisPasien: 'BPJS',
klinik: ['AN', 'AS', 'OB', 'PR', 'RT', 'RM', 'SR'],
namaAnjungan: 'Anjungan Eksekutif',
jenisPasien: 'Eksekutif',
klinik: ['AK', 'AN', 'BD', 'GM', 'GR', 'GZ', 'HO', 'IP', 'JT', 'JW', 'KD', 'KK', 'KM', 'KO', 'MC', 'MT', 'ON', 'PR', 'RD', 'RM', 'RT', 'SR', 'TH'],
},
]);
+723 -299
View File
File diff suppressed because it is too large Load diff
+27 -1
View File
@@ -22,7 +22,23 @@ export const useMasterStore = defineStore('master', () => {
// Ini untuk backward compatibility dengan pages yang masih menggunakan masterStore.klinikData
// Format: { id, no, kode, nama, shift, totalQuota, jamShiftList, autoShift, jadwalKlinik }
const klinikData = computed(() => {
return clinicStore.clinics.map((clinic, index) => {
console.log('📊 masterStore.klinikData computed called');
console.log('📊 clinicStore.clinics.length:', clinicStore.clinics.length);
// Sort by kode first, then by jenisLayanan (Reguler before Eksekutif)
const sortedClinics = [...clinicStore.clinics].sort((a, b) => {
// First sort by kode
if (a.kode < b.kode) return -1;
if (a.kode > b.kode) return 1;
// If same kode, sort by jenisLayanan (Reguler before Eksekutif)
if (a.jenisLayanan === 'Reguler' && b.jenisLayanan === 'Eksekutif') return -1;
if (a.jenisLayanan === 'Eksekutif' && b.jenisLayanan === 'Reguler') return 1;
// Finally sort by ID if both are same
return a.id - b.id;
});
console.log('📊 sortedClinics.length:', sortedClinics.length);
const result = sortedClinics.map((clinic, index) => {
// Convert clinic data ke format masterStore (untuk backward compatibility)
return {
id: clinic.id,
@@ -32,10 +48,20 @@ export const useMasterStore = defineStore('master', () => {
shift: typeof clinic.shift === 'string' ? parseInt(clinic.shift.replace(/[^0-9]/g, '')) || 1 : clinic.shift || 1,
totalQuota: clinic.totalQuota || 0,
jamShiftList: clinic.jamShiftList || [],
jamShiftPerHari: clinic.jamShiftPerHari || {}, // New: shift configuration per day
autoShift: clinic.autoShift || false,
jadwalKlinik: clinic.jadwalKlinik || [],
tanggalTutup: clinic.tanggalTutup || [], // New: array of closed dates
jenisLayanan: clinic.jenisLayanan || 'Reguler',
};
});
console.log('📊 masterStore.klinikData result:', result.length, 'items');
if (result.length > 0) {
console.log('📋 First item:', result[0]);
}
return result;
});
// Computed - Get klinik list for dropdowns