update anjungan, store management
This commit is contained in:
No files matched your search
@@ -1,7 +1,20 @@
|
||||
<template>
|
||||
<v-card class="current-patient-card" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<div class="section-label mb-3">SEDANG DIPROSES</div>
|
||||
<div class="section-header mb-3">
|
||||
<div class="section-label">SEDANG DIPROSES</div>
|
||||
<v-btn
|
||||
v-if="patient"
|
||||
class="call-button"
|
||||
color="primary-600"
|
||||
variant="flat"
|
||||
size="small"
|
||||
@click="$emit('call')"
|
||||
>
|
||||
<v-icon start size="18">mdi-bullhorn</v-icon>
|
||||
Panggil
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="patient" class="patient-details" :class="`patient-details-${theme}`">
|
||||
<div class="patient-number mb-2">{{ patient.noAntrian.split(" |")[0] }}</div>
|
||||
@@ -80,7 +93,7 @@ defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['action', 'change-klinik', 'process-next']);
|
||||
defineEmits(['action', 'change-klinik', 'process-next', 'call']);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -90,6 +103,12 @@ defineEmits(['action', 'change-klinik', 'process-next']);
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -98,6 +117,12 @@ defineEmits(['action', 'change-klinik', 'process-next']);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.call-button {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.patient-details {
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// composables/useAPI.ts
|
||||
// Generic API composable untuk semua API calls
|
||||
// Base composable yang bisa di-extend untuk specific resources
|
||||
|
||||
export interface APIResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const useAPI = () => {
|
||||
const config = useRuntimeConfig();
|
||||
const baseURL = config.public.apiBaseUrl || '/api';
|
||||
|
||||
/**
|
||||
* Generic GET request
|
||||
*/
|
||||
const get = async <T = any>(endpoint: string, options: any = {}): Promise<T> => {
|
||||
try {
|
||||
const response = await $fetch<APIResponse<T>>(`${baseURL}${endpoint}`, {
|
||||
method: 'GET',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.success && response.data !== undefined) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to fetch data');
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error GET ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic POST request
|
||||
*/
|
||||
const post = async <T = any>(endpoint: string, body: any, options: any = {}): Promise<T> => {
|
||||
try {
|
||||
const response = await $fetch<APIResponse<T>>(`${baseURL}${endpoint}`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.success && response.data !== undefined) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to create data');
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error POST ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic PATCH request
|
||||
*/
|
||||
const patch = async <T = any>(endpoint: string, body: any, options: any = {}): Promise<T> => {
|
||||
try {
|
||||
const response = await $fetch<APIResponse<T>>(`${baseURL}${endpoint}`, {
|
||||
method: 'PATCH',
|
||||
body,
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.success && response.data !== undefined) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to update data');
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error PATCH ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic DELETE request
|
||||
*/
|
||||
const del = async <T = any>(endpoint: string, options: any = {}): Promise<T> => {
|
||||
try {
|
||||
const response = await $fetch<APIResponse<T>>(`${baseURL}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return response.data as T;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to delete data');
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error DELETE ${endpoint}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
get,
|
||||
post,
|
||||
patch,
|
||||
delete: del,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// composables/useClinicAPI.ts
|
||||
// Composable untuk API calls terkait klinik/poli
|
||||
|
||||
import { useAPI } from './useAPI';
|
||||
|
||||
export interface Clinic {
|
||||
id: number;
|
||||
kode: string;
|
||||
name: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
shift?: string;
|
||||
schedule?: string;
|
||||
available?: boolean;
|
||||
doctors?: string[];
|
||||
shifts?: Array<{ name: string; quota: number }>;
|
||||
totalQuota?: number;
|
||||
jamShiftList?: Array<{ dari: string; sampai: string; kuota: number }>;
|
||||
autoShift?: boolean;
|
||||
jadwalKlinik?: string[];
|
||||
}
|
||||
|
||||
export const useClinicAPI = () => {
|
||||
const api = useAPI();
|
||||
|
||||
/**
|
||||
* Fetch all clinics from database
|
||||
*/
|
||||
const fetchAllClinics = async (): Promise<Clinic[]> => {
|
||||
try {
|
||||
return await api.get<Clinic[]>('/clinics');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error fetching clinics:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch single clinic by ID
|
||||
*/
|
||||
const fetchClinic = async (id: number): Promise<Clinic | null> => {
|
||||
try {
|
||||
return await api.get<Clinic>(`/clinics/${id}`);
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error fetching clinic:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch clinics by kode
|
||||
*/
|
||||
const fetchClinicByKode = async (kode: string): Promise<Clinic | null> => {
|
||||
try {
|
||||
return await api.get<Clinic>(`/clinics/kode/${kode}`);
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error fetching clinic by kode:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new clinic
|
||||
*/
|
||||
const createClinic = async (clinicData: Partial<Clinic>): Promise<Clinic> => {
|
||||
try {
|
||||
return await api.post<Clinic>('/clinics', clinicData);
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error creating clinic:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update clinic
|
||||
*/
|
||||
const updateClinic = async (id: number, updates: Partial<Clinic>): Promise<Clinic> => {
|
||||
try {
|
||||
return await api.patch<Clinic>(`/clinics/${id}`, updates);
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error updating clinic:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete clinic
|
||||
*/
|
||||
const deleteClinic = async (id: number): Promise<boolean> => {
|
||||
try {
|
||||
await api.delete(`/clinics/${id}`);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error deleting clinic:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get clinics for dropdown (formatted for dropdown use)
|
||||
*/
|
||||
const fetchClinicsForDropdown = async (): Promise<Clinic[]> => {
|
||||
try {
|
||||
const clinics = await fetchAllClinics();
|
||||
// Format sesuai kebutuhan dropdown
|
||||
return clinics.map(clinic => ({
|
||||
id: clinic.id,
|
||||
name: clinic.name,
|
||||
kode: clinic.kode,
|
||||
icon: clinic.icon,
|
||||
available: clinic.available ?? true,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error fetching clinics for dropdown:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync local clinics with database
|
||||
*/
|
||||
const syncWithDatabase = async (localClinics: Clinic[]): Promise<Clinic[]> => {
|
||||
try {
|
||||
const dbClinics = await fetchAllClinics();
|
||||
|
||||
// Merge strategy: prefer database data
|
||||
const merged = new Map<number, Clinic>();
|
||||
|
||||
// Add database clinics
|
||||
dbClinics.forEach(clinic => {
|
||||
merged.set(clinic.id, clinic);
|
||||
});
|
||||
|
||||
// Add local clinics that don't exist in DB
|
||||
localClinics.forEach(localClinic => {
|
||||
if (!merged.has(localClinic.id)) {
|
||||
merged.set(localClinic.id, localClinic);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(merged.values());
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error syncing clinics with database:', error);
|
||||
// Return local clinics as fallback
|
||||
return localClinics;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetchAllClinics,
|
||||
fetchClinic,
|
||||
fetchClinicByKode,
|
||||
createClinic,
|
||||
updateClinic,
|
||||
deleteClinic,
|
||||
fetchClinicsForDropdown,
|
||||
syncWithDatabase,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// composables/useQueueAPI.ts
|
||||
// Composable untuk API calls terkait antrian pasien
|
||||
|
||||
export interface Patient {
|
||||
no: number;
|
||||
jamPanggil: string;
|
||||
barcode: string;
|
||||
noAntrian: string;
|
||||
shift: string;
|
||||
klinik: string;
|
||||
fastTrack: string;
|
||||
pembayaran: string;
|
||||
status: 'waiting' | 'pending' | 'di-loket' | 'di-klinik' | 'selesai' | 'terlambat';
|
||||
processStage: 'loket' | 'klinik' | 'penunjang';
|
||||
createdAt: string;
|
||||
registrationType?: 'online' | 'onsite';
|
||||
visitType?: string;
|
||||
visitDate?: string;
|
||||
}
|
||||
|
||||
export interface QueueAPIResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const useQueueAPI = () => {
|
||||
const config = useRuntimeConfig();
|
||||
const baseURL = config.public.apiBaseUrl || '/api/queue';
|
||||
|
||||
/**
|
||||
* Fetch all patients from database
|
||||
*/
|
||||
const fetchAllPatients = async (): Promise<Patient[]> => {
|
||||
try {
|
||||
const response = await $fetch<QueueAPIResponse<Patient[]>>(`${baseURL}/patients`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to fetch patients');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error fetching patients:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch single patient by ID or barcode
|
||||
*/
|
||||
const fetchPatient = async (idOrBarcode: string): Promise<Patient | null> => {
|
||||
try {
|
||||
const response = await $fetch<QueueAPIResponse<Patient>>(`${baseURL}/patients/${idOrBarcode}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error fetching patient:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create new patient (register from Anjungan)
|
||||
*/
|
||||
const createPatient = async (patientData: Partial<Patient>): Promise<Patient> => {
|
||||
try {
|
||||
const response = await $fetch<QueueAPIResponse<Patient>>(`${baseURL}/patients`, {
|
||||
method: 'POST',
|
||||
body: patientData,
|
||||
});
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to create patient');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error creating patient:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update patient status (check-in, process, etc)
|
||||
*/
|
||||
const updatePatient = async (
|
||||
idOrBarcode: string,
|
||||
updates: Partial<Patient>
|
||||
): Promise<Patient> => {
|
||||
try {
|
||||
const response = await $fetch<QueueAPIResponse<Patient>>(
|
||||
`${baseURL}/patients/${idOrBarcode}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: updates,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.message || 'Failed to update patient');
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error updating patient:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check-in patient (update status to di-loket)
|
||||
*/
|
||||
const checkInPatient = async (idOrBarcode: string): Promise<Patient> => {
|
||||
return updatePatient(idOrBarcode, { status: 'di-loket' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Process patient at loket (update status and processStage)
|
||||
*/
|
||||
const processPatientAtLoket = async (
|
||||
idOrBarcode: string,
|
||||
updates: { status?: string; processStage?: string }
|
||||
): Promise<Patient> => {
|
||||
return updatePatient(idOrBarcode, updates);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync local state with database
|
||||
*/
|
||||
const syncWithDatabase = async (localPatients: Patient[]): Promise<Patient[]> => {
|
||||
try {
|
||||
// Fetch latest from database
|
||||
const dbPatients = await fetchAllPatients();
|
||||
|
||||
// Merge strategy: prefer database data, but keep local if newer
|
||||
const merged = new Map<string, Patient>();
|
||||
|
||||
// Add database patients
|
||||
dbPatients.forEach(patient => {
|
||||
merged.set(patient.barcode, patient);
|
||||
});
|
||||
|
||||
// Add local patients that don't exist in DB or are newer
|
||||
localPatients.forEach(localPatient => {
|
||||
const existing = merged.get(localPatient.barcode);
|
||||
if (!existing || new Date(localPatient.createdAt) > new Date(existing.createdAt)) {
|
||||
merged.set(localPatient.barcode, localPatient);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(merged.values());
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error syncing with database:', error);
|
||||
// Return local patients as fallback
|
||||
return localPatients;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Batch sync: save multiple patients to database
|
||||
*/
|
||||
const batchSyncPatients = async (patients: Patient[]): Promise<boolean> => {
|
||||
try {
|
||||
const response = await $fetch<QueueAPIResponse>(`${baseURL}/patients/batch`, {
|
||||
method: 'POST',
|
||||
body: { patients },
|
||||
});
|
||||
|
||||
return response.success || false;
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error batch syncing patients:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetchAllPatients,
|
||||
fetchPatient,
|
||||
createPatient,
|
||||
updatePatient,
|
||||
checkInPatient,
|
||||
processPatientAtLoket,
|
||||
syncWithDatabase,
|
||||
batchSyncPatients,
|
||||
};
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load diff
+2
-2
@@ -48,7 +48,7 @@ export default defineNuxtConfig({
|
||||
// @ts-expect-error
|
||||
config.server.https = true;
|
||||
// @ts-expect-error
|
||||
config.server.host = '10.10.150.175';
|
||||
config.server.host = '10.10.150.114';
|
||||
// @ts-expect-error
|
||||
config.server.port = 3001;
|
||||
} catch (e) {
|
||||
@@ -96,7 +96,7 @@ export default defineNuxtConfig({
|
||||
|
||||
devServer: {
|
||||
port: 3001,
|
||||
host: '10.10.150.175'
|
||||
host: '10.10.150.114'
|
||||
},
|
||||
|
||||
vite: {
|
||||
|
||||
Generated
+40
@@ -28,6 +28,7 @@
|
||||
"nuxt": "^3.17.7",
|
||||
"nuxt-qrcode": "^0.4.8",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
@@ -39,6 +40,7 @@
|
||||
"devDependencies": {
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@types/node": "^24.7.2",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||
"sass": "^1.93.3",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"vite-plugin-vuetify": "^2.1.2",
|
||||
@@ -6085,6 +6087,19 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-basic-ssl": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
|
||||
"integrity": "sha512-dOxxrhgyDIEUADhb/8OlV9JIqYLgos03YorAueTIeOUskLJSEsfwCByjbu98ctXitUN3znXKp0bYD/WHSudCeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
@@ -13843,6 +13858,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia-plugin-persistedstate": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz",
|
||||
"integrity": "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nuxt/kit": ">=3.0.0",
|
||||
"@pinia/nuxt": ">=0.10.0",
|
||||
"pinia": ">=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nuxt/kit": {
|
||||
"optional": true
|
||||
},
|
||||
"@pinia/nuxt": {
|
||||
"optional": true
|
||||
},
|
||||
"pinia": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@
|
||||
"build": "nuxt build",
|
||||
"_command_dev": "nuxt dev -o --host --port 3001",
|
||||
"_command_dev2": "nuxt dev -o --port 3001",
|
||||
"_command_dev3": "nuxt dev -o --host 10.10.150.175 --port 3001",
|
||||
"_command_dev3": "nuxt dev -o --host 10.10.150.114 --port 3001",
|
||||
"dev": "nuxt dev -o",
|
||||
"dev:https": "nuxt dev -o --host localhost --port 3001",
|
||||
"generate": "nuxt generate",
|
||||
@@ -35,6 +35,7 @@
|
||||
"nuxt": "^3.17.7",
|
||||
"nuxt-qrcode": "^0.4.8",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
+12
-68
@@ -23,6 +23,7 @@
|
||||
@action="handlePatientAction"
|
||||
@change-klinik="showChangeKlinikDialog = true"
|
||||
@process-next="handleProcessNext"
|
||||
@call="handleCallPatient"
|
||||
/>
|
||||
|
||||
<!-- Queue Actions Card -->
|
||||
@@ -76,38 +77,6 @@
|
||||
<div class="section-label">DATA PASIEN</div>
|
||||
|
||||
<div class="filters">
|
||||
<!-- Status Filter -->
|
||||
<v-chip-group v-model="selectedStatus" mandatory class="status-filter">
|
||||
<v-chip value="all" :class="{ 'active-chip': selectedStatus === 'all' }">
|
||||
Semua ({{ allPatients.length }})
|
||||
</v-chip>
|
||||
<v-chip value="diloket" :class="{ 'active-chip': selectedStatus === 'diloket' }">
|
||||
Di Klinik ({{ (diLoketPatients || []).length }})
|
||||
</v-chip>
|
||||
<v-chip value="terlambat" :class="{ 'active-chip': selectedStatus === 'terlambat' }">
|
||||
Terlambat ({{ (terlambatPatients || []).length }})
|
||||
</v-chip>
|
||||
<v-chip value="pending" :class="{ 'active-chip': selectedStatus === 'pending' }">
|
||||
Pending ({{ (pendingPatients || []).length }})
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
|
||||
<!-- Fast Track Filter -->
|
||||
<v-select
|
||||
v-model="selectedFastTrack"
|
||||
:items="fastTrackOptions"
|
||||
label="Filter Fast Track"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
class="fast-track-filter"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #prepend-inner>
|
||||
<v-icon size="20">mdi-flash</v-icon>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- Search Field -->
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
@@ -130,6 +99,7 @@
|
||||
:pending-count="(pendingPatients || []).length"
|
||||
:status-labels="statusLabels"
|
||||
:show-diproses="false"
|
||||
:fast-track-options="fastTrackOptions"
|
||||
@action="handleTableAction"
|
||||
/>
|
||||
</v-card-text>
|
||||
@@ -297,6 +267,15 @@ const handleTableAction = (item, action) => {
|
||||
const handleProcessNext = () => {
|
||||
processNextQueue();
|
||||
};
|
||||
|
||||
const handleCallPatient = () => {
|
||||
// TODO: Integrate text-to-speech library here
|
||||
// Example: speak(`Nomor antrian ${currentProcessingPatient.value?.noAntrian.split(" |")[0]}, silakan menuju ke loket`)
|
||||
if (currentProcessingPatient.value) {
|
||||
console.log('Calling patient:', currentProcessingPatient.value);
|
||||
// Placeholder for text-to-speech integration
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -335,36 +314,6 @@ const handleProcessNext = () => {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.status-filter .v-chip {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
height: 32px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
color: var(--color-neutral-600);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.status-filter .v-chip:hover {
|
||||
background: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.status-filter .v-chip.active-chip {
|
||||
background: var(--color-secondary-600);
|
||||
color: var(--color-neutral-100);
|
||||
border-color: var(--color-secondary-600);
|
||||
}
|
||||
|
||||
.fast-track-filter {
|
||||
min-width: 180px;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
max-width: 300px;
|
||||
min-width: 250px;
|
||||
@@ -406,12 +355,7 @@ const handleProcessNext = () => {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.search-field,
|
||||
.fast-track-filter {
|
||||
.search-field {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
@action="handlePatientAction"
|
||||
@change-klinik="showChangeKlinikDialog = true"
|
||||
@process-next="handleProcessNext"
|
||||
@call="handleCallPatient"
|
||||
/>
|
||||
|
||||
<!-- Queue Actions Card -->
|
||||
@@ -278,6 +279,15 @@ const handleTableAction = (item, action) => {
|
||||
const handleProcessNext = () => {
|
||||
processNextQueue();
|
||||
};
|
||||
|
||||
const handleCallPatient = () => {
|
||||
// TODO: Integrate text-to-speech library here
|
||||
// Example: speak(`Nomor antrian ${currentProcessingPatient.value?.noAntrian.split(" |")[0]}, silakan menuju ke loket`)
|
||||
if (currentProcessingPatient.value) {
|
||||
console.log('Calling patient:', currentProcessingPatient.value);
|
||||
// Placeholder for text-to-speech integration
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -121,8 +121,9 @@
|
||||
|
||||
<v-dialog
|
||||
v-model="showVisitTypeDialog"
|
||||
max-width="400"
|
||||
max-width="900"
|
||||
@click:outside="showVisitTypeDialog = false"
|
||||
persistent
|
||||
>
|
||||
<v-card class="dialog-card">
|
||||
<v-card-title class="dialog-header">
|
||||
@@ -147,13 +148,67 @@
|
||||
<v-divider class="my-4"></v-divider>
|
||||
|
||||
<div class="dialog-info">
|
||||
<div class="doctor-list-section">
|
||||
<p><strong>Dokter yang Tersedia:</strong></p>
|
||||
<ul class="doctor-list">
|
||||
<li v-for="(doctor, index) in dialogDoctors" :key="index">
|
||||
{{ doctor }}
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Section Pemilihan Dokter untuk Pasien Eksekutif -->
|
||||
<div v-if="isEksekutif" class="doctor-selection-section mb-4">
|
||||
<p class="mb-3"><strong>Pilih Dokter:</strong></p>
|
||||
<div class="doctor-buttons-container">
|
||||
<div
|
||||
v-for="(doctor, index) in dialogDoctors"
|
||||
:key="index"
|
||||
class="doctor-button-wrapper"
|
||||
>
|
||||
<v-btn
|
||||
:class="{
|
||||
'doctor-button': true,
|
||||
'doctor-button-selected': selectedDoctor === doctor
|
||||
}"
|
||||
variant="outlined"
|
||||
size="large"
|
||||
block
|
||||
@click="selectedDoctor = doctor"
|
||||
>
|
||||
<v-icon
|
||||
v-if="selectedDoctor === doctor"
|
||||
size="18"
|
||||
color="success-600"
|
||||
class="doctor-icon"
|
||||
>
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
size="18"
|
||||
color="neutral-600"
|
||||
class="doctor-icon"
|
||||
>
|
||||
mdi-circle-outline
|
||||
</v-icon>
|
||||
<span class="doctor-name">{{ doctor }}</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
<p v-if="dialogDoctors.length === 0" class="text-error text-caption mt-2">
|
||||
Tidak ada dokter tersedia untuk klinik ini
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Info Dokter untuk Non-Eksekutif (hanya tampil) -->
|
||||
<div v-else class="doctor-list-section">
|
||||
<p class="mb-3"><strong>Dokter yang Tersedia:</strong></p>
|
||||
<div class="doctor-info-grid">
|
||||
<div
|
||||
v-for="(doctor, index) in dialogDoctors"
|
||||
:key="index"
|
||||
class="doctor-info-item"
|
||||
>
|
||||
<div class="doctor-info-card">
|
||||
<v-icon size="18" color="primary-600" class="doctor-info-icon">
|
||||
mdi-doctor
|
||||
</v-icon>
|
||||
<span class="doctor-info-name">{{ doctor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quota-section">
|
||||
@@ -190,14 +245,17 @@
|
||||
size="large"
|
||||
block
|
||||
@click="selectVisitType('SEKARANG')"
|
||||
:disabled="isShift1Full(selectedClinic)"
|
||||
:class="{ 'btn-disabled': isShift1Full(selectedClinic) }"
|
||||
:disabled="isShift1Full(selectedClinic) || (isEksekutif && !selectedDoctor)"
|
||||
:class="{ 'btn-disabled': isShift1Full(selectedClinic) || (isEksekutif && !selectedDoctor) }"
|
||||
>
|
||||
DAFTAR SEKARANG
|
||||
</v-btn>
|
||||
<p v-if="isShift1Full(selectedClinic)" class="disabled-hint">
|
||||
Kuota hari ini habis
|
||||
</p>
|
||||
<p v-else-if="isEksekutif && !selectedDoctor" class="disabled-hint">
|
||||
Pilih dokter terlebih dahulu
|
||||
</p>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
@@ -206,9 +264,14 @@
|
||||
size="large"
|
||||
block
|
||||
@click="selectVisitType('JADWAL_LAIN')"
|
||||
:disabled="isEksekutif && !selectedDoctor"
|
||||
:class="{ 'btn-disabled': isEksekutif && !selectedDoctor }"
|
||||
>
|
||||
JADWAL LAIN
|
||||
</v-btn>
|
||||
<p v-if="isEksekutif && !selectedDoctor" class="disabled-hint">
|
||||
Pilih dokter terlebih dahulu
|
||||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
@@ -267,9 +330,71 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Dialog Pemilihan Dokter untuk Pasien Eksekutif -->
|
||||
<v-dialog
|
||||
v-model="showDoctorSelectionDialog"
|
||||
max-width="600"
|
||||
@click:outside="showDoctorSelectionDialog = false"
|
||||
persistent
|
||||
>
|
||||
<v-card class="dialog-card">
|
||||
<v-card-title class="dialog-header">
|
||||
<v-icon class="mr-2">mdi-doctor</v-icon>
|
||||
Pilih Dokter
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6" v-if="selectedClinic">
|
||||
<div class="dialog-content">
|
||||
<p class="mb-4">
|
||||
<strong>Klinik:</strong> {{ selectedClinic.name }}
|
||||
</p>
|
||||
<v-select
|
||||
v-model="selectedDoctor"
|
||||
:items="dialogDoctors"
|
||||
label="Pilih Dokter"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
required
|
||||
:rules="[v => !!v || 'Pilih dokter terlebih dahulu']"
|
||||
class="mb-3"
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<v-list-item-title>{{ item.raw }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<p v-if="dialogDoctors.length === 0" class="text-error text-caption">
|
||||
Tidak ada dokter tersedia untuk klinik ini
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="neutral-600"
|
||||
@click="showDoctorSelectionDialog = false"
|
||||
>
|
||||
Batal
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="success-600"
|
||||
class="text-white"
|
||||
variant="flat"
|
||||
@click="confirmDoctorSelection"
|
||||
:disabled="!selectedDoctor || dialogDoctors.length === 0"
|
||||
>
|
||||
Konfirmasi
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="showBookingFormDialog"
|
||||
max-width="500"
|
||||
max-width="600"
|
||||
@click:outside="showBookingFormDialog = false"
|
||||
>
|
||||
<v-card class="dialog-card">
|
||||
@@ -309,7 +434,23 @@
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
required
|
||||
class="mb-3"
|
||||
></v-select>
|
||||
|
||||
<!-- Field dokter untuk pasien Eksekutif (sudah dipilih di dialog sebelumnya) -->
|
||||
<v-text-field
|
||||
v-if="isEksekutif && bookingForm.doctor"
|
||||
:model-value="bookingForm.doctor"
|
||||
label="Dokter yang Dipilih"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
readonly
|
||||
class="mb-3"
|
||||
>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-icon size="20" color="success-600">mdi-check-circle</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-4">
|
||||
@@ -359,6 +500,7 @@ import { ref, computed, onMounted, nextTick } from "vue";
|
||||
import { useRoute } from "#app";
|
||||
import { useAnjunganStore } from "@/stores/anjunganStore";
|
||||
import { useClinicStore } from "@/stores/clinicStore";
|
||||
import { useQueueStore } from "@/stores/queueStore";
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
@@ -367,6 +509,7 @@ definePageMeta({
|
||||
const route = useRoute();
|
||||
const anjunganStore = useAnjunganStore();
|
||||
const clinicStore = useClinicStore();
|
||||
const queueStore = useQueueStore();
|
||||
|
||||
const anjunganId = computed(() => {
|
||||
const raw = route.params.id;
|
||||
@@ -642,7 +785,10 @@ const filteredClinics = computed(() => {
|
||||
const showVisitTypeDialog = ref(false);
|
||||
const showPaymentTypeDialog = ref(false);
|
||||
const showBookingFormDialog = ref(false);
|
||||
const showDoctorSelectionDialog = ref(false);
|
||||
const selectedClinic = ref(null);
|
||||
const selectedDoctor = ref(null);
|
||||
const pendingPaymentType = ref(null);
|
||||
const snackbar = ref(false);
|
||||
const snackbarText = ref("");
|
||||
const snackbarColor = ref("success");
|
||||
@@ -651,6 +797,12 @@ const bookingForm = ref({
|
||||
date: new Date().toISOString().substring(0, 10),
|
||||
shift: "Shift 1",
|
||||
payment: null,
|
||||
doctor: null,
|
||||
});
|
||||
|
||||
// Cek apakah jenis pasien adalah Eksekutif
|
||||
const isEksekutif = computed(() => {
|
||||
return anjunganData.value?.jenisPasien === 'Eksekutif';
|
||||
});
|
||||
|
||||
// tampilkan doctor info dengan prioritas API
|
||||
@@ -735,10 +887,18 @@ const selectClinic = (clinic) => {
|
||||
if (!clinic || !clinic.available) return;
|
||||
|
||||
selectedClinic.value = clinic;
|
||||
// Reset selected doctor ketika memilih klinik baru
|
||||
selectedDoctor.value = null;
|
||||
showVisitTypeDialog.value = true;
|
||||
};
|
||||
|
||||
const selectVisitType = (type) => {
|
||||
// Validasi: untuk pasien Eksekutif, dokter harus dipilih
|
||||
if (isEksekutif.value && !selectedDoctor.value) {
|
||||
showSnackbar("Mohon pilih dokter terlebih dahulu", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
showVisitTypeDialog.value = false;
|
||||
|
||||
if (type === "SEKARANG") {
|
||||
@@ -752,6 +912,7 @@ const selectVisitType = (type) => {
|
||||
date: tomorrowStr,
|
||||
shift: "",
|
||||
payment: null,
|
||||
doctor: isEksekutif.value ? selectedDoctor.value : null,
|
||||
};
|
||||
showBookingFormDialog.value = true;
|
||||
}
|
||||
@@ -759,9 +920,50 @@ const selectVisitType = (type) => {
|
||||
|
||||
const selectPaymentType = (paymentType) => {
|
||||
showPaymentTypeDialog.value = false;
|
||||
const message = `Pendaftaran **${selectedClinic.value.name}** untuk kunjungan **HARI INI** dengan pembayaran **${paymentType}** berhasil diproses.`;
|
||||
showSnackbar(message, "success");
|
||||
|
||||
// Untuk pasien Eksekutif, dokter sudah dipilih di dialog sebelumnya
|
||||
// Untuk Reguler/BPJS, tidak perlu dokter
|
||||
const namaDokter = isEksekutif.value ? selectedDoctor.value : null;
|
||||
|
||||
registerPatient('SEKARANG', paymentType, namaDokter);
|
||||
};
|
||||
|
||||
// Fungsi ini tidak lagi digunakan karena dokter dipilih di dialog konfirmasi
|
||||
// Tapi tetap dipertahankan untuk kompatibilitas jika ada referensi lain
|
||||
const confirmDoctorSelection = () => {
|
||||
if (!selectedDoctor.value) {
|
||||
showSnackbar("Mohon pilih dokter terlebih dahulu", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
showDoctorSelectionDialog.value = false;
|
||||
|
||||
// Register dengan dokter yang dipilih untuk kunjungan hari ini
|
||||
registerPatient('SEKARANG', pendingPaymentType.value, selectedDoctor.value);
|
||||
};
|
||||
|
||||
const registerPatient = (visitType, paymentType, namaDokter) => {
|
||||
// Register patient to queueStore
|
||||
const result = queueStore.registerPatientFromAnjungan(
|
||||
selectedClinic.value,
|
||||
paymentType,
|
||||
visitType,
|
||||
null,
|
||||
'Shift 1',
|
||||
namaDokter
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
|
||||
const message = `Pendaftaran ${selectedClinic.value.name} untuk kunjungan HARI INI dengan pembayaran ${paymentType}${doctorInfo} berhasil diproses. Nomor antrean: ${result.patient.noAntrian.split(" |")[0]}`;
|
||||
showSnackbar(message, "success");
|
||||
} else {
|
||||
showSnackbar(result.message || "Gagal melakukan pendaftaran", "error");
|
||||
}
|
||||
|
||||
selectedClinic.value = null;
|
||||
selectedDoctor.value = null;
|
||||
pendingPaymentType.value = null;
|
||||
};
|
||||
|
||||
const submitBooking = () => {
|
||||
@@ -774,15 +976,47 @@ const submitBooking = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validasi dokter untuk pasien Eksekutif
|
||||
if (isEksekutif.value && !bookingForm.value.doctor) {
|
||||
showSnackbar("Mohon pilih dokter terlebih dahulu.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
showBookingFormDialog.value = false;
|
||||
const message = `Jadwal kunjungan **${selectedClinic.value.name}** pada ${bookingForm.value.date} - ${bookingForm.value.shift} (**${bookingForm.value.payment}**) berhasil disimpan.`;
|
||||
showSnackbar(message, "success");
|
||||
|
||||
// Register patient to queueStore for future visit
|
||||
const paymentType = bookingForm.value.payment === 'BPJS' ? 'BPJS' : 'UMUM';
|
||||
const namaDokter = isEksekutif.value ? bookingForm.value.doctor : null;
|
||||
|
||||
const result = queueStore.registerPatientFromAnjungan(
|
||||
selectedClinic.value,
|
||||
paymentType,
|
||||
'JADWAL_LAIN',
|
||||
bookingForm.value.date,
|
||||
bookingForm.value.shift,
|
||||
namaDokter
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const doctorInfo = namaDokter ? ` dengan dokter ${namaDokter}` : '';
|
||||
const message = `Jadwal kunjungan ${selectedClinic.value.name} pada ${bookingForm.value.date} - ${bookingForm.value.shift} (${paymentType})${doctorInfo} berhasil disimpan. Nomor antrean: ${result.patient.noAntrian.split(" |")[0]}`;
|
||||
showSnackbar(message, "success");
|
||||
} else {
|
||||
showSnackbar(result.message || "Gagal menyimpan jadwal", "error");
|
||||
}
|
||||
|
||||
selectedClinic.value = null;
|
||||
selectedDoctor.value = null;
|
||||
bookingForm.value.doctor = null;
|
||||
};
|
||||
|
||||
const backToList = () => {
|
||||
navigateTo("/anjungan/anjungan");
|
||||
};
|
||||
|
||||
const navigateToCheckIn = () => {
|
||||
navigateTo("/check-in-pasien/check-in");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1077,25 +1311,207 @@ const backToList = () => {
|
||||
}
|
||||
|
||||
.doctor-list-section {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 15px;
|
||||
color: var(--color-neutral-800);
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.doctor-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 6px 12px;
|
||||
margin: 4px 0;
|
||||
background: var(--color-primary-100);
|
||||
border-radius: 6px;
|
||||
.doctor-info-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.doctor-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 10px;
|
||||
background: var(--color-primary-100);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-primary-300);
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.doctor-info-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.doctor-info-name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: var(--color-neutral-800);
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Responsive untuk layar lebih kecil - doctor info grid */
|
||||
@media (max-width: 768px) {
|
||||
.doctor-info-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.doctor-info-card {
|
||||
padding: 10px 8px;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
.doctor-info-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.doctor-info-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.doctor-info-card {
|
||||
padding: 10px 6px;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
.doctor-info-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-selection-section {
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
font-size: 15px;
|
||||
color: var(--color-neutral-800);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-buttons-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.doctor-button-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.doctor-button {
|
||||
justify-content: center !important;
|
||||
text-transform: none !important;
|
||||
text-align: center !important;
|
||||
padding: 16px 10px !important;
|
||||
border: 2px solid var(--color-neutral-500) !important;
|
||||
background-color: var(--color-neutral-100) !important;
|
||||
color: var(--color-neutral-800) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.3 !important;
|
||||
border-radius: 8px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
min-height: 90px !important;
|
||||
height: auto !important;
|
||||
flex-direction: column !important;
|
||||
touch-action: manipulation !important;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
|
||||
.v-btn__content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-100) !important;
|
||||
border-color: var(--color-primary-600) !important;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-button-selected {
|
||||
background-color: var(--color-success-200) !important;
|
||||
border-color: var(--color-success-600) !important;
|
||||
color: var(--color-success-800) !important;
|
||||
font-weight: 600 !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 146, 98, 0.2) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-success-300) !important;
|
||||
border-color: var(--color-success-700) !important;
|
||||
box-shadow: 0 3px 10px rgba(0, 146, 98, 0.3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.doctor-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.doctor-name {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Responsive untuk layar lebih kecil */
|
||||
@media (max-width: 768px) {
|
||||
.doctor-buttons-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doctor-button {
|
||||
padding: 14px 8px !important;
|
||||
font-size: 12px !important;
|
||||
min-height: 85px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.doctor-buttons-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.doctor-button {
|
||||
padding: 12px 6px !important;
|
||||
font-size: 11px !important;
|
||||
min-height: 80px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1263,4 +1679,15 @@ const backToList = () => {
|
||||
padding-right: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-footer {
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-neutral-500);
|
||||
background: var(--color-neutral-100);
|
||||
}
|
||||
|
||||
.navigation-footer .v-btn {
|
||||
text-transform: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
+3428
-2812
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
@@ -1,717 +0,0 @@
|
||||
<template>
|
||||
<v-app class="bg-modern">
|
||||
<v-main class="no-overflow">
|
||||
<v-container fluid class="no-scroll-container pa-2 pa-md-4">
|
||||
<v-row align="start" justify="center" class="fill-height">
|
||||
<v-col cols="12" sm="11" md="10" lg="8" xl="7" class="d-flex flex-column">
|
||||
<!-- Main Card dengan Glassmorphism -->
|
||||
<v-card class="main-card" elevation="0">
|
||||
|
||||
<!-- Header Minimalis -->
|
||||
<div class="header-modern">
|
||||
<div class="header-content">
|
||||
<div class="icon-circle">
|
||||
<v-icon size="36" color="white">mdi-hospital-building</v-icon>
|
||||
</div>
|
||||
<div class="header-text">
|
||||
<h1 class="title-modern">Check-in Pasien</h1>
|
||||
<p class="subtitle-modern">Sistem Antrean Rumah Sakit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Minimalis -->
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
class="tabs-modern mt-3"
|
||||
bg-color="transparent"
|
||||
slider-color="#FB8C00"
|
||||
height="40"
|
||||
>
|
||||
<v-tab value="scan" class="tab-modern">
|
||||
<v-icon size="20" class="mr-2">mdi-qrcode-scan</v-icon>
|
||||
<span>Scan QR</span>
|
||||
</v-tab>
|
||||
<v-tab value="manual" class="tab-modern">
|
||||
<v-icon size="20" class="mr-2">mdi-keyboard</v-icon>
|
||||
<span>Manual</span>
|
||||
</v-tab>
|
||||
<v-tab value="generate" class="tab-modern">
|
||||
<v-icon size="20" class="mr-2">mdi-qrcode</v-icon>
|
||||
<span>Generate QR</span>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</div>
|
||||
|
||||
<v-card-text class="content-modern pa-4 pa-md-6">
|
||||
<v-window v-model="tab">
|
||||
<!-- Tab Scan QR -->
|
||||
<v-window-item value="scan">
|
||||
<QRScanTab
|
||||
:is-scanning="isScanning"
|
||||
:has-camera="hasCamera"
|
||||
:camera-checking="cameraChecking"
|
||||
:camera-ready="cameraReady"
|
||||
:primary-color="primaryColor"
|
||||
@start-scanning="startScanning"
|
||||
@stop-scanning="stopScanning"
|
||||
@test-camera="testCamera"
|
||||
@open-history="openHistoryDialog"
|
||||
@open-qr-history="openQRHistoryDialog"
|
||||
/>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Tab Manual -->
|
||||
<v-window-item value="manual">
|
||||
<ManualInputTab
|
||||
v-model="manualInput"
|
||||
:primary-color="primaryColor"
|
||||
:secondary-color="secondaryColor"
|
||||
@submit="checkInManual"
|
||||
@open-history="openHistoryDialog"
|
||||
ref="manualInputTabRef"
|
||||
/>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Tab Generate QR -->
|
||||
<v-window-item value="generate">
|
||||
<div class="tab-content">
|
||||
<!-- Status Header -->
|
||||
<div class="status-header mb-3">
|
||||
<div class="status-icon-wrapper">
|
||||
<v-icon :color="primaryColor" size="24">mdi-qrcode</v-icon>
|
||||
</div>
|
||||
<div class="status-text">
|
||||
<h3 class="status-title">Generate QR Code untuk Testing</h3>
|
||||
<p class="status-subtitle">Buat QR code yang bisa Anda scan di tab "Scan QR"</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Preset Buttons -->
|
||||
<div class="mb-3">
|
||||
<p class="text-caption text-grey text-center mb-2">Quick Test QR Codes:</p>
|
||||
<v-row dense>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="success"
|
||||
size="small"
|
||||
@click="generateQuickQR(generateRandomPatientId(), 'ALLOWED')"
|
||||
class="text-none"
|
||||
block
|
||||
>
|
||||
<v-icon start size="16">mdi-check-circle</v-icon>
|
||||
Test ALLOWED
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
size="small"
|
||||
@click="generateQuickQR(generateRandomPatientId(), 'NOT_ALLOWED')"
|
||||
class="text-none"
|
||||
block
|
||||
>
|
||||
<v-icon start size="16">mdi-clock-alert</v-icon>
|
||||
Test NOT_ALLOWED
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- Form Generate -->
|
||||
<v-form @submit.prevent="generateQRCode">
|
||||
<v-text-field
|
||||
v-model="generatePatientId"
|
||||
label="ID Pasien"
|
||||
placeholder="Contoh: P-123456"
|
||||
prepend-inner-icon="mdi-identifier"
|
||||
variant="outlined"
|
||||
:color="primaryColor"
|
||||
class="input-modern mb-3"
|
||||
density="comfortable"
|
||||
clearable
|
||||
hide-details="auto"
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="generateRandomId()"
|
||||
title="Generate Random ID"
|
||||
>
|
||||
<v-icon size="20">mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<v-select
|
||||
v-model="generateStatus"
|
||||
label="Status Check-in"
|
||||
:items="statusOptions"
|
||||
prepend-inner-icon="mdi-shield-check"
|
||||
variant="outlined"
|
||||
:color="primaryColor"
|
||||
class="input-modern mb-4"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-select>
|
||||
|
||||
<div class="d-flex justify-center">
|
||||
<v-btn
|
||||
class="btn-primary-modern btn-centered"
|
||||
size="large"
|
||||
type="submit"
|
||||
elevation="0"
|
||||
:disabled="!generatePatientId"
|
||||
>
|
||||
<v-icon start size="20">mdi-qrcode-plus</v-icon>
|
||||
Generate QR Code
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
|
||||
<!-- QR Code Display -->
|
||||
<div v-if="generatedQRData" class="qr-display mt-6">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<div class="text-center">
|
||||
<p class="text-subtitle-2 text-grey mb-3">QR Code Anda:</p>
|
||||
<div id="qrcode" class="qr-code-container mb-4"></div>
|
||||
|
||||
<v-chip :color="generateStatus === 'ALLOWED' ? 'success' : 'warning'" class="mb-3">
|
||||
<v-icon start>{{ generateStatus === 'ALLOWED' ? 'mdi-check' : 'mdi-clock-alert' }}</v-icon>
|
||||
{{ generateStatus === 'ALLOWED' ? 'Diizinkan Check-in' : 'Belum Diizinkan' }}
|
||||
</v-chip>
|
||||
|
||||
<p class="text-body-2 text-grey mb-4">
|
||||
Data: {{ generatedQRData }}
|
||||
</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-row dense>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
:color="primaryColor"
|
||||
block
|
||||
size="small"
|
||||
@click="downloadQR"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start size="18">mdi-download</v-icon>
|
||||
Download
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
:color="primaryColor"
|
||||
block
|
||||
size="small"
|
||||
@click="copyQRToClipboard"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start size="18">mdi-content-copy</v-icon>
|
||||
Copy
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
:color="primaryColor"
|
||||
block
|
||||
size="small"
|
||||
@click="shareQR"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start size="18">mdi-share-variant</v-icon>
|
||||
Share
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- Instructions -->
|
||||
<v-alert
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mt-4 text-body-2"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
<strong>Cara menggunakan untuk Testing:</strong>
|
||||
<ol class="ml-4 mt-2">
|
||||
<li>Gunakan tombol <strong>"Test ALLOWED"</strong> atau <strong>"Test NOT_ALLOWED"</strong> untuk generate QR cepat, atau isi form manual</li>
|
||||
<li>Klik <strong>"Download"</strong> untuk menyimpan QR code ke komputer</li>
|
||||
<li>Buka file QR code yang didownload (bisa di HP atau layar lain)</li>
|
||||
<li>Pindah ke tab <strong>"Scan QR"</strong> dan scan QR code tersebut</li>
|
||||
<li>Atau gunakan <strong>"Copy"</strong> untuk menyalin QR ke clipboard dan paste di aplikasi lain</li>
|
||||
</ol>
|
||||
</v-alert>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Stats Footer Minimalis -->
|
||||
<div class="stats-footer-modern mt-3">
|
||||
<v-row dense>
|
||||
<v-col cols="4">
|
||||
<div class="stat-card-modern">
|
||||
<div class="stat-icon-modern">
|
||||
<v-icon :color="primaryColor" size="20">mdi-clock-outline</v-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ todayCount }}</div>
|
||||
<div class="stat-label">Hari Ini</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<div class="stat-card-modern">
|
||||
<div class="stat-icon-modern">
|
||||
<v-icon color="success" size="20">mdi-check-circle</v-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ completedCount }}</div>
|
||||
<div class="stat-label">Selesai</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="4">
|
||||
<div class="stat-card-modern">
|
||||
<div class="stat-icon-modern">
|
||||
<v-icon :color="secondaryColor" size="20">mdi-account-group</v-icon>
|
||||
</div>
|
||||
<div class="stat-value">{{ pendingCount }}</div>
|
||||
<div class="stat-label">Menunggu</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<!-- Check-in Result Dialog -->
|
||||
<CheckInDialog
|
||||
v-model="infoDialog"
|
||||
:last-check-in-result="lastCheckInResult"
|
||||
:info-message="infoMessage"
|
||||
:info-action="infoAction"
|
||||
:scanned-data="scannedData"
|
||||
@close="handleInfoAction"
|
||||
/>
|
||||
|
||||
<!-- Enhanced Snackbar -->
|
||||
<v-snackbar
|
||||
v-model="snackbarShow"
|
||||
:color="snackbar.color"
|
||||
:timeout="snackbar.timeout"
|
||||
location="bottom right"
|
||||
rounded="pill"
|
||||
elevation="24"
|
||||
class="custom-snackbar"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar :color="snackbar.color" size="32" class="mr-3">
|
||||
<v-icon size="20" color="white">{{ snackbar.icon }}</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="font-weight-bold">{{ snackbar.title }}</div>
|
||||
<div class="text-body-2">{{ snackbar.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-snackbar>
|
||||
|
||||
<!-- History Dialog -->
|
||||
<HistoryDialog
|
||||
v-model="historyDialog"
|
||||
:search="historySearch"
|
||||
:date-filter="historyDateFilter"
|
||||
:status-filter="historyStatusFilter"
|
||||
:filtered-history="filteredHistory"
|
||||
:history="checkInHistory"
|
||||
:status-options="historyStatusOptions"
|
||||
:primary-color="primaryColor"
|
||||
:get-status-color="getStatusColor"
|
||||
:get-status-icon="getStatusIcon"
|
||||
:get-status-text="getStatusText"
|
||||
:get-status-class="getStatusClass"
|
||||
:format-date-time="formatDateTime"
|
||||
:format-date="formatDate"
|
||||
@update:search="historySearch = $event"
|
||||
@update:date-filter="historyDateFilter = $event"
|
||||
@update:status-filter="historyStatusFilter = $event"
|
||||
@delete-item="handleDeleteHistoryItem"
|
||||
@clear="handleClearHistory"
|
||||
/>
|
||||
|
||||
<!-- QR History Dialog -->
|
||||
<QRHistoryDialog
|
||||
v-model="qrHistoryDialog"
|
||||
:search="historySearch"
|
||||
:date-filter="historyDateFilter"
|
||||
:filtered-history="filteredQRHistory"
|
||||
:history="scannedQRHistory"
|
||||
:primary-color="primaryColor"
|
||||
@update:search="historySearch = $event"
|
||||
@update:date-filter="historyDateFilter = $event"
|
||||
@use-data="useQRData"
|
||||
@delete-item="handleDeleteQRHistoryItem"
|
||||
@clear="handleClearQRHistory"
|
||||
/>
|
||||
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import type { TabValue, InfoAction, CheckInHistoryItem } from '~/types/checkin';
|
||||
import { PRIMARY_COLOR, SECONDARY_COLOR, QR_STATUS_OPTIONS } from '~/constants/checkin';
|
||||
import { useSnackbar } from '~/composables/useSnackbar';
|
||||
import { useCheckInHistory } from '~/composables/useCheckInHistory';
|
||||
import { useQRScanner } from '~/composables/useQRScanner';
|
||||
import { useCheckIn } from '~/composables/useCheckIn';
|
||||
import { useQRGenerator } from '~/composables/useQRGenerator';
|
||||
import CheckInDialog from '~/components/checkin/CheckInDialog.vue';
|
||||
import HistoryDialog from '~/components/checkin/HistoryDialog.vue';
|
||||
import QRHistoryDialog from '~/components/checkin/QRHistoryDialog.vue';
|
||||
import ManualInputTab from '~/components/checkin/ManualInputTab.vue';
|
||||
import QRScanTab from '~/components/checkin/QRScanTab.vue';
|
||||
|
||||
definePageMeta({
|
||||
middleware:['auth'],
|
||||
layout: false,
|
||||
})
|
||||
|
||||
// TypeScript declaration for QRCode
|
||||
declare global {
|
||||
interface Window {
|
||||
QRCode: typeof import('qrcode').default;
|
||||
}
|
||||
}
|
||||
|
||||
// --- DESAIN & TEMA ---
|
||||
const primaryColor = ref(PRIMARY_COLOR);
|
||||
const secondaryColor = ref(SECONDARY_COLOR);
|
||||
|
||||
// --- LOGIKA ---
|
||||
const tab = ref<TabValue>('scan');
|
||||
const infoDialog = ref(false);
|
||||
const infoMessage = ref('');
|
||||
const infoAction = ref<InfoAction>('kembali');
|
||||
const scannedData = ref<string | null>(null);
|
||||
const manualInput = ref('');
|
||||
const manualInputTabRef = ref<InstanceType<typeof ManualInputTab> | null>(null);
|
||||
// lastCheckInResult sudah dipindahkan ke useCheckIn composable
|
||||
|
||||
// Timer untuk auto-close dialog
|
||||
let dialogAutoCloseTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Fungsi untuk auto-close dialog setelah 15 detik
|
||||
const autoCloseDialog = () => {
|
||||
// Clear timer sebelumnya jika ada
|
||||
if (dialogAutoCloseTimer) {
|
||||
clearTimeout(dialogAutoCloseTimer);
|
||||
}
|
||||
|
||||
// Set timer untuk menutup dialog setelah 15 detik
|
||||
dialogAutoCloseTimer = setTimeout(() => {
|
||||
if (infoDialog.value) {
|
||||
infoDialog.value = false;
|
||||
dialogAutoCloseTimer = null;
|
||||
}
|
||||
}, 15000); // 15 detik
|
||||
};
|
||||
|
||||
// Snackbar composable (harus didefinisikan sebelum useQRScanner)
|
||||
const { snackbar, snackbarShow, showSnackbar } = useSnackbar();
|
||||
|
||||
// History composable
|
||||
const {
|
||||
checkInHistory,
|
||||
scannedQRHistory,
|
||||
historySearch,
|
||||
historyDateFilter,
|
||||
historyStatusFilter,
|
||||
filteredHistory,
|
||||
filteredQRHistory,
|
||||
loadHistory,
|
||||
saveToHistory,
|
||||
deleteHistoryItem,
|
||||
clearHistory,
|
||||
loadScannedQRHistory,
|
||||
saveScannedQRData,
|
||||
clearQRHistory,
|
||||
deleteQRHistoryItem,
|
||||
getStatusColor,
|
||||
getStatusIcon,
|
||||
getStatusText,
|
||||
getStatusClass,
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
historyStatusOptions,
|
||||
} = useCheckInHistory();
|
||||
|
||||
// Computed untuk stats footer
|
||||
const todayCount = computed(() => {
|
||||
const today = new Date().toDateString()
|
||||
return checkInHistory.value.filter((item: CheckInHistoryItem) => {
|
||||
const itemDate = new Date(item.checkInDate).toDateString()
|
||||
return itemDate === today
|
||||
}).length
|
||||
})
|
||||
|
||||
const completedCount = computed(() => {
|
||||
return checkInHistory.value.filter((item: CheckInHistoryItem) =>
|
||||
item.status === 'success' || item.status === 'ALLOWED'
|
||||
).length
|
||||
})
|
||||
|
||||
const pendingCount = computed(() => {
|
||||
return checkInHistory.value.filter((item: CheckInHistoryItem) =>
|
||||
item.status === 'pending' || item.status === 'NOT_ALLOWED'
|
||||
).length
|
||||
})
|
||||
|
||||
// Check-in composable (didefinisikan dulu, saveSuccessfulScan akan di-set nanti)
|
||||
let saveSuccessfulScanRef: ((qrData: string) => void) | null = null
|
||||
|
||||
const {
|
||||
lastCheckInResult,
|
||||
processQRCode,
|
||||
checkInManual: checkInManualComposable,
|
||||
} = useCheckIn({
|
||||
showSnackbar,
|
||||
saveToHistory,
|
||||
saveSuccessfulScan: (qrData: string) => {
|
||||
// Delegate ke saveSuccessfulScan dari useQRScanner
|
||||
if (saveSuccessfulScanRef) {
|
||||
saveSuccessfulScanRef(qrData)
|
||||
}
|
||||
},
|
||||
onCheckInSuccess: (result) => {
|
||||
infoMessage.value = result.message
|
||||
infoAction.value = result.action
|
||||
infoDialog.value = true
|
||||
},
|
||||
autoCloseDialog,
|
||||
})
|
||||
|
||||
// QR Scanner composable (setelah useCheckIn agar bisa akses processQRCode)
|
||||
const {
|
||||
isScanning,
|
||||
hasCamera,
|
||||
cameraChecking,
|
||||
cameraReady,
|
||||
checkCameraAvailability,
|
||||
testCamera,
|
||||
startScanning,
|
||||
stopScanning,
|
||||
saveSuccessfulScan,
|
||||
} = useQRScanner(
|
||||
(decodedText: string) => {
|
||||
// Callback ketika QR berhasil di-scan
|
||||
scannedData.value = decodedText
|
||||
processQRCode(decodedText)
|
||||
// Simpan ke history scanned QR
|
||||
saveScannedQRData(decodedText)
|
||||
},
|
||||
showSnackbar
|
||||
)
|
||||
|
||||
// Set saveSuccessfulScanRef setelah useQRScanner didefinisikan
|
||||
saveSuccessfulScanRef = saveSuccessfulScan
|
||||
|
||||
// History Dialog
|
||||
const historyDialog = ref(false);
|
||||
const qrHistoryDialog = ref(false);
|
||||
|
||||
// Generate random patient ID function (untuk digunakan di composable dan quick test buttons)
|
||||
const generateRandomPatientId = () => {
|
||||
// Generate random 6-digit number
|
||||
const randomNum = Math.floor(100000 + Math.random() * 900000);
|
||||
return `P-${randomNum}`;
|
||||
};
|
||||
|
||||
// QR Generator composable
|
||||
const {
|
||||
generatePatientId,
|
||||
generateStatus,
|
||||
generatedQRData,
|
||||
generateRandomId,
|
||||
generateQuickQR,
|
||||
generateQRCode,
|
||||
downloadQR,
|
||||
copyQRToClipboard,
|
||||
shareQR,
|
||||
} = useQRGenerator({
|
||||
showSnackbar,
|
||||
generateRandomPatientId,
|
||||
});
|
||||
|
||||
const statusOptions = [...QR_STATUS_OPTIONS] as Array<{ title: string; value: string }>;
|
||||
|
||||
|
||||
// Watch tab changes untuk stop scanner saat pindah tab
|
||||
watch(tab, (newTab: TabValue) => {
|
||||
if (newTab !== 'scan' && isScanning.value) {
|
||||
stopScanning();
|
||||
}
|
||||
// Check camera when switching to scan tab
|
||||
if (newTab === 'scan' && !hasCamera.value && !cameraChecking.value) {
|
||||
checkCameraAvailability();
|
||||
}
|
||||
});
|
||||
|
||||
// Check camera on mount
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined' && typeof navigator !== 'undefined') {
|
||||
if (navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function') {
|
||||
checkCameraAvailability();
|
||||
} else {
|
||||
console.warn('MediaDevices API not available. Make sure you are using HTTPS or localhost.');
|
||||
hasCamera.value = false;
|
||||
cameraChecking.value = false;
|
||||
showSnackbar('Warning', 'MediaDevices API tidak tersedia. Pastikan menggunakan HTTPS atau localhost.', 'warning', 'mdi-alert');
|
||||
}
|
||||
} else {
|
||||
hasCamera.value = false;
|
||||
cameraChecking.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup saat component unmount
|
||||
onUnmounted(() => {
|
||||
if (isScanning.value) {
|
||||
stopScanning();
|
||||
}
|
||||
// Clear dialog auto-close timer
|
||||
if (dialogAutoCloseTimer) {
|
||||
clearTimeout(dialogAutoCloseTimer);
|
||||
dialogAutoCloseTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// onDetect sudah dipindahkan ke useCheckIn composable sebagai processQRCode
|
||||
|
||||
const handleInfoAction = async () => {
|
||||
// Clear timer jika dialog ditutup secara manual
|
||||
if (dialogAutoCloseTimer) {
|
||||
clearTimeout(dialogAutoCloseTimer);
|
||||
dialogAutoCloseTimer = null;
|
||||
}
|
||||
|
||||
infoDialog.value = false;
|
||||
|
||||
// Scanner tetap berjalan setelah dialog ditutup
|
||||
// Ini memungkinkan user untuk segera memproses QR code berikutnya
|
||||
// tanpa perlu memulai scanner lagi
|
||||
};
|
||||
|
||||
// performCheckIn sudah dipindahkan ke useCheckIn composable
|
||||
|
||||
const checkInManual = async () => {
|
||||
try {
|
||||
if (!manualInput.value || !manualInput.value.trim()) {
|
||||
showSnackbar('Error', 'Mohon isi nomor antrean atau ID pasien', 'error', 'mdi-alert');
|
||||
return;
|
||||
}
|
||||
|
||||
const patientId = manualInput.value.trim();
|
||||
|
||||
// Validate form if available
|
||||
if (manualInputTabRef.value?.form) {
|
||||
try {
|
||||
const isValid = manualInputTabRef.value.form.validate();
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// If validate fails, continue anyway
|
||||
console.warn('Form validation error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
await checkInManualComposable(
|
||||
patientId,
|
||||
() => {
|
||||
// onSuccess callback
|
||||
try {
|
||||
manualInput.value = '';
|
||||
if (manualInputTabRef.value?.form) {
|
||||
try {
|
||||
manualInputTabRef.value.form.reset();
|
||||
} catch (e) {
|
||||
// Ignore reset errors
|
||||
console.warn('Form reset error:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error in success callback:', e);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// onError callback - tidak perlu melakukan apa-apa karena error sudah ditangani di composable
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in checkInManual:', error);
|
||||
showSnackbar('Error', 'Terjadi kesalahan saat melakukan check-in. Silakan coba lagi.', 'error', 'mdi-alert');
|
||||
}
|
||||
};
|
||||
|
||||
// generateQRCode, generateQuickQR, downloadQR, copyQRToClipboard, shareQR sudah dipindahkan ke useQRGenerator composable
|
||||
|
||||
// History Functions - semua sudah dipindahkan ke useCheckInHistory composable
|
||||
|
||||
const openHistoryDialog = () => {
|
||||
loadHistory();
|
||||
historyDialog.value = true;
|
||||
};
|
||||
|
||||
const openQRHistoryDialog = () => {
|
||||
loadScannedQRHistory();
|
||||
qrHistoryDialog.value = true;
|
||||
};
|
||||
|
||||
const handleDeleteHistoryItem = (index: number) => {
|
||||
deleteHistoryItem(index);
|
||||
showSnackbar('Berhasil', 'Riwayat berhasil dihapus', 'success', 'mdi-check');
|
||||
};
|
||||
|
||||
const handleClearHistory = () => {
|
||||
clearHistory();
|
||||
showSnackbar('Berhasil', 'Semua riwayat berhasil dihapus', 'success', 'mdi-check');
|
||||
};
|
||||
|
||||
const handleDeleteQRHistoryItem = (index: number) => {
|
||||
deleteQRHistoryItem(index);
|
||||
showSnackbar('Berhasil', 'Riwayat QR scan berhasil dihapus', 'success', 'mdi-check');
|
||||
};
|
||||
|
||||
const handleClearQRHistory = () => {
|
||||
clearQRHistory();
|
||||
showSnackbar('Berhasil', 'Semua riwayat QR scan berhasil dihapus', 'success', 'mdi-check');
|
||||
};
|
||||
|
||||
const useQRData = (qrData: string) => {
|
||||
qrHistoryDialog.value = false;
|
||||
scannedData.value = qrData;
|
||||
processQRCode(qrData);
|
||||
};
|
||||
|
||||
// filteredHistory, filteredQRHistory, dan helper functions sudah dipindahkan ke useCheckInHistory composable
|
||||
|
||||
// loadSuccessfulScans sudah dipanggil di dalam useQRScanner composable saat initialization
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~/assets/scss/checkin/variables';
|
||||
@import '~/assets/scss/checkin/components';
|
||||
@import '~/assets/scss/checkin/dialogs';
|
||||
</style>
|
||||
@@ -264,7 +264,7 @@ const headers = ref([
|
||||
{ title: 'Actions', value: 'actions', sortable: false, width: 'auto' },
|
||||
]);
|
||||
|
||||
const jenisPasienOptions = ['Reguler', 'BPJS'];
|
||||
const jenisPasienOptions = ['Reguler', 'BPJS', 'Eksekutif'];
|
||||
|
||||
const anjunganItems = computed(() => anjunganStore.getAllAnjungan);
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.$pinia.use(createPersistedState({
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// server/api/queue/patients.ts
|
||||
// Contoh implementasi API endpoints untuk antrian pasien
|
||||
// NOTE: Ini adalah contoh - sesuaikan dengan database dan framework yang digunakan
|
||||
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
// Mock database (ganti dengan database real)
|
||||
const mockDB: any[] = []
|
||||
|
||||
interface Patient {
|
||||
no: number
|
||||
jamPanggil: string
|
||||
barcode: string
|
||||
noAntrian: string
|
||||
shift: string
|
||||
klinik: string
|
||||
fastTrack: string
|
||||
pembayaran: string
|
||||
status: string
|
||||
processStage: string
|
||||
createdAt: string
|
||||
registrationType?: string
|
||||
visitType?: string
|
||||
visitDate?: string
|
||||
}
|
||||
|
||||
// GET /api/queue/patients - Get all patients
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
try {
|
||||
// TODO: Replace with actual database query
|
||||
// const patients = await db.query('SELECT * FROM patients ORDER BY created_at DESC')
|
||||
|
||||
// Mock response
|
||||
const patients = mockDB
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: patients,
|
||||
message: 'Patients fetched successfully',
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Failed to fetch patients',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// GET /api/queue/patients/:id - Get single patient
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
const idOrBarcode = getRouterParam(event, 'id')
|
||||
|
||||
try {
|
||||
// TODO: Replace with actual database query
|
||||
// const patient = await db.query('SELECT * FROM patients WHERE barcode = ? OR id = ?', [idOrBarcode, idOrBarcode])
|
||||
|
||||
const patient = mockDB.find(
|
||||
(p: Patient) => p.barcode === idOrBarcode || p.no.toString() === idOrBarcode
|
||||
)
|
||||
|
||||
if (!patient) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'Patient not found',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: patient,
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Failed to fetch patient',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/queue/patients - Create new patient
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
const body = await readBody(event)
|
||||
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!body.clinic || !body.paymentType) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'Missing required fields: clinic, paymentType',
|
||||
})
|
||||
}
|
||||
|
||||
// Generate patient data
|
||||
const newPatient: Patient = {
|
||||
no: mockDB.length + 1,
|
||||
jamPanggil: new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }),
|
||||
barcode: `250811${String(Date.now()).slice(-6)}${String(mockDB.length + 1).padStart(3, '0')}`,
|
||||
noAntrian: `UM${String(mockDB.length + 1).padStart(4, '0')} | Onsite - ${barcode}`,
|
||||
shift: body.shift || 'Shift 1',
|
||||
klinik: body.clinic,
|
||||
fastTrack: 'TIDAK',
|
||||
pembayaran: body.paymentType,
|
||||
status: body.visitType === 'SEKARANG' ? 'waiting' : 'pending',
|
||||
processStage: 'loket',
|
||||
createdAt: new Date().toISOString(),
|
||||
registrationType: 'onsite',
|
||||
visitType: body.visitType || 'SEKARANG',
|
||||
visitDate: body.visitDate || new Date().toISOString().substring(0, 10),
|
||||
}
|
||||
|
||||
// TODO: Insert to database
|
||||
// await db.query('INSERT INTO patients SET ?', newPatient)
|
||||
mockDB.push(newPatient)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: newPatient,
|
||||
message: 'Patient created successfully',
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Failed to create patient',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// PATCH /api/queue/patients/:id - Update patient
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
const idOrBarcode = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
|
||||
try {
|
||||
// TODO: Replace with actual database query
|
||||
// const patient = await db.query('SELECT * FROM patients WHERE barcode = ? OR id = ?', [idOrBarcode, idOrBarcode])
|
||||
|
||||
const patientIndex = mockDB.findIndex(
|
||||
(p: Patient) => p.barcode === idOrBarcode || p.no.toString() === idOrBarcode
|
||||
)
|
||||
|
||||
if (patientIndex === -1) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
message: 'Patient not found',
|
||||
})
|
||||
}
|
||||
|
||||
// Update patient
|
||||
const updatedPatient = {
|
||||
...mockDB[patientIndex],
|
||||
...body,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
// TODO: Update in database
|
||||
// await db.query('UPDATE patients SET ? WHERE barcode = ?', [body, idOrBarcode])
|
||||
mockDB[patientIndex] = updatedPatient
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedPatient,
|
||||
message: 'Patient updated successfully',
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Failed to update patient',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// POST /api/queue/patients/batch - Batch sync
|
||||
export default defineEventHandler(async (event: H3Event) => {
|
||||
const body = await readBody(event)
|
||||
const { patients } = body
|
||||
|
||||
try {
|
||||
if (!Array.isArray(patients)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: 'patients must be an array',
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Batch insert/update to database
|
||||
// await db.query('INSERT INTO patients VALUES ? ON DUPLICATE KEY UPDATE ...', [patients])
|
||||
|
||||
patients.forEach((patient: Patient) => {
|
||||
const index = mockDB.findIndex((p: Patient) => p.barcode === patient.barcode)
|
||||
if (index === -1) {
|
||||
mockDB.push(patient)
|
||||
} else {
|
||||
mockDB[index] = patient
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Synced ${patients.length} patients`,
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.statusCode) throw error
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Failed to batch sync',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -83,5 +83,11 @@ export const useAnjunganStore = defineStore('anjungan', () => {
|
||||
updateAnjungan,
|
||||
deleteAnjungan,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'anjungan-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['anjunganItems'],
|
||||
},
|
||||
});
|
||||
|
||||
+173
-25
@@ -3,7 +3,8 @@ import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useClinicStore = defineStore('clinic', () => {
|
||||
// Data clinics dari Anjungan.vue
|
||||
// Data clinics - Single source of truth untuk semua data klinik
|
||||
// Includes basic info (name, kode, icon, doctors, shifts) + master config (totalQuota, jamShiftList, jadwalKlinik, autoShift)
|
||||
const clinics = ref([
|
||||
{
|
||||
id: 1,
|
||||
@@ -18,7 +19,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 15 },
|
||||
{ name: "Shift 2", quota: 20 }
|
||||
]
|
||||
],
|
||||
|
||||
totalQuota: 1000,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 1000 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -33,7 +39,15 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 0 },
|
||||
{ name: "Shift 2", quota: 12 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 1500,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '11:00', kuota: 1000 },
|
||||
{ dari: '13:00', sampai: '16:00', kuota: 500 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -48,7 +62,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 8 },
|
||||
{ name: "Shift 2", quota: 10 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 800,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 800 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -63,7 +82,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 10 },
|
||||
{ name: "Shift 2", quota: 8 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 600,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu'],
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
@@ -78,7 +102,16 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 18 },
|
||||
{ name: "Shift 2", quota: 15 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 2100,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '11:00', kuota: 1000 },
|
||||
{ dari: '13:00', sampai: '16:00', kuota: 700 },
|
||||
{ dari: '18:00', sampai: '20:00', kuota: 400 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
@@ -93,7 +126,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 12 },
|
||||
{ name: "Shift 2", quota: 10 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 500,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 500 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
@@ -105,7 +143,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
schedule: "",
|
||||
available: false,
|
||||
doctors: [],
|
||||
shifts: []
|
||||
shifts: [],
|
||||
// Master config
|
||||
totalQuota: 750,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 750 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu'],
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
@@ -120,7 +163,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 25 },
|
||||
{ name: "Shift 2", quota: 20 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 900,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 900 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
@@ -135,7 +183,15 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 14 },
|
||||
{ name: "Shift 2", quota: 12 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 1200,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '11:00', kuota: 800 },
|
||||
{ dari: '13:00', sampai: '16:00', kuota: 400 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
@@ -150,7 +206,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 10 },
|
||||
{ name: "Shift 2", quota: 8 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 700,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 700 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu'],
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
@@ -165,7 +226,15 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 16 },
|
||||
{ name: "Shift 2", quota: 14 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 1000,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '11:00', kuota: 600 },
|
||||
{ dari: '13:00', sampai: '16:00', kuota: 400 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
@@ -180,7 +249,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 8 },
|
||||
{ name: "Shift 2", quota: 6 }
|
||||
]
|
||||
],
|
||||
// Master config (default values, not in masterStore)
|
||||
totalQuota: 0,
|
||||
jamShiftList: [],
|
||||
autoShift: false,
|
||||
jadwalKlinik: [],
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
@@ -195,7 +269,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 10 },
|
||||
{ name: "Shift 2", quota: 8 }
|
||||
]
|
||||
],
|
||||
// Master config (default values, not in masterStore)
|
||||
totalQuota: 0,
|
||||
jamShiftList: [],
|
||||
autoShift: false,
|
||||
jadwalKlinik: [],
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
@@ -210,7 +289,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 12 },
|
||||
{ name: "Shift 2", quota: 10 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 600,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Selasa', 'Kamis'],
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
@@ -225,7 +309,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 15 },
|
||||
{ name: "Shift 2", quota: 12 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 800,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 800 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
@@ -240,7 +329,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 20 },
|
||||
{ name: "Shift 2", quota: 15 }
|
||||
]
|
||||
],
|
||||
// Master config (default values, not in masterStore)
|
||||
totalQuota: 0,
|
||||
jamShiftList: [],
|
||||
autoShift: false,
|
||||
jadwalKlinik: [],
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
@@ -255,7 +349,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 10 },
|
||||
{ name: "Shift 2", quota: 8 }
|
||||
]
|
||||
],
|
||||
// Master config (default values, not in masterStore)
|
||||
totalQuota: 0,
|
||||
jamShiftList: [],
|
||||
autoShift: false,
|
||||
jadwalKlinik: [],
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
@@ -270,7 +369,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 14 },
|
||||
{ name: "Shift 2", quota: 12 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 700,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 700 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu'],
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
@@ -285,7 +389,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 12 },
|
||||
{ name: "Shift 2", quota: 10 }
|
||||
]
|
||||
],
|
||||
// Master config (default values, not in masterStore)
|
||||
totalQuota: 0,
|
||||
jamShiftList: [],
|
||||
autoShift: false,
|
||||
jadwalKlinik: [],
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
@@ -300,7 +409,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 8 },
|
||||
{ name: "Shift 2", quota: 6 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 500,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 500 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
@@ -315,7 +429,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 16 },
|
||||
{ name: "Shift 2", quota: 14 }
|
||||
]
|
||||
],
|
||||
// Master config
|
||||
totalQuota: 600,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
@@ -327,7 +446,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
schedule: "Mulai Pukul 07:00",
|
||||
available: false,
|
||||
doctors: [],
|
||||
shifts: []
|
||||
shifts: [],
|
||||
// Master config
|
||||
totalQuota: 700,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 700 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu'],
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
@@ -342,7 +466,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
shifts: [
|
||||
{ name: "Shift 1", quota: 12 },
|
||||
{ name: "Shift 2", quota: 10 }
|
||||
]
|
||||
],
|
||||
// Master config (default values, not in masterStore)
|
||||
totalQuota: 0,
|
||||
jamShiftList: [],
|
||||
autoShift: false,
|
||||
jadwalKlinik: [],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -371,12 +500,31 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
}));
|
||||
};
|
||||
|
||||
// Actions untuk update master config (sync dengan masterStore operations)
|
||||
const updateClinicMasterConfig = (kode, masterConfig) => {
|
||||
const clinic = clinics.value.find(c => c.kode === kode);
|
||||
if (clinic) {
|
||||
clinic.totalQuota = masterConfig.totalQuota || clinic.totalQuota;
|
||||
clinic.jamShiftList = masterConfig.jamShiftList || clinic.jamShiftList;
|
||||
clinic.autoShift = masterConfig.autoShift !== undefined ? masterConfig.autoShift : clinic.autoShift;
|
||||
clinic.jadwalKlinik = masterConfig.jadwalKlinik || clinic.jadwalKlinik;
|
||||
return { success: true, clinic };
|
||||
}
|
||||
return { success: false, message: 'Clinic not found' };
|
||||
};
|
||||
|
||||
return {
|
||||
clinics,
|
||||
getAllClinics,
|
||||
getClinicByName,
|
||||
getClinicByKode,
|
||||
getClinicsForDropdown,
|
||||
updateClinicMasterConfig,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'clinic-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['clinics'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -276,4 +276,10 @@ export const useKlinikRuangStore = defineStore('klinikRuang', () => {
|
||||
removeRuangFromKlinik,
|
||||
searchKlinikRuang,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'klinikruang-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['klinikRuangList'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
// stores/loketStore.js
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useClinicStore } from './clinicStore';
|
||||
|
||||
export const useLoketStore = defineStore('loket', () => {
|
||||
const clinicStore = useClinicStore();
|
||||
|
||||
// State - Loket Data
|
||||
const loketData = ref([
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
namaLoket: "Loket 1",
|
||||
kuota: 500,
|
||||
pelayanan: ["RT", "RM", "TD"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
no: 2,
|
||||
namaLoket: "Loket 2",
|
||||
kuota: 666,
|
||||
pelayanan: ["JW", "SR"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
no: 3,
|
||||
namaLoket: "Loket 3",
|
||||
kuota: 666,
|
||||
pelayanan: ["AS", "JT"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
no: 4,
|
||||
namaLoket: "Loket 4",
|
||||
kuota: 3676,
|
||||
pelayanan: ["KK", "PR"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
]);
|
||||
|
||||
// Computed - Available services (reference dari clinicStore)
|
||||
const availableServices = computed(() => {
|
||||
const clinics = clinicStore.getAllClinics.value || [];
|
||||
return clinics.map(c => ({
|
||||
id: c.kode,
|
||||
nama: c.name,
|
||||
kode: c.kode,
|
||||
}));
|
||||
});
|
||||
|
||||
// Actions - CRUD Operations
|
||||
const addLoket = (loketPayload) => {
|
||||
const newId = Math.max(...loketData.value.map(l => l.id), 0) + 1;
|
||||
const newNo = loketData.value.length + 1;
|
||||
|
||||
const newLoket = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
...loketPayload,
|
||||
};
|
||||
|
||||
loketData.value.push(newLoket);
|
||||
return { success: true, message: `Loket ${newLoket.namaLoket} berhasil ditambahkan`, data: newLoket };
|
||||
};
|
||||
|
||||
const updateLoket = (loketPayload) => {
|
||||
const index = loketData.value.findIndex(l => l.id === loketPayload.id);
|
||||
if (index !== -1) {
|
||||
loketData.value[index] = {
|
||||
...loketData.value[index],
|
||||
...loketPayload,
|
||||
};
|
||||
return { success: true, message: `Loket ${loketPayload.namaLoket} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Loket tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deleteLoket = (loketId) => {
|
||||
const index = loketData.value.findIndex(l => l.id === loketId);
|
||||
if (index !== -1) {
|
||||
const loketName = loketData.value[index].namaLoket;
|
||||
loketData.value.splice(index, 1);
|
||||
loketData.value.forEach((l, idx) => {
|
||||
l.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Loket ${loketName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Loket tidak ditemukan' };
|
||||
};
|
||||
|
||||
const getLoketById = (id) => {
|
||||
return loketData.value.find(l => l.id === id);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
loketData,
|
||||
availableServices,
|
||||
|
||||
// Actions
|
||||
addLoket,
|
||||
updateLoket,
|
||||
deleteLoket,
|
||||
getLoketById,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'loket-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['loketData'],
|
||||
},
|
||||
});
|
||||
|
||||
+124
-581
@@ -1,85 +1,44 @@
|
||||
// stores/masterStore.js - Integrated dengan Penunjang
|
||||
// stores/masterStore.js - Backward Compatibility Layer
|
||||
// NOTE: Phase 3 - masterStore sekarang hanya sebagai compatibility layer
|
||||
// Data sebenarnya ada di: loketStore, ruangStore, penunjangStore, clinicStore
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useClinicStore } from './clinicStore';
|
||||
import { useLoketStore } from './loketStore';
|
||||
import { useRuangStore } from './ruangStore';
|
||||
import { usePenunjangStore } from './penunjangStore';
|
||||
|
||||
export const useMasterStore = defineStore('master', () => {
|
||||
// Store referensi utama daftar klinik (single source of truth)
|
||||
// Store references
|
||||
const clinicStore = useClinicStore();
|
||||
const loketStore = useLoketStore();
|
||||
const ruangStore = useRuangStore();
|
||||
const penunjangStore = usePenunjangStore();
|
||||
|
||||
// ============================================
|
||||
// MASTER KLINIK
|
||||
// MASTER KLINIK (Backward Compatibility)
|
||||
// ============================================
|
||||
const klinikData = ref([
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
kode: 'AN',
|
||||
nama: 'ANAK',
|
||||
shift: 1,
|
||||
totalQuota: 1000,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 1000 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
no: 2,
|
||||
kode: 'AS',
|
||||
nama: 'ANESTESI',
|
||||
shift: 2,
|
||||
totalQuota: 1500,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '11:00', kuota: 1000 },
|
||||
{ dari: '13:00', sampai: '16:00', kuota: 500 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
no: 3,
|
||||
kode: 'BD',
|
||||
nama: 'BEDAH',
|
||||
shift: 1,
|
||||
totalQuota: 800,
|
||||
jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 800 }],
|
||||
autoShift: false,
|
||||
jadwalKlinik: ['Senin', 'Rabu', 'Jumat'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
no: 4,
|
||||
kode: 'GI',
|
||||
nama: 'GIGI DAN MULUT',
|
||||
shift: 3,
|
||||
totalQuota: 2100,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '11:00', kuota: 1000 },
|
||||
{ dari: '13:00', sampai: '16:00', kuota: 700 },
|
||||
{ dari: '18:00', sampai: '20:00', kuota: 400 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
},
|
||||
{ id: 5, no: 5, kode: 'GR', nama: 'GERIATRI', shift: 1, totalQuota: 600, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu'] },
|
||||
{ id: 6, no: 6, kode: 'GZ', nama: 'GIZI', shift: 1, totalQuota: 500, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 500 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu', 'Jumat'] },
|
||||
{ id: 7, no: 7, kode: 'HO', nama: 'HOM', shift: 1, totalQuota: 750, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 750 }], autoShift: false, jadwalKlinik: ['Senin', 'Selasa', 'Rabu'] },
|
||||
{ id: 8, no: 8, kode: 'IP', nama: 'IPD', shift: 1, totalQuota: 900, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 900 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu', 'Jumat'] },
|
||||
{ id: 9, no: 9, kode: 'JT', nama: 'JANTUNG', shift: 2, totalQuota: 1200, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 800 }, { dari: '13:00', sampai: '16:00', kuota: 400 }], autoShift: false, jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'] },
|
||||
{ id: 10, no: 10, kode: 'JW', nama: 'JIWA', shift: 1, totalQuota: 700, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 700 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu'] },
|
||||
{ id: 11, no: 11, kode: 'KK', nama: 'KULIT KELAMIN', shift: 1, totalQuota: 600, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }], autoShift: false, jadwalKlinik: ['Selasa', 'Kamis'] },
|
||||
{ id: 12, no: 12, kode: 'MT', nama: 'MATA', shift: 1, totalQuota: 800, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 800 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu', 'Jumat'] },
|
||||
{ id: 13, no: 13, kode: 'OB', nama: 'KANDUNGAN', shift: 2, totalQuota: 1000, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }, { dari: '13:00', sampai: '16:00', kuota: 400 }], autoShift: false, jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'] },
|
||||
{ id: 14, no: 14, kode: 'PR', nama: 'PARU', shift: 1, totalQuota: 700, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 700 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu'] },
|
||||
{ id: 15, no: 15, kode: 'RT', nama: 'RADIOTERAPI', shift: 1, totalQuota: 500, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 500 }], autoShift: false, jadwalKlinik: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'] },
|
||||
{ id: 16, no: 16, kode: 'RM', nama: 'REHAB MEDIK', shift: 1, totalQuota: 600, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 600 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu', 'Jumat'] },
|
||||
{ id: 17, no: 17, kode: 'SR', nama: 'SARAF', shift: 1, totalQuota: 700, jamShiftList: [{ dari: '07:00', sampai: '11:00', kuota: 700 }], autoShift: false, jadwalKlinik: ['Senin', 'Rabu'] },
|
||||
]);
|
||||
// NOTE: klinikData sekarang adalah computed yang reference clinicStore (single source of truth)
|
||||
// Ini untuk backward compatibility dengan pages yang masih menggunakan masterStore.klinikData
|
||||
// Format: { id, no, kode, nama, shift, totalQuota, jamShiftList, autoShift, jadwalKlinik }
|
||||
const klinikData = computed(() => {
|
||||
return clinicStore.clinics.map((clinic, index) => {
|
||||
// Convert clinic data ke format masterStore (untuk backward compatibility)
|
||||
return {
|
||||
id: clinic.id,
|
||||
no: index + 1,
|
||||
kode: clinic.kode,
|
||||
nama: clinic.name, // Convert 'name' to 'nama' for compatibility
|
||||
shift: typeof clinic.shift === 'string' ? parseInt(clinic.shift.replace(/[^0-9]/g, '')) || 1 : clinic.shift || 1,
|
||||
totalQuota: clinic.totalQuota || 0,
|
||||
jamShiftList: clinic.jamShiftList || [],
|
||||
autoShift: clinic.autoShift || false,
|
||||
jadwalKlinik: clinic.jadwalKlinik || [],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Computed - Get klinik list for dropdowns
|
||||
// Sumber data kode & nama klinik diambil dari clinicStore (1 pintu),
|
||||
// lalu difilter hanya untuk kode yang terdaftar di master klinik ini.
|
||||
const klinikList = computed(() => {
|
||||
const availableCodes = new Set(klinikData.value.map((k) => k.kode));
|
||||
|
||||
@@ -97,551 +56,132 @@ export const useMasterStore = defineStore('master', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
// Actions - Klinik
|
||||
// Actions - Klinik (delegate to clinicStore)
|
||||
const addKlinik = (klinikPayload) => {
|
||||
const newId = Math.max(...klinikData.value.map(k => k.id), 0) + 1;
|
||||
const newNo = klinikData.value.length + 1;
|
||||
|
||||
const totalQuota = klinikPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
const newKlinik = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
// Map masterStore format ke clinicStore format
|
||||
const clinicPayload = {
|
||||
...klinikPayload,
|
||||
totalQuota: totalQuota,
|
||||
name: klinikPayload.nama, // Convert 'nama' to 'name'
|
||||
shift: `Shift ${klinikPayload.shift}`, // Convert number to string format
|
||||
};
|
||||
|
||||
klinikData.value.push(newKlinik);
|
||||
return { success: true, message: `Klinik ${newKlinik.nama} berhasil ditambahkan`, data: newKlinik };
|
||||
return clinicStore.addClinic(clinicPayload);
|
||||
};
|
||||
|
||||
const updateKlinik = (klinikPayload) => {
|
||||
const index = klinikData.value.findIndex(k => k.id === klinikPayload.id);
|
||||
if (index !== -1) {
|
||||
const totalQuota = klinikPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
klinikData.value[index] = {
|
||||
...klinikData.value[index],
|
||||
...klinikPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
return { success: true, message: `Klinik ${klinikPayload.nama} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Klinik tidak ditemukan' };
|
||||
// Map masterStore format ke clinicStore format
|
||||
const clinicUpdates = {
|
||||
...klinikPayload,
|
||||
name: klinikPayload.nama, // Convert 'nama' to 'name'
|
||||
shift: `Shift ${klinikPayload.shift}`, // Convert number to string format
|
||||
};
|
||||
return clinicStore.updateClinic(klinikPayload.id, clinicUpdates);
|
||||
};
|
||||
|
||||
const deleteKlinik = (klinikId) => {
|
||||
const index = klinikData.value.findIndex(k => k.id === klinikId);
|
||||
if (index !== -1) {
|
||||
const klinikName = klinikData.value[index].nama;
|
||||
klinikData.value.splice(index, 1);
|
||||
klinikData.value.forEach((k, idx) => {
|
||||
k.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Klinik ${klinikName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Klinik tidak ditemukan' };
|
||||
// Deletion should ideally happen directly in clinicStore
|
||||
return { success: false, message: 'Penghapusan klinik harus dilakukan melalui Clinic Store.' };
|
||||
};
|
||||
|
||||
const getKlinikById = (id) => {
|
||||
return klinikData.value.find(k => k.id === id);
|
||||
// Menggunakan getter dari clinicStore
|
||||
const clinic = clinicStore.clinics.find(c => c.id === id);
|
||||
if (clinic) {
|
||||
return {
|
||||
id: clinic.id,
|
||||
no: klinikData.value.findIndex(k => k.id === clinic.id) + 1, // Recalculate 'no'
|
||||
kode: clinic.kode,
|
||||
nama: clinic.name,
|
||||
shift: typeof clinic.shift === 'string' ? parseInt(clinic.shift.replace(/[^0-9]/g, '')) || 1 : clinic.shift || 1,
|
||||
totalQuota: clinic.totalQuota,
|
||||
jamShiftList: clinic.jamShiftList,
|
||||
autoShift: clinic.autoShift,
|
||||
jadwalKlinik: clinic.jadwalKlinik,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getKlinikByKode = (kode) => {
|
||||
return klinikData.value.find(k => k.kode === kode);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// MASTER LOKET
|
||||
// ============================================
|
||||
const loketData = ref([
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
namaLoket: "Loket 1",
|
||||
kuota: 500,
|
||||
pelayanan: ["RT", "RM", "TD"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
no: 2,
|
||||
namaLoket: "Loket 2",
|
||||
kuota: 666,
|
||||
pelayanan: ["JW", "SR"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
no: 3,
|
||||
namaLoket: "Loket 3",
|
||||
kuota: 666,
|
||||
pelayanan: ["AS", "JT"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
no: 4,
|
||||
namaLoket: "Loket 4",
|
||||
kuota: 3676,
|
||||
pelayanan: ["KK", "PR"],
|
||||
pembayaran: "JKN",
|
||||
keterangan: "ONLINE",
|
||||
statusPelayanan: "RAWAT JALAN",
|
||||
},
|
||||
]);
|
||||
|
||||
const availableServices = computed(() =>
|
||||
klinikData.value.map(k => ({
|
||||
id: k.kode,
|
||||
nama: k.nama,
|
||||
}))
|
||||
);
|
||||
|
||||
// Actions - Loket
|
||||
const addLoket = (loketPayload) => {
|
||||
const newId = Math.max(...loketData.value.map(l => l.id), 0) + 1;
|
||||
const newNo = loketData.value.length + 1;
|
||||
|
||||
const newLoket = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
...loketPayload,
|
||||
};
|
||||
|
||||
loketData.value.push(newLoket);
|
||||
return { success: true, message: `Loket ${newLoket.namaLoket} berhasil ditambahkan`, data: newLoket };
|
||||
};
|
||||
|
||||
const updateLoket = (loketPayload) => {
|
||||
const index = loketData.value.findIndex(l => l.id === loketPayload.id);
|
||||
if (index !== -1) {
|
||||
loketData.value[index] = {
|
||||
...loketData.value[index],
|
||||
...loketPayload,
|
||||
// Menggunakan getter dari clinicStore
|
||||
const clinic = clinicStore.getClinicConfigByKode(kode);
|
||||
if (clinic) {
|
||||
return {
|
||||
id: clinic.id,
|
||||
no: klinikData.value.findIndex(k => k.kode === clinic.kode) + 1, // Recalculate 'no'
|
||||
kode: clinic.kode,
|
||||
nama: clinic.name,
|
||||
shift: typeof clinic.shift === 'string' ? parseInt(clinic.shift.replace(/[^0-9]/g, '')) || 1 : clinic.shift || 1,
|
||||
totalQuota: clinic.totalQuota,
|
||||
jamShiftList: clinic.jamShiftList,
|
||||
autoShift: clinic.autoShift,
|
||||
jadwalKlinik: clinic.jadwalKlinik,
|
||||
};
|
||||
return { success: true, message: `Loket ${loketPayload.namaLoket} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Loket tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deleteLoket = (loketId) => {
|
||||
const index = loketData.value.findIndex(l => l.id === loketId);
|
||||
if (index !== -1) {
|
||||
const loketName = loketData.value[index].namaLoket;
|
||||
loketData.value.splice(index, 1);
|
||||
loketData.value.forEach((l, idx) => {
|
||||
l.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Loket ${loketName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Loket tidak ditemukan' };
|
||||
};
|
||||
|
||||
const getLoketById = (id) => {
|
||||
return loketData.value.find(l => l.id === id);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// MASTER KLINIK RUANG
|
||||
// MASTER LOKET (Backward Compatibility)
|
||||
// ============================================
|
||||
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: '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' }
|
||||
]
|
||||
},
|
||||
]);
|
||||
// Reference dari loketStore
|
||||
const loketData = computed(() => loketStore.loketData);
|
||||
const availableServices = computed(() => loketStore.availableServices);
|
||||
|
||||
// Computed
|
||||
const totalKlinikRuang = computed(() => ruangData.value.length);
|
||||
const totalRuangan = computed(() => {
|
||||
return ruangData.value.reduce((total, klinik) => {
|
||||
return total + klinik.ruangList.length;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Actions - Ruang
|
||||
const addRuang = (ruangPayload) => {
|
||||
const newId = Math.max(...ruangData.value.map(r => r.id), 0) + 1;
|
||||
const newNo = ruangData.value.length + 1;
|
||||
|
||||
const namaRuangDisplay = ruangPayload.ruangList
|
||||
.map(r => r.namaRuang)
|
||||
.filter(n => n)
|
||||
.join(', ');
|
||||
|
||||
const newRuang = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
...ruangPayload,
|
||||
namaRuang: namaRuangDisplay,
|
||||
};
|
||||
|
||||
ruangData.value.push(newRuang);
|
||||
return { success: true, message: `Ruang klinik ${newRuang.namaKlinik} berhasil ditambahkan`, data: newRuang };
|
||||
};
|
||||
|
||||
const updateRuang = (ruangPayload) => {
|
||||
const index = ruangData.value.findIndex(r => r.id === ruangPayload.id);
|
||||
if (index !== -1) {
|
||||
const namaRuangDisplay = ruangPayload.ruangList
|
||||
.map(r => r.namaRuang)
|
||||
.filter(n => n)
|
||||
.join(', ');
|
||||
|
||||
ruangData.value[index] = {
|
||||
...ruangData.value[index],
|
||||
...ruangPayload,
|
||||
namaRuang: namaRuangDisplay,
|
||||
};
|
||||
return { success: true, message: `Ruang klinik ${ruangPayload.namaKlinik} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Ruang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deleteRuang = (ruangId) => {
|
||||
const index = ruangData.value.findIndex(r => r.id === ruangId);
|
||||
if (index !== -1) {
|
||||
const ruangName = `${ruangData.value[index].namaKlinik} - ${ruangData.value[index].namaRuang}`;
|
||||
ruangData.value.splice(index, 1);
|
||||
ruangData.value.forEach((r, idx) => {
|
||||
r.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Ruang ${ruangName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Ruang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const getRuangByKlinik = (kodeKlinik) => {
|
||||
return ruangData.value.filter(r => r.kodeKlinik === kodeKlinik);
|
||||
};
|
||||
|
||||
// Get all ruang list (for antrian display)
|
||||
const getAllRuangList = computed(() => {
|
||||
const allRuang = [];
|
||||
ruangData.value.forEach(klinik => {
|
||||
klinik.ruangList.forEach(ruang => {
|
||||
allRuang.push({
|
||||
kodeKlinik: klinik.kodeKlinik,
|
||||
namaKlinik: klinik.namaKlinik,
|
||||
...ruang
|
||||
});
|
||||
});
|
||||
});
|
||||
return allRuang;
|
||||
});
|
||||
// Actions - Loket (delegate to loketStore)
|
||||
const addLoket = (loketPayload) => loketStore.addLoket(loketPayload);
|
||||
const updateLoket = (loketPayload) => loketStore.updateLoket(loketPayload);
|
||||
const deleteLoket = (loketId) => loketStore.deleteLoket(loketId);
|
||||
const getLoketById = (id) => loketStore.getLoketById(id);
|
||||
|
||||
// ============================================
|
||||
// MASTER PENUNJANG
|
||||
// MASTER KLINIK RUANG (Backward Compatibility)
|
||||
// ============================================
|
||||
const penunjangData = ref([
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
kode: 'LAB',
|
||||
nama: 'LABORATORIUM',
|
||||
jenis: 'Medis',
|
||||
shift: 3,
|
||||
totalQuota: 150,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 60 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 50 },
|
||||
{ dari: '21:00', sampai: '07:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 30,
|
||||
keterangan: 'Pemeriksaan darah lengkap, urine, dan pemeriksaan laboratorium lainnya'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
no: 2,
|
||||
kode: 'RAD',
|
||||
nama: 'RADIOLOGI',
|
||||
jenis: 'Medis',
|
||||
shift: 2,
|
||||
totalQuota: 100,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 60 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 45,
|
||||
keterangan: 'Rontgen, CT Scan, MRI, USG, dan pemeriksaan radiologi lainnya'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
no: 3,
|
||||
kode: 'PAT',
|
||||
nama: 'PATOLOGI ANATOMI',
|
||||
jenis: 'Medis',
|
||||
shift: 1,
|
||||
totalQuota: 50,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 50 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 60,
|
||||
keterangan: 'Pemeriksaan jaringan dan sitologi'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
no: 4,
|
||||
kode: 'RHB',
|
||||
nama: 'REHABILITASI MEDIK',
|
||||
jenis: 'Medis',
|
||||
shift: 2,
|
||||
totalQuota: 80,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '13:00', kuota: 40 },
|
||||
{ dari: '13:00', sampai: '19:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 30,
|
||||
keterangan: 'Fisioterapi, terapi okupasi, dan terapi wicara'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
no: 5,
|
||||
kode: 'RME',
|
||||
nama: 'REKAM MEDIS',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 120,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 70 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 15,
|
||||
keterangan: 'Pengurusan dokumen rekam medis dan surat keterangan'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
no: 6,
|
||||
kode: 'GIZ',
|
||||
nama: 'GIZI',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 60,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '13:00', kuota: 30 },
|
||||
{ dari: '13:00', sampai: '19:00', kuota: 30 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 20,
|
||||
keterangan: 'Konsultasi gizi dan diet'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
no: 7,
|
||||
kode: 'LND',
|
||||
nama: 'LAUNDRY',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 3,
|
||||
totalQuota: 200,
|
||||
jamShiftList: [
|
||||
{ dari: '06:00', sampai: '14:00', kuota: 80 },
|
||||
{ dari: '14:00', sampai: '22:00', kuota: 70 },
|
||||
{ dari: '22:00', sampai: '06:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 10,
|
||||
keterangan: 'Layanan laundry linen rumah sakit'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
no: 8,
|
||||
kode: 'INF',
|
||||
nama: 'PELAYANAN INFORMASI',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 150,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 80 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 70 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 10,
|
||||
keterangan: 'Informasi umum rumah sakit dan bantuan navigasi'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
no: 9,
|
||||
kode: 'FRM',
|
||||
nama: 'FARMASI',
|
||||
jenis: 'Medis',
|
||||
shift: 3,
|
||||
totalQuota: 200,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 80 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 70 },
|
||||
{ dari: '21:00', sampai: '07:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 20,
|
||||
keterangan: 'Pelayanan obat dan konsultasi farmasi'
|
||||
}
|
||||
]);
|
||||
// Reference dari ruangStore
|
||||
const ruangData = computed(() => ruangStore.ruangData);
|
||||
const totalKlinikRuang = computed(() => ruangStore.totalKlinikRuang);
|
||||
const totalRuangan = computed(() => ruangStore.totalRuangan);
|
||||
const getAllRuangList = computed(() => ruangStore.getAllRuangList);
|
||||
|
||||
// Computed - Penunjang
|
||||
const totalPenunjang = computed(() => penunjangData.value.length);
|
||||
const activePenunjang = computed(() => penunjangData.value.filter(p => p.status === 'Aktif'));
|
||||
const penunjangMedis = computed(() => penunjangData.value.filter(p => p.jenis === 'Medis'));
|
||||
const penunjangNonMedis = computed(() => penunjangData.value.filter(p => p.jenis === 'Non-Medis'));
|
||||
// Actions - Ruang (delegate to ruangStore)
|
||||
const addRuang = (ruangPayload) => ruangStore.addRuang(ruangPayload);
|
||||
const updateRuang = (ruangPayload) => ruangStore.updateRuang(ruangPayload);
|
||||
const deleteRuang = (ruangId) => ruangStore.deleteRuang(ruangId);
|
||||
const getRuangByKlinik = (kodeKlinik) => ruangStore.getRuangByKlinik(kodeKlinik);
|
||||
|
||||
// Get penunjang list for dropdowns
|
||||
const penunjangList = computed(() =>
|
||||
penunjangData.value
|
||||
.filter(p => p.status === 'Aktif')
|
||||
.map(p => ({
|
||||
id: p.id,
|
||||
kode: p.kode,
|
||||
nama: p.nama
|
||||
}))
|
||||
);
|
||||
// ============================================
|
||||
// MASTER PENUNJANG (Backward Compatibility)
|
||||
// ============================================
|
||||
// Reference dari penunjangStore
|
||||
const penunjangData = computed(() => penunjangStore.penunjangData);
|
||||
const totalPenunjang = computed(() => penunjangStore.totalPenunjang);
|
||||
const activePenunjang = computed(() => penunjangStore.activePenunjang);
|
||||
const penunjangMedis = computed(() => penunjangStore.penunjangMedis);
|
||||
const penunjangNonMedis = computed(() => penunjangStore.penunjangNonMedis);
|
||||
const penunjangList = computed(() => penunjangStore.penunjangList);
|
||||
|
||||
// Actions - Penunjang
|
||||
const addPenunjang = (penunjangPayload) => {
|
||||
const newId = Math.max(...penunjangData.value.map(p => p.id), 0) + 1;
|
||||
const newNo = penunjangData.value.length + 1;
|
||||
|
||||
const totalQuota = penunjangPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
const newPenunjang = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
...penunjangPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
|
||||
penunjangData.value.push(newPenunjang);
|
||||
return { success: true, message: `Penunjang ${newPenunjang.nama} berhasil ditambahkan`, data: newPenunjang };
|
||||
};
|
||||
|
||||
const updatePenunjang = (penunjangPayload) => {
|
||||
const index = penunjangData.value.findIndex(p => p.id === penunjangPayload.id);
|
||||
if (index !== -1) {
|
||||
const totalQuota = penunjangPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
penunjangData.value[index] = {
|
||||
...penunjangData.value[index],
|
||||
...penunjangPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
return { success: true, message: `Penunjang ${penunjangPayload.nama} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Penunjang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deletePenunjang = (penunjangId) => {
|
||||
const index = penunjangData.value.findIndex(p => p.id === penunjangId);
|
||||
if (index !== -1) {
|
||||
const penunjangName = penunjangData.value[index].nama;
|
||||
penunjangData.value.splice(index, 1);
|
||||
penunjangData.value.forEach((p, idx) => {
|
||||
p.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Penunjang ${penunjangName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Penunjang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const getPenunjangById = (id) => {
|
||||
return penunjangData.value.find(p => p.id === id);
|
||||
};
|
||||
|
||||
const getPenunjangByKode = (kode) => {
|
||||
return penunjangData.value.find(p => p.kode === kode);
|
||||
};
|
||||
|
||||
// Get active penunjang list (untuk Admin Penunjang)
|
||||
const getActivePenunjangList = () => {
|
||||
return penunjangData.value
|
||||
.filter(p => p.status === 'Aktif')
|
||||
.map(p => ({ id: p.id, name: p.nama, kode: p.kode }));
|
||||
};
|
||||
// Actions - Penunjang (delegate to penunjangStore)
|
||||
const addPenunjang = (penunjangPayload) => penunjangStore.addPenunjang(penunjangPayload);
|
||||
const updatePenunjang = (penunjangPayload) => penunjangStore.updatePenunjang(penunjangPayload);
|
||||
const deletePenunjang = (penunjangId) => penunjangStore.deletePenunjang(penunjangId);
|
||||
const getPenunjangById = (id) => penunjangStore.getPenunjangById(id);
|
||||
const getPenunjangByKode = (kode) => penunjangStore.getPenunjangByKode(kode);
|
||||
const getActivePenunjangList = () => penunjangStore.getActivePenunjangList();
|
||||
|
||||
// ============================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================
|
||||
const getKlinikNameByKode = (kode) => {
|
||||
// Utamakan nama dari clinicStore agar konsisten di seluruh aplikasi
|
||||
if (typeof clinicStore.getClinicByKode === 'function') {
|
||||
const clinic = clinicStore.getClinicByKode(kode);
|
||||
if (clinic) {
|
||||
return clinic.name;
|
||||
}
|
||||
// Single source of truth: selalu ambil dari clinicStore
|
||||
const clinic = clinicStore.getClinicByKode(kode);
|
||||
if (clinic) {
|
||||
return clinic.name;
|
||||
}
|
||||
|
||||
// Fallback ke data lokal master jika belum terdaftar di clinicStore
|
||||
const klinik = klinikData.value.find((k) => k.kode === kode);
|
||||
return klinik ? klinik.nama : kode;
|
||||
// Fallback jika tidak ditemukan
|
||||
return kode;
|
||||
};
|
||||
|
||||
const getPenunjangNameByKode = (kode) => {
|
||||
const penunjang = penunjangData.value.find(p => p.kode === kode);
|
||||
return penunjang ? penunjang.nama : kode;
|
||||
return penunjangStore.getPenunjangNameByKode(kode);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -712,4 +252,7 @@ export const useMasterStore = defineStore('master', () => {
|
||||
getKlinikNameByKode,
|
||||
getPenunjangNameByKode,
|
||||
};
|
||||
});
|
||||
}, {
|
||||
// NOTE: masterStore tidak perlu persist karena semua data sudah di-persist di stores masing-masing
|
||||
persist: false,
|
||||
});
|
||||
@@ -0,0 +1,289 @@
|
||||
// stores/penunjangStore.js
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const usePenunjangStore = defineStore('penunjang', () => {
|
||||
// State - Penunjang Data
|
||||
const penunjangData = ref([
|
||||
{
|
||||
id: 1,
|
||||
no: 1,
|
||||
kode: 'LAB',
|
||||
nama: 'LABORATORIUM',
|
||||
jenis: 'Medis',
|
||||
shift: 3,
|
||||
totalQuota: 150,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 60 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 50 },
|
||||
{ dari: '21:00', sampai: '07:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 30,
|
||||
keterangan: 'Pemeriksaan darah lengkap, urine, dan pemeriksaan laboratorium lainnya'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
no: 2,
|
||||
kode: 'RAD',
|
||||
nama: 'RADIOLOGI',
|
||||
jenis: 'Medis',
|
||||
shift: 2,
|
||||
totalQuota: 100,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 60 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 45,
|
||||
keterangan: 'Rontgen, CT Scan, MRI, USG, dan pemeriksaan radiologi lainnya'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
no: 3,
|
||||
kode: 'PAT',
|
||||
nama: 'PATOLOGI ANATOMI',
|
||||
jenis: 'Medis',
|
||||
shift: 1,
|
||||
totalQuota: 50,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 50 }
|
||||
],
|
||||
autoShift: false,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 60,
|
||||
keterangan: 'Pemeriksaan jaringan dan sitologi'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
no: 4,
|
||||
kode: 'RHB',
|
||||
nama: 'REHABILITASI MEDIK',
|
||||
jenis: 'Medis',
|
||||
shift: 2,
|
||||
totalQuota: 80,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '13:00', kuota: 40 },
|
||||
{ dari: '13:00', sampai: '19:00', kuota: 40 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 30,
|
||||
keterangan: 'Fisioterapi, terapi okupasi, dan terapi wicara'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
no: 5,
|
||||
kode: 'RME',
|
||||
nama: 'REKAM MEDIS',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 120,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 70 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 15,
|
||||
keterangan: 'Pengurusan dokumen rekam medis dan surat keterangan'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
no: 6,
|
||||
kode: 'GIZ',
|
||||
nama: 'GIZI',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 60,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '13:00', kuota: 30 },
|
||||
{ dari: '13:00', sampai: '19:00', kuota: 30 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 20,
|
||||
keterangan: 'Konsultasi gizi dan diet'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
no: 7,
|
||||
kode: 'LND',
|
||||
nama: 'LAUNDRY',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 3,
|
||||
totalQuota: 200,
|
||||
jamShiftList: [
|
||||
{ dari: '06:00', sampai: '14:00', kuota: 80 },
|
||||
{ dari: '14:00', sampai: '22:00', kuota: 70 },
|
||||
{ dari: '22:00', sampai: '06:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 10,
|
||||
keterangan: 'Layanan laundry linen rumah sakit'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
no: 8,
|
||||
kode: 'INF',
|
||||
nama: 'PELAYANAN INFORMASI',
|
||||
jenis: 'Non-Medis',
|
||||
shift: 2,
|
||||
totalQuota: 150,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 80 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 70 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 10,
|
||||
keterangan: 'Informasi umum rumah sakit dan bantuan navigasi'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
no: 9,
|
||||
kode: 'FRM',
|
||||
nama: 'FARMASI',
|
||||
jenis: 'Medis',
|
||||
shift: 3,
|
||||
totalQuota: 200,
|
||||
jamShiftList: [
|
||||
{ dari: '07:00', sampai: '15:00', kuota: 80 },
|
||||
{ dari: '13:00', sampai: '21:00', kuota: 70 },
|
||||
{ dari: '21:00', sampai: '07:00', kuota: 50 }
|
||||
],
|
||||
autoShift: true,
|
||||
jadwalOperasional: ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jum\'at', 'Sabtu', 'Minggu'],
|
||||
status: 'Aktif',
|
||||
estimasiLayanan: 20,
|
||||
keterangan: 'Pelayanan obat dan konsultasi farmasi'
|
||||
}
|
||||
]);
|
||||
|
||||
// Computed
|
||||
const totalPenunjang = computed(() => penunjangData.value.length);
|
||||
const activePenunjang = computed(() => penunjangData.value.filter(p => p.status === 'Aktif'));
|
||||
const penunjangMedis = computed(() => penunjangData.value.filter(p => p.jenis === 'Medis'));
|
||||
const penunjangNonMedis = computed(() => penunjangData.value.filter(p => p.jenis === 'Non-Medis'));
|
||||
|
||||
// Get penunjang list for dropdowns
|
||||
// Standardized to use 'name' for consistency (keeping 'nama' for backward compatibility)
|
||||
const penunjangList = computed(() =>
|
||||
penunjangData.value
|
||||
.filter(p => p.status === 'Aktif')
|
||||
.map(p => ({
|
||||
id: p.id,
|
||||
kode: p.kode,
|
||||
nama: p.nama, // Keep 'nama' for internal consistency
|
||||
name: p.nama // Add 'name' alias for external compatibility
|
||||
}))
|
||||
);
|
||||
|
||||
// Actions - CRUD Operations
|
||||
const addPenunjang = (penunjangPayload) => {
|
||||
const newId = Math.max(...penunjangData.value.map(p => p.id), 0) + 1;
|
||||
const newNo = penunjangData.value.length + 1;
|
||||
|
||||
const totalQuota = penunjangPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
const newPenunjang = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
...penunjangPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
|
||||
penunjangData.value.push(newPenunjang);
|
||||
return { success: true, message: `Penunjang ${newPenunjang.nama} berhasil ditambahkan`, data: newPenunjang };
|
||||
};
|
||||
|
||||
const updatePenunjang = (penunjangPayload) => {
|
||||
const index = penunjangData.value.findIndex(p => p.id === penunjangPayload.id);
|
||||
if (index !== -1) {
|
||||
const totalQuota = penunjangPayload.jamShiftList.reduce((total, shift) => {
|
||||
return total + (parseInt(shift.kuota) || 0);
|
||||
}, 0);
|
||||
|
||||
penunjangData.value[index] = {
|
||||
...penunjangData.value[index],
|
||||
...penunjangPayload,
|
||||
totalQuota: totalQuota,
|
||||
};
|
||||
return { success: true, message: `Penunjang ${penunjangPayload.nama} berhasil diupdate` };
|
||||
}
|
||||
return { success: false, message: 'Penunjang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const deletePenunjang = (penunjangId) => {
|
||||
const index = penunjangData.value.findIndex(p => p.id === penunjangId);
|
||||
if (index !== -1) {
|
||||
const penunjangName = penunjangData.value[index].nama;
|
||||
penunjangData.value.splice(index, 1);
|
||||
penunjangData.value.forEach((p, idx) => {
|
||||
p.no = idx + 1;
|
||||
});
|
||||
return { success: true, message: `Penunjang ${penunjangName} berhasil dihapus` };
|
||||
}
|
||||
return { success: false, message: 'Penunjang tidak ditemukan' };
|
||||
};
|
||||
|
||||
const getPenunjangById = (id) => {
|
||||
return penunjangData.value.find(p => p.id === id);
|
||||
};
|
||||
|
||||
const getPenunjangByKode = (kode) => {
|
||||
return penunjangData.value.find(p => p.kode === kode);
|
||||
};
|
||||
|
||||
// Get active penunjang list (untuk Admin Penunjang)
|
||||
const getActivePenunjangList = () => {
|
||||
return penunjangData.value
|
||||
.filter(p => p.status === 'Aktif')
|
||||
.map(p => ({ id: p.id, name: p.nama, kode: p.kode }));
|
||||
};
|
||||
|
||||
// Utility function
|
||||
const getPenunjangNameByKode = (kode) => {
|
||||
const penunjang = penunjangData.value.find(p => p.kode === kode);
|
||||
return penunjang ? penunjang.nama : kode;
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
penunjangData,
|
||||
totalPenunjang,
|
||||
activePenunjang,
|
||||
penunjangMedis,
|
||||
penunjangNonMedis,
|
||||
penunjangList,
|
||||
|
||||
// Actions
|
||||
addPenunjang,
|
||||
updatePenunjang,
|
||||
deletePenunjang,
|
||||
getPenunjangById,
|
||||
getPenunjangByKode,
|
||||
getActivePenunjangList,
|
||||
getPenunjangNameByKode,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'penunjang-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['penunjangData'],
|
||||
},
|
||||
});
|
||||
|
||||
+161
-13
@@ -2,9 +2,11 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useClinicStore } from './clinicStore';
|
||||
import { usePenunjangStore } from './penunjangStore';
|
||||
|
||||
export const useQueueStore = defineStore('queue', () => {
|
||||
const clinicStore = useClinicStore();
|
||||
const penunjangStore = usePenunjangStore();
|
||||
// Seed data for easy reset during dev
|
||||
const seedPatients = [
|
||||
{
|
||||
@@ -167,9 +169,9 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
|
||||
const cloneSeed = () => seedPatients.map(p => ({ ...p }));
|
||||
|
||||
// State
|
||||
// Initialize state - will be automatically hydrated from localStorage by pinia-plugin-persistedstate
|
||||
// If localStorage has data, it will override these defaults
|
||||
const allPatients = ref(cloneSeed());
|
||||
|
||||
const quotaUsed = ref(5);
|
||||
const currentProcessingPatient = ref({
|
||||
loket: null,
|
||||
@@ -193,17 +195,17 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
const penunjangs = ref([
|
||||
{ id: 1, name: "LABORATORIUM" },
|
||||
{ id: 2, name: "RADIOLOGI" },
|
||||
{ id: 3, name: "USG" },
|
||||
{ id: 4, name: "CT SCAN" },
|
||||
{ id: 5, name: "MRI" },
|
||||
{ id: 6, name: "EKG" },
|
||||
{ id: 7, name: "ENDOSKOPI" },
|
||||
{ id: 8, name: "FISIOTERAPI" },
|
||||
{ id: 9, name: "FARMASI" },
|
||||
]);
|
||||
// Penunjang data - reference dari penunjangStore (single source of truth)
|
||||
// Menggunakan computed untuk reactive reference
|
||||
const penunjangs = computed(() => {
|
||||
// Get penunjang list from penunjangStore dan map ke format yang diharapkan
|
||||
const penunjangList = penunjangStore.penunjangList || [];
|
||||
return penunjangList.map(p => ({
|
||||
id: p.id,
|
||||
name: p.nama || p.name, // Support both nama and name for backward compatibility
|
||||
kode: p.kode
|
||||
}));
|
||||
});
|
||||
|
||||
// Computed - Filter berdasarkan process stage dan status
|
||||
const getPatientsByStage = (stage) => {
|
||||
@@ -528,6 +530,144 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
currentProcessingPatient.value[adminType] = patient;
|
||||
};
|
||||
|
||||
// Register patient from Anjungan (onsite registration)
|
||||
const registerPatientFromAnjungan = (clinic, paymentType, visitType = 'SEKARANG', visitDate = null, shift = 'Shift 1', namaDokter = null) => {
|
||||
const newNo = allPatients.value.length > 0
|
||||
? Math.max(...allPatients.value.map(p => p.no)) + 1
|
||||
: 1;
|
||||
|
||||
const timestamp = new Date();
|
||||
const visitDateTime = visitDate ? new Date(visitDate) : timestamp;
|
||||
const jamPanggil = visitDate
|
||||
? `${String(visitDateTime.getHours()).padStart(2, "0")}:${String(visitDateTime.getMinutes()).padStart(2, "0")}`
|
||||
: `${String(timestamp.getHours()).padStart(2, "0")}:${String(timestamp.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
// Generate barcode (simulasi - bisa diganti dengan API call)
|
||||
const barcode = `250811${String(Date.now()).slice(-6)}${String(newNo).padStart(3, "0")}`;
|
||||
|
||||
// Format nomor antrean: UMXXXX | Onsite - barcode
|
||||
const noAntrian = `UM${String(newNo).padStart(4, "0")} | Onsite - ${barcode}`;
|
||||
|
||||
// Determine status based on visit type
|
||||
const status = visitType === 'SEKARANG' ? 'waiting' : 'pending';
|
||||
|
||||
const newPatient = {
|
||||
no: newNo,
|
||||
jamPanggil: jamPanggil,
|
||||
barcode: barcode,
|
||||
noAntrian: noAntrian,
|
||||
shift: shift,
|
||||
klinik: clinic.name || clinic,
|
||||
fastTrack: "TIDAK", // Default, bisa diubah nanti
|
||||
pembayaran: paymentType,
|
||||
status: status,
|
||||
processStage: "loket",
|
||||
createdAt: timestamp.toISOString(),
|
||||
registrationType: 'onsite',
|
||||
visitType: visitType,
|
||||
visitDate: visitDate || timestamp.toISOString().substring(0, 10),
|
||||
namaDokter: namaDokter || null, // Nama dokter hanya untuk pasien Eksekutif
|
||||
};
|
||||
|
||||
allPatients.value.push(newPatient);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Pendaftaran ${clinic.name || clinic} untuk kunjungan ${visitType === 'SEKARANG' ? 'HARI INI' : visitDate} dengan pembayaran ${paymentType} berhasil diproses.`,
|
||||
patient: newPatient,
|
||||
};
|
||||
};
|
||||
|
||||
// Check-in patient (update status from waiting to di-loket)
|
||||
const checkInPatient = (patientIdOrBarcode) => {
|
||||
console.log('🔍 checkInPatient called with:', patientIdOrBarcode);
|
||||
console.log('📊 Total patients in store:', allPatients.value.length);
|
||||
console.log('📋 First few patients:', allPatients.value.slice(0, 3).map(p => ({
|
||||
no: p.no,
|
||||
noAntrian: p.noAntrian,
|
||||
barcode: p.barcode,
|
||||
status: p.status
|
||||
})));
|
||||
|
||||
// Clean input - remove whitespace and convert to uppercase for comparison
|
||||
const cleanInput = String(patientIdOrBarcode).trim().toUpperCase();
|
||||
|
||||
// Try to find by multiple criteria:
|
||||
// 1. Exact barcode match
|
||||
// 2. Parse as number and match with no
|
||||
// 3. Extract number from input (e.g., "UM0014" -> "0014" or "14") and match with noAntrian
|
||||
// 4. Check if noAntrian includes the input
|
||||
const patientIndex = allPatients.value.findIndex(p => {
|
||||
// Exact barcode match
|
||||
if (p.barcode === cleanInput || p.barcode === patientIdOrBarcode) {
|
||||
console.log('✅ Found by barcode:', p.barcode);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try parsing as number
|
||||
const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(patientIdOrBarcode);
|
||||
if (!isNaN(parsedNo) && p.no === parsedNo) {
|
||||
console.log('✅ Found by no:', p.no);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if noAntrian includes the input (case insensitive)
|
||||
const noAntrianUpper = (p.noAntrian || '').toUpperCase();
|
||||
if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(patientIdOrBarcode)) {
|
||||
console.log('✅ Found by noAntrian:', p.noAntrian);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to extract number from noAntrian (e.g., "UM0014 | Onsite - ..." -> "0014")
|
||||
const noAntrianNumber = noAntrianUpper.match(/([A-Z]+)(\d+)/);
|
||||
if (noAntrianNumber) {
|
||||
const extractedNumber = noAntrianNumber[2];
|
||||
const inputNumber = cleanInput.replace(/[^0-9]/g, '');
|
||||
if (inputNumber && extractedNumber.includes(inputNumber) || inputNumber.includes(extractedNumber)) {
|
||||
console.log('✅ Found by extracted number from noAntrian');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (patientIndex === -1) {
|
||||
console.log('❌ Patient not found. Searched for:', cleanInput);
|
||||
console.log('📋 Available noAntrian values:', allPatients.value.map(p => p.noAntrian));
|
||||
return { success: false, message: "Pasien tidak ditemukan" };
|
||||
}
|
||||
|
||||
const patient = allPatients.value[patientIndex];
|
||||
console.log('✅ Patient found:', patient);
|
||||
|
||||
// Only allow check-in if status is waiting or pending
|
||||
if (patient.status !== 'waiting' && patient.status !== 'pending') {
|
||||
console.log('⚠️ Patient status is not waiting/pending:', patient.status);
|
||||
return {
|
||||
success: false,
|
||||
message: `Pasien tidak dapat check-in. Status saat ini: ${patient.status}`
|
||||
};
|
||||
}
|
||||
|
||||
// Update status to di-loket
|
||||
allPatients.value[patientIndex] = {
|
||||
...allPatients.value[patientIndex],
|
||||
status: "di-loket",
|
||||
};
|
||||
|
||||
console.log('✅ Check-in successful, patient status updated to di-loket');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Check-in berhasil. Pasien ${patient.noAntrian.split(" |")[0]} siap diproses di loket.`,
|
||||
patient: allPatients.value[patientIndex],
|
||||
};
|
||||
};
|
||||
|
||||
// State persistence is handled automatically by pinia-plugin-persistedstate
|
||||
// No need for manual watch or save logic
|
||||
|
||||
return {
|
||||
// State
|
||||
allPatients,
|
||||
@@ -551,5 +691,13 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
getTotalPasienByStage,
|
||||
getCurrentProcessing,
|
||||
setCurrentProcessing,
|
||||
registerPatientFromAnjungan,
|
||||
checkInPatient,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'queue-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['allPatients', 'quotaUsed', 'currentProcessingPatient'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
// stores/ruangStore.js
|
||||
// Merged dari masterStore.ruangData + klinikruangstore.klinikRuangList
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useClinicStore } from './clinicStore';
|
||||
|
||||
export const useRuangStore = defineStore('ruang', () => {
|
||||
const clinicStore = useClinicStore();
|
||||
|
||||
// State/Computed - List Master Klinik (disinkronkan dengan clinicStore)
|
||||
const masterKlinikList = computed(() => {
|
||||
const baseList = typeof clinicStore.getClinicsForDropdown === 'function'
|
||||
? clinicStore.getClinicsForDropdown()
|
||||
: [];
|
||||
|
||||
return baseList.map((c) => ({
|
||||
kode: c.kode,
|
||||
nama: c.name,
|
||||
id: c.id,
|
||||
}));
|
||||
});
|
||||
|
||||
// State - Ruang Data (merged dari masterStore.ruangData + klinikruangstore.klinikRuangList)
|
||||
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
|
||||
const totalKlinikRuang = computed(() => ruangData.value.length);
|
||||
|
||||
const totalRuangan = computed(() => {
|
||||
return ruangData.value.reduce((total, klinik) => {
|
||||
return total + klinik.ruangList.length;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Get all ruang list (for antrian display)
|
||||
const getAllRuangList = computed(() => {
|
||||
const allRuang = [];
|
||||
ruangData.value.forEach(klinik => {
|
||||
klinik.ruangList.forEach(ruang => {
|
||||
allRuang.push({
|
||||
kodeKlinik: klinik.kodeKlinik,
|
||||
namaKlinik: klinik.namaKlinik,
|
||||
...ruang
|
||||
});
|
||||
});
|
||||
});
|
||||
return allRuang;
|
||||
});
|
||||
|
||||
// Getters
|
||||
const getKlinikByCode = (kode) => {
|
||||
return ruangData.value.find(k => k.kodeKlinik === kode);
|
||||
};
|
||||
|
||||
const getRuangByKlinik = (kodeKlinik) => {
|
||||
return ruangData.value.filter(r => r.kodeKlinik === kodeKlinik);
|
||||
};
|
||||
|
||||
// Actions - CRUD Operations
|
||||
const createKlinikRuang = (data) => {
|
||||
const newId = Math.max(...ruangData.value.map(r => r.id), 0) + 1;
|
||||
const newNo = ruangData.value.length + 1;
|
||||
|
||||
// Generate nama ruang untuk display
|
||||
const namaRuangDisplay = data.ruangList
|
||||
.map(r => r.namaRuang)
|
||||
.filter(n => n)
|
||||
.join(', ');
|
||||
|
||||
const newKlinikRuang = {
|
||||
id: newId,
|
||||
no: newNo,
|
||||
kodeKlinik: data.kodeKlinik,
|
||||
namaKlinik: data.namaKlinik,
|
||||
namaRuang: namaRuangDisplay,
|
||||
ruangList: data.ruangList
|
||||
};
|
||||
|
||||
ruangData.value.push(newKlinikRuang);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Klinik Ruang ${data.namaKlinik} berhasil ditambahkan`,
|
||||
data: newKlinikRuang
|
||||
};
|
||||
};
|
||||
|
||||
const updateKlinikRuang = (id, data) => {
|
||||
const index = ruangData.value.findIndex(r => r.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Klinik Ruang tidak ditemukan'
|
||||
};
|
||||
}
|
||||
|
||||
// Generate nama ruang untuk display
|
||||
const namaRuangDisplay = data.ruangList
|
||||
.map(r => r.namaRuang)
|
||||
.filter(n => n)
|
||||
.join(', ');
|
||||
|
||||
ruangData.value[index] = {
|
||||
...ruangData.value[index],
|
||||
kodeKlinik: data.kodeKlinik,
|
||||
namaKlinik: data.namaKlinik,
|
||||
namaRuang: namaRuangDisplay,
|
||||
ruangList: data.ruangList
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Klinik Ruang ${data.namaKlinik} berhasil diupdate`,
|
||||
data: ruangData.value[index]
|
||||
};
|
||||
};
|
||||
|
||||
const deleteKlinikRuang = (id) => {
|
||||
const index = ruangData.value.findIndex(r => r.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Klinik Ruang tidak ditemukan'
|
||||
};
|
||||
}
|
||||
|
||||
const deletedKlinik = ruangData.value[index];
|
||||
ruangData.value.splice(index, 1);
|
||||
|
||||
// Reorder numbers
|
||||
ruangData.value.forEach((r, idx) => {
|
||||
r.no = idx + 1;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Klinik Ruang ${deletedKlinik.namaKlinik} berhasil dihapus`
|
||||
};
|
||||
};
|
||||
|
||||
const addRuangToKlinik = (klinikId, ruangDataItem) => {
|
||||
const klinik = ruangData.value.find(k => k.id === klinikId);
|
||||
|
||||
if (!klinik) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Klinik tidak ditemukan'
|
||||
};
|
||||
}
|
||||
|
||||
klinik.ruangList.push(ruangDataItem);
|
||||
|
||||
// Update display name
|
||||
klinik.namaRuang = klinik.ruangList
|
||||
.map(r => r.namaRuang)
|
||||
.filter(n => n)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Ruang ${ruangDataItem.namaRuang} berhasil ditambahkan`,
|
||||
data: klinik
|
||||
};
|
||||
};
|
||||
|
||||
const removeRuangFromKlinik = (klinikId, ruangIndex) => {
|
||||
const klinik = ruangData.value.find(k => k.id === klinikId);
|
||||
|
||||
if (!klinik) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Klinik tidak ditemukan'
|
||||
};
|
||||
}
|
||||
|
||||
if (klinik.ruangList.length <= 1) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Minimal harus ada 1 ruangan'
|
||||
};
|
||||
}
|
||||
|
||||
const removedRuang = klinik.ruangList[ruangIndex];
|
||||
klinik.ruangList.splice(ruangIndex, 1);
|
||||
|
||||
// Update display name
|
||||
klinik.namaRuang = klinik.ruangList
|
||||
.map(r => r.namaRuang)
|
||||
.filter(n => n)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Ruang ${removedRuang.namaRuang} berhasil dihapus`,
|
||||
data: klinik
|
||||
};
|
||||
};
|
||||
|
||||
const searchKlinikRuang = (searchTerm) => {
|
||||
if (!searchTerm) return ruangData.value;
|
||||
|
||||
const term = searchTerm.toLowerCase();
|
||||
return ruangData.value.filter(k =>
|
||||
k.kodeKlinik.toLowerCase().includes(term) ||
|
||||
k.namaKlinik.toLowerCase().includes(term) ||
|
||||
k.namaRuang.toLowerCase().includes(term)
|
||||
);
|
||||
};
|
||||
|
||||
// Alias methods untuk backward compatibility dengan masterStore
|
||||
const addRuang = (ruangPayload) => {
|
||||
return createKlinikRuang(ruangPayload);
|
||||
};
|
||||
|
||||
const updateRuang = (ruangPayload) => {
|
||||
return updateKlinikRuang(ruangPayload.id, ruangPayload);
|
||||
};
|
||||
|
||||
const deleteRuang = (ruangId) => {
|
||||
return deleteKlinikRuang(ruangId);
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
masterKlinikList,
|
||||
ruangData,
|
||||
|
||||
// Computed
|
||||
totalKlinikRuang,
|
||||
totalRuangan,
|
||||
getAllRuangList,
|
||||
|
||||
// Getters
|
||||
getKlinikByCode,
|
||||
getRuangByKlinik,
|
||||
|
||||
// Actions (new names)
|
||||
createKlinikRuang,
|
||||
updateKlinikRuang,
|
||||
deleteKlinikRuang,
|
||||
addRuangToKlinik,
|
||||
removeRuangFromKlinik,
|
||||
searchKlinikRuang,
|
||||
|
||||
// Alias for backward compatibility
|
||||
addRuang,
|
||||
updateRuang,
|
||||
deleteRuang,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'ruang-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['ruangData'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -86,5 +86,11 @@ export const useScreenStore = defineStore('screen', () => {
|
||||
updateScreen,
|
||||
deleteScreen,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'screen-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: ['screenItems'],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user