update sidebar dan menu layanan penunjang

This commit is contained in:
bagus-arie05
2025-10-14 08:49:54 +07:00
parent 51809468a9
commit 31e18c5965
16 changed files with 3253 additions and 1362 deletions

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
<template>
<v-menu
v-model="menu"
:close-on-content-click="false"
location="top end"
offset="8"
origin="bottom right"
transition="slide-y-transition"
>
<template v-slot:activator="{ props: menuProps }">
<div
v-bind="menuProps"
class="d-flex align-center cursor-pointer pa-2 rounded-lg hover-bg"
>
<v-avatar size="40">
<v-img
:src="user.picture"
:alt="`${user.name} Profile`"
></v-img>
<v-badge
dot
color="orange"
location="bottom right"
offset-x="2"
offset-y="2"
></v-badge>
</v-avatar>
<div v-show="!rail" class="ml-3 flex-grow-1">
<div class="text-subtitle-2 font-weight-bold">
{{ user.name }}
</div>
<div class="text-caption text-grey">
{{ user.email }}
</div>
</div>
<v-btn v-show="!rail" icon size="small" variant="text">
<v-icon size="20">mdi-dots-vertical</v-icon>
</v-btn>
</div>
</template>
<v-card width="300" elevation="8" rounded="lg">
<v-card-text class="pa-4">
<div class="d-flex align-center mb-4">
<v-avatar size="48">
<v-img :src="user.picture"></v-img>
</v-avatar>
<div class="ml-3">
<div class="text-subtitle-1 font-weight-bold">
{{ user.name }}
</div>
<div class="text-caption text-grey">
{{ user.email }}
</div>
</div>
</div>
<v-list density="compact" class="pa-0">
<v-list-item
prepend-icon="mdi-cog-outline"
class="px-2 rounded-lg"
link
@click="handleAction('setting')"
>
<v-list-item-title class="text-body-2">Setting</v-list-item-title>
</v-list-item>
<v-list-item
prepend-icon="mdi-help-circle-outline"
class="px-2 rounded-lg"
link
@click="handleAction('help')"
>
<v-list-item-title class="text-body-2">Bantuan</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
prepend-icon="mdi-logout"
class="px-2 rounded-lg text-red"
link
@click="signOut"
:disabled="isLoggingOut"
>
<v-list-item-title class="text-body-2">
{{ isLoggingOut ? 'Log out...' : 'Log out' }}
</v-list-item-title>
</v-list-item>
</v-list>
<div class="text-caption text-grey mt-3 text-center">
v2.5.18 · Terms & Conditions
</div>
</v-card-text>
</v-card>
</v-menu>
</template>
<script setup>
import { ref } from 'vue';
// --- PROPS & EMITS ---
const props = defineProps({
user: {
type: Object,
// Diubah menjadi required: false dengan default yang aman
required: false,
default: () => ({
name: 'Guest',
email: '[email protected]',
picture: 'https://i.pravatar.cc/150?img=33'
})
},
rail: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['logout']); // Hanya emit logout
// --- STATE ---
const menu = ref(false);
const isLoggingOut = ref(false);
// --- METHODS ---
const signOut = () => {
if (isLoggingOut.value) return;
isLoggingOut.value = true;
menu.value = false;
// Memicu event untuk ditangani oleh komponen induk (SideBar)
emit('logout');
// Biarkan parent yang mengurus redirect/state global
setTimeout(() => isLoggingOut.value = false, 1000);
};
const handleAction = (action) => {
console.log(`[PopupSidebar] Action: ${action} triggered.`);
// TODO: Tambahkan logika navigasi/fungsi di sini, misal: router.push('/settings')
menu.value = false;
};
</script>
<style scoped>
/* Tambahkan kembali style yang relevan untuk aktivator di sini */
.hover-bg:hover {
background-color: #f5f5f5;
}
.cursor-pointer {
cursor: pointer;
}
.text-red {
color: rgb(244, 67, 54) !important;
}
</style>
+232 -75
View File
@@ -1,103 +1,260 @@
<!-- components/sideBar.vue -->
<template>
<v-navigation-drawer
:model-value="drawer"
v-model="drawer"
:rail="rail"
permanent
app
@update:model-value="emit('update:drawer', $event)"
color="white"
width="280"
rail-width="72"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<v-list density="compact" nav>
<template v-for="item in items" :key="item.name">
<div class="pa-4 d-flex align-center" style="height: 64px">
<v-icon color="#ff9248" size="32">mdi-hospital-building</v-icon>
<v-toolbar-title v-show="!rail" class="ml-3 text-h6">
Antrean RSSA
</v-toolbar-title>
</div>
<v-divider></v-divider>
<v-list density="default" nav class="py-2">
<!-- search bar
<v-list-item
prepend-icon="mdi-magnify"
:title="!rail ? 'Search' : ''"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item> -->
<template v-for="item in navItemsStore.getNavItems" :key="item.id">
<v-menu
v-if="item.children"
open-on-hover
:location="rail ? 'end' : undefined"
:offset="10"
>
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.name"
:value="item.name"
:to="!rail ? item.path : undefined"
:link="!rail"
></v-list-item>
</template>
<v-card class="py-2" min-width="200">
<v-card-title class="text-subtitle-1 font-weight-bold px-4 py-2">
{{ item.name }}
</v-card-title>
<v-divider></v-divider>
<v-list density="compact" nav>
<v-list-item
v-for="child in item.children"
:key="child.name"
:to="child.path"
:title="child.name"
:prepend-icon="child.icon"
link
class="px-4"
></v-list-item>
</v-list>
</v-card>
</v-menu>
<v-tooltip
v-else
:disabled="!rail"
v-if="item.children && item.children.length > 0"
open-on-hover
location="end"
:text="item.name"
:disabled="!rail"
>
<template #activator="{ props }">
<template v-slot:activator="{ props }">
<v-list-group v-if="!rail" :value="item.name">
<template v-slot:activator="{ props: listGroupProps }">
<v-list-item
v-bind="listGroupProps"
:prepend-icon="item.icon"
:title="item.name"
:active="item.name === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<v-list-item
v-for="child in item.children"
:key="child.id"
:to="child.path"
:title="child.name"
:active="child.path === currentRoute.path"
rounded="lg"
class="mx-2 my-1 pl-12"
>
</v-list-item>
</v-list-group>
<v-list-item
v-else
v-bind="props"
:prepend-icon="item.icon"
:title="item.name"
:to="item.path"
link
></v-list-item>
:active="item.name === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
</v-tooltip>
<v-list class="py-2" style="min-width: 200px">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ item.name }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.id"
:to="child.path"
:title="child.name"
:active="child.path === currentRoute.path"
rounded="lg"
class="mx-2"
>
</v-list-item>
</v-list>
</v-menu>
<v-list-item
v-else
:prepend-icon="item.icon"
:title="!rail ? item.name : ''"
:to="item.path"
:active="item.path === currentRoute.path"
rounded="lg"
class="mx-2 my-1"
>
<template v-slot:append v-if="item.badge && !rail">
<v-chip size="x-small" color="error" variant="flat">{{
item.badge
}}</v-chip>
</template>
</v-list-item>
</template>
<v-divider class="my-4 mx-2"></v-divider>
</v-list>
<v-spacer></v-spacer>
<template v-slot:append>
<div class="pa-3">
<ProfileMenu
:user="user.value" :rail="rail"
@logout="handleLogout"
class="full-width-on-expand"
/>
</div>
</template>
</v-navigation-drawer>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue';
<script setup>
import { ref, computed } from "vue";
import { useRoute } from "vue-router";
// Import store Pinia
import { useNavItemsStore } from "@/stores/navItems"; // Sesuaikan path ini jika perlu
import ProfileMenu from "./ProfileMenu.vue";
interface NavItem {
id: number;
name: string;
path: string;
icon: string;
children?: NavItem[];
}
// State
const drawer = ref(true);
const rail = ref(true);
const hoverTimeout = ref(null);
const props = defineProps({
items: {
type: Array as () => NavItem[],
required: true,
},
rail: {
type: Boolean,
required: true,
},
drawer: {
type: Boolean,
required: true,
},
// Akses Pinia Store
const user = ref({
name: 'Adam Sulfat',
email: '[email protected]',
picture: 'https://i.pravatar.cc/150?img=33', // Ganti dengan URL gambar dinamis
id: 'a1b2c3d4e5f6g7h8'
});
const navItemsStore = useNavItemsStore();
const currentRoute = useRoute();
const currentActiveMenu = computed(() => {
// Menggunakan Getter untuk mendapatkan array menu yang pasti valid
const items = navItemsStore.getNavItems;
// Pengecekan keamanan (walaupun Getter seharusnya sudah menjamin array)
if (!Array.isArray(items) || items.length === 0) {
return "";
}
// Cari item utama yang sedang aktif
const currentItem = items.find((item) => item.path === currentRoute.path);
if (currentItem) {
return currentItem.name;
}
// Cari item anak (children) yang sedang aktif
for (const item of items) {
if (item.children && Array.isArray(item.children)) {
const childItem = item.children.find(
(child) => child.path === currentRoute.path
);
if (childItem) {
return item.name;
}
}
}
return "";
});
const emit = defineEmits(['update:drawer']);
// Hover handlers
const handleMouseEnter = () => {
if (hoverTimeout.value) {
clearTimeout(hoverTimeout.value);
}
hoverTimeout.value = setTimeout(() => {
rail.value = false;
}, 100);
};
const handleMouseLeave = () => {
if (hoverTimeout.value) {
clearTimeout(hoverTimeout.value);
}
hoverTimeout.value = setTimeout(() => {
rail.value = true;
}, 200);
};
const handleLogout = () => {
console.log("Logging out...");
// Tambahkan logika logout di sini
};
</script>
<style scoped>
.v-navigation-drawer__content {
background-color: #ffffff;
.v-list-item--active {
background-color: #e3f2fd !important;
color: #1976d2 !important;
}
.v-list-item--active :deep(.v-list-item__prepend) {
color: #1976d2 !important;
}
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
.v-navigation-drawer {
border-right: 1px solid #e0e0e0 !important;
transition: width 0.2s ease-in-out !important;
}
.hover-bg:hover {
background-color: #f5f5f5;
}
.cursor-pointer {
cursor: pointer;
}
/* Custom scrollbar */
:deep(.v-navigation-drawer__content) {
overflow-y: auto;
overflow-x: hidden;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar {
width: 6px;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-track {
background: transparent;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
:deep(.v-navigation-drawer__content)::-webkit-scrollbar-thumb:hover {
background: #999;
}
</style>
+5 -267
View File
@@ -1,206 +1,20 @@
<template>
<v-app>
<!-- Navigation Drawer -->
<v-navigation-drawer
v-model="drawer"
:rail="rail"
permanent
app
color="white"
width="280"
rail-width="72"
>
<!-- Header Sidebar -->
<div class="pa-4 d-flex align-center" style="height: 64px;">
<v-icon color="#ff9248" size="32">mdi-hospital-building</v-icon>
<v-toolbar-title v-show="!rail" class="ml-3 text-h6">
Antrean RSSA
</v-toolbar-title>
</div>
<Sidebar />
<v-divider></v-divider>
<!-- Menu Items -->
<v-list density="default" nav class="py-2">
<v-list-subheader v-show="!rail" class="text-caption text-grey">
OVERVIEW
</v-list-subheader>
<template v-for="item in items" :key="item.title">
<!-- Item dengan Children -->
<v-menu
v-if="item.children"
open-on-hover
location="end"
:disabled="!rail"
>
<template v-slot:activator="{ props }">
<v-list-group v-if="!rail" :value="item.title">
<template v-slot:activator="{ props }">
<v-list-item
v-bind="props"
:prepend-icon="item.icon"
:title="item.title"
:active="item.title === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<v-list-item
v-for="child in item.children"
:key="child.title"
:to="child.to"
:title="child.title"
:active="child.to === currentRoute.path"
rounded="lg"
class="mx-2 my-1 pl-12"
>
</v-list-item>
</v-list-group>
<!-- Rail mode menu -->
<v-list-item
v-else
v-bind="props"
:prepend-icon="item.icon"
:active="item.title === currentActiveMenu"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</template>
<!-- Submenu untuk rail mode -->
<v-list class="py-2" style="min-width: 200px;">
<v-list-item>
<v-list-item-title class="font-weight-bold">
{{ item.title }}
</v-list-item-title>
</v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
v-for="child in item.children"
:key="child.title"
:to="child.to"
:title="child.title"
:active="child.to === currentRoute.path"
rounded="lg"
class="mx-2"
>
</v-list-item>
</v-list>
</v-menu>
<!-- Item tanpa Children -->
<v-list-item
v-else
:prepend-icon="item.icon"
:title="!rail ? item.title : ''"
:to="item.to"
:active="item.to === currentRoute.path"
rounded="lg"
class="mx-2 my-1"
>
<template v-slot:append v-if="item.badge && !rail">
<v-chip size="x-small" color="primary">{{ item.badge }}</v-chip>
</template>
</v-list-item>
</template>
<v-divider class="my-4"></v-divider>
<v-list-subheader v-show="!rail" class="text-caption text-grey">
ACCOUNT
</v-list-subheader>
<v-list-item
prepend-icon="mdi-cog-outline"
:title="!rail ? 'Settings' : ''"
to="/settings"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
<v-list-item
prepend-icon="mdi-logout"
:title="!rail ? 'Log out' : ''"
rounded="lg"
class="mx-2 my-1"
>
</v-list-item>
</v-list>
<template v-slot:append>
<!-- Theme Toggle -->
<div class="pa-4 d-flex justify-center align-center">
<v-icon color="primary" size="20">mdi-white-balance-sunny</v-icon>
<v-switch
v-show="!rail"
v-model="darkMode"
hide-details
density="compact"
color="primary"
class="mx-2"
></v-switch>
<v-icon v-show="!rail" size="20">mdi-moon-waning-crescent</v-icon>
</div>
<v-divider></v-divider>
<!-- User Profile -->
<div class="pa-4">
<div class="d-flex align-center">
<v-avatar color="#ff9248" size="40">
<span class="text-white">AS</span>
</v-avatar>
<div v-show="!rail" class="ml-3 flex-grow-1">
<div class="text-subtitle-2 font-weight-bold">Adam Sulfat</div>
<div class="text-caption text-grey">adam@rssa.com</div>
</div>
<v-btn
v-show="!rail"
icon
size="small"
variant="text"
>
<v-icon size="20">mdi-dots-vertical</v-icon>
</v-btn>
</div>
</div>
</template>
</v-navigation-drawer>
<!-- Main Content -->
<v-main>
<!-- Top Bar untuk Toggle -->
<v-app-bar flat color="transparent" height="64">
<v-btn
icon
@click="rail = !rail"
class="ml-2"
>
<v-icon>mdi-menu</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>mdi-bell-outline</v-icon>
</v-btn>
</v-app-bar>
<v-container fluid class="pa-6">
<v-container fluid class="bg-grey-lighten-4 pa-6">
<slot></slot>
</v-container>
</v-main>
<!-- Footer -->
<v-footer app class="bg-grey-lighten-4">
<v-container fluid class="py-2">
<v-row no-gutters align="center">
<v-col cols="12" md="6">
<span class="text-caption text-grey-darken-2">
RSUD Dr. Saiful Anwar Malang | Jl. Jaksa Agung Suprapto No. 2 Malang | Telp: 0341-362101
RSUD Dr. Saiful Anwar Malang | Jl. Jaksa Agung Suprapto No. 2
Malang | Telp: 0341-362101
</span>
</v-col>
<v-col cols="12" md="6" class="text-right">
@@ -215,85 +29,9 @@
</template>
<script setup>
import { ref, computed } from "vue";
import { useRoute } from 'vue-router';
// State
const drawer = ref(true);
const rail = ref(false);
const darkMode = ref(false);
// Menu Items
const items = ref([
{ title: "Dashboard", icon: "mdi-view-dashboard-outline", to: "/dashboard" },
// { title: "Marketplace", icon: "mdi-storefront-outline", to: "/marketplace" },
// { title: "My Properties", icon: "mdi-home-outline", to: "/properties" },
// { title: "Auctions", icon: "mdi-gavel", to: "/auctions" },
// { title: "Wallet", icon: "mdi-wallet-outline", to: "/wallet" },
// { title: "Favorites", icon: "mdi-heart-outline", to: "/favorites" },
{
title: "Setting",
icon: "mdi-cog-outline",
children: [
{ title: "Hak Akses", to: "/setting/hak-akses" },
{ title: "User Login", to: "/setting/user-login" },
{ title: "Master Loket", to: "/setting/master-loket" },
{ title: "Master Klinik", to: "/setting/master-klinik" },
{ title: "Master Klinik Ruang", to: "/setting/master-klinik-ruang" },
{ title: "Screen", to: "/setting/screen" },
],
},
{ title: "Loket Admin", icon: "mdi-account-supervisor-outline", to: "/loket-admin" },
{ title: "Ranap Admin", icon: "mdi-bed-outline", to: "/ranap-admin" },
{
title: "Anjungan",
icon: "mdi-account-box-multiple-outline",
children: [
{ title: "Anjungan", to: "/anjungan/anjungan" },
{ title: "Klinik Ruang", to: "/anjungan/AntrianKlinik"},
],
},
{ title: "Data Pasien", icon: "mdi-account-multiple-outline", to: "/data-pasien" },
]);
const currentRoute = useRoute();
const currentActiveMenu = computed(() => {
const currentItem = items.value.find(item => item.to === currentRoute.path);
if (currentItem) {
return currentItem.title;
}
for (const item of items.value) {
if (item.children) {
const childItem = item.children.find(child => child.to === currentRoute.path);
if (childItem) {
return item.title;
}
}
}
return '';
});
import Sidebar from "@/components/Sidebar.vue";
</script>
<style scoped>
.v-list-item--active {
background-color: #e8f5e9 !important;
color: #2e7d32 !important;
}
.v-list-item--active :deep(.v-list-item__prepend) {
color: #2e7d32 !important;
}
.v-list-item {
transition: all 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
.v-navigation-drawer {
border-right: 1px solid #e0e0e0 !important;
}
</style>
+218 -139
View File
@@ -23,12 +23,11 @@
</div>
</div>
</div>
<div>
<v-card class="next-patient-card mb-4" elevation="2">
<!-- <v-card class="next-patient-card mb-4" elevation="2">
<v-card-text class="text-center pa-6">
<v-chip color="success" variant="flat" class="mb-3" size="large">
<v-icon start>mdi-account-arrow-right</v-icon>
PASIEN SELANJUTNYA
PASIEN ANTREAN ANJUNGAN
</v-chip>
<div class="text-h4 font-weight-bold mb-2 text-success">
{{ nextPatient ? nextPatient.noAntrian.split(" |")[0] : "UM1004" }}
@@ -48,11 +47,10 @@
PANGGIL NEXT
</v-btn>
</v-card-text>
</v-card>
</div>
</v-card> -->
<!-- Combined Control Section -->
<v-row class="mb-4">
<!-- Left Side: Call Controls and Current Processing -->
<!-- Left Side: Current Processing -->
<v-col cols="12" lg="6">
<!-- Current Patient Processing Card -->
<v-card
@@ -75,115 +73,134 @@
{{ currentProcessingPatient.klinik }}
</div>
</div>
<div class="action-buttons">
<v-btn
color="success"
variant="flat"
class="mr-2"
@click="processPatient(currentProcessingPatient, 'check-in')"
>
<v-icon start>mdi-check-circle</v-icon>
Check In
</v-btn>
<v-btn
color="warning"
variant="flat"
class="mr-2"
@click="processPatient(currentProcessingPatient, 'terlambat')"
>
<v-icon start>mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn
color="error"
variant="flat"
@click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Pending
</v-btn>
</div>
<div class="action-buttons d-flex flex-column">
<v-btn
color="success"
variant="flat"
class="mb-2" width="120" @click="processPatient(currentProcessingPatient, 'check-in')"
>
<v-icon start>mdi-check-circle</v-icon>
Check In
</v-btn>
<v-btn
color="warning"
variant="flat"
class="mb-2" width="120" @click="processPatient(currentProcessingPatient, 'terlambat')"
>
<v-icon start>mdi-clock-alert</v-icon>
Terlambat
</v-btn>
<v-btn
color="error"
variant="flat"
class="mb-2" width="120" @click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Pending
</v-btn>
<v-btn
color="error"
variant="flat"
width="120" @click="processPatient(currentProcessingPatient, 'pending')"
>
<v-icon start>mdi-pause-circle</v-icon>
Ubah Klinik
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
</v-col>
<!-- Right Side: Patient Queue Info -->
<!-- Right Side: Queue Info and Call Controls -->
<v-col cols="12" lg="6">
<!-- Next Patient Card -->
<!-- Quota Info Card -->
<v-card class="quota-info-card" elevation="2">
<!-- Quota Info and Call Controls Combined Card -->
<v-card class="quota-call-card" elevation="2">
<v-card-text class="pa-4">
<div class="text-center">
<div class="text-h6 font-weight-medium mb-3">
Panggil Antrean Anjungan
</div>
<v-row class="mb-3">
<v-col cols="6" class="text-center">
<div class="text-caption text-grey-darken-1">Kuota</div>
<div class="text-h4 font-weight-bold">150</div>
</v-col>
<v-col cols="6" class="text-center">
<div class="text-caption text-grey-darken-1">Tersedia</div>
<div class="text-h4 font-weight-bold text-success">
{{ 150 - quotaUsed }}
</div>
</v-col>
</v-row>
<div class="text-body-2 text-grey-darken-1 mb-2">
<!-- Header with icon -->
<div class="d-flex align-center justify-center mb-3">
<v-chip
color="success"
variant="flat"
size="large"
>
<v-icon start>mdi-account-arrow-right</v-icon>
PANGGIL ANTREAN ANJUNGAN
</v-chip>
</div>
<!-- Quota Display -->
<v-row class="mb-3">
<v-col cols="6" class="text-center">
<div class="quota-label">Kuota</div>
<div class="quota-number">150</div>
</v-col>
<v-col cols="6" class="text-center">
<div class="quota-label">Tersedia</div>
<div class="quota-number quota-available">
{{ 150 - quotaUsed }}
</div>
</v-col>
</v-row>
<!-- Progress Bar -->
<div class="text-center mb-3">
<div class="quota-used-text">
Total Quota Terpakai: {{ quotaUsed }}
</div>
<v-progress-linear
:model-value="(quotaUsed / 150) * 100"
color="success"
height="8"
height="6"
rounded
class="mt-2"
></v-progress-linear>
</div>
<!-- Divider -->
<v-divider class="my-3"></v-divider>
<!-- Call Controls -->
<div class="call-controls">
<div class="call-label mb-2">Kumulatif Panggil Antrean</div>
<div class="call-buttons">
<v-btn
color="success"
variant="flat"
class="call-btn"
@click="callMultiplePatients(1)"
:disabled="!nextPatient"
>
<span class="call-btn-text">1</span>
</v-btn>
<v-btn
color="info"
variant="flat"
class="call-btn"
@click="callMultiplePatients(5)"
>
<span class="call-btn-text">5</span>
</v-btn>
<v-btn
color="warning"
variant="flat"
class="call-btn"
@click="callMultiplePatients(10)"
>
<span class="call-btn-text">10</span>
</v-btn>
<v-btn
color="error"
variant="flat"
class="call-btn"
@click="callMultiplePatients(20)"
>
<span class="call-btn-text">20</span>
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
<v-card-text class="call-controls-card align-center py-4">
<div class="text-subtitle-1 font-weight-medium"
>Panggil Pasien:</div>
<div class="d-flex align-center flex-wrap mb-3">
<v-btn
color="success"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(1)"
>
<span class="text-h6 font-weight-bold">1</span>
</v-btn>
<v-btn
color="info"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(5)"
>
<span class="text-h6 font-weight-bold">5</span>
</v-btn>
<v-btn
color="warning"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(10)"
>
<span class="text-h6 font-weight-bold">10</span>
</v-btn>
<v-btn
color="error"
variant="flat"
size="large"
class="px-6 ma-4"
@click="callMultiplePatients(20)"
>
<span class="text-h6 font-weight-bold">20</span>
</v-btn>
</div>
</v-card-text>
</v-col>
</v-row>
@@ -312,7 +329,7 @@ const allPatients = ref([
klinik: "KANDUNGAN",
fastTrack: "UMUM",
pembayaran: "UMUM",
status: "waiting", // waiting, di-loket, terlambat, pending, processed
status: "waiting",
},
{
no: 2,
@@ -349,9 +366,15 @@ const allPatients = ref([
},
...Array.from({ length: 16 }, (_, i) => ({
no: i + 5,
jamPanggil: `${String(Math.floor(Math.random() * 12) + 1).padStart(2, "0")}:${String(Math.floor(Math.random() * 60)).padStart(2, "0")}`,
jamPanggil: `${String(Math.floor(Math.random() * 12) + 1).padStart(
2,
"0"
)}:${String(Math.floor(Math.random() * 60)).padStart(2, "0")}`,
barcode: `25081110${String(i + 300).padStart(4, "0")}`,
noAntrian: `UM100${i + 5} | Online - 25081110${String(i + 300).padStart(4, "0")}`,
noAntrian: `UM100${i + 5} | Online - 25081110${String(i + 300).padStart(
4,
"0"
)}`,
shift: "Shift 1",
klinik: ["KANDUNGAN", "IPD", "THT", "SARAF"][Math.floor(Math.random() * 4)],
fastTrack: "UMUM",
@@ -430,13 +453,11 @@ const callMultiplePatients = (count) => {
return;
}
// Check quota
if (quotaUsed.value + patientsToCall.length > 150) {
showSnackbar("Quota tidak mencukupi", "error");
return;
}
// Move patients to "di-loket" status
patientsToCall.forEach((patient) => {
patient.status = "di-loket";
});
@@ -456,7 +477,6 @@ const callNext = () => {
return;
}
// Move next patient to processing
nextPatient.value.status = "di-loket";
currentProcessingPatient.value = nextPatient.value;
quotaUsed.value++;
@@ -570,10 +590,8 @@ const getRowClass = (item) => {
align-items: center;
}
.call-controls-card,
.next-patient-card,
.current-processing-card,
.quota-info-card,
.quota-call-card,
.main-table-card,
.late-table-card,
.pending-table-card {
@@ -581,18 +599,13 @@ const getRowClass = (item) => {
border: 1px solid rgba(0, 0, 0, 0.05);
}
.next-patient-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border: 2px solid rgba(76, 175, 80, 0.2);
}
.current-processing-card {
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
border: 2px solid rgba(255, 152, 0, 0.2);
}
.quota-info-card {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
.quota-call-card {
background: linear-gradient(135deg, #e3f2fd 0%, #f5f5f5 100%);
border: 2px solid rgba(33, 150, 243, 0.2);
}
@@ -605,6 +618,61 @@ const getRowClass = (item) => {
align-items: center;
}
/* Quota Display Styling */
.quota-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
margin-bottom: 4px;
}
.quota-number {
font-size: 32px;
font-weight: 700;
color: #1f2937;
line-height: 1;
}
.quota-available {
color: #4caf50;
}
.quota-used-text {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
/* Call Controls Styling */
.call-controls {
text-align: center;
}
.call-label {
font-size: 14px;
font-weight: 600;
color: #374151;
text-align: center;
}
.call-buttons {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.call-btn {
min-width: 80px;
height: 48px;
border-radius: 8px;
}
.call-btn-text {
font-size: 18px;
font-weight: 700;
}
/* Enhanced table styling */
.main-table-card :deep(.v-data-table th),
.late-table-card :deep(.v-data-table th),
@@ -679,9 +747,13 @@ const getRowClass = (item) => {
min-width: 120px;
}
/* Stack layout vertically on medium screens */
.call-controls-card .d-flex {
justify-content: center;
.call-buttons {
gap: 8px;
}
.call-btn {
min-width: 70px;
height: 44px;
}
}
@@ -702,25 +774,19 @@ const getRowClass = (item) => {
padding: 12px;
}
.call-controls-card .d-flex {
flex-direction: column;
align-items: center;
gap: 16px;
.call-buttons {
flex-direction: row;
gap: 8px;
}
.call-controls-card .v-btn {
min-width: 80px;
.call-btn {
flex: 1;
min-width: 60px;
height: 42px;
}
/* Stack all control buttons vertically on mobile */
.call-controls-card .d-flex.flex-wrap {
flex-direction: column;
align-items: stretch;
}
.call-controls-card .v-btn {
width: 100%;
margin: 4px 0;
.call-btn-text {
font-size: 16px;
}
.action-buttons {
@@ -732,6 +798,10 @@ const getRowClass = (item) => {
width: 100%;
margin: 4px 0;
}
.quota-number {
font-size: 28px;
}
}
@media (max-width: 600px) {
@@ -739,16 +809,25 @@ const getRowClass = (item) => {
font-size: 20px;
}
.next-patient-card .text-h4 {
font-size: 1.75rem !important;
}
.current-processing-card .patient-info .text-h5 {
font-size: 1.25rem !important;
}
.quota-info-card .text-h4 {
font-size: 1.5rem !important;
.quota-number {
font-size: 24px;
}
.call-buttons {
flex-wrap: wrap;
}
.call-btn {
min-width: calc(50% - 8px);
height: 40px;
}
.call-btn-text {
font-size: 14px;
}
}
</style>
</style>
+491
View File
@@ -0,0 +1,491 @@
<template>
<div class="master-klinik-page pa-4">
<!-- Breadcrumbs -->
<v-breadcrumbs :items="breadcrumbs" class="pl-0">
<template v-slot:divider>
<v-icon icon="mdi-chevron-right"></v-icon>
</template>
</v-breadcrumbs>
<!-- Tampilan Formulir Tambah/Edit/View -->
<v-card v-if="showForm" class="pa-4 rounded-lg elevation-2">
<v-card-title class="text-h5 font-weight-bold mb-4">
{{ isEditMode ? 'Edit Klinik' : readOnly ? 'Detail Klinik' : 'Tambah Klinik' }}
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Kode</v-label>
<v-text-field
v-model="editedItem.kode"
placeholder="Masukkan Kode Klinik"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Nama</v-label>
<v-text-field
v-model="editedItem.namaKlinik"
placeholder="Masukkan Nama Klinik"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Shift</v-label>
<v-text-field
v-model="editedItem.shift"
placeholder="Jumlah Shift"
type="number"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Kuota Shift</v-label>
<v-text-field
v-model="editedItem.kuotaShift"
placeholder="Kuota Per Shift"
type="number"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
<v-row class="d-flex align-center">
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Jam Buka Shift</v-label>
<div class="d-flex align-center">
<v-text-field
v-model="editedItem.jamBuka"
placeholder="Jam Buka Shift 1"
variant="outlined"
density="comfortable"
class="mr-2"
:readonly="readOnly"
></v-text-field>
<span class="text-h6 font-weight-bold mr-2">:</span>
<v-text-field
v-model="editedItem.menitBuka"
placeholder="Menit Buka Shift 1"
variant="outlined"
density="comfortable"
:readonly="readOnly"
></v-text-field>
</div>
</v-col>
<v-col cols="12" md="6">
<v-switch
v-model="editedItem.autoShift"
inset
color="primary"
label="Auto Shift"
hide-details
:readonly="readOnly"
></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-label class="font-weight-bold">Kuota Bangku</v-label>
<v-text-field
v-model="editedItem.kuotaBangku"
placeholder="Masukkan Kuota Bangku Klinik"
type="number"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-label class="font-weight-bold">Jadwal Klinik</v-label>
<v-table class="rounded-lg elevation-1 mt-2">
<thead>
<tr>
<th class="text-left text-uppercase font-weight-bold">#</th>
<th class="text-left text-uppercase font-weight-bold">Hari</th>
<th class="text-left text-uppercase font-weight-bold">Pilih</th>
</tr>
</thead>
<tbody>
<tr v-for="(day, index) in days" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ day.name }}</td>
<td>
<v-checkbox
v-model="day.selected"
color="primary"
hide-details
:readonly="readOnly"
></v-checkbox>
</td>
</tr>
</tbody>
</v-table>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="d-flex justify-start pa-4">
<v-btn
color="grey-darken-1"
variant="flat"
class="text-capitalize rounded-lg mr-2"
@click="cancelForm"
>
{{ readOnly ? 'Tutup' : 'Batal' }}
</v-btn>
<v-btn
v-if="!readOnly"
color="orange-darken-2"
variant="flat"
class="text-capitalize rounded-lg"
@click="saveItem"
>
Submit
</v-btn>
</v-card-actions>
</v-card>
<!-- Tampilan Tabel Data -->
<div v-else>
<!-- Banner biru sebagai pengganti header h1 -->
<v-card class="d-flex justify-space-between align-center pa-4 mb-4 rounded-lg bg-blue-darken-2 text-white elevation-2">
<v-card-title class="d-flex align-center text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-hospital" class="mr-2" size="40"></v-icon>
<span>Master Klinik</span>
</v-card-title>
<v-btn
color="success"
prepend-icon="mdi-plus"
rounded
class="text-capitalize"
@click="showForm = true"
>
Tambah Baru
</v-btn>
</v-card>
<v-card class="pa-4 rounded-lg elevation-2">
<v-card-text>
<!-- Table controls -->
<div class="d-flex flex-wrap align-center justify-space-between mb-4">
<div class="d-flex align-center">
<span class="mr-2 text-subtitle-1">Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50]"
variant="outlined"
density="compact"
hide-details
style="max-width: 80px;"
rounded
class="mr-2"
></v-select>
<span class="text-subtitle-1">entries</span>
</div>
<div class="d-flex align-center">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="Search"
variant="outlined"
density="compact"
hide-details
rounded
clearable
></v-text-field>
</div>
</div>
<!-- Data Table dengan paginasi kustom -->
<v-data-table
:headers="headers"
:items="allKlinikData"
:search="search"
:items-per-page="itemsPerPage"
v-model:page="page"
class="rounded-lg elevation-0 custom-table"
>
<!-- Slot untuk aksi di setiap baris -->
<template v-slot:[`item.actions`]="{ item }">
<!-- Tombol "View" yang hanya menampilkan detail -->
<v-btn icon color="blue" size="small" class="mr-2" @click="viewItem(item)">
<v-icon>mdi-eye</v-icon>
</v-btn>
<v-btn icon color="orange" size="small" class="mr-2" @click="editItem(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon color="red" size="small" @click="deleteItem(item)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<template v-slot:no-data>
<v-alert :value="true" color="grey-lighten-3" icon="mdi-information">
Tidak ada data yang tersedia.
</v-alert>
</template>
<!-- Slot kustom untuk footer tabel (paginasi) -->
<template v-slot:bottom>
<v-row class="ma-2">
<v-col cols="12" sm="6" class="d-flex align-center justify-start text-caption text-grey">
{{ showingEntriesText }}
</v-col>
<v-col cols="12" sm="6" class="d-flex align-center justify-end">
<v-pagination
v-model="page"
:length="pageCount"
rounded="circle"
:total-visible="5"
></v-pagination>
</v-col>
</v-row>
</template>
</v-data-table>
</v-card-text>
</v-card>
</div>
</div>
<!-- Delete Dialog -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card class="pa-4 rounded-lg">
<v-card-title class="text-h6 font-weight-bold">Hapus Data</v-card-title>
<v-card-text>Apakah Anda yakin ingin menghapus data ini?</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn color="grey-darken-1" variant="text" @click="closeDeleteDialog">Batal</v-btn>
<v-btn color="red" variant="text" @click="confirmDelete">Hapus</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, computed } from 'vue';
definePageMeta({
middleware:['auth']
})
// State untuk menampilkan/menyembunyikan formulir
const showForm = ref(false);
const readOnly = ref(false); // State untuk mode "view"
// Data dummy untuk Master Klinik
const allKlinikData = ref([
{ id: 1, kode: 'AN', namaKlinik: 'ANAK', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 2, kode: 'AS', namaKlinik: 'ANESTESI', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 3, kode: 'BD', namaKlinik: 'BEDAH', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 4, kode: 'GR', namaKlinik: 'GERIATRI', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 5, kode: 'GI', namaKlinik: 'GIGI DAN MULUT', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 6, kode: 'GZ', namaKlinik: 'GIZI', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 7, kode: 'HO', namaKlinik: 'HOM', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 8, kode: 'IP', namaKlinik: 'IPD', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 9, kode: 'JT', namaKlinik: 'JANTUNG', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 10, kode: 'JW', namaKlinik: 'JIWA', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 11, kode: 'OB', namaKlinik: 'KANDUNGAN', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 12, kode: 'KT', namaKlinik: 'KEMOTERAPI', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 13, kode: 'KN', namaKlinik: 'KOMPLEMENTER', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 14, kode: 'KL', namaKlinik: 'KUL KEL', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 15, kode: 'MT', namaKlinik: 'MATA', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 16, kode: 'MC', namaKlinik: 'MCU', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 17, kode: 'ON', namaKlinik: 'ONKOLOGI', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 18, kode: 'PR', namaKlinik: 'PARU', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 19, kode: 'RT', namaKlinik: 'R. TINDAKAN', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 20, kode: 'RD', namaKlinik: 'RADIOTERAPI', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 21, kode: 'RM', namaKlinik: 'REHAB MEDIK', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 22, kode: 'NU', namaKlinik: 'SARAF', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
{ id: 23, kode: 'TH', namaKlinik: 'THT', shift: 1, kuotaShift: 1000, kuotaBangku: 50, jamBuka: '08', menitBuka: '00', autoShift: true, jadwal: [] },
]);
const headers = ref([
{ title: 'No', key: 'id' },
{ title: 'Kode', key: 'kode', sortable: true },
{ title: 'Nama Klinik', key: 'namaKlinik', sortable: true },
{ title: 'Shift', key: 'shift', sortable: true },
{ title: 'Kuota Shift', key: 'kuotaShift', sortable: true },
{ title: 'Aksi', key: 'actions', sortable: false },
]);
// Breadcrumbs
const breadcrumbs = ref([
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik', disabled: false, href: '/setting/masterklinik' },
]);
const days = ref([
{ name: 'Senin', selected: false },
{ name: 'Selasa', selected: false },
{ name: 'Rabu', selected: false },
{ name: 'Kamis', selected: false },
{ name: 'Jum`at', selected: false },
]);
// State untuk tabel dan paginasi
const itemsPerPage = ref(10);
const search = ref('');
const page = ref(1);
// Computed properties untuk paginasi kustom
const pageCount = computed(() => {
return Math.ceil(allKlinikData.value.length / itemsPerPage.value);
});
const showingEntriesText = computed(() => {
const start = (page.value - 1) * itemsPerPage.value + 1;
const end = Math.min(page.value * itemsPerPage.value, allKlinikData.value.length);
const total = allKlinikData.value.length;
return `Showing ${start} to ${end} of ${total} entries`;
});
// State untuk dialog
const showDeleteDialog = ref(false);
const isEditMode = ref(false);
const editedIndex = ref(-1);
const editedItem = ref({
id: 0,
kode: '',
namaKlinik: '',
shift: 1,
kuotaShift: 0,
kuotaBangku: 0,
jamBuka: '',
menitBuka: '',
autoShift: false,
});
// Fungsi untuk tombol aksi
// Fungsi untuk tombol View yang hanya menampilkan data tanpa bisa diedit
const viewItem = (item) => {
editedItem.value = { ...item };
readOnly.value = true;
isEditMode.value = false;
showForm.value = true;
// Update breadcrumbs untuk mode view
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik', disabled: false, href: '/setting/masterklinik' },
{ title: 'Detail Klinik', disabled: true, href: '/setting/viewklinik' },
];
};
const editItem = (item) => {
editedIndex.value = allKlinikData.value.findIndex(d => d.id === item.id);
editedItem.value = { ...item };
isEditMode.value = true;
readOnly.value = false;
showForm.value = true;
// Update breadcrumbs untuk mode edit
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik', disabled: false, href: '/setting/masterklinik' },
{ title: 'Edit Klinik', disabled: true, href: '/setting/editklinik' },
];
};
const deleteItem = (item) => {
editedIndex.value = allKlinikData.value.findIndex(d => d.id === item.id);
showDeleteDialog.value = true;
};
const confirmDelete = () => {
if (editedIndex.value > -1) {
allKlinikData.value.splice(editedIndex.value, 1);
}
closeDeleteDialog();
};
const cancelForm = () => {
showForm.value = false;
isEditMode.value = false;
readOnly.value = false;
editedItem.value = {
id: 0,
kode: '',
namaKlinik: '',
shift: 1,
kuotaShift: 0,
kuotaBangku: 0,
jamBuka: '',
menitBuka: '',
autoShift: false,
};
editedIndex.value = -1;
// Reset breadcrumbs ke mode tabel
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik', disabled: false, href: '/setting/masterklinik' },
];
};
const closeDeleteDialog = () => {
showDeleteDialog.value = false;
editedIndex.value = -1;
};
const saveItem = () => {
if (isEditMode.value) {
Object.assign(allKlinikData.value[editedIndex.value], editedItem.value);
} else {
// Generate new ID
const newId = allKlinikData.value.length > 0 ? Math.max(...allKlinikData.value.map(item => item.id)) + 1 : 1;
editedItem.value.id = newId;
allKlinikData.value.push(editedItem.value);
}
cancelForm(); // Kembali ke tampilan tabel setelah menyimpan
};
</script>
<style scoped>
.master-klinik-page {
font-family: 'Roboto', sans-serif;
background-color: #f5f7fa;
min-height: 100vh;
}
.custom-table {
border: none;
}
.v-data-table :deep(th) {
font-weight: bold !important;
color: #333 !important;
}
.v-data-table :deep(td) {
vertical-align: middle;
}
.v-btn--icon {
border-radius: 8px;
}
.v-select :deep(.v-field__input) {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
</style>
+376
View File
@@ -0,0 +1,376 @@
<template>
<div class="master-klinik-ruang-page pa-4">
<!-- Breadcrumbs -->
<v-breadcrumbs :items="breadcrumbs" class="pl-0">
<template v-slot:divider>
<v-icon icon="mdi-chevron-right"></v-icon>
</template>
</v-breadcrumbs>
<!-- Tampilan Formulir Tambah/Edit/View -->
<v-card v-if="showForm" class="pa-4 rounded-lg elevation-2">
<v-card-title class="text-h5 font-weight-bold mb-4">
{{ isEditMode ? 'Edit Ruang Klinik' : readOnly ? 'Detail Ruang Klinik' : 'Tambah Ruang Klinik' }}
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Nama Klinik</v-label>
<v-select
v-model="editedItem.namaKlinik"
:items="['ANAK', 'ANESTESI', 'BEDAH', 'GERIATRI', 'GIGI DAN MULUT', 'GIZI', 'HOM', 'IPD', 'JANTUNG', 'JIWA', 'KANDUNGAN', 'KEMOTERAPI', 'KOMPLEMENTER', 'KUL KEL', 'MATA', 'MCU', 'ONKOLOGI', 'PARU', 'R. TINDAKAN', 'RADIOTERAPI', 'REHAB MEDIK', 'SARAF', 'THT']"
placeholder="Pilih Nama Klinik"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Kode Ruang</v-label>
<v-text-field
v-model="editedItem.kode"
placeholder="Masukkan Kode Ruang"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-label class="font-weight-bold">Nama Ruang</v-label>
<v-text-field
v-model="editedItem.namaRuang"
placeholder="Masukkan Nama Ruang"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="d-flex justify-start pa-4">
<v-btn
color="grey-darken-1"
variant="flat"
class="text-capitalize rounded-lg mr-2"
@click="cancelForm"
>
{{ readOnly ? 'Tutup' : 'Batal' }}
</v-btn>
<v-btn
v-if="!readOnly"
color="orange-darken-2"
variant="flat"
class="text-capitalize rounded-lg"
@click="saveItem"
>
Submit
</v-btn>
</v-card-actions>
</v-card>
<!-- Tampilan Tabel Data -->
<div v-else>
<!-- Banner biru sebagai pengganti header h1 -->
<v-card class="d-flex justify-space-between align-center pa-4 mb-4 rounded-lg bg-blue-darken-2 text-white elevation-2">
<v-card-title class="d-flex align-center text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-hospital-box-outline" class="mr-2" size="40"></v-icon>
<span>Master Klinik Ruang</span>
</v-card-title>
<v-btn
color="success"
prepend-icon="mdi-plus"
rounded
class="text-capitalize"
@click="showForm = true"
>
Tambah Baru
</v-btn>
</v-card>
<v-card class="pa-4 rounded-lg elevation-2">
<v-card-text>
<!-- Table controls -->
<div class="d-flex flex-wrap align-center justify-space-between mb-4">
<div class="d-flex align-center">
<span class="mr-2 text-subtitle-1">Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50]"
variant="outlined"
density="compact"
hide-details
style="max-width: 80px;"
rounded
class="mr-2"
></v-select>
<span class="text-subtitle-1">entries</span>
</div>
<div class="d-flex align-center">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="Search"
variant="outlined"
density="compact"
hide-details
rounded
clearable
></v-text-field>
</div>
</div>
<!-- Data Table dengan paginasi kustom -->
<v-data-table
:headers="headers"
:items="allRuangData"
:search="search"
:items-per-page="itemsPerPage"
v-model:page="page"
class="rounded-lg elevation-0 custom-table"
>
<!-- Slot untuk aksi di setiap baris -->
<template v-slot:[`item.actions`]="{ item }">
<v-btn icon color="blue" size="small" class="mr-2" @click="viewItem(item)">
<v-icon>mdi-eye</v-icon>
</v-btn>
<v-btn icon color="orange" size="small" class="mr-2" @click="editItem(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon color="red" size="small" @click="deleteItem(item)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<template v-slot:no-data>
<v-alert :value="true" color="grey-lighten-3" icon="mdi-information">
Tidak ada data yang tersedia.
</v-alert>
</template>
<!-- Slot kustom untuk footer tabel (paginasi) -->
<template v-slot:bottom>
<v-row class="ma-2">
<v-col cols="12" sm="6" class="d-flex align-center justify-start text-caption text-grey">
{{ showingEntriesText }}
</v-col>
<v-col cols="12" sm="6" class="d-flex align-center justify-end">
<v-pagination
v-model="page"
:length="pageCount"
rounded="circle"
:total-visible="5"
></v-pagination>
</v-col>
</v-row>
</template>
</v-data-table>
</v-card-text>
</v-card>
</div>
</div>
<!-- Delete Dialog -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card class="pa-4 rounded-lg">
<v-card-title class="text-h6 font-weight-bold">Hapus Data</v-card-title>
<v-card-text>Apakah Anda yakin ingin menghapus data ini?</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn color="grey-darken-1" variant="text" @click="closeDeleteDialog">Batal</v-btn>
<v-btn color="red" variant="text" @click="confirmDelete">Hapus</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup>
import { ref, computed } from 'vue';
definePageMeta({
middleware:['auth']
})
// State untuk menampilkan/menyembunyikan formulir
const showForm = ref(false);
const readOnly = ref(false);
// Data dummy untuk Master Klinik Ruang
const allRuangData = ref([
{ id: 1, namaKlinik: 'ANAK', kode: 'AN-01', namaRuang: 'RUANG ANAK 1' },
{ id: 2, namaKlinik: 'ANAK', kode: 'AN-02', namaRuang: 'RUANG ANAK 2' },
{ id: 3, namaKlinik: 'ANESTESI', kode: 'AS-01', namaRuang: 'RUANG ANESTESI 1' },
{ id: 4, namaKlinik: 'BEDAH', kode: 'BD-01', namaRuang: 'RUANG BEDAH 1' },
{ id: 5, namaKlinik: 'GIGI DAN MULUT', kode: 'GI-01', namaRuang: 'RUANG GIGI DAN MULUT 1' },
{ id: 6, namaKlinik: 'GERIATRI', kode: 'GR-01', namaRuang: 'RUANG GERIATRI 1' },
{ id: 7, namaKlinik: 'GIZI', kode: 'GZ-01', namaRuang: 'RUANG GIZI 1' },
{ id: 8, namaKlinik: 'HOM', kode: 'HO-01', namaRuang: 'RUANG HOM 1' },
{ id: 9, namaKlinik: 'IPD', kode: 'IP-01', namaRuang: 'RUANG IPD 1' },
{ id: 10, namaKlinik: 'JANTUNG', kode: 'JT-01', namaRuang: 'RUANG JANTUNG 1' },
{ id: 11, namaKlinik: 'JIWA', kode: 'JW-01', namaRuang: 'RUANG JIWA 1' },
{ id: 12, namaKlinik: 'KANDUNGAN', kode: 'OB-01', namaRuang: 'RUANG KANDUNGAN 1' },
{ id: 13, namaKlinik: 'KANDUNGAN', kode: 'OB-02', namaRuang: 'RUANG KANDUNGAN 2' },
{ id: 14, namaKlinik: 'KEMOTERAPI', kode: 'KT-01', namaRuang: 'RUANG KEMOTERAPI 1' },
{ id: 15, namaKlinik: 'KOMPLEMENTER', kode: 'KN-01', namaRuang: 'RUANG KOMPLEMENTER 1' },
{ id: 16, namaKlinik: 'KUL KEL', kode: 'KL-01', namaRuang: 'RUANG KUL KEL 1' },
{ id: 17, namaKlinik: 'MATA', kode: 'MT-01', namaRuang: 'RUANG MATA 1' },
{ id: 18, namaKlinik: 'MCU', kode: 'MC-01', namaRuang: 'RUANG MCU 1' },
{ id: 19, namaKlinik: 'ONKOLOGI', kode: 'ON-01', namaRuang: 'RUANG ONKOLOGI 1' },
{ id: 20, namaKlinik: 'PARU', kode: 'PR-01', namaRuang: 'RUANG PARU 1' },
{ id: 21, namaKlinik: 'R. TINDAKAN', kode: 'RT-01', namaRuang: 'RUANG R. TINDAKAN 1' },
{ id: 22, namaKlinik: 'RADIOTERAPI', kode: 'RD-01', namaRuang: 'RUANG RADIOTERAPI 1' },
{ id: 23, namaKlinik: 'REHAB MEDIK', kode: 'RM-01', namaRuang: 'RUANG REHAB MEDIK 1' },
{ id: 24, namaKlinik: 'SARAF', kode: 'NU-01', namaRuang: 'RUANG SARAF 1' },
{ id: 25, namaKlinik: 'THT', kode: 'TH-01', namaRuang: 'RUANG THT 1' },
]);
const headers = ref([
{ title: 'No', key: 'id' },
{ title: 'Nama Klinik', key: 'namaKlinik', sortable: true },
{ title: 'Kode Ruang', key: 'kode', sortable: true },
{ title: 'Nama Ruang', key: 'namaRuang', sortable: true },
{ title: 'Aksi', key: 'actions', sortable: false },
]);
// Breadcrumbs
const breadcrumbs = ref([
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
]);
// State untuk tabel dan paginasi
const itemsPerPage = ref(10);
const search = ref('');
const page = ref(1);
// Computed properties untuk paginasi kustom
const pageCount = computed(() => {
return Math.ceil(allRuangData.value.length / itemsPerPage.value);
});
const showingEntriesText = computed(() => {
const start = (page.value - 1) * itemsPerPage.value + 1;
const end = Math.min(page.value * itemsPerPage.value, allRuangData.value.length);
const total = allRuangData.value.length;
return `Showing ${start} to ${end} of ${total} entries`;
});
// State untuk dialog
const showDeleteDialog = ref(false);
const isEditMode = ref(false);
const editedIndex = ref(-1);
const editedItem = ref({
id: 0,
namaKlinik: '',
kode: '',
namaRuang: '',
});
// Fungsi untuk tombol aksi
const viewItem = (item) => {
editedItem.value = { ...item };
readOnly.value = true;
isEditMode.value = false;
showForm.value = true;
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
{ title: 'Detail Ruang Klinik', disabled: true, href: '/setting/viewruang' },
];
};
const editItem = (item) => {
editedIndex.value = allRuangData.value.findIndex(d => d.id === item.id);
editedItem.value = { ...item };
isEditMode.value = true;
readOnly.value = false;
showForm.value = true;
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
{ title: 'Edit Ruang Klinik', disabled: true, href: '/setting/editruang' },
];
};
const deleteItem = (item) => {
editedIndex.value = allRuangData.value.findIndex(d => d.id === item.id);
showDeleteDialog.value = true;
};
const confirmDelete = () => {
if (editedIndex.value > -1) {
allRuangData.value.splice(editedIndex.value, 1);
}
closeDeleteDialog();
};
const cancelForm = () => {
showForm.value = false;
isEditMode.value = false;
readOnly.value = false;
editedItem.value = {
id: 0,
namaKlinik: '',
kode: '',
namaRuang: '',
};
editedIndex.value = -1;
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
];
};
const closeDeleteDialog = () => {
showDeleteDialog.value = false;
editedIndex.value = -1;
};
const saveItem = () => {
if (isEditMode.value) {
Object.assign(allRuangData.value[editedIndex.value], editedItem.value);
} else {
const newId = allRuangData.value.length > 0 ? Math.max(...allRuangData.value.map(item => item.id)) + 1 : 1;
editedItem.value.id = newId;
allRuangData.value.push(editedItem.value);
}
cancelForm();
};
</script>
<style scoped>
.master-klinik-ruang-page {
font-family: 'Roboto', sans-serif;
background-color: #f5f7fa;
min-height: 100vh;
}
.custom-table {
border: none;
}
.v-data-table :deep(th) {
font-weight: bold !important;
color: #333 !important;
}
.v-data-table :deep(td) {
vertical-align: middle;
}
.v-btn--icon {
border-radius: 8px;
}
.v-select :deep(.v-field__input) {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
</style>
+1 -1
View File
@@ -110,7 +110,7 @@ const deleteLoket = (item) => {
};
const tambahLoket = () => {
router.push({ path: "/Setting/Tambah-Loket" });
router.push({ path: "/Setting/TambahLoket" });
};
</script>
<style scoped>
File diff suppressed because it is too large Load diff
+550 -329
View File
@@ -1,376 +1,597 @@
<template>
<div class="master-klinik-ruang-page pa-4">
<!-- Breadcrumbs -->
<v-breadcrumbs :items="breadcrumbs" class="pl-0">
<template v-slot:divider>
<v-icon icon="mdi-chevron-right"></v-icon>
</template>
</v-breadcrumbs>
<!-- Tampilan Formulir Tambah/Edit/View -->
<v-card v-if="showForm" class="pa-4 rounded-lg elevation-2">
<v-card-title class="text-h5 font-weight-bold mb-4">
{{ isEditMode ? 'Edit Ruang Klinik' : readOnly ? 'Detail Ruang Klinik' : 'Tambah Ruang Klinik' }}
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Nama Klinik</v-label>
<v-select
v-model="editedItem.namaKlinik"
:items="['ANAK', 'ANESTESI', 'BEDAH', 'GERIATRI', 'GIGI DAN MULUT', 'GIZI', 'HOM', 'IPD', 'JANTUNG', 'JIWA', 'KANDUNGAN', 'KEMOTERAPI', 'KOMPLEMENTER', 'KUL KEL', 'MATA', 'MCU', 'ONKOLOGI', 'PARU', 'R. TINDAKAN', 'RADIOTERAPI', 'REHAB MEDIK', 'SARAF', 'THT']"
placeholder="Pilih Nama Klinik"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-select>
</v-col>
<v-col cols="12" md="6">
<v-label class="font-weight-bold">Kode Ruang</v-label>
<v-text-field
v-model="editedItem.kode"
placeholder="Masukkan Kode Ruang"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-label class="font-weight-bold">Nama Ruang</v-label>
<v-text-field
v-model="editedItem.namaRuang"
placeholder="Masukkan Nama Ruang"
variant="outlined"
density="comfortable"
class="mb-2"
:readonly="readOnly"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions class="d-flex justify-start pa-4">
<v-btn
color="grey-darken-1"
variant="flat"
class="text-capitalize rounded-lg mr-2"
@click="cancelForm"
>
{{ readOnly ? 'Tutup' : 'Batal' }}
</v-btn>
<v-btn
v-if="!readOnly"
color="orange-darken-2"
variant="flat"
class="text-capitalize rounded-lg"
@click="saveItem"
>
Submit
</v-btn>
</v-card-actions>
</v-card>
<!-- Tampilan Tabel Data -->
<div v-else>
<!-- Banner biru sebagai pengganti header h1 -->
<v-card class="d-flex justify-space-between align-center pa-4 mb-4 rounded-lg bg-blue-darken-2 text-white elevation-2">
<v-card-title class="d-flex align-center text-h5 font-weight-bold pa-0">
<v-icon icon="mdi-hospital-box-outline" class="mr-2" size="40"></v-icon>
<span>Master Klinik Ruang</span>
</v-card-title>
<v-btn
color="success"
prepend-icon="mdi-plus"
rounded
class="text-capitalize"
@click="showForm = true"
>
Tambah Baru
</v-btn>
</v-card>
<v-card class="pa-4 rounded-lg elevation-2">
<v-card-text>
<!-- Table controls -->
<div class="d-flex flex-wrap align-center justify-space-between mb-4">
<div class="d-flex align-center">
<span class="mr-2 text-subtitle-1">Show</span>
<v-select
v-model="itemsPerPage"
:items="[10, 25, 50]"
variant="outlined"
density="compact"
hide-details
style="max-width: 80px;"
rounded
class="mr-2"
></v-select>
<span class="text-subtitle-1">entries</span>
<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="d-flex align-center">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="Search"
variant="outlined"
density="compact"
hide-details
rounded
clearable
></v-text-field>
<div class="header-text">
<h1 class="page-title">Master Klinik Ruang</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Ruangan</p>
</div>
</div>
<!-- Data Table dengan paginasi kustom -->
<v-data-table
:headers="headers"
:items="allRuangData"
:search="search"
:items-per-page="itemsPerPage"
v-model:page="page"
class="rounded-lg elevation-0 custom-table"
<v-btn
color="white"
@click="openTambahDialog"
elevation="0"
class="add-btn"
>
<!-- Slot untuk aksi di setiap baris -->
<template v-slot:[`item.actions`]="{ item }">
<v-btn icon color="blue" size="small" class="mr-2" @click="viewItem(item)">
<v-icon>mdi-eye</v-icon>
</v-btn>
<v-btn icon color="orange" size="small" class="mr-2" @click="editItem(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon color="red" size="small" @click="deleteItem(item)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<template v-slot:no-data>
<v-alert :value="true" color="grey-lighten-3" icon="mdi-information">
Tidak ada data yang tersedia.
</v-alert>
</template>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Ruang
</v-btn>
</div>
</div>
<!-- Slot kustom untuk footer tabel (paginasi) -->
<template v-slot:bottom>
<v-row class="ma-2">
<v-col cols="12" sm="6" class="d-flex align-center justify-start text-caption text-grey">
{{ showingEntriesText }}
</v-col>
<v-col cols="12" sm="6" class="d-flex align-center justify-end">
<v-pagination
v-model="page"
:length="pageCount"
rounded="circle"
:total-visible="5"
></v-pagination>
</v-col>
</v-row>
</template>
</v-data-table>
</v-card-text>
</v-card>
</div>
</div>
<!-- Delete Dialog -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card class="pa-4 rounded-lg">
<v-card-title class="text-h6 font-weight-bold">Hapus Data</v-card-title>
<v-card-text>Apakah Anda yakin ingin menghapus data ini?</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn color="grey-darken-1" variant="text" @click="closeDeleteDialog">Batal</v-btn>
<v-btn color="red" variant="text" @click="confirmDelete">Hapus</v-btn>
</v-card-actions>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="ruangData"
:items-per-page="10"
class="elevation-0"
>
<template v-slot:item.namaKlinik="{ item }">
<v-chip size="small" color="primary" variant="outlined">
{{ item.kodeKlinik }}
</v-chip>
<span class="ml-2">{{ item.namaKlinik }}</span>
</template>
<template v-slot:item.aksi="{ item }">
<v-btn
size="small"
color="#1976d2"
@click="openEditDialog(item)"
class="mr-2"
variant="flat"
>
<v-icon size="16" left>mdi-pencil</v-icon>
Edit
</v-btn>
<v-btn
size="small"
color="#f44336"
@click="deleteRuang(item)"
variant="flat"
>
<v-icon size="16" left>mdi-delete</v-icon>
Delete
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-dialog>
<!-- Dialog Tambah/Edit -->
<v-dialog v-model="dialog" max-width="800px" persistent>
<v-card>
<v-card-title class="dialog-header">
<span class="dialog-title">{{ isEdit ? 'Edit Klinik Ruang' : 'Tambah Klinik Ruang' }}</span>
<v-btn icon variant="text" @click="closeDialog" size="small">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="dialog-content">
<v-form ref="formRef">
<!-- Klinik Selection -->
<v-row>
<v-col cols="12">
<div class="section-header">
<v-icon color="primary" size="20">mdi-hospital-building</v-icon>
<span class="section-title">Pilih Klinik</span>
</div>
</v-col>
<v-col cols="12" md="6">
<v-autocomplete
label="Kode Klinik"
v-model="formData.kodeKlinik"
:items="klinikList"
item-title="kode"
item-value="kode"
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Kode klinik harus dipilih']"
required
hide-details="auto"
placeholder="Masukkan Kode Klinik"
@update:model-value="onKlinikChange"
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-chip size="small" color="primary">{{ item.raw.kode }}</v-chip>
</template>
<v-list-item-title>{{ item.raw.nama }}</v-list-item-title>
</v-list-item>
</template>
</v-autocomplete>
<small class="text-grey">Masukan Kode Ruang 2 Huruf</small>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Nama Klinik"
v-model="formData.namaKlinik"
variant="outlined"
density="comfortable"
readonly
hide-details="auto"
placeholder="Nama akan terisi otomatis"
bg-color="grey-lighten-4"
></v-text-field>
</v-col>
</v-row>
<!-- Ruang Section -->
<v-row class="mt-4">
<v-col cols="12">
<div class="section-header">
<v-icon color="primary" size="20">mdi-door</v-icon>
<span class="section-title">Daftar Ruangan</span>
<v-spacer></v-spacer>
<v-btn
size="small"
color="success"
variant="flat"
@click="addRuang"
>
<v-icon size="18" left>mdi-plus</v-icon>
Add Ruang
</v-btn>
</div>
</v-col>
<v-col cols="12">
<div class="ruang-list">
<div
v-for="(ruang, index) in formData.ruangList"
:key="index"
class="ruang-item"
>
<v-row align="center">
<v-col cols="12" md="1">
<div class="ruang-number">
<v-icon size="16">mdi-numeric-{{ index + 1 }}-circle</v-icon>
</div>
</v-col>
<v-col cols="12" md="3">
<v-text-field
label="Nomor Ruang"
v-model="ruang.nomorRuang"
variant="outlined"
density="compact"
hide-details="auto"
placeholder="Masukan Nomor Ruangan"
prepend-inner-icon="mdi-pound"
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-text-field
label="Nama Ruang"
v-model="ruang.namaRuang"
variant="outlined"
density="compact"
hide-details="auto"
placeholder="Masukan Nama Klinik Ruang"
prepend-inner-icon="mdi-door-open"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field
label="Nomor Screen"
v-model="ruang.nomorScreen"
variant="outlined"
density="compact"
hide-details="auto"
placeholder="Screen Klinik Ruang"
prepend-inner-icon="mdi-monitor"
></v-text-field>
<small class="text-grey">Masukan Nomor Screen Ruang</small>
</v-col>
<v-col cols="12" md="1" class="d-flex justify-center">
<v-btn
v-if="formData.ruangList.length > 1"
icon
size="small"
color="error"
variant="text"
@click="removeRuang(index)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
<v-alert
v-if="formData.ruangList.length === 0"
type="info"
variant="tonal"
class="mt-2"
>
<v-icon left>mdi-information</v-icon>
Klik "Add Ruang" untuk menambahkan ruangan
</v-alert>
</div>
</v-col>
</v-row>
</v-form>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="dialog-actions">
<v-spacer></v-spacer>
<v-btn
color="grey-lighten-2"
@click="closeDialog"
class="action-btn"
>
<v-icon left size="18">mdi-close</v-icon>
Cancel
</v-btn>
<v-btn
color="warning"
@click="submitForm"
class="action-btn"
>
<v-icon left size="18">mdi-content-save</v-icon>
Submit
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref } from 'vue';
definePageMeta({
middleware:['auth']
})
// State untuk menampilkan/menyembunyikan formulir
const showForm = ref(false);
const readOnly = ref(false);
// Data dummy untuk Master Klinik Ruang
const allRuangData = ref([
{ id: 1, namaKlinik: 'ANAK', kode: 'AN-01', namaRuang: 'RUANG ANAK 1' },
{ id: 2, namaKlinik: 'ANAK', kode: 'AN-02', namaRuang: 'RUANG ANAK 2' },
{ id: 3, namaKlinik: 'ANESTESI', kode: 'AS-01', namaRuang: 'RUANG ANESTESI 1' },
{ id: 4, namaKlinik: 'BEDAH', kode: 'BD-01', namaRuang: 'RUANG BEDAH 1' },
{ id: 5, namaKlinik: 'GIGI DAN MULUT', kode: 'GI-01', namaRuang: 'RUANG GIGI DAN MULUT 1' },
{ id: 6, namaKlinik: 'GERIATRI', kode: 'GR-01', namaRuang: 'RUANG GERIATRI 1' },
{ id: 7, namaKlinik: 'GIZI', kode: 'GZ-01', namaRuang: 'RUANG GIZI 1' },
{ id: 8, namaKlinik: 'HOM', kode: 'HO-01', namaRuang: 'RUANG HOM 1' },
{ id: 9, namaKlinik: 'IPD', kode: 'IP-01', namaRuang: 'RUANG IPD 1' },
{ id: 10, namaKlinik: 'JANTUNG', kode: 'JT-01', namaRuang: 'RUANG JANTUNG 1' },
{ id: 11, namaKlinik: 'JIWA', kode: 'JW-01', namaRuang: 'RUANG JIWA 1' },
{ id: 12, namaKlinik: 'KANDUNGAN', kode: 'OB-01', namaRuang: 'RUANG KANDUNGAN 1' },
{ id: 13, namaKlinik: 'KANDUNGAN', kode: 'OB-02', namaRuang: 'RUANG KANDUNGAN 2' },
{ id: 14, namaKlinik: 'KEMOTERAPI', kode: 'KT-01', namaRuang: 'RUANG KEMOTERAPI 1' },
{ id: 15, namaKlinik: 'KOMPLEMENTER', kode: 'KN-01', namaRuang: 'RUANG KOMPLEMENTER 1' },
{ id: 16, namaKlinik: 'KUL KEL', kode: 'KL-01', namaRuang: 'RUANG KUL KEL 1' },
{ id: 17, namaKlinik: 'MATA', kode: 'MT-01', namaRuang: 'RUANG MATA 1' },
{ id: 18, namaKlinik: 'MCU', kode: 'MC-01', namaRuang: 'RUANG MCU 1' },
{ id: 19, namaKlinik: 'ONKOLOGI', kode: 'ON-01', namaRuang: 'RUANG ONKOLOGI 1' },
{ id: 20, namaKlinik: 'PARU', kode: 'PR-01', namaRuang: 'RUANG PARU 1' },
{ id: 21, namaKlinik: 'R. TINDAKAN', kode: 'RT-01', namaRuang: 'RUANG R. TINDAKAN 1' },
{ id: 22, namaKlinik: 'RADIOTERAPI', kode: 'RD-01', namaRuang: 'RUANG RADIOTERAPI 1' },
{ id: 23, namaKlinik: 'REHAB MEDIK', kode: 'RM-01', namaRuang: 'RUANG REHAB MEDIK 1' },
{ id: 24, namaKlinik: 'SARAF', kode: 'NU-01', namaRuang: 'RUANG SARAF 1' },
{ id: 25, namaKlinik: 'THT', kode: 'TH-01', namaRuang: 'RUANG THT 1' },
]);
const dialog = ref(false);
const isEdit = ref(false);
const formRef = ref(null);
const headers = ref([
{ title: 'No', key: 'id' },
{ title: 'Nama Klinik', key: 'namaKlinik', sortable: true },
{ title: 'Kode Ruang', key: 'kode', sortable: true },
{ title: 'Nama Ruang', key: 'namaRuang', sortable: true },
{ title: 'Aksi', key: 'actions', sortable: false },
{ title: 'No', value: 'no', sortable: true },
{ title: 'Nama Klinik', value: 'namaKlinik', sortable: true },
{ title: 'Kode', value: 'kodeKlinik', sortable: true },
{ title: 'Nama Ruang', value: 'namaRuang', sortable: true },
{ title: 'Aksi', value: 'aksi', sortable: false },
]);
// Breadcrumbs
const breadcrumbs = ref([
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
// List klinik dari Master Klinik
const klinikList = ref([
{ kode: 'AN', nama: 'ANAK' },
{ kode: 'AS', nama: 'ANESTESI' },
{ kode: 'BD', nama: 'BEDAH' },
{ kode: 'GR', nama: 'GERIATRI' },
{ kode: 'GI', nama: 'GIGI DAN MULUT' },
{ kode: 'GZ', nama: 'GIZI' },
{ kode: 'HO', nama: 'HOM' },
{ kode: 'IP', nama: 'IPD' },
{ kode: 'JT', nama: 'JANTUNG' },
{ kode: 'JW', nama: 'JIWA' },
{ kode: 'KK', nama: 'KULIT KELAMIN' },
{ kode: 'MT', nama: 'MATA' },
{ kode: 'OB', nama: 'OBGYN' },
{ kode: 'PR', nama: 'PARU' },
{ kode: 'RT', nama: 'RADIOTERAPI' },
]);
// State untuk tabel dan paginasi
const itemsPerPage = ref(10);
const search = ref('');
const page = ref(1);
const ruangData = ref([
{
id: 1,
no: 1,
kodeKlinik: 'AN',
namaKlinik: 'ANAK',
namaRuang: 'R. TINDAKAN',
ruangList: [
{ nomorRuang: '1', namaRuang: 'R. TINDAKAN', nomorScreen: '101' }
]
},
{
id: 2,
no: 2,
kodeKlinik: 'AS',
namaKlinik: 'ANESTESI',
namaRuang: 'Ruang 1, Ruang 2',
ruangList: [
{ nomorRuang: '1', namaRuang: 'Ruang 1', nomorScreen: '201' },
{ nomorRuang: '2', namaRuang: 'Ruang 2', nomorScreen: '202' }
]
},
{
id: 3,
no: 3,
kodeKlinik: 'BD',
namaKlinik: 'BEDAH',
namaRuang: 'Ruang Konsultasi',
ruangList: [
{ nomorRuang: '1', namaRuang: 'Ruang Konsultasi', nomorScreen: '301' }
]
},
{
id: 4,
no: 4,
kodeKlinik: 'GR',
namaKlinik: 'GERIATRI',
namaRuang: 'Ruang Pemeriksaan',
ruangList: [
{ nomorRuang: '1', namaRuang: 'Ruang Pemeriksaan', nomorScreen: '401' }
]
},
{
id: 5,
no: 5,
kodeKlinik: 'GI',
namaKlinik: 'GIGI DAN MULUT',
namaRuang: 'Ruang 1, Ruang 2, Ruang 3',
ruangList: [
{ nomorRuang: '1', namaRuang: 'Ruang 1', nomorScreen: '501' },
{ nomorRuang: '2', namaRuang: 'Ruang 2', nomorScreen: '502' },
{ nomorRuang: '3', namaRuang: 'Ruang 3', nomorScreen: '503' }
]
},
]);
// Computed properties untuk paginasi kustom
const pageCount = computed(() => {
return Math.ceil(allRuangData.value.length / itemsPerPage.value);
});
const showingEntriesText = computed(() => {
const start = (page.value - 1) * itemsPerPage.value + 1;
const end = Math.min(page.value * itemsPerPage.value, allRuangData.value.length);
const total = allRuangData.value.length;
return `Showing ${start} to ${end} of ${total} entries`;
});
// State untuk dialog
const showDeleteDialog = ref(false);
const isEditMode = ref(false);
const editedIndex = ref(-1);
const editedItem = ref({
id: 0,
const formData = ref({
id: null,
kodeKlinik: '',
namaKlinik: '',
kode: '',
namaRuang: '',
ruangList: [
{ nomorRuang: '', namaRuang: '', nomorScreen: '' }
],
});
// Fungsi untuk tombol aksi
const viewItem = (item) => {
editedItem.value = { ...item };
readOnly.value = true;
isEditMode.value = false;
showForm.value = true;
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
{ title: 'Detail Ruang Klinik', disabled: true, href: '/setting/viewruang' },
];
};
const editItem = (item) => {
editedIndex.value = allRuangData.value.findIndex(d => d.id === item.id);
editedItem.value = { ...item };
isEditMode.value = true;
readOnly.value = false;
showForm.value = true;
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
{ title: 'Edit Ruang Klinik', disabled: true, href: '/setting/editruang' },
];
};
const deleteItem = (item) => {
editedIndex.value = allRuangData.value.findIndex(d => d.id === item.id);
showDeleteDialog.value = true;
};
const confirmDelete = () => {
if (editedIndex.value > -1) {
allRuangData.value.splice(editedIndex.value, 1);
const onKlinikChange = (kode) => {
const selectedKlinik = klinikList.value.find(k => k.kode === kode);
if (selectedKlinik) {
formData.value.namaKlinik = selectedKlinik.nama;
}
closeDeleteDialog();
};
const cancelForm = () => {
showForm.value = false;
isEditMode.value = false;
readOnly.value = false;
editedItem.value = {
id: 0,
namaKlinik: '',
kode: '',
const addRuang = () => {
formData.value.ruangList.push({
nomorRuang: '',
namaRuang: '',
};
editedIndex.value = -1;
breadcrumbs.value = [
{ title: 'Dashboard', disabled: false, href: '/dashboard' },
{ title: 'Setting', disabled: false, href: '/setting' },
{ title: 'Master Klinik Ruang', disabled: false, href: '/setting/masterklinikruang' },
];
nomorScreen: ''
});
};
const closeDeleteDialog = () => {
showDeleteDialog.value = false;
editedIndex.value = -1;
const removeRuang = (index) => {
if (formData.value.ruangList.length > 1) {
formData.value.ruangList.splice(index, 1);
}
};
const saveItem = () => {
if (isEditMode.value) {
Object.assign(allRuangData.value[editedIndex.value], editedItem.value);
const openTambahDialog = () => {
isEdit.value = false;
resetForm();
dialog.value = true;
};
const openEditDialog = (item) => {
isEdit.value = true;
formData.value = {
id: item.id,
kodeKlinik: item.kodeKlinik,
namaKlinik: item.namaKlinik,
ruangList: JSON.parse(JSON.stringify(item.ruangList)),
};
dialog.value = true;
};
const closeDialog = () => {
dialog.value = false;
resetForm();
};
const resetForm = () => {
formData.value = {
id: null,
kodeKlinik: '',
namaKlinik: '',
ruangList: [
{ nomorRuang: '', namaRuang: '', nomorScreen: '' }
],
};
if (formRef.value) {
formRef.value.reset();
}
};
const submitForm = async () => {
const { valid } = await formRef.value.validate();
if (!valid) {
return;
}
if (formData.value.ruangList.length === 0) {
alert('Tambahkan minimal 1 ruangan');
return;
}
// Generate nama ruang untuk display di table
const namaRuangDisplay = formData.value.ruangList
.map(r => r.namaRuang)
.filter(n => n)
.join(', ');
if (isEdit.value) {
const index = ruangData.value.findIndex(r => r.id === formData.value.id);
if (index !== -1) {
ruangData.value[index] = {
...ruangData.value[index],
...formData.value,
namaRuang: namaRuangDisplay,
};
}
} else {
const newId = allRuangData.value.length > 0 ? Math.max(...allRuangData.value.map(item => item.id)) + 1 : 1;
editedItem.value.id = newId;
allRuangData.value.push(editedItem.value);
const newId = Math.max(...ruangData.value.map(r => r.id)) + 1;
const newNo = ruangData.value.length + 1;
ruangData.value.push({
id: newId,
no: newNo,
...formData.value,
namaRuang: namaRuangDisplay,
});
}
closeDialog();
};
const deleteRuang = (item) => {
if (confirm(`Hapus ruangan ${item.namaRuang} dari klinik ${item.namaKlinik}?`)) {
const index = ruangData.value.findIndex(r => r.id === item.id);
if (index !== -1) {
ruangData.value.splice(index, 1);
ruangData.value.forEach((r, idx) => {
r.no = idx + 1;
});
}
}
cancelForm();
};
</script>
<style scoped>
.master-klinik-ruang-page {
font-family: 'Roboto', sans-serif;
background-color: #f5f7fa;
min-height: 100vh;
.page-header {
background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
border-radius: 16px 16px 0 0;
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.2);
}
.custom-table {
border: none;
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: white;
}
.v-data-table :deep(th) {
font-weight: bold !important;
color: #333 !important;
.header-left {
display: flex;
align-items: center;
}
.v-data-table :deep(td) {
vertical-align: middle;
.header-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 16px;
margin-right: 20px;
backdrop-filter: blur(10px);
}
.v-btn--icon {
.page-title {
font-size: 32px;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
}
.add-btn {
font-weight: 600;
text-transform: none;
letter-spacing: 0.5px;
padding: 0 24px !important;
height: 44px !important;
color: #4caf50 !important;
}
.dialog-header {
background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
color: white;
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-title {
font-size: 20px;
font-weight: 600;
}
.dialog-content {
padding: 24px !important;
max-height: 650px;
overflow-y: auto;
}
.dialog-actions {
padding: 16px 24px;
background: #f5f5f5;
}
.action-btn {
text-transform: none;
font-weight: 600;
min-width: 120px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #4caf50;
margin-bottom: 16px;
}
.v-select :deep(.v-field__input) {
padding-top: 0 !important;
padding-bottom: 0 !important;
.section-title {
font-size: 16px;
font-weight: 600;
color: #424242;
}
</style>
.ruang-list {
background: #f8f9fa;
border: 2px solid #e8f5e9;
border-radius: 12px;
padding: 16px;
}
.ruang-item {
background: white;
padding: 16px;
border-radius: 8px;
margin-bottom: 12px;
border: 1px solid #e0e0e0;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.ruang-item:last-child {
margin-bottom: 0;
}
.ruang-number {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
color: white;
padding: 8px;
border-radius: 8px;
font-weight: 600;
font-size: 14px;
}
</style>
+176
View File
@@ -0,0 +1,176 @@
<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-view-dashboard</v-icon>
</div>
<div class="header-text">
<h1 class="page-title">Master Loket</h1>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Pelayanan</p>
</div>
</div>
<v-btn
color="white"
@click="tambahLoket"
elevation="0"
class="add-btn"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Loket
</v-btn>
</div>
</div>
<TabelData
:headers="loketHeaders"
:items="loketData"
title="Master Loket"
>
<template #actions="{ item }">
<v-btn
small
color="#ff9248"
@click="editLoket(item)"
class="mr-2"
style="color: white"
>
Edit
</v-btn>
<v-btn
small
color="grey-lighten-4"
@click="deleteLoket(item)"
>
Delete
</v-btn>
</template>
</TabelData>
</v-card>
</v-container>
</template>
<script setup>
import { ref } from "vue";
import TabelData from "../../components/TabelData.vue";
import { useRouter } from "vue-router";
const router = useRouter();
const loketHeaders = ref([
{ title: "No", value: "no" },
{ title: "Nama Loket", value: "namaLoket" },
{ title: "Kuota", value: "kuota" },
{ title: "Pelayanan", value: "pelayanan" },
{ title: "Pembayaran", value: "pembayaran" },
{ title: "Keterangan", value: "keterangan" },
{ title: "Aksi", value: "aksi", sortable: false },
]);
const loketData = ref([
{
id: 1,
no: 1,
namaLoket: "Loket 1",
kuota: 500,
pelayanan: ["RADIOTERAPI", "REHAB MEDIK", "TINDAKAN"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
{
id: 2,
no: 2,
namaLoket: "Loket 2",
kuota: 666,
pelayanan: ["JIWA", "SARAF"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
{
id: 3,
no: 3,
namaLoket: "Loket 3",
kuota: 666,
pelayanan: ["ANESTESI", "JANTUNG"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
{
id: 4,
no: 4,
namaLoket: "Loket 4",
kuota: 3676,
pelayanan: ["KULIT KELAMIN", "PARU"],
pembayaran: "JKN",
keterangan: "ONLINE",
},
]);
const editLoket = (item) => {
router.push({ path: `/Setting/Edit-Loket/${item.id}` });
};
const deleteLoket = (item) => {
const index = loketData.value.findIndex((loket) => loket.id === item.id);
if (index !== -1) {
loketData.value.splice(index, 1);
}
};
const tambahLoket = () => {
router.push({ path: "/Setting/TambahLoket" });
};
</script>
<style scoped>
.page-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
border-radius: 16px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(25, 118, 210, 0.3);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: white;
}
.header-left {
display: flex;
align-items: center;
}
.header-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
padding: 16px;
margin-right: 20px;
backdrop-filter: blur(10px);
}
.page-title {
font-size: 32px;
font-weight: 700;
margin: 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
}
.add-btn {
font-weight: 600;
text-transform: none;
letter-spacing: 0.5px;
padding: 0 24px !important;
height: 44px !important;
color: #1976d2 !important;
}
</style>
+310
View File
@@ -0,0 +1,310 @@
<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>
</div>
</div>
<!-- Form Content -->
<v-card-text class="form-content">
<v-form @submit.prevent="simpanLoket" ref="formRef">
<!-- Section: Informasi Loket -->
<div class="form-section">
<h3 class="section-title">Informasi Loket</h3>
<v-row>
<v-col cols="12" md="6">
<v-text-field
label="Nama Loket"
v-model="loket.namaLoket"
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Nama loket harus diisi']"
required
hide-details="auto"
class="form-field"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
label="Kuota Bangku"
v-model="loket.kuota"
type="number"
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Kuota harus diisi', v => v > 0 || 'Kuota harus lebih dari 0']"
required
hide-details="auto"
class="form-field"
></v-text-field>
</v-col>
</v-row>
</div>
<!-- Section: Konfigurasi -->
<div class="form-section">
<h3 class="section-title">Konfigurasi</h3>
<v-row>
<v-col cols="12" md="4">
<v-select
label="Status Pelayanan"
:items="['RAWAT JALAN', 'RAWAT INAP']"
v-model="loket.statusPelayanan"
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Status pelayanan harus dipilih']"
required
hide-details="auto"
class="form-field"
></v-select>
</v-col>
<v-col cols="12" md="4">
<v-select
label="Pembayaran"
:items="['JKN', 'UMUM', 'SPM', 'JKMM', 'JAMPERSAL', 'T4', 'KARYAWAN']"
v-model="loket.pembayaran"
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Pembayaran harus dipilih']"
required
hide-details="auto"
class="form-field"
></v-select>
</v-col>
<v-col cols="12" md="4">
<v-select
label="Keterangan"
:items="['ONLINE', 'OFFLINE']"
v-model="loket.keterangan"
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Keterangan harus dipilih']"
required
hide-details="auto"
class="form-field"
></v-select>
</v-col>
</v-row>
</div>
<!-- Section: Pelayanan -->
<div class="form-section">
<h3 class="section-title">Pilih Pelayanan</h3>
<div class="table-wrapper">
<TabelLayanan
:headers="serviceHeaders"
:items="availableServices"
v-model:selected-items="loket.pelayanan"
/>
</div>
</div>
<!-- Action Buttons -->
<div class="form-actions">
<v-btn
color="#1976d2"
type="submit"
size="large"
class="action-btn save-btn"
>
<v-icon left>mdi-content-save</v-icon>
Simpan Loket
</v-btn>
<v-btn
color="grey-lighten-2"
@click="cancelAdd"
size="large"
class="action-btn cancel-btn"
>
<v-icon left>mdi-close</v-icon>
Batal
</v-btn>
</div>
</v-form>
</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import TabelLayanan from '../../components/TabelLayanan.vue';
const router = useRouter();
const formRef = ref(null);
const loket = ref({
namaLoket: '',
kuota: null,
statusPelayanan: '',
pembayaran: '',
keterangan: '',
pelayanan: [],
});
const serviceHeaders = ref([
{ title: '#', value: 'no', sortable: false },
{ title: 'Kode', value: 'id' },
{ title: 'Klinik', value: 'nama' },
{ title: 'Pilih', value: 'pilih', sortable: false },
]);
const availableServices = ref([
{ no: 1, id: 'AN', nama: 'ANAK' },
{ no: 2, id: 'AS', nama: 'ANESTESI' },
{ no: 3, id: 'BD', nama: 'BEDAH' },
{ no: 4, id: 'GR', nama: 'GERIATRI' },
{ no: 5, id: 'GI', nama: 'GIGI DAN MULUT' },
{ no: 6, id: 'GZ', nama: 'GIZI' },
{ no: 7, id: 'HO', nama: 'HOM' },
{ no: 8, id: 'IP', nama: 'IPD' },
{ no: 9, id: 'JT', nama: 'JANTUNG' },
{ no: 10, id: 'JW', nama: 'JIWA' },
{ no: 11, id: 'KK', nama: 'KULIT KELAMIN' },
{ no: 12, id: 'PR', nama: 'PARU' },
{ no: 13, id: 'RT', nama: 'RADIOTERAPI' },
{ no: 14, id: 'RM', nama: 'REHAB MEDIK' },
{ no: 15, id: 'SR', nama: 'SARAF' },
{ no: 16, id: 'TD', nama: 'TINDAKAN' },
]);
const simpanLoket = async () => {
const { valid } = await formRef.value.validate();
if (!valid) {
return;
}
// Validasi pelayanan
if (loket.value.pelayanan.length === 0) {
alert('Pilih minimal 1 pelayanan');
return;
}
// Dalam aplikasi nyata, ini adalah tempat untuk memanggil API create data
console.log('Data loket baru:', loket.value);
// Kembali ke halaman master
router.push('/Setting/Master-Loket');
};
const cancelAdd = () => {
router.back();
};
</script>
<style scoped>
.form-card {
border-radius: 16px;
overflow: hidden;
border: 1px solid #e0e0e0;
}
.form-header {
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.2);
}
.header-content {
padding: 28px 32px;
color: white;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.back-btn {
background: rgba(255, 255, 255, 0.15);
}
.page-title {
font-size: 26px;
font-weight: 700;
margin: 0;
line-height: 1.2;
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 14px;
}
.form-content {
padding: 32px !important;
background: #fafafa;
}
.form-section {
background: white;
padding: 24px;
border-radius: 12px;
margin-bottom: 24px;
border: 1px solid #e0e0e0;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #1976d2;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 2px solid #e3f2fd;
}
.form-field {
margin-bottom: 4px;
}
.table-wrapper {
background: #fafafa;
padding: 16px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 24px;
background: white;
border-radius: 12px;
border: 1px solid #e0e0e0;
}
.action-btn {
text-transform: none;
font-weight: 600;
letter-spacing: 0.5px;
min-width: 140px;
}
.save-btn {
color: white;
}
.cancel-btn {
color: #616161;
}
</style>
+75 -76
View File
@@ -2,94 +2,93 @@
// Helper function to safely decode the JWT payload (Access Token or ID Token)
const decodeTokenPayload = (token: string | undefined): any | null => {
if (!token) return null;
try {
// Tokens are base64 encoded and separated by '.'
const parts = token.split('.');
if (parts.length < 2) return null; // Not a valid JWT format
const payloadBase64 = parts[1];
// Decode from base64 and parse the JSON
// Note: Using Buffer.from is standard in Node.js server environments (like Nitro/H3)
return JSON.parse(Buffer.from(payloadBase64, 'base64').toString());
} catch (e) {
console.error('❌ Failed to decode token payload:', e);
return null;
}
if (!token) return null;
try {
// Tokens are base64 encoded and separated by '.'
const parts = token.split(".");
if (parts.length < 2) return null; // Not a valid JWT format
const payloadBase64 = parts[1];
// Decode from base64 and parse the JSON
// Note: Using Buffer.from is standard in Node.js server environments (like Nitro/H3)
return JSON.parse(Buffer.from(payloadBase64, "base64").toString());
} catch (e) {
console.error("❌ Failed to decode token payload:", e);
return null;
}
};
// --- START OF THE SINGLE EXPORT DEFAULT HANDLER ---
export default defineEventHandler(async (event) => {
console.log('🔍 Session endpoint called');
console.log("🔍 Session endpoint called");
const sessionCookie = getCookie(event, 'user_session');
console.log('🍪 Session cookie exists:', !!sessionCookie);
const sessionCookie = getCookie(event, "user_session");
console.log("🍪 Session cookie exists:", !!sessionCookie);
if (!sessionCookie) {
console.log('❌ No session cookie found');
throw createError({
statusCode: 401,
statusMessage: 'No session cookie found'
});
if (!sessionCookie) {
console.log("❌ No session cookie found");
throw createError({
statusCode: 401,
statusMessage: "No session cookie found",
});
}
try {
const session = JSON.parse(sessionCookie);
console.log("📋 Session parsed successfully");
const isExpired = Date.now() > session.expiresAt;
console.log("   Is Expired:", isExpired);
// Check if the token has expired
if (isExpired) {
console.log("⏰ Session has expired, clearing cookie");
deleteCookie(event, "user_session");
throw createError({
statusCode: 401,
statusMessage: "Session expired",
});
}
try {
const session = JSON.parse(sessionCookie);
console.log('📋 Session parsed successfully');
const isExpired = Date.now() > session.expiresAt;
console.log('   Is Expired:', isExpired);
// Decode tokens and prepare the enhanced response data
const idTokenPayload = decodeTokenPayload(session.idToken);
const accessTokenPayload = decodeTokenPayload(session.accessToken);
// Check if the token has expired
if (isExpired) {
console.log('⏰ Session has expired, clearing cookie');
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: 'Session expired'
});
}
// Final response object for the frontend debug page
const sessionResponse = {
// Basic User Info
user: session.user,
// Decode tokens and prepare the enhanced response data
const idTokenPayload = decodeTokenPayload(session.idToken);
const accessTokenPayload = decodeTokenPayload(session.accessToken);
// Final response object for the frontend debug page
const sessionResponse = {
// Basic User Info
user: session.user,
// Raw Tokens
idToken: session.idToken,
accessToken: session.accessToken,
refreshToken: session.refreshToken,
// Session Timestamps
expiresAt: session.expiresAt,
createdAt: session.createdAt,
// Raw Tokens
idToken: session.idToken,
accessToken: session.accessToken,
refreshToken: session.refreshToken,
// Parsed Payloads
idTokenPayload: idTokenPayload,
accessTokenPayload: accessTokenPayload,
// Session Timestamps
expiresAt: session.expiresAt,
createdAt: session.createdAt,
// Raw Session Data (for Debug section)
fullSessionObject: session,
status: 'authenticated',
};
// Parsed Payloads
idTokenPayload: idTokenPayload,
accessTokenPayload: accessTokenPayload,
console.log('✅ Session is valid, returning full session data');
return sessionResponse;
// Raw Session Data (for Debug section)
fullSessionObject: session,
} catch (parseError) {
console.error('❌ Failed to parse session cookie:', parseError);
// If JSON parsing fails or any other error occurs, the session is invalid
deleteCookie(event, 'user_session');
throw createError({
statusCode: 401,
statusMessage: 'Invalid session data'
});
}
status: "authenticated",
};
console.log("✅ Session is valid, returning full session data");
return sessionResponse;
} catch (parseError) {
console.error("❌ Failed to parse session cookie:", parseError);
// If JSON parsing fails or any other error occurs, the session is invalid
deleteCookie(event, "user_session");
throw createError({
statusCode: 401,
statusMessage: "Invalid session data",
});
}
});
// --- END OF THE SINGLE EXPORT DEFAULT HANDLER ---
// --- END OF THE SINGLE EXPORT DEFAULT HANDLER ---
+44 -37
View File
@@ -1,59 +1,55 @@
// stores/navItems.ts
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
import { computed } from 'vue'; // Import computed dari Vue
interface NavItem {
id: number;
name: string;
path: string;
name: string; // Menggantikan 'title'
path: string; // Menggantikan 'to'
icon: string;
children?: NavItem[];
badge?: string; // Tambahkan properti badge
}
// Initial default navigation items
const defaultNavItems: NavItem[] = [
{ id: 1, name: "Dashboard", icon: "mdi-view-dashboard", path: "/dashboard" },
// Add other main menu items
{ id: 2, name: "Loket Admin", icon: "mdi-account-supervisor", path: "/LoketAdmin" },
{ id: 3, name: "Ranap Admin", icon: "mdi-bed", path: "/RanapAdmin" },
{ id: 4, name: "Klinik Admin", icon: "mdi-hospital-box", path: "/KlinikAdmin" },
{ id: 5, name: "Klinik Ruang Admin", icon: "mdi-hospital-marker", path: "/KlinikRuangAdmin" },
{ id: 2, name: "Verifikasi Akun", icon: "mdi-account-check-outline", path:"/VerifikasiAkun" },
{
id: 3,
name: "Check In",
icon: "mdi-file-document-edit-outline",
path: "/CheckIn"
// badge: "3",
},
{ id: 4, name: "Admin Loket", icon: "mdi-account-supervisor-outline", path: "/AdminLoket" },
{ id: 5, name: "Admin Klinik", icon: "mdi-text-box-plus-outline", path: "/AdminKlinik" },
{ id: 6, name: "Admin Penunjang", icon: "mdi-plus-box-outline", path: "/AdminPenunjang" },
{ id: 7, name: "Buat Antrean", icon: "mdi-account-multiple-plus-outline", path: "/BuatAntrean" },
{ id: 8, name: "Monitoring Pasien", icon: "mdi-account-group-outline", path: "/data-pasien" },
{
id: 6,
name: "Anjungan",
icon: "mdi-account-box-multiple",
id: 9,
name: "Layar Informasi",
icon: "mdi-monitor-multiple",
path: "",
children: [
{ id: 7, name: "Anjungan", path: "/Anjungan/Anjungan", icon: "mdi-account-box" },
{ id: 8, name: "Admin Anjungan", path: "/Anjungan/AdminAnjungan", icon: "mdi-account-cog" },
{ id: 10, name: "Anjungan", path: "/anjungan/anjungan", icon: "mdi-circle-small" },
{ id: 11, name: "Klinik Ruang", path: "/anjungan/AntrianKlinik", icon: "mdi-circle-small" },
],
},
{ id: 9, name: "Fast Track", icon: "mdi-clock-fast", path: "/FastTrack" },
{ id: 10, name: "Data Pasien", icon: "mdi-account-multiple", path: "/DataPasien" },
{
id: 11,
name: "Screen",
icon: "mdi-monitor",
{
id: 12,
name: "Master Data",
icon: "mdi-cog-outline",
path: "",
children: [
{ id: 12, name: "Antrian Masuk 1", path: "/Screen/Antrian Masuk 1", icon: "mdi-monitor" },
{ id: 13, name: "Antrian Masuk 2", path: "/Screen/Antrian Masuk 2", icon: "mdi-monitor" },
// ... more screen pages
],
},
{ id: 14, name: "List Pasien", icon: "mdi-format-list-bulleted", path: "/ListPasien" },
{
id: 15 ,
name: "Setting",
icon: "mdi-cog",
path: "",
children: [
{ id: 16, name: "Hak Akses", path: "/setting/HakAkses", icon: "mdi-shield-lock-outline" },
{ id: 17, name: "User Login", path: "/setting/UserLogin", icon: "mdi-account-circle" },
{ id: 18, name: "Master Loket", path: "/setting/MasterLoket", icon: "mdi-counter" },
{ id: 19, name: "Master Klinik", path: "/setting/MasterKlinik", icon: "mdi-hospital" },
{ id: 20, name: "Master Klinik Ruang", path: "/setting/MasterKlinikRuang", icon: "mdi-hospital-box" },
{ id: 21, name: "Screen", path: "/setting/Screen", icon: "mdi-monitor" },
{ id: 13, name: "Hak Akses", path: "/setting/hak-akses", icon: "mdi-circle-small" },
{ id: 14, name: "User Login", path: "/setting/user-login", icon: "mdi-circle-small" },
{ id: 15, name: "Master Loket", path: "/setting/masterloket", icon: "mdi-circle-small" },
{ id: 16, name: "Master Klinik", path: "/setting/masterklinik", icon: "mdi-circle-small" },
{ id: 17, name: "Master Klinik Ruang", path: "/setting/masterklinikruang", icon: "mdi-circle-small" },
{ id: 18, name: "Screen", path: "/setting/screen", icon: "mdi-circle-small" },
],
},
];
@@ -61,8 +57,18 @@ const defaultNavItems: NavItem[] = [
export const useNavItemsStore = defineStore('navItems', () => {
const navItems = useLocalStorage<NavItem[]>('navItems', defaultNavItems);
// === GETTER PENTING UNTUK MENGATASI MASALAH 'NULL' DI AWAL ===
const getNavItems = computed(() => {
// Jika navItems.value null, undefined, atau bukan array yang valid,
// kembalikan array default.
if (!Array.isArray(navItems.value) || navItems.value === null || navItems.value === undefined) {
return defaultNavItems;
}
return navItems.value;
});
// =============================================================
function updateNavItems(newItems: NavItem[]) {
// This will update the local storage and the state
navItems.value = newItems.map((item, index) => ({
...item,
id: index + 1
@@ -76,6 +82,7 @@ export const useNavItemsStore = defineStore('navItems', () => {
return {
navItems,
getNavItems, // Diekspor untuk digunakan di Sidebar
updateNavItems,
addNavItem,
};