Files
web-antrean/pages/Setting/MasterKlinik.vue
T

769 lines
20 KiB
Vue

<!-- eslint-disable vue/valid-v-slot -->
<template>
<v-container>
<v-card>
<!-- Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<div class="header-icon">
<v-icon size="32" color="white">mdi-hospital-building</v-icon>
</div>
<div class="header-text">
<h2 class="page-title">Master Klinik</h2>
<p class="page-subtitle">Rabu, 13 Agustus 2025 - Manajemen Klinik</p>
</div>
</div>
<v-btn
color="white"
elevation="0"
class="add-btn"
@click="openTambahDialog"
>
<v-icon left size="20">mdi-plus-circle</v-icon>
Tambah Klinik
</v-btn>
</div>
</div>
<!-- Table -->
<v-card-text>
<v-data-table
:headers="headers"
:items="masterStore.klinikData"
:items-per-page="10"
class="elevation-0 data-table"
>
<template #item.shift="{ item }">
<v-chip size="small" class="chip-secondary">
{{ item.shift }}
</v-chip>
</template>
<template #item.totalQuota="{ item }">
<span class="body-2 text-semibold">{{ item.totalQuota }}</span>
</template>
<template #item.aksi="{ item }">
<v-btn
size="small"
class="btn-edit mr-2"
variant="flat"
@click="openEditDialog(item)"
>
<v-icon size="16" left>mdi-pencil</v-icon>
Edit
</v-btn>
<v-btn
size="small"
class="btn-delete"
variant="outlined"
@click="handleDelete(item)"
>
<v-icon size="16" left>mdi-delete</v-icon>
Delete
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
<!-- Dialog Tambah/Edit -->
<v-dialog v-model="dialog" max-width="900px" persistent scrollable>
<v-card class="dialog-card">
<v-card-title class="dialog-header">
<span class="headline-4">{{ isEdit ? 'Edit Klinik' : 'Tambah Klinik' }}</span>
<v-btn icon variant="text" size="small" class="btn-close" @click="closeDialog">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-divider/>
<v-card-text class="dialog-content">
<v-form ref="formRef">
<!-- Informasi Dasar -->
<div class="field-group">
<div class="group-label">
<span>Informasi Dasar</span>
</div>
<v-row dense>
<v-col cols="4">
<v-text-field
v-model="formData.kode"
label="Kode"
variant="outlined"
density="compact"
:rules="[v => !!v || 'Kode harus diisi']"
hide-details="auto"
placeholder="AN"
class="mb-3 input-field"
/>
<small class="caption-2">2 Huruf</small>
</v-col>
<v-col cols="4">
<v-text-field
v-model="formData.nama"
label="Nama"
variant="outlined"
density="compact"
:rules="[v => !!v || 'Nama harus diisi']"
hide-details="auto"
placeholder="Gigi dan Mulut"
class="mb-3 input-field"
/>
</v-col>
<v-col cols="4">
<v-text-field
v-model.number="formData.shift"
label="Jumlah Shift"
type="number"
variant="outlined"
density="compact"
:rules="[v => !!v || 'Shift harus diisi', v => v > 0 || 'Minimal 1']"
hide-details="auto"
class="mb-3 input-field"
@update:model-value="updateShiftCount"
/>
</v-col>
</v-row>
<v-checkbox
v-model="formData.autoShift"
label="Auto Shift"
hide-details
color="#0671E0"
density="compact"
class="checkbox-field"
/>
</div>
<v-divider class="my-4 divider-section"/>
<!-- Konfigurasi Shift & Kuota -->
<div class="field-group">
<div class="group-label">
<v-icon size="18" class="icon-label">mdi-clock-outline</v-icon>
<span>Konfigurasi Shift & Kuota</span>
</div>
<div v-for="(shift, index) in formData.jamShiftList" :key="index" class="shift-item">
<v-row dense align="center">
<v-col cols="2">
<div class="shift-badge body-3">Shift {{ index + 1 }}</div>
</v-col>
<v-col cols="3">
<v-text-field
v-model="shift.dari"
label="Mulai"
type="time"
variant="outlined"
density="compact"
hide-details
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model="shift.sampai"
label="Selesai"
type="time"
variant="outlined"
density="compact"
hide-details
class="input-field"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="shift.kuota"
label="Kuota"
type="number"
variant="outlined"
density="compact"
hide-details
placeholder="0"
class="input-field"
/>
</v-col>
<v-col cols="1">
<v-btn
v-if="formData.jamShiftList.length > 1"
icon
size="small"
variant="text"
class="btn-delete-shift"
@click="removeShift(index)"
>
<v-icon size="18">mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
</div>
<div class="total-badge">
<v-icon size="18" class="icon-success">mdi-sigma</v-icon>
<span class="body-3 text-medium">Total Kuota:</span>
<v-chip size="small" variant="flat" class="chip-success">
{{ calculateTotalQuota() }}
</v-chip>
</div>
</div>
<v-divider class="my-4 divider-section"/>
<!-- Jadwal Klinik -->
<div class="field-group">
<div class="group-label">
<v-icon size="18" class="icon-label">mdi-calendar-check</v-icon>
<span>Jadwal Klinik</span>
</div>
<div class="day-selector">
<v-chip
v-for="hari in hariList"
:key="hari.hari"
class="day-chip"
:class="{ 'day-chip-active': formData.jadwalKlinik.includes(hari.hari) }"
label
@click="toggleHari(hari.hari)"
>
<v-icon v-if="formData.jadwalKlinik.includes(hari.hari)" left size="16">mdi-check</v-icon>
{{ hari.hari }}
</v-chip>
</div>
</div>
</v-form>
</v-card-text>
<v-divider/>
<v-card-actions class="dialog-actions">
<v-spacer/>
<v-btn
variant="outlined"
class="btn-cancel"
@click="closeDialog"
>
<v-icon left size="18">mdi-close</v-icon>
Batal
</v-btn>
<v-btn
variant="flat"
class="btn-submit"
@click="submitForm"
>
<v-icon left size="18">mdi-content-save</v-icon>
Simpan
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Snackbar -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
<span class="body-3">{{ snackbar.message }}</span>
<template #actions>
<v-btn variant="text" size="small" @click="snackbar.show = false">Tutup</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script setup>
import { ref } from 'vue';
import { useMasterStore } from '@/stores/masterStore';
const masterStore = useMasterStore();
const dialog = ref(false);
const isEdit = ref(false);
const formRef = ref(null);
const snackbar = ref({
show: false,
message: '',
color: 'success'
});
const headers = ref([
{ title: 'No', value: 'no', sortable: true },
{ title: 'Kode', value: 'kode', sortable: true },
{ title: 'Nama Klinik', value: 'nama', sortable: true },
{ title: 'Shift', value: 'shift', sortable: true },
{ title: 'Total Kuota', value: 'totalQuota', sortable: true },
{ title: 'Aksi', value: 'aksi', sortable: false },
]);
const hariList = ref([
{ no: 1, hari: 'Senin' },
{ no: 2, hari: 'Selasa' },
{ no: 3, hari: 'Rabu' },
{ no: 4, hari: 'Kamis' },
{ no: 5, hari: 'Jum\'at' },
]);
const formData = ref({
id: null,
kode: '',
nama: '',
shift: 1,
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 0 }],
autoShift: false,
jadwalKlinik: [],
});
const calculateTotalQuota = () => {
return formData.value.jamShiftList.reduce((total, shift) => {
return total + (parseInt(shift.kuota) || 0);
}, 0);
};
const updateShiftCount = (newShiftCount) => {
const currentCount = formData.value.jamShiftList.length;
if (newShiftCount > currentCount) {
for (let i = currentCount; i < newShiftCount; i++) {
let defaultDari = '07:00', defaultSampai = '11:00';
if (i === 1) { defaultDari = '13:00'; defaultSampai = '16:00'; }
else if (i === 2) { defaultDari = '18:00'; defaultSampai = '20:00'; }
else if (i > 2) {
const prevShift = formData.value.jamShiftList[i - 1];
const [prevHour] = prevShift.sampai.split(':');
const nextHour = (parseInt(prevHour) + 1) % 24;
defaultDari = `${String(nextHour).padStart(2, '0')}:00`;
defaultSampai = `${String((nextHour + 4) % 24).padStart(2, '0')}:00`;
}
formData.value.jamShiftList.push({ dari: defaultDari, sampai: defaultSampai, kuota: 0 });
}
} else if (newShiftCount < currentCount && newShiftCount > 0) {
formData.value.jamShiftList = formData.value.jamShiftList.slice(0, newShiftCount);
}
};
const removeShift = (index) => {
if (formData.value.jamShiftList.length > 1) {
formData.value.jamShiftList.splice(index, 1);
formData.value.shift = formData.value.jamShiftList.length;
}
};
const toggleHari = (hari) => {
const index = formData.value.jadwalKlinik.indexOf(hari);
if (index > -1) {
formData.value.jadwalKlinik.splice(index, 1);
} else {
formData.value.jadwalKlinik.push(hari);
}
};
const openTambahDialog = () => {
isEdit.value = false;
resetForm();
dialog.value = true;
};
const openEditDialog = (item) => {
isEdit.value = true;
formData.value = {
id: item.id,
kode: item.kode,
nama: item.nama,
shift: item.shift,
jamShiftList: JSON.parse(JSON.stringify(item.jamShiftList)),
autoShift: item.autoShift || false,
jadwalKlinik: [...(item.jadwalKlinik || [])],
};
dialog.value = true;
};
const closeDialog = () => {
dialog.value = false;
resetForm();
};
const resetForm = () => {
formData.value = {
id: null,
kode: '',
nama: '',
shift: 1,
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 0 }],
autoShift: false,
jadwalKlinik: [],
};
if (formRef.value) {
formRef.value.reset();
}
};
const submitForm = async () => {
const { valid } = await formRef.value.validate();
if (!valid) return;
let result;
if (isEdit.value) {
result = masterStore.updateKlinik(formData.value);
} else {
result = masterStore.addKlinik(formData.value);
}
if (result.success) {
snackbar.value = { show: true, message: result.message, color: 'success' };
closeDialog();
} else {
snackbar.value = { show: true, message: result.message, color: 'error' };
}
};
const handleDelete = (item) => {
if (confirm(`Hapus klinik ${item.nama}?`)) {
const result = masterStore.deleteKlinik(item.id);
snackbar.value = {
show: true,
message: result.message,
color: result.success ? 'success' : 'error'
};
}
};
</script>
<style scoped lang="scss">
// Import Design System SCSS Variables & Mixins
// @import '@/assets/styles/variables';
// @import '@/assets/styles/typography';
// Colors from Design System
$neutral-900: #212121;
$neutral-800: #4D4D4D;
$neutral-700: #717171;
$neutral-600: #89939E;
$neutral-500: #ABBED1;
$neutral-400: #E5F7FA;
$neutral-300: #F5F7FA;
$neutral-100: #FFFFFF;
$primary-700: #FF9B1B;
$primary-600: #FFA532;
$secondary-700: #0053AD;
$secondary-600: #0671E0;
$secondary-300: #DBEDFF;
$success-600: #009262;
$success-300: #84DFC1;
$success-200: #F1FBF8;
$danger-600: #E02B1D;
$danger-400: #FF5A4F;
// Font Family & Weights
$font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
$font-weight-regular: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
// Apply font family globally
* {
font-family: $font-family-base;
}
// ============================================
// PAGE HEADER
// ============================================
.page-header {
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
border-radius: 16px 16px 0 0;
box-shadow: 0 4px 16px rgba(6, 113, 224, 0.2);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32px;
color: $neutral-100;
}
.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: 36px;
line-height: 44px;
font-weight: $font-weight-semibold;
margin: 0;
color: $neutral-100;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.page-subtitle {
margin: 4px 0 0 0;
opacity: 0.9;
font-size: 16px;
line-height: 24px;
font-weight: $font-weight-regular;
color: $neutral-100;
}
.add-btn {
font-weight: $font-weight-semibold;
text-transform: none;
letter-spacing: 0.5px;
font-size: 16px;
line-height: 24px;
color: $secondary-600 !important;
}
// ============================================
// DATA TABLE
// ============================================
.data-table {
font-family: $font-family-base;
}
.chip-secondary {
background-color: $secondary-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-medium;
}
.btn-edit {
background-color: $primary-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
.btn-delete {
border-color: $danger-600 !important;
color: $danger-600 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 14px;
line-height: 20px;
}
// ============================================
// DIALOG
// ============================================
.dialog-card {
font-family: $font-family-base;
}
.dialog-header {
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
color: $neutral-100;
padding: 20px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.headline-4 {
font-size: 20px;
line-height: 28px;
font-weight: $font-weight-semibold;
margin: 0;
}
.btn-close {
color: $neutral-100 !important;
}
.dialog-content {
padding: 24px !important;
background: $neutral-300;
}
.dialog-actions {
padding: 16px 24px;
background: $neutral-300;
}
// ============================================
// FORM ELEMENTS
// ============================================
.field-group {
background: $neutral-100;
padding: 20px;
border-radius: 12px;
margin-bottom: 0;
border: 1px solid $neutral-400;
}
.group-label {
font-size: 14px;
line-height: 20px;
font-weight: $font-weight-semibold;
color: $secondary-600;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 6px;
}
.icon-label {
color: $secondary-600 !important;
}
.caption-2 {
font-size: 12px;
line-height: 16px;
font-weight: $font-weight-regular;
color: $neutral-700;
}
.body-2 {
font-size: 16px;
line-height: 24px;
font-weight: $font-weight-regular;
}
.body-3 {
font-size: 14px;
line-height: 20px;
font-weight: $font-weight-regular;
}
.text-semibold {
font-weight: $font-weight-semibold !important;
}
.text-medium {
font-weight: $font-weight-medium !important;
}
.input-field {
font-size: 14px;
line-height: 20px;
}
.checkbox-field {
font-size: 14px;
line-height: 20px;
}
.divider-section {
border-color: $neutral-400 !important;
}
// ============================================
// SHIFT ITEMS
// ============================================
.shift-item {
background: $neutral-300;
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
border: 1px solid $neutral-500;
}
.shift-badge {
background: linear-gradient(135deg, $secondary-600 0%, $secondary-700 100%);
color: $neutral-100;
padding: 8px;
border-radius: 6px;
text-align: center;
font-weight: $font-weight-semibold;
}
.btn-delete-shift {
color: $danger-600 !important;
}
.total-badge {
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
background: $success-200;
border-radius: 8px;
margin-top: 8px;
border: 1px solid $success-300;
}
.icon-success {
color: $success-600 !important;
}
.chip-success {
background-color: $success-600 !important;
color: $neutral-100 !important;
font-weight: $font-weight-semibold;
}
// ============================================
// DAY SELECTOR
// ============================================
.day-selector {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.day-chip {
cursor: pointer;
font-size: 14px;
line-height: 20px;
font-weight: $font-weight-medium;
border: 1px solid $neutral-500;
background-color: $neutral-100;
color: $neutral-800;
&:hover {
background-color: $secondary-300;
}
}
.day-chip-active {
background-color: $secondary-600 !important;
color: $neutral-100 !important;
border-color: $secondary-600 !important;
}
// ============================================
// BUTTONS
// ============================================
.btn-cancel {
border-color: $neutral-600 !important;
color: $neutral-800 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 16px;
line-height: 24px;
min-width: 100px;
}
.btn-submit {
background-color: $secondary-600 !important;
color: $neutral-100 !important;
text-transform: none;
font-weight: $font-weight-semibold;
font-size: 16px;
line-height: 24px;
min-width: 100px;
}
</style>