1262 lines
33 KiB
Vue
1262 lines
33 KiB
Vue
<!-- eslint-disable vue/valid-v-slot -->
|
|
<template>
|
|
<div>
|
|
<!-- Header -->
|
|
<div class="page-header">
|
|
<div class="header-content">
|
|
<div class="header-left">
|
|
<div class="header-icon">
|
|
<v-icon size="28" 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>
|
|
</div>
|
|
</div>
|
|
|
|
<v-container>
|
|
<!-- Action Bar -->
|
|
<div class="action-bar mb-4">
|
|
<v-spacer></v-spacer>
|
|
<v-btn
|
|
color="primary-600"
|
|
variant="flat"
|
|
elevation="0"
|
|
class="action-btn text-white"
|
|
@click="openTambahDialog"
|
|
>
|
|
<v-icon left size="20">mdi-plus-circle</v-icon>
|
|
Tambah Klinik
|
|
</v-btn>
|
|
</div>
|
|
|
|
<v-card>
|
|
<!-- Table -->
|
|
<v-card-text>
|
|
<!-- Filter Row -->
|
|
<div class="d-flex flex-wrap align-center justify-space-between mb-4">
|
|
<div class="d-flex align-center">
|
|
<span class="mr-2 body-3">Show</span>
|
|
<v-select
|
|
v-model="itemsPerPage"
|
|
:items="[10, 25, 50, 100]"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
style="max-width: 80px;"
|
|
rounded
|
|
class="mr-2"
|
|
></v-select>
|
|
<span class="body-3">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
|
|
style="min-width: 250px;"
|
|
></v-text-field>
|
|
</div>
|
|
</div>
|
|
|
|
<v-data-table
|
|
v-model:page="page"
|
|
:headers="headers"
|
|
:items="masterStore.klinikData"
|
|
:items-per-page="itemsPerPage"
|
|
:search="search"
|
|
item-value="id"
|
|
class="elevation-0 data-table"
|
|
hover
|
|
@update:itemsLength="filteredTotal = $event"
|
|
>
|
|
<template #item.jenisLayanan="{ item }">
|
|
<v-chip
|
|
size="small"
|
|
:class="['Reguler', 'REGULER', 'JKN'].includes(item.jenisLayanan) ? 'chip-reguler' : 'chip-eksekutif'"
|
|
>
|
|
{{ item.jenisLayanan }}
|
|
</v-chip>
|
|
</template>
|
|
|
|
<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"
|
|
color="warning-600"
|
|
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"
|
|
color="error-600"
|
|
class="btn-delete"
|
|
variant="flat"
|
|
@click="handleDelete(item)"
|
|
>
|
|
<v-icon size="16" left>mdi-delete</v-icon>
|
|
Delete
|
|
</v-btn>
|
|
</template>
|
|
|
|
<template #bottom>
|
|
<v-row class="ma-2" align="center">
|
|
<v-col cols="12" sm="6" class="d-flex align-center justify-start body-3 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"
|
|
total-visible="5"
|
|
rounded="circle"
|
|
size="small"
|
|
></v-pagination>
|
|
</v-col>
|
|
</v-row>
|
|
</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="6">
|
|
<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="6">
|
|
<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-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"/>
|
|
|
|
<!-- Jadwal Klinik -->
|
|
<div class="field-group">
|
|
<div class="group-label">
|
|
<v-icon size="18" class="icon-label">mdi-calendar-check</v-icon>
|
|
<span>Pilih Hari Operasional</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-divider class="my-4 divider-section"/>
|
|
|
|
<!-- Konfigurasi Shift Per Hari -->
|
|
<div v-if="formData.jadwalKlinik.length > 0" class="field-group">
|
|
<div class="group-label">
|
|
<v-icon size="18" class="icon-label">mdi-clock-outline</v-icon>
|
|
<span>Konfigurasi Shift & Kuota Per Hari</span>
|
|
</div>
|
|
|
|
<!-- Day Tabs -->
|
|
<div class="day-tabs">
|
|
<v-chip
|
|
v-for="hari in formData.jadwalKlinik"
|
|
:key="hari"
|
|
class="day-tab"
|
|
:class="{ 'day-tab-active': activeDay === hari }"
|
|
@click="activeDay = hari"
|
|
>
|
|
{{ hari }}
|
|
</v-chip>
|
|
</div>
|
|
|
|
<!-- Shift Configuration for Active Day -->
|
|
<div class="shifts-container">
|
|
<div v-for="(shift, index) in getShiftsForDay(activeDay)" :key="index" class="shift-item">
|
|
<v-row dense align="center">
|
|
<v-col cols="2">
|
|
<div class="shift-badge body-3">Shift {{ index + 1 }}</div>
|
|
</v-col>
|
|
|
|
<v-col cols="3">
|
|
<v-text-field
|
|
v-model="shift.dari"
|
|
label="Mulai"
|
|
type="time"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
class="input-field"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="3">
|
|
<v-text-field
|
|
v-model="shift.sampai"
|
|
label="Selesai"
|
|
type="time"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
class="input-field"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="3">
|
|
<v-text-field
|
|
v-model.number="shift.kuota"
|
|
label="Kuota"
|
|
type="number"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
placeholder="0"
|
|
class="input-field"
|
|
/>
|
|
</v-col>
|
|
|
|
<v-col cols="1">
|
|
<v-btn
|
|
v-if="getShiftsForDay(activeDay).length > 1"
|
|
icon
|
|
size="small"
|
|
variant="text"
|
|
class="btn-delete-shift"
|
|
@click="removeShiftFromDay(activeDay, index)"
|
|
>
|
|
<v-icon size="18">mdi-delete</v-icon>
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</div>
|
|
|
|
<v-btn
|
|
size="small"
|
|
variant="outlined"
|
|
class="btn-add-shift mt-2"
|
|
@click="addShiftToDay(activeDay)"
|
|
>
|
|
<v-icon left size="16">mdi-plus</v-icon>
|
|
Tambah Shift untuk {{ activeDay }}
|
|
</v-btn>
|
|
</div>
|
|
|
|
<div class="total-badge mt-3">
|
|
<v-icon size="18" class="icon-success">mdi-sigma</v-icon>
|
|
<span class="body-3 text-medium">Total Kuota Semua Hari:</span>
|
|
<v-chip size="small" variant="flat" class="chip-success">
|
|
{{ calculateTotalQuota() }}
|
|
</v-chip>
|
|
</div>
|
|
</div>
|
|
|
|
<v-divider class="my-4 divider-section"/>
|
|
|
|
<!-- Tanggal Tutup Klinik -->
|
|
<div class="field-group">
|
|
<div class="group-label">
|
|
<v-icon size="18" class="icon-label">mdi-calendar-remove</v-icon>
|
|
<span>Jadwal Tutup Klinik</span>
|
|
</div>
|
|
|
|
<p class="helper-text mb-3">Tambahkan tanggal ketika klinik ini tutup (tidak beroperasi)</p>
|
|
|
|
<!-- Input Tanggal Tutup -->
|
|
<v-row dense>
|
|
<v-col cols="9">
|
|
<v-text-field
|
|
v-model="newClosedDate"
|
|
type="date"
|
|
label="Pilih Tanggal Tutup"
|
|
variant="outlined"
|
|
density="compact"
|
|
hide-details
|
|
:min="new Date().toISOString().split('T')[0]"
|
|
class="input-field"
|
|
/>
|
|
</v-col>
|
|
<v-col cols="3">
|
|
<v-btn
|
|
color="danger-600"
|
|
class="text-white btn-add-closed-date"
|
|
size="large"
|
|
block
|
|
@click="addClosedDate"
|
|
:disabled="!newClosedDate"
|
|
>
|
|
<v-icon left size="18">mdi-plus</v-icon>
|
|
Tambah
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- List Tanggal Tutup -->
|
|
<div v-if="formData.tanggalTutup.length > 0" class="closed-dates-list mt-4">
|
|
<div class="list-header">
|
|
<span class="body-3 text-medium">{{ formData.tanggalTutup.length }} Hari Tutup</span>
|
|
<v-btn
|
|
size="x-small"
|
|
variant="text"
|
|
color="danger-600"
|
|
@click="clearAllClosedDates"
|
|
>
|
|
<v-icon size="14">mdi-delete-sweep</v-icon>
|
|
Hapus Semua
|
|
</v-btn>
|
|
</div>
|
|
|
|
<div class="closed-dates-items">
|
|
<div
|
|
v-for="(date, idx) in sortedClosedDates"
|
|
:key="idx"
|
|
class="closed-date-item"
|
|
>
|
|
<div class="date-info">
|
|
<v-icon size="16" color="danger-600">mdi-calendar-remove</v-icon>
|
|
<span class="date-text">{{ formatClosedDate(date) }}</span>
|
|
</div>
|
|
<v-btn
|
|
icon
|
|
size="x-small"
|
|
variant="text"
|
|
color="danger-600"
|
|
@click="removeClosedDate(date)"
|
|
>
|
|
<v-icon size="16">mdi-close</v-icon>
|
|
</v-btn>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="empty-closed-dates mt-3">
|
|
<v-icon size="24" color="neutral-600">mdi-calendar-check</v-icon>
|
|
<p class="empty-text">Belum ada tanggal tutup</p>
|
|
</div>
|
|
</div>
|
|
</v-form>
|
|
</v-card-text>
|
|
|
|
<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>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useMasterStore } from '@/stores/masterStore';
|
|
import { useClinicStore } from '@/stores/clinicStore';
|
|
|
|
const masterStore = useMasterStore();
|
|
const clinicStore = useClinicStore();
|
|
const page = ref(1);
|
|
const itemsPerPage = ref(10);
|
|
const search = ref('');
|
|
const filteredTotal = ref(masterStore.klinikData.length);
|
|
|
|
import { watch } from 'vue';
|
|
watch(() => masterStore.klinikData.length, (newLen) => {
|
|
if (!search.value) filteredTotal.value = newLen;
|
|
}, { immediate: true });
|
|
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: 'Jenis Layanan', value: 'jenisLayanan', sortable: true },
|
|
{ title: 'Shift', value: 'shift', sortable: true },
|
|
{ title: 'Total Kuota', value: 'totalQuota', sortable: true },
|
|
{ title: 'Aksi', value: 'aksi', sortable: false },
|
|
]);
|
|
|
|
const pageCount = computed(() => {
|
|
return Math.ceil(filteredTotal.value / itemsPerPage.value) || 1;
|
|
});
|
|
|
|
const showingEntriesText = computed(() => {
|
|
if (filteredTotal.value === 0) return 'Showing 0 to 0 of 0 entries';
|
|
const start = (page.value - 1) * itemsPerPage.value + 1;
|
|
const end = Math.min(page.value * itemsPerPage.value, filteredTotal.value);
|
|
return `Showing ${start} to ${end} of ${filteredTotal.value} entries${search.value ? ' (filtered)' : ''}`;
|
|
});
|
|
|
|
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,
|
|
jamShiftPerHari: {}, // { 'Senin': [{ dari, sampai, kuota }], ... }
|
|
jamShiftList: [], // Legacy, kept for compatibility
|
|
autoShift: false,
|
|
jadwalKlinik: [],
|
|
tanggalTutup: [], // Array of closed dates
|
|
});
|
|
|
|
const activeDay = ref('Senin'); // Currently selected day for shift configuration
|
|
const newClosedDate = ref(''); // For adding new closed date
|
|
|
|
// Computed property for sorted closed dates
|
|
const sortedClosedDates = computed(() => {
|
|
return [...formData.value.tanggalTutup].sort((a, b) => new Date(a) - new Date(b));
|
|
});
|
|
|
|
// Format closed date for display
|
|
const formatClosedDate = (dateString) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('id-ID', {
|
|
weekday: 'long',
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric'
|
|
});
|
|
};
|
|
|
|
// Add new closed date
|
|
const addClosedDate = () => {
|
|
if (!newClosedDate.value) return;
|
|
|
|
// Check if date already exists
|
|
if (formData.value.tanggalTutup.includes(newClosedDate.value)) {
|
|
snackbar.value = {
|
|
show: true,
|
|
message: 'Tanggal sudah ada dalam daftar',
|
|
color: 'warning'
|
|
};
|
|
return;
|
|
}
|
|
|
|
formData.value.tanggalTutup.push(newClosedDate.value);
|
|
newClosedDate.value = ''; // Reset input
|
|
};
|
|
|
|
// Remove a specific closed date
|
|
const removeClosedDate = (dateToRemove) => {
|
|
const index = formData.value.tanggalTutup.findIndex(d => d === dateToRemove);
|
|
if (index > -1) {
|
|
formData.value.tanggalTutup.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
// Clear all closed dates
|
|
const clearAllClosedDates = () => {
|
|
if (confirm('Hapus semua tanggal tutup?')) {
|
|
formData.value.tanggalTutup = [];
|
|
}
|
|
};
|
|
|
|
const calculateTotalQuota = () => {
|
|
let total = 0;
|
|
Object.values(formData.value.jamShiftPerHari || {}).forEach(shifts => {
|
|
if (Array.isArray(shifts)) {
|
|
shifts.forEach(shift => {
|
|
total += parseInt(shift.kuota) || 0;
|
|
});
|
|
}
|
|
});
|
|
return total;
|
|
};
|
|
|
|
// Add shift for specific day
|
|
const addShiftToDay = (hari) => {
|
|
if (!formData.value.jamShiftPerHari[hari]) {
|
|
formData.value.jamShiftPerHari[hari] = [];
|
|
}
|
|
|
|
const shifts = formData.value.jamShiftPerHari[hari];
|
|
let defaultDari = '07:00', defaultSampai = '11:00';
|
|
|
|
if (shifts.length === 1) { defaultDari = '13:00'; defaultSampai = '16:00'; }
|
|
else if (shifts.length === 2) { defaultDari = '18:00'; defaultSampai = '20:00'; }
|
|
else if (shifts.length > 2) {
|
|
const prevShift = shifts[shifts.length - 1];
|
|
const [prevHour]= prevShift.sampai.split(':');
|
|
const nextHour = (parseInt(prevHour) + 1) % 24;
|
|
defaultDari = `${String(nextHour).padStart(2, '0')}:00`;
|
|
defaultSampai = `${String((nextHour + 4) % 24).padStart(2, '0')}:00`;
|
|
}
|
|
|
|
shifts.push({ dari: defaultDari, sampai: defaultSampai, kuota: 0 });
|
|
};
|
|
|
|
// Remove shift from specific day
|
|
const removeShiftFromDay = (hari, index) => {
|
|
if (formData.value.jamShiftPerHari[hari] && formData.value.jamShiftPerHari[hari].length > 1) {
|
|
formData.value.jamShiftPerHari[hari].splice(index, 1);
|
|
}
|
|
};
|
|
|
|
// Get shifts for a specific day
|
|
const getShiftsForDay = (hari) => {
|
|
return formData.value.jamShiftPerHari[hari] || [];
|
|
};
|
|
|
|
// Legacy compatibility methods
|
|
const updateShiftCount = (newShiftCount) => {
|
|
// This is kept for compatibility but not used with new per-day structure
|
|
formData.value.shift = newShiftCount;
|
|
};
|
|
|
|
const removeShift = (index) => {
|
|
// Legacy method, kept for compatibility
|
|
};
|
|
|
|
const toggleHari = (hari) => {
|
|
const index = formData.value.jadwalKlinik.indexOf(hari);
|
|
if (index > -1) {
|
|
// Removing day - also remove its shifts
|
|
formData.value.jadwalKlinik.splice(index, 1);
|
|
delete formData.value.jamShiftPerHari[hari];
|
|
} else {
|
|
// Adding day - initialize with one shift
|
|
formData.value.jadwalKlinik.push(hari);
|
|
if (!formData.value.jamShiftPerHari[hari]) {
|
|
formData.value.jamShiftPerHari[hari] = [{ dari: '07:00', sampai: '11:00', kuota: 100 }];
|
|
}
|
|
}
|
|
};
|
|
|
|
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,
|
|
jamShiftPerHari: JSON.parse(JSON.stringify(item.jamShiftPerHari || {})),
|
|
jamShiftList: [], // Legacy
|
|
autoShift: item.autoShift || false,
|
|
jadwalKlinik: [...(item.jadwalKlinik || [])],
|
|
tanggalTutup: [...(item.tanggalTutup || [])],
|
|
};
|
|
activeDay.value = formData.value.jadwalKlinik[0] || 'Senin';
|
|
dialog.value = true;
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
dialog.value = false;
|
|
resetForm();
|
|
};
|
|
|
|
const resetForm = () => {
|
|
formData.value = {
|
|
id: null,
|
|
kode: '',
|
|
nama: '',
|
|
shift: 1,
|
|
jamShiftPerHari: {},
|
|
jamShiftList: [],
|
|
autoShift: false,
|
|
jadwalKlinik: [],
|
|
tanggalTutup: [],
|
|
};
|
|
activeDay.value = 'Senin';
|
|
newClosedDate.value = '';
|
|
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'
|
|
};
|
|
}
|
|
};
|
|
|
|
// Fetch Reguler clinics from API on mount
|
|
onMounted(async () => {
|
|
console.log('🚀 MasterKlinik mounted, fetching clinics...');
|
|
console.log('📊 Current clinic count before fetch:', masterStore.klinikData.length);
|
|
|
|
try {
|
|
const result = await clinicStore.fetchRegulerClinics();
|
|
console.log('✅ Fetch result:', result);
|
|
console.log('📊 Current clinic count after fetch:', masterStore.klinikData.length);
|
|
|
|
if (!result.success) {
|
|
console.error('❌ Fetch failed:', result.message);
|
|
snackbar.value = {
|
|
show: true,
|
|
message: result.message,
|
|
color: 'warning'
|
|
};
|
|
} else {
|
|
console.log('🎉 Fetch successful:', result.message);
|
|
// Show success message
|
|
snackbar.value = {
|
|
show: true,
|
|
message: result.message,
|
|
color: 'success'
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error in onMounted:', error);
|
|
snackbar.value = {
|
|
show: true,
|
|
message: `Error: ${error.message}`,
|
|
color: 'error'
|
|
};
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
// 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: #3556AE;
|
|
$primary-600: #3A61C9;
|
|
|
|
$secondary-700: #E65A0D;
|
|
$secondary-600: #F16F29;
|
|
$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, $primary-600 0%, $primary-700 100%);
|
|
border-radius: 0 !important;
|
|
box-shadow: 0 4px 16px rgba(58, 97, 201, 0.2);
|
|
}
|
|
|
|
.header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 28px;
|
|
height: 80px;
|
|
color: $neutral-100;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.header-icon {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
margin-right: 16px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 32px;
|
|
line-height: 40px;
|
|
font-weight: $font-weight-semibold;
|
|
margin: 0;
|
|
color: $neutral-100;
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.page-subtitle {
|
|
margin: 2px 0 0 0;
|
|
opacity: 0.9;
|
|
font-size: 15px;
|
|
line-height: 22px;
|
|
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: $primary-600 !important;
|
|
}
|
|
|
|
// ============================================
|
|
// ACTION BAR
|
|
// ============================================
|
|
.action-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px;
|
|
background: $neutral-100;
|
|
border-radius: 12px;
|
|
border: 1px solid $neutral-400;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.action-btn {
|
|
font-weight: $font-weight-semibold;
|
|
text-transform: none;
|
|
letter-spacing: 0.5px;
|
|
font-size: 16px;
|
|
line-height: 24px;
|
|
}
|
|
|
|
// ============================================
|
|
// DATA TABLE
|
|
// ============================================
|
|
.data-table {
|
|
font-family: $font-family-base;
|
|
}
|
|
|
|
.chip-secondary {
|
|
background-color: $primary-600 !important;
|
|
color: $neutral-100 !important;
|
|
font-weight: $font-weight-medium;
|
|
}
|
|
|
|
.chip-reguler {
|
|
background-color: $success-600 !important;
|
|
color: #FFFFFF !important;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.chip-eksekutif {
|
|
background-color: $secondary-600 !important;
|
|
color: #FFFFFF !important;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.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, $primary-600 0%, $primary-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;
|
|
}
|
|
|
|
// ============================================
|
|
// DAY TABS (for shift configuration)
|
|
// ============================================
|
|
.day-tabs {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
padding: 12px;
|
|
background: $neutral-300;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.day-tab {
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
font-weight: $font-weight-medium;
|
|
border: 1px solid $neutral-500;
|
|
background-color: $neutral-100;
|
|
color: $neutral-800;
|
|
|
|
&:hover {
|
|
background-color: $primary-600;
|
|
color: $neutral-100;
|
|
}
|
|
}
|
|
|
|
.day-tab-active {
|
|
background-color: $primary-600 !important;
|
|
color: $neutral-100 !important;
|
|
border-color: $primary-600 !important;
|
|
}
|
|
|
|
.shifts-container {
|
|
padding: 12px;
|
|
background: $neutral-300;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.btn-add-shift {
|
|
border-color: $primary-600 !important;
|
|
color: $primary-600 !important;
|
|
text-transform: none;
|
|
font-weight: $font-weight-semibold;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
}
|
|
|
|
// ============================================
|
|
// CLOSED DATES
|
|
// ============================================
|
|
.helper-text {
|
|
font-size: 13px;
|
|
line-height: 18px;
|
|
font-weight: $font-weight-regular;
|
|
color: $neutral-700;
|
|
margin: 0;
|
|
}
|
|
|
|
.btn-add-closed-date {
|
|
height: 40px !important;
|
|
font-weight: $font-weight-semibold;
|
|
text-transform: none;
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.closed-dates-list {
|
|
background: $neutral-300;
|
|
border-radius: 8px;
|
|
border: 1px solid $neutral-400;
|
|
padding: 12px;
|
|
}
|
|
|
|
.list-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding-bottom: 8px;
|
|
margin-bottom: 8px;
|
|
border-bottom: 1px solid $neutral-400;
|
|
|
|
span {
|
|
color: $neutral-800;
|
|
}
|
|
}
|
|
|
|
.closed-dates-items {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.closed-date-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 12px;
|
|
background: $neutral-100;
|
|
border-radius: 6px;
|
|
border: 1px solid $neutral-400;
|
|
transition: all 0.2s ease;
|
|
|
|
&:hover {
|
|
border-color: $danger-400;
|
|
background: rgba($danger-600, 0.02);
|
|
}
|
|
}
|
|
|
|
.date-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex: 1;
|
|
}
|
|
|
|
.date-text {
|
|
font-size: 14px;
|
|
line-height: 20px;
|
|
font-weight: $font-weight-medium;
|
|
color: $neutral-800;
|
|
}
|
|
|
|
.empty-closed-dates {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px 16px;
|
|
background: $neutral-300;
|
|
border-radius: 8px;
|
|
border: 1px dashed $neutral-500;
|
|
gap: 8px;
|
|
}
|
|
|
|
.empty-text {
|
|
font-size: 13px;
|
|
line-height: 18px;
|
|
font-weight: $font-weight-medium;
|
|
color: $neutral-700;
|
|
margin: 0;
|
|
}
|
|
|
|
// ============================================
|
|
// BUTTONS
|
|
// ============================================
|
|
.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> |