update api klinik ruang
This commit is contained in:
No files matched your search
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
// Basic test to verify Vitest is working
|
||||
describe('Vitest Setup', () => {
|
||||
it('works correctly', () => {
|
||||
expect(1 + 1).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
// Basic test for Vue component mounting (optional, depends on having a simple component)
|
||||
/*
|
||||
import MyComponent from './MyComponent.vue'
|
||||
describe('Component Test', () => {
|
||||
it('mounts properly', () => {
|
||||
const wrapper = mount(MyComponent)
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
*/
|
||||
@@ -144,8 +144,8 @@ const {
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 30mm;
|
||||
height: 30mm;
|
||||
width: 45mm;
|
||||
height: 45mm;
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
padding: 0.5mm;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useThermalPrint = () => {
|
||||
type: 'image/png',
|
||||
quality: 1,
|
||||
margin: 2, // Added margin (quiet zone) for better scanner detection
|
||||
width: 150, // Slightly larger width for better clarity
|
||||
width: 400, // High quality width for clear printing
|
||||
version: 3,
|
||||
color: {
|
||||
dark: '#000000',
|
||||
@@ -230,8 +230,8 @@ export const useThermalPrint = () => {
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 40mm;
|
||||
height: 40mm;
|
||||
width: 45mm;
|
||||
height: 45mm;
|
||||
margin: 0 auto;
|
||||
border: none;
|
||||
padding: 0.5mm;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: "http://10.10.150.175:3000",
|
||||
supportFile: false,
|
||||
specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
|
||||
},
|
||||
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "vue",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
describe('Login Page', () => {
|
||||
it('should load the login page', () => {
|
||||
cy.visit('/')
|
||||
// Adjust this to match a selector on your home or login page
|
||||
cy.contains('h1', /login/i).should('be.visible')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "[email protected]",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,36 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
import { mount } from 'cypress/vue'
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
// with a <reference path="./component" /> at the top of your spec.
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(MyComponent)
|
||||
Generated
+2486
-7
File diff suppressed because it is too large
Load diff
+10
-1
@@ -10,7 +10,11 @@
|
||||
"dev:https": "nuxt dev -o --host 0.0.0.0 --port 3000",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
"postinstall": "nuxt prepare",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
@@ -49,9 +53,14 @@
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@types/node": "^24.7.2",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"cypress": "^15.9.0",
|
||||
"happy-dom": "^20.5.0",
|
||||
"sass": "^1.93.3",
|
||||
"sass-embedded": "^1.89.2",
|
||||
"vite-plugin-vuetify": "^2.1.2",
|
||||
"vitest": "^3.2.4",
|
||||
"vuetify": "^3.9.3"
|
||||
}
|
||||
}
|
||||
@@ -795,6 +795,8 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useQueueStore } from '@/stores/queueStore';
|
||||
import { useMasterStore } from '@/stores/masterStore';
|
||||
import { useClinicStore } from '@/stores/clinicStore';
|
||||
import { useRuangStore } from '@/stores/ruangStore';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import PatientCard from '@/components/features/queue/PatientCard.vue';
|
||||
import AppSnackbar from '@/components/common/AppSnackbar.vue';
|
||||
@@ -805,6 +807,8 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const queueStore = useQueueStore();
|
||||
const masterStore = useMasterStore();
|
||||
const clinicStore = useClinicStore();
|
||||
const ruangStore = useRuangStore();
|
||||
|
||||
const kodeKlinik = computed(() => {
|
||||
const kode = route.params.kodeKlinik;
|
||||
@@ -1769,18 +1773,36 @@ const applyFilters = (ruang) => {
|
||||
pendingPage.value[ruang.nomorRuang] = 1;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!klinikData.value) {
|
||||
router.push('/admin-klinik-ruang');
|
||||
// Watch for ruangList changes to re-initialize filters
|
||||
watch(ruangList, (newList) => {
|
||||
if (newList && newList.length > 0) {
|
||||
newList.forEach(ruang => {
|
||||
if (!pendingPage.value[ruang.nomorRuang]) {
|
||||
pendingPage.value[ruang.nomorRuang] = 1;
|
||||
}
|
||||
initializeFilterOptions(ruang);
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(async () => {
|
||||
console.log('🚀 AdminKlinikRuang mounted, проверяем data...');
|
||||
|
||||
// Initialize pagination, sort options, and filter options for each room
|
||||
ruangList.value.forEach(ruang => {
|
||||
if (!pendingPage.value[ruang.nomorRuang]) {
|
||||
pendingPage.value[ruang.nomorRuang] = 1;
|
||||
}
|
||||
initializeFilterOptions(ruang);
|
||||
});
|
||||
// 1. Sync data if needed
|
||||
try {
|
||||
// Fetch clinics first
|
||||
await clinicStore.fetchRegulerClinics();
|
||||
// Then sync rooms
|
||||
await ruangStore.fetchRuangFromAPI();
|
||||
} catch (error) {
|
||||
console.error('❌ Error syncing data in AdminKlinikRuang:', error);
|
||||
}
|
||||
|
||||
if (!klinikData.value) {
|
||||
console.warn('⚠️ Klinik data not found after sync');
|
||||
// We don't automatically redirect anymore to allow sync to complete
|
||||
// but we can check again after a short delay or just let the computed handle it
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -119,80 +119,20 @@ const navigateToSettings = () => {
|
||||
router.push('/setting/masterklinikruang');
|
||||
};
|
||||
|
||||
// Generate rooms from clinics based on spesialis field (same logic as MasterKlinikRuang)
|
||||
const generateRoomsFromClinics = () => {
|
||||
console.log('🔄 AdminKlinikRuang/index - Generating rooms from clinics...');
|
||||
const allRooms = [];
|
||||
let roomIdCounter = 1;
|
||||
|
||||
const allClinics = clinicStore.clinics;
|
||||
console.log('📊 Total clinics:', allClinics.length);
|
||||
|
||||
allClinics.forEach(clinic => {
|
||||
if (clinic.jenisLayanan === 'Reguler') {
|
||||
if (clinic.spesialis && Array.isArray(clinic.spesialis) && clinic.spesialis.length > 0) {
|
||||
// Create room ONLY for each specialist (no duplicate clinic-name room)
|
||||
clinic.spesialis.forEach((spec, index) => {
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
|
||||
namaRuang: spec.Spesialis,
|
||||
nomorRuang: (index + 1).toString(),
|
||||
nomorScreen: `${clinic.kode}${index + 1}`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Only create 1 room with clinic name when NO spesialis
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
}
|
||||
} else if (clinic.jenisLayanan === 'Eksekutif') {
|
||||
// For Eksekutif: always 1 room (no spesialis in seed data)
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: 'Eksekutif'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Generated ${allRooms.length} rooms from ${allClinics.length} clinics`);
|
||||
|
||||
// Update ruangStore with generated rooms
|
||||
const result = ruangStore.replaceAllRooms(allRooms);
|
||||
console.log('💾 Rooms saved to ruangStore:', result.message);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Load data on mount (same as MasterKlinikRuang)
|
||||
// Load data on mount
|
||||
onMounted(async () => {
|
||||
console.log('🚀 AdminKlinikRuang/index mounted, loading data...');
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// Fetch clinics from API (uses cache if available)
|
||||
const fetchResult = await clinicStore.fetchRegulerClinics();
|
||||
console.log('📥 Fetch result:', fetchResult);
|
||||
// 1. Fetch clinics from API (uses cache if available)
|
||||
await clinicStore.fetchRegulerClinics();
|
||||
|
||||
// Generate rooms based on fetched data
|
||||
generateRoomsFromClinics();
|
||||
// 2. Sync room definitions from API (centralized logic)
|
||||
// This will update ruangStore.ruangData which klinikRuangList depends on
|
||||
await ruangStore.fetchRuangFromAPI();
|
||||
|
||||
console.log('✅ Data loading and sync completed in AdminKlinikRuang/index');
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading data:', error);
|
||||
} finally {
|
||||
|
||||
@@ -385,31 +385,15 @@ const displayedLokets = computed(() => {
|
||||
|
||||
// Calculate grid columns based on number of displayed lokets
|
||||
// User wants all lokets in a single row (columns only)
|
||||
const gridColumns = computed(() => {
|
||||
return displayedLokets.value.length || 1
|
||||
})
|
||||
|
||||
const gridStyle = computed(() => {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(${gridColumns.value}, 1fr)`
|
||||
gridTemplateColumns: `repeat(${displayedLokets.value.length || 1}, 1fr)`
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate ticket grid columns based on loket grid columns
|
||||
const ticketGridColumns = computed(() => {
|
||||
const loketCols = gridColumns.value
|
||||
// If few lokets, show tickets in multiple columns
|
||||
if (loketCols <= 2) return 5
|
||||
if (loketCols === 3) return 4
|
||||
if (loketCols === 4) return 3
|
||||
if (loketCols === 5) return 2
|
||||
// If many lokets, show tickets in 1 column (vertical)
|
||||
return 1
|
||||
})
|
||||
|
||||
const ticketGridStyle = computed(() => {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(${ticketGridColumns.value}, 1fr)`
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(min(115px, 100%), 1fr))`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -906,25 +890,24 @@ onUnmounted(() => {
|
||||
|
||||
.ticket-cards-grid {
|
||||
display: grid;
|
||||
gap: clamp(4px, 0.5vw, 10px);
|
||||
gap: clamp(4px, 0.4vw, 8px);
|
||||
width: 100%;
|
||||
padding: clamp(2px, 0.3vw, 5px);
|
||||
/* grid-template-columns is set dynamically via :style binding */
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.ticket-card {
|
||||
background: var(--color-neutral-400, #E1E5EA);
|
||||
border: 1px solid var(--color-neutral-500, #CDD4DC);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
padding: clamp(6px, 0.8vh, 10px) 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 2px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
min-height: 80px;
|
||||
min-height: clamp(55px, 7vh, 80px);
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -948,12 +931,16 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.ticket-number {
|
||||
font-size: clamp(20px, 2.5vw, 32px);
|
||||
font-size: clamp(14px, 3.5vw, 32px);
|
||||
font-weight: 800;
|
||||
color: var(--color-primary-600, #3A61C9);
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
overflow: visible; /* Don't hide/ellipsis, let it fit via clamp font-size */
|
||||
}
|
||||
|
||||
.ticket-klinik {
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useMasterStore } from '@/stores/masterStore'
|
||||
import { useClinicStore } from '@/stores/clinicStore'
|
||||
import { useRuangStore } from '@/stores/ruangStore'
|
||||
import { useRoute } from '#app'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
|
||||
@@ -159,6 +161,8 @@ useHead({
|
||||
const route = useRoute()
|
||||
const queueStore = useQueueStore()
|
||||
const masterStore = useMasterStore()
|
||||
const clinicStore = useClinicStore()
|
||||
const ruangStore = useRuangStore()
|
||||
|
||||
const kodeKlinik = computed(() => {
|
||||
const kode = route.params.kodeKlinik
|
||||
@@ -566,9 +570,20 @@ const initWebSocket = () => {
|
||||
return wsInstance
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Redirect to index if klinik not found
|
||||
onMounted(async () => {
|
||||
console.log('🚀 AntrianKlinikRuang (Anjungan) mounted, syncing data...')
|
||||
|
||||
// 1. Sync data if needed
|
||||
try {
|
||||
await clinicStore.fetchRegulerClinics()
|
||||
await ruangStore.fetchRuangFromAPI()
|
||||
} catch (error) {
|
||||
console.error('❌ Error syncing data in AntrianKlinikRuang:', error)
|
||||
}
|
||||
|
||||
// Redirect to index if klinik not found after sync
|
||||
if (!klinikData.value) {
|
||||
console.warn('⚠️ Klinik data not found after sync, redirecting...')
|
||||
navigateTo('/anjungan/antrianklinikruang')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,53 +121,13 @@ const clinicStore = useClinicStore();
|
||||
const ruangStore = useRuangStore();
|
||||
const route = useRoute();
|
||||
|
||||
// Group klinik berdasarkan data flatten getAllRuangList agar nama ruang baru langsung muncul
|
||||
// Simplified computed to use masterStore.ruangData (which is already grouped/processed by ruangStore)
|
||||
const kliniksWithRuang = computed(() => {
|
||||
const fromGetter = masterStore.getAllRuangList?.value;
|
||||
const fromState = masterStore.ruangData?.value || masterStore.ruangData || [];
|
||||
|
||||
let allRuang = Array.isArray(fromGetter) ? fromGetter : [];
|
||||
|
||||
// Jika getter kosong, flatten dari ruangData (grouped)
|
||||
if (allRuang.length === 0 && Array.isArray(fromState)) {
|
||||
fromState.forEach((r) => {
|
||||
(r?.ruangList || []).forEach((ru) => {
|
||||
allRuang.push({
|
||||
kodeKlinik: r.kodeKlinik,
|
||||
namaKlinik: r.namaKlinik,
|
||||
nomorRuang: ru.nomorRuang,
|
||||
namaRuang: ru.namaRuang,
|
||||
nomorScreen: ru.nomorScreen,
|
||||
jenisLayanan: r.jenisLayanan
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const klinikMap = {};
|
||||
allRuang.forEach((ruang) => {
|
||||
// Use kodeKlinik + jenisLayanan as key to differentiate same clinic with different service types
|
||||
const key = `${ruang.kodeKlinik}-${ruang.jenisLayanan || 'default'}`;
|
||||
if (!klinikMap[key]) {
|
||||
klinikMap[key] = {
|
||||
kodeKlinik: ruang.kodeKlinik,
|
||||
namaKlinik: ruang.namaKlinik,
|
||||
jenisLayanan: ruang.jenisLayanan,
|
||||
ruangList: [],
|
||||
};
|
||||
}
|
||||
klinikMap[key].ruangList.push({
|
||||
namaRuang: ruang.namaRuang,
|
||||
nomorRuang: ruang.nomorRuang,
|
||||
nomorScreen: ruang.nomorScreen,
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(klinikMap);
|
||||
return masterStore.ruangData || [];
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const itemsPerPage = 10;
|
||||
const itemsPerPage = 12; // Increased for better fill
|
||||
const page = computed({
|
||||
get: () => Number(route.query.page || 1),
|
||||
set: (val) => navigateTo({ query: { ...route.query, page: val } }),
|
||||
@@ -199,79 +159,18 @@ const navigateToSettings = () => {
|
||||
navigateTo('/setting/masterklinikruang');
|
||||
};
|
||||
|
||||
// Generate rooms from clinics (same logic as AdminKlinikRuang)
|
||||
const generateRoomsFromClinics = () => {
|
||||
console.log('🔄 Anjungan/index - Generating rooms from clinics...');
|
||||
const allRooms = [];
|
||||
let roomIdCounter = 1;
|
||||
|
||||
const allClinics = clinicStore.clinics;
|
||||
console.log('📊 Total clinics:', allClinics.length);
|
||||
|
||||
allClinics.forEach(clinic => {
|
||||
if (clinic.jenisLayanan === 'Reguler') {
|
||||
if (clinic.spesialis && Array.isArray(clinic.spesialis) && clinic.spesialis.length > 0) {
|
||||
// Create room ONLY for each specialist (no duplicate clinic-name room)
|
||||
clinic.spesialis.forEach((spec, index) => {
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
|
||||
namaRuang: spec.Spesialis,
|
||||
nomorRuang: (index + 1).toString(),
|
||||
nomorScreen: `${clinic.kode}${index + 1}`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Only create 1 room with clinic name when NO spesialis
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
}
|
||||
} else if (clinic.jenisLayanan === 'Eksekutif') {
|
||||
// For Eksekutif: always 1 room (no spesialis in seed data)
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: 'Eksekutif'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Generated ${allRooms.length} rooms from ${allClinics.length} clinics`);
|
||||
|
||||
// Update ruangStore with generated rooms
|
||||
const result = ruangStore.replaceAllRooms(allRooms);
|
||||
console.log('💾 Rooms saved to ruangStore:', result.message);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Load data on mount
|
||||
onMounted(async () => {
|
||||
console.log('🚀 Anjungan/AntrianKlinikRuang/index mounted, loading data...');
|
||||
|
||||
try {
|
||||
// Fetch clinics from API (uses cache if available)
|
||||
const fetchResult = await clinicStore.fetchRegulerClinics();
|
||||
console.log('📥 Fetch result:', fetchResult);
|
||||
// 1. Fetch clinics from API (uses cache if available)
|
||||
await clinicStore.fetchRegulerClinics();
|
||||
|
||||
// Generate rooms based on fetched data
|
||||
generateRoomsFromClinics();
|
||||
// 2. Sync room definitions from API (centralized logic)
|
||||
await ruangStore.fetchRuangFromAPI();
|
||||
|
||||
console.log('✅ Anjungan/index data sync completed');
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading data:', error);
|
||||
}
|
||||
|
||||
@@ -3576,8 +3576,7 @@ onUnmounted(() => {
|
||||
background: $neutral-100;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
padding: 0 4px 0 4px;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
max-width: 100vw;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
@@ -4769,7 +4768,7 @@ onUnmounted(() => {
|
||||
|
||||
/* Content Grid - Same pattern as AdminLoket.vue */
|
||||
.content-grid {
|
||||
margin: 0 8px;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
@@ -5981,7 +5980,7 @@ onUnmounted(() => {
|
||||
/* Mobile Optimization */
|
||||
@media (max-width: 600px) {
|
||||
.checkin-page-container {
|
||||
padding: 0 8px 8px 8px !important;
|
||||
padding: 0 12px 12px 12px !important;
|
||||
}
|
||||
|
||||
.checkin-page-container :deep(.page-header) {
|
||||
@@ -6224,10 +6223,18 @@ onUnmounted(() => {
|
||||
|
||||
/* Tablet - Medium screens (961px to 1263px) */
|
||||
@media (min-width: 961px) and (max-width: 1263px) {
|
||||
.checkin-page-container {
|
||||
max-height: none !important;
|
||||
overflow-y: auto !important;
|
||||
height: auto !important;
|
||||
padding-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.qr-scan-card,
|
||||
.manual-input-card {
|
||||
max-height: calc(100vh - 220px) !important;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
overflow-y: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
@@ -6237,31 +6244,48 @@ onUnmounted(() => {
|
||||
|
||||
/* Small tablets and large phones (721px to 960px) */
|
||||
@media (min-width: 721px) and (max-width: 960px) {
|
||||
.checkin-page-container {
|
||||
max-height: none !important;
|
||||
overflow-y: auto !important;
|
||||
height: auto !important;
|
||||
padding-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.qr-scan-card,
|
||||
.manual-input-card {
|
||||
max-height: calc(50vh - 100px) !important;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
overflow-y: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small screens (up to 720px) */
|
||||
@media (max-width: 720px) {
|
||||
.checkin-page-container {
|
||||
max-height: none !important;
|
||||
overflow-y: auto !important;
|
||||
height: auto !important;
|
||||
padding-bottom: 16px !important;
|
||||
}
|
||||
|
||||
.qr-scan-card,
|
||||
.manual-input-card {
|
||||
max-height: calc(45vh - 70px) !important;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
overflow-y: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.stats-footer-horizontal {
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra small screens (phones in portrait, up to 480px) */
|
||||
@media (max-width: 480px) {
|
||||
.checkin-page-container {
|
||||
padding: 0 4px 4px 4px !important;
|
||||
padding: 0 8px 8px 8px !important;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
@@ -6516,4 +6540,11 @@ onUnmounted(() => {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Padding Consistency Overrides for Tablet/Intermediate */
|
||||
@media (min-width: 601px) and (max-width: 1263px) {
|
||||
.checkin-page-container {
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -90,6 +90,13 @@
|
||||
</v-chip>
|
||||
<span class="ml-2 body-3 text-medium">{{ item.namaKlinik }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.jumlahRuang="{ item }">
|
||||
<v-chip size="small" color="primary" variant="flat" class="text-white">
|
||||
{{ item.ruangList?.length || 0 }} Ruang
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
|
||||
<template v-slot:item.namaRuang="{ item }">
|
||||
<div class="ruang-tags">
|
||||
@@ -400,12 +407,14 @@ const headers = ref([
|
||||
{ title: 'No', value: 'no', sortable: true },
|
||||
{ title: 'Nama Klinik', value: 'namaKlinik', sortable: true },
|
||||
{ title: 'Kode', value: 'kodeKlinik', sortable: true },
|
||||
{ title: 'Nama Ruang', value: 'namaRuang', sortable: true },
|
||||
{ title: 'Jumlah Ruang', value: 'jumlahRuang', sortable: true },
|
||||
{ title: 'Daftar Ruang', value: 'namaRuang', sortable: true },
|
||||
{ title: 'Jenis Layanan', value: 'jenisLayanan', sortable: true },
|
||||
{ title: 'Layar Informasi', value: 'layarInformasi', sortable: false, width: '150px' },
|
||||
{ title: 'Aksi', value: 'aksi', sortable: false },
|
||||
]);
|
||||
|
||||
|
||||
const pageCount = computed(() => {
|
||||
return Math.ceil(filteredTotal.value / itemsPerPage.value) || 1;
|
||||
});
|
||||
@@ -536,109 +545,31 @@ const closePreviewDialog = () => {
|
||||
previewUrl.value = '';
|
||||
};
|
||||
|
||||
// Generate rooms from clinics based on spesialis field
|
||||
const generateRoomsFromClinics = () => {
|
||||
console.log('🔄 Generating rooms from clinics...');
|
||||
const allRooms = [];
|
||||
let roomIdCounter = 1;
|
||||
|
||||
// Get all clinics (Reguler from API + Eksekutif from seed)
|
||||
const allClinics = clinicStore.clinics;
|
||||
console.log('📊 Total clinics:', allClinics.length);
|
||||
console.log('📊 Clinics by type:', {
|
||||
Reguler: allClinics.filter(c => c.jenisLayanan === 'Reguler').length,
|
||||
Eksekutif: allClinics.filter(c => c.jenisLayanan === 'Eksekutif').length
|
||||
});
|
||||
|
||||
allClinics.forEach(clinic => {
|
||||
console.log(`\n Processing clinic: ${clinic.name} (${clinic.kode}) - ${clinic.jenisLayanan}`);
|
||||
|
||||
if (clinic.jenisLayanan === 'Reguler') {
|
||||
// For Reguler: check spesialis from API
|
||||
if (clinic.spesialis && Array.isArray(clinic.spesialis) && clinic.spesialis.length > 0) {
|
||||
// Create room ONLY for each specialist (no duplicate clinic-name room)
|
||||
console.log(` ✅ Has ${clinic.spesialis.length} specialists - creating rooms ONLY from specialists`);
|
||||
clinic.spesialis.forEach((spec, index) => {
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
|
||||
namaRuang: spec.Spesialis,
|
||||
nomorRuang: (index + 1).toString(),
|
||||
nomorScreen: `${clinic.kode}${index + 1}`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
console.log(` → Room: ${spec.Spesialis}`);
|
||||
});
|
||||
} else {
|
||||
// Only create 1 room with clinic name when NO spesialis
|
||||
console.log(' ⚠️ No specialists, creating 1 room with clinic name');
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
}
|
||||
} else if (clinic.jenisLayanan === 'Eksekutif') {
|
||||
// For Eksekutif: always 1 room (no spesialis in seed data)
|
||||
console.log(' 📦 Eksekutif clinic, creating 1 room');
|
||||
allRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: 'Eksekutif'
|
||||
});
|
||||
} else {
|
||||
console.log(` ⚠️ Unknown jenisLayanan: ${clinic.jenisLayanan}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\n✅ Generated ${allRooms.length} rooms from ${allClinics.length} clinics`);
|
||||
console.log('📋 Rooms by Jenis Layanan:');
|
||||
console.log(' - Reguler:', allRooms.filter(r => r.jenisLayanan === 'Reguler').length);
|
||||
console.log(' - Eksekutif:', allRooms.filter(r => r.jenisLayanan === 'Eksekutif').length);
|
||||
|
||||
// Update ruangStore with generated rooms
|
||||
const result = ruangStore.replaceAllRooms(allRooms);
|
||||
console.log('💾 Rooms saved to ruangStore:', result.message);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Fetch clinics and generate rooms on mount
|
||||
// Fetch clinics and sync rooms on mount
|
||||
onMounted(async () => {
|
||||
console.log('🚀 MasterKlinikRuang mounted, fetching clinics...');
|
||||
console.log('🚀 MasterKlinikRuang mounted, syncing data...');
|
||||
|
||||
try {
|
||||
// Fetch clinics from API (uses cache if available)
|
||||
const fetchResult = await clinicStore.fetchRegulerClinics();
|
||||
console.log('📥 Fetch result:', fetchResult);
|
||||
// 1. Fetch clinics from API (uses cache if available)
|
||||
const fetchClinicResult = await clinicStore.fetchRegulerClinics();
|
||||
console.log('📥 Clinic fetch result:', fetchClinicResult);
|
||||
|
||||
if (!fetchResult.success) {
|
||||
if (!fetchClinicResult.success) {
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: fetchResult.message,
|
||||
message: `Klinik: ${fetchClinicResult.message}`,
|
||||
color: 'warning'
|
||||
};
|
||||
}
|
||||
|
||||
// Generate rooms based on fetched data
|
||||
const generateResult = generateRoomsFromClinics();
|
||||
// 2. Fetch and merge rooms from API
|
||||
const syncRuangResult = await ruangStore.fetchRuangFromAPI();
|
||||
console.log('🔄 Room sync result:', syncRuangResult);
|
||||
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: generateResult.message,
|
||||
color: 'success'
|
||||
message: syncRuangResult.message,
|
||||
color: syncRuangResult.success ? 'success' : 'error'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('❌ Error in onMounted:', error);
|
||||
@@ -649,6 +580,7 @@ onMounted(async () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -322,6 +322,177 @@ export const useRuangStore = defineStore('ruang', () => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch room data from API and merge with clinic metadata
|
||||
* preserving seed/local data (Eksekutif)
|
||||
*/
|
||||
const fetchRuangFromAPI = async (force = false) => {
|
||||
try {
|
||||
console.log('🔄 Fetching rooms from API...');
|
||||
|
||||
// 1. Ensure clinics are loaded (from clinicStore)
|
||||
// clinicStore.fetchRegulerClinics is already called in MasterKlinikRuang.vue
|
||||
// but we use the existing clinics in the store
|
||||
const allClinics = clinicStore.clinics;
|
||||
|
||||
// 2. Fetch room data from new API
|
||||
const response = await fetch('http://10.10.150.131:8089/api/v1/loket/ruang');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const rawData = await response.json();
|
||||
const apiRoomsRaw = rawData.data || [];
|
||||
console.log('📥 Received API rooms:', apiRoomsRaw.length);
|
||||
|
||||
// 3. Prepare rooms list (API + Seed)
|
||||
const mergedRooms = [];
|
||||
let roomIdCounter = 1;
|
||||
|
||||
// Track which clinics are handled by API to avoid duplicates for Reguler
|
||||
const clinicCodesInAPI = new Set();
|
||||
|
||||
// Process API Clinics and their nested Rooms
|
||||
apiRoomsRaw.forEach(apiClinic => {
|
||||
const clinicId = apiClinic.idklinik;
|
||||
const clinicCode = apiClinic.kodeloket || apiClinic.kodeklinik;
|
||||
const apiRuangan = apiClinic.ruangan || [];
|
||||
|
||||
// Find clinic metadata in store
|
||||
// Find clinic metadata in store - Specifically look for Reguler clinic first
|
||||
// as API data (idklinik, kodeloket) is for Reguler clinics.
|
||||
const clinic = allClinics.find(c =>
|
||||
c.jenisLayanan === 'Reguler' && (
|
||||
(clinicId && String(c.id) === String(clinicId)) ||
|
||||
(clinicCode && String(c.kode) === String(clinicCode)) ||
|
||||
(apiClinic.namaklinik && c.name.toUpperCase() === apiClinic.namaklinik.toUpperCase())
|
||||
)
|
||||
) || allClinics.find(c =>
|
||||
(clinicId && String(c.id) === String(clinicId)) ||
|
||||
(clinicCode && String(c.kode) === String(clinicCode)) ||
|
||||
(apiClinic.namaklinik && c.name.toUpperCase() === apiClinic.namaklinik.toUpperCase())
|
||||
);
|
||||
|
||||
if (clinic) {
|
||||
clinicCodesInAPI.add(clinic.kode);
|
||||
|
||||
// Check if API room data is "generic" (only 1 room with same name as clinic)
|
||||
// to decide whether to prefer Specialist fallback
|
||||
const isGenericSingleRoom = apiRuangan.length === 1 &&
|
||||
(apiRuangan[0].namaruangan?.trim().toUpperCase() === clinic.name.toUpperCase() ||
|
||||
!apiRuangan[0].namaruangan ||
|
||||
apiRuangan[0].namaruangan === '-');
|
||||
|
||||
const hasManySpecialists = clinic.spesialis && clinic.spesialis.length > 1;
|
||||
|
||||
// Priority 1: Use nested "ruangan" array from new API
|
||||
// Skip this if it's just 1 generic room and we have better data in Specialists
|
||||
if (apiRuangan.length > 0 && !(isGenericSingleRoom && hasManySpecialists)) {
|
||||
apiRuangan.forEach((apiRoom, index) => {
|
||||
const specialistName = (clinic.spesialis && clinic.spesialis[index])
|
||||
? clinic.spesialis[index].Spesialis
|
||||
: null;
|
||||
|
||||
const fallbackName = `Ruang ${apiRoom.idruangan || apiRoom.nomor_ruang || index + 1}`;
|
||||
|
||||
mergedRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: apiRoom.idruangan || apiRoom.idruang || `${clinic.kode}-${roomIdCounter}`,
|
||||
namaRuang: apiRoom.namaruangan || apiRoom.nama_ruang || apiRoom.nama || specialistName || fallbackName,
|
||||
nomorRuang: apiRoom.idruangan || apiRoom.nomor_ruang || (index + 1).toString(),
|
||||
nomorScreen: apiRoom.nomor_screen || `${clinic.kode}${apiRoom.idruangan || index + 1}`,
|
||||
jenisLayanan: clinic.jenisLayanan || 'Reguler'
|
||||
});
|
||||
});
|
||||
}
|
||||
// Priority 2: Fallback to specialist metadata (Old API) if new API is empty OR generic
|
||||
else if (clinic.spesialis && clinic.spesialis.length > 0) {
|
||||
console.log(`ℹ️ Using specialists fallback for ${clinic.name} (${apiRuangan.length === 1 ? 'API generic' : 'API empty'})`);
|
||||
clinic.spesialis.forEach((spec, index) => {
|
||||
mergedRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
|
||||
namaRuang: spec.Spesialis,
|
||||
nomorRuang: (index + 1).toString(),
|
||||
nomorScreen: `${clinic.kode}${index + 1}`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
});
|
||||
}
|
||||
// Priority 3: Single room fallback
|
||||
else {
|
||||
const fallbackName = apiClinic.nama_ruang || apiClinic.nama || clinic.name || 'Ruang 1';
|
||||
mergedRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: apiClinic.idruang || clinic.kode,
|
||||
namaRuang: fallbackName,
|
||||
nomorRuang: apiClinic.nomor_ruang || '1',
|
||||
nomorScreen: apiClinic.nomor_screen || `${clinic.kode}1`,
|
||||
jenisLayanan: clinic.jenisLayanan || 'Reguler'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 4. Add Seed Data (Eksekutif) + Reguler clinics not in API
|
||||
allClinics.forEach(clinic => {
|
||||
if (clinic.jenisLayanan === 'Eksekutif' || !clinicCodesInAPI.has(clinic.kode)) {
|
||||
console.log(`➕ Adding clinic from seed/metadata: ${clinic.name} (${clinic.jenisLayanan})`);
|
||||
|
||||
// Fallback logic similar to original generateRoomsFromClinics
|
||||
if (clinic.jenisLayanan === 'Reguler' && clinic.spesialis?.length > 0) {
|
||||
clinic.spesialis.forEach((spec, index) => {
|
||||
mergedRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: `${clinic.kode}-${spec.idspesialis}`,
|
||||
namaRuang: spec.Spesialis,
|
||||
nomorRuang: (index + 1).toString(),
|
||||
nomorScreen: `${clinic.kode}${index + 1}`,
|
||||
jenisLayanan: 'Reguler'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
mergedRooms.push({
|
||||
id: roomIdCounter++,
|
||||
kodeKlinik: clinic.kode,
|
||||
namaKlinik: clinic.name,
|
||||
kodeRuang: clinic.kode,
|
||||
namaRuang: clinic.name,
|
||||
nomorRuang: '1',
|
||||
nomorScreen: `${clinic.kode}1`,
|
||||
jenisLayanan: clinic.jenisLayanan || 'Reguler'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Update store
|
||||
const result = replaceAllRooms(mergedRooms);
|
||||
return {
|
||||
success: true,
|
||||
message: `Berhasil sinkronisasi ${apiRoomsRaw.length} ruang dari API dan ${allClinics.length} data klinik`
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in fetchRuangFromAPI:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `Gagal memuat data ruang: ${error.message}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Helper to generate unique room ID
|
||||
const generateRoomId = () => {
|
||||
return Date.now() + Math.floor(Math.random() * 1000);
|
||||
@@ -356,6 +527,7 @@ export const useRuangStore = defineStore('ruang', () => {
|
||||
|
||||
// New method for API-based room generation
|
||||
replaceAllRooms,
|
||||
fetchRuangFromAPI,
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineVitestConfig } from '@nuxt/test-utils/config'
|
||||
|
||||
export default defineVitestConfig({
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
globals: true,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user