penambahan docker dan lain lain untuk penyesuaian docker

This commit is contained in:
Fanrouver
2026-05-19 09:57:46 +07:00
parent cb3310b44b
commit bc74b98cfd
23 changed files with 506 additions and 67 deletions
+36
View File
@@ -0,0 +1,36 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Database files
data/
*.db
*.sqlite
*.sqlite3
# Node dependencies
node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDEs and OS files
.DS_Store
.fleet
.idea
.vscode
Thumbs.db
# Local env files
.env
.env.*
!.env.example
!.env.docker.example
+6
View File
@@ -79,3 +79,9 @@ EXTERNAL_API_TIMEOUT=10000
# Digunakan di pages/verifikasiAkun/VerifikasiAkun.vue
EKSTRAK_EXPERTISE_URL="http://10.10.123.218/ekstrakexpertise"
VERIFICATION_API_BASE_URL="http://10.10.123.140:8089/api/v1"
# Simplified Modular API URLs (New Config)
ANTRIAN_API_URL="http://10.10.123.140:8089/api/v1"
VISIT_API_URL="http://10.10.123.135:8084/api/v1"
WS_API_URL="ws://10.10.123.135:8084/api/v1/ws"
+51
View File
@@ -0,0 +1,51 @@
# Dockerfile for web-antrean Nuxt.js Application
# Multi-stage build for optimized image size and native modules support
# Stage 1: Build Stage
FROM node:20-alpine AS builder
# Install build dependencies for compiling native addons (like better-sqlite3)
RUN apk add --no-cache python3 make g++ gcc libc-dev
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies (including devDependencies for build)
RUN npm ci
# Copy application files
COPY . .
# Build the application for production
RUN npm run build
# Stage 2: Production Stage
FROM node:20-alpine AS runner
# Install runtime dependencies (like libc6-compat for compatibility with precompiled binaries)
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Copy built output and compiled node_modules from builder stage
# This avoids needing to install build tools (g++, make, python) in this slim production stage
COPY --from=builder /app/.output /app/.output
COPY --from=builder /app/node_modules /app/node_modules
# Expose port
EXPOSE 3000
# Set environment variables
ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000
# Start the application
CMD ["node", ".output/server/index.mjs"]
+101
View File
@@ -0,0 +1,101 @@
# Panduan Deployment Docker - web-antrean
Panduan ini berisi langkah-langkah untuk menjalankan aplikasi `web-antrean` (Nuxt 3) menggunakan Docker & Docker Compose.
---
## 📦 Prasyarat (Prerequisites)
Sebelum memulai, pastikan Anda telah menginstal:
- **Docker Desktop** (atau Docker Engine di Linux)
- **Docker Compose** (biasanya otomatis terinstal dengan Docker Desktop)
---
## 🚀 Quick Start (Mulai Cepat)
### 1. Siapkan File `.env`
Pastikan file `.env` sudah ada di root folder `web-antrean` dengan konfigurasi yang benar. Anda bisa menyalin dari template yang disediakan:
```bash
cp .env.docker.example .env
```
> [!IMPORTANT]
> Pastikan variabel `HOST` diatur ke `0.0.0.0` agar aplikasi dapat menerima request dari luar container Docker.
> Sesuaikan `AUTH_ORIGIN` dan `POST_LOGOUT_REDIRECT_URI` dengan alamat/domain server Anda (misal: `http://10.10.150.175:3000`).
### 2. Bangun dan Jalankan Container
Jalankan perintah berikut di terminal untuk melakukan build image sekaligus menjalankan container di background:
```bash
docker-compose up -d --build
```
### 3. Akses Aplikasi
Setelah container berhasil berjalan, aplikasi dapat diakses di browser melalui:
- **http://localhost:3000** (atau IP server Anda, misal: **http://10.10.150.175:3000**)
---
## 🔧 Perintah Docker yang Sering Digunakan (Useful Commands)
### Menjalankan Container (Jalankan di background)
```bash
docker-compose up -d
```
### Mematikan Container
```bash
docker-compose down
```
### Membangun Ulang & Menjalankan Ulang (Jika ada perubahan kode)
```bash
docker-compose up -d --build
```
### Melihat Log Aplikasi secara Real-time
```bash
docker-compose logs -f web-antrean
```
### Masuk ke Terminal di Dalam Container
```bash
docker exec -it web-antrean sh
```
---
## 💾 Persistensi Database (SQLite)
Aplikasi `web-antrean` menggunakan `better-sqlite3` untuk menyimpan data sinkronisasi pengguna di `/app/data/users.db`.
Agar data ini tidak hilang saat container dihapus atau dibangun ulang, kami menggunakan **Volume Mounting** di `docker-compose.yml`:
```yaml
volumes:
- ./data:/app/data
```
Ini akan otomatis memetakan folder local `./data` di host server Anda ke folder `/app/data` di dalam container. File database Anda akan tersimpan dengan aman di folder local `./data`.
---
## 🔍 Troubleshooting (Penyelesaian Masalah)
### 1. Masalah dengan Native Module compilation (`better-sqlite3`)
Jika image gagal dibangun karena error kompilasi C/C++, Dockerfile kami telah dikonfigurasi menggunakan **multi-stage build** dengan base image `node:20-alpine` dan menginstal package compiler (`make`, `g++`, `gcc`, `python3`) di builder stage. Ini menjamin proses build aman dan hasil image final tetap berukuran kecil (~150MB - 200MB).
### 2. Cek Status Container dan Health Check
Container ini dilengkapi dengan health check otomatis. Anda bisa mengecek statusnya dengan:
```bash
docker ps
```
Jika statusnya `Up (healthy)`, berarti server Nuxt Anda telah siap dan dapat diakses.
### 3. Hapus Cache Docker jika Mengalami Error Aneh
```bash
docker-compose down --rmi all --volumes
```
---
## 🛡️ Catatan Keamanan
- Jangan pernah meng-commit file `.env` ke Git repository.
- Selalu gunakan `NUXT_AUTH_SECRET` yang aman dan unik untuk setiap environment (Development/Production).
+4 -1
View File
@@ -17,7 +17,10 @@ export const usePermission = () => {
*/
const fetchPermission = async (path: string) => {
const { group, role } = parsePath(path);
const url = `http://10.10.123.140:8089/api/permission?roles=${role}&groups=${group}`;
const config = useRuntimeConfig();
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const origin = new URL(apiBase).origin;
const url = `${origin}/api/permission?roles=${role}&groups=${group}`;
const { data, error } = await useFetch(url, {
method: "GET",
+47
View File
@@ -0,0 +1,47 @@
services:
web-antrean:
build:
context: .
dockerfile: Dockerfile
container_name: web-antrean
ports:
- "3000:3000"
env_file:
- .env
environment:
- NODE_ENV=production
- HOST=0.0.0.0
- PORT=3000
- NUXT_AUTH_SECRET=${NUXT_AUTH_SECRET}
- NUXT_KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID}
- NUXT_KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET}
- NUXT_KEYCLOAK_ISSUER=${KEYCLOAK_ISSUER}
- NUXT_KEYCLOAK_LOGOUT_URI=${KEYCLOAK_LOGOUT_URI}
- NUXT_POST_LOGOUT_REDIRECT_URI=${POST_LOGOUT_REDIRECT_URI}
- NUXT_PUBLIC_AUTH_URL=${AUTH_ORIGIN}
- NUXT_EXTERNAL_API_BASE_URL=${EXTERNAL_API_BASE_URL}
- NUXT_PUBLIC_VERIFICATION_API_BASE_URL=${VERIFICATION_API_BASE_URL}
- NUXT_PUBLIC_ANTRIAN_API_URL=${ANTRIAN_API_URL}
- NUXT_PUBLIC_VISIT_API_URL=${VISIT_API_URL}
- NUXT_PUBLIC_WS_API_URL=${WS_API_URL}
- NUXT_SESSION_DURATION_HOURS=${SESSION_DURATION_HOURS}
- NUXT_OAUTH_STATE_DURATION_MINUTES=${OAUTH_STATE_DURATION_MINUTES}
volumes:
# Mount local data directory to persist the sqlite users database
- ./data:/app/data
restart: unless-stopped
networks:
- antrean-network
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode < 500 ? 0 : 1)})"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
antrean-network:
driver: bridge
volumes:
data:
+4 -14
View File
@@ -82,9 +82,9 @@ export default defineNuxtConfig({
authUrl: process.env.AUTH_ORIGIN,
// authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001",
// authUrl: process.env.AUTH_ORIGIN || "http://localhost:3001",
wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.123.135:8084/api/v1/ws',
verificationApiBaseUrl: process.env.VERIFICATION_API_BASE_URL || 'http://10.10.123.140:8089/api/v1',
wsBaseUrl: process.env.WS_API_URL || process.env.WS_BASE_URL || 'ws://10.10.123.135:8084/api/v1/ws',
verificationApiBaseUrl: process.env.ANTRIAN_API_URL || process.env.VERIFICATION_API_BASE_URL || 'http://10.10.123.140:8089/api/v1',
externalApiBaseUrl: process.env.VISIT_API_URL || (process.env.EXTERNAL_API_BASE_URL ? `${process.env.EXTERNAL_API_BASE_URL}/api/v1` : 'http://10.10.123.135:8084/api/v1'),
},
},
@@ -121,17 +121,7 @@ export default defineNuxtConfig({
};
})(),
routeRules: {
'/stats-api/**': {
proxy: 'http://10.10.123.135:8084/api/v1/**'
},
'/visit-api/**': {
proxy: 'http://10.10.123.135:8084/api/v1/**'
},
'/klinik-api/**': {
proxy: 'http://10.10.123.140:8089/api/v1/**'
},
},
vite: {
css: {
+2 -2
View File
@@ -1716,8 +1716,8 @@ const handleCallPatientByTipe = async (ruang, tipeLayanan) => {
visit_code: patient.barcode || patient.visitCode,
visit_status_id: [visitStatusId]
};
console.log(`📤 [AdminKlinikRuang] Sending call status (idvisit ${visitStatusId}) to API:`, apiPayload);
const apiResponse = await fetch('http://10.10.123.135:8084/api/v1/visit/status/finish', {
const visitApiBase = '/visit-api';
const apiResponse = await fetch(`${visitApiBase}/visit/status/finish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(apiPayload)
+4 -2
View File
@@ -492,8 +492,9 @@ const apiQuota = ref(null);
// Fetch latest quota data specifically for this loket
const fetchQuotaFromAPI = async () => {
try {
const apiBase = '/klinik-api';
const response = await fetch(
"http://10.10.123.140:8089/api/v1/klinik/loket",
`${apiBase}/klinik/loket`,
);
const data = await response.json();
@@ -1202,8 +1203,9 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => {
console.log("📤 Sending visit ticket to API:", visitTicketBody);
const visitApiBase = '/visit-api';
const visitResponse = await fetch(
"http://10.10.123.135:8084/api/v1/visit/ticket/klinik",
`${visitApiBase}/visit/ticket/klinik`,
{
method: "POST",
headers: {
@@ -466,12 +466,23 @@ const statistics = computed(() => {
}
const all = klinikPatients.value
// A patient is considered active (sedang dilayani) if they are currently being processed in a room
const activePatients = all.filter(p => {
return Object.values(queueStore.currentProcessingPatient || {}).some(
proc => proc && String(proc.no) === String(p.no)
)
})
// All other patients in the list are considered waiting (menunggu)
const waitingPatients = all.filter(p => {
return !activePatients.some(act => act.no === p.no)
})
return {
total: all.length,
// Counting 'anjungan', 'menunggu', 'waiting', 'pending', 'terlambat' as waiting
anjungan: all.filter(p => ['anjungan', 'menunggu', 'waiting', 'pending', 'terlambat'].includes(p.status)).length,
// Counting 'di-loket' and 'pemeriksaan' as active/processing
active: all.filter(p => ['di-loket', 'pemeriksaan'].includes(p.status)).length
anjungan: waitingPatients.length,
active: activePatients.length
}
})
+44 -25
View File
@@ -225,6 +225,7 @@ const uniqueSessionSuffix = ref(process.client ? Math.random().toString(36).subs
const currentTime = ref('')
const currentDate = ref('')
let timeInterval = null
let pollInterval = null
let broadcastChannel = null
const broadcastedPatient = ref(null)
@@ -605,11 +606,21 @@ const displayedClinics = computed(() => {
}
// Prioritas 2: Persistence - If no active Hero call for this clinic,
// find the patient who was explicitly called by admin and is still in servingQueues (di-loket)
// find the patient who was explicitly called by admin OR has idvisit === 8 or 7 (from database)
if (!currentQueue) {
const calledServingQueues = servingQueues
.filter(q => q.calledByAdmin) // Use calledByAdmin for explicit loket call
.sort((a, b) => new Date(b.lastCalledAt) - new Date(a.lastCalledAt))
.filter(q => q.calledByAdmin || q.idvisit === 8 || q.idvisit === 7)
.sort((a, b) => {
// Priority: idvisit === 8 or 7 has higher precedence
const isDbActiveA = a.idvisit === 8 || a.idvisit === 7;
const isDbActiveB = b.idvisit === 8 || b.idvisit === 7;
if (isDbActiveA && !isDbActiveB) return -1;
if (!isDbActiveA && isDbActiveB) return 1;
const timeA = new Date(a.lastCalledAt || a.createdAt || 0).getTime();
const timeB = new Date(b.lastCalledAt || b.createdAt || 0).getTime();
return timeB - timeA;
})
if (calledServingQueues.length > 0) {
currentQueue = calledServingQueues[0]
@@ -623,8 +634,8 @@ const displayedClinics = computed(() => {
return q.no !== currentQueue?.no
})
// Separate di-loket and pending grids
const diLoketQueuesInGrid = servingQueues.filter(q => q.no !== currentQueue?.no && q.status !== 'pending');
// Separate di-loket and pending grids (exclude currently served patients from grid)
const diLoketQueuesInGrid = servingQueues.filter(q => q.no !== currentQueue?.no && q.status !== 'pending' && q.idvisit !== 8 && q.idvisit !== 7);
const pendingQueues = servingQueues.filter(q => q.status === 'pending');
return {
@@ -691,22 +702,9 @@ const currentCalledQueue = computed(() => {
// Hanya tampilkan jika sudah dipanggil eksplisit oleh admin (calledByAdmin === true)
if (currentProcessingPatient.value) {
const processingPatient = currentProcessingPatient.value
// Cek apakah data ini milik loket ini (berdasarkan metadata loketId)
// Jika tidak ada loketId (misal data lama), fallback ke true untuk Admin 1
const patientLoketId = processingPatient.loketId ? String(processingPatient.loketId) : "1"
if (patientLoketId !== targetLoketId) {
return null
}
// Cek apakah sudah dipanggil oleh admin
if (!processingPatient.calledByAdmin) {
return null
}
// Pastikan pasien memiliki noAntrian dan status 'di-loket' (sudah check-in)
if (processingPatient.noAntrian && processingPatient.status === 'di-loket') {
// Dapatkan nama klinik dari nomor antrian
if (patientLoketId === targetLoketId && processingPatient.calledByAdmin && processingPatient.noAntrian && processingPatient.status === 'di-loket') {
const klinikName = getKlinikNameFromPatient(processingPatient)
return {
...processingPatient,
@@ -714,6 +712,17 @@ const currentCalledQueue = computed(() => {
}
}
}
// Prioritas 2: Database state - Find patient in the fetched list who is currently being served (idvisit === 8 or 7)
const allPatientsList = filteredPatientsForLoket.value
const dbActive = allPatientsList.find(p => p.idvisit === 8 || p.idvisit === 7)
if (dbActive) {
const klinikName = getKlinikNameFromPatient(dbActive)
return {
...dbActive,
klinik: klinikName || dbActive.klinik || 'Klinik'
}
}
return null
})
@@ -831,7 +840,7 @@ const isCalled = (queue) => {
// Statistics
const statistics = computed(() => {
const all = filteredPatientsForLoket.value
const active = all.filter(p => p.status === 'di-loket').length
const active = currentCalledQueue.value ? 1 : 0
return {
total: all.length,
active: active
@@ -1028,16 +1037,26 @@ onMounted(async () => {
// Register interest in this specific loket for scoped WebSocket refreshes
queueStore.registerInterest(loketId.value);
onUnmounted(() => {
// Unregister interest when leaving the page
queueStore.unregisterInterest(loketId.value);
});
// Polling fallback (safety net for cross-browser / cross-device sync)
let lastPollTime = Date.now();
pollInterval = setInterval(async () => {
const wsConnected = queueStore.isWsConnected;
const now = Date.now();
const timeSinceLastPoll = now - lastPollTime;
// Poll if WebSocket is disconnected (every 4s) OR as a safety net (every 15s)
if (!wsConnected || timeSinceLastPoll >= 15000) {
lastPollTime = now;
await fetchAllData();
}
}, 4000);
});
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
if (pollInterval) clearInterval(pollInterval)
if (broadcastChannel) broadcastChannel.close()
// wsInstance is now global
queueStore.unregisterInterest(loketId.value);
})
</script>
+1 -1
View File
@@ -45,7 +45,7 @@
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import TabelLayanan from '../../components/TabelLayanan.vue';
import TabelLayanan from '../../components/features/monitoring/TabelLayanan.vue';
const route = useRoute();
const router = useRouter();
+1 -1
View File
@@ -53,7 +53,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import TabelLayanan from '~/components/TabelLayanan.vue';
import TabelLayanan from '~/components/features/monitoring/TabelLayanan.vue';
// Get route parameter
const route = useRoute();
+1 -1
View File
@@ -72,7 +72,7 @@
<script setup>
import { ref, computed } from "vue";
import TabelData from "@/components/TabelData.vue";
import TabelData from "@/components/features/monitoring/TabelData.vue";
// Headers untuk tabel
const headers = [
+2 -1
View File
@@ -67,7 +67,8 @@ export default defineEventHandler(async (event) => {
console.log('🎲 Generated state:', state.substring(0, 8) + '...')
const oauthDuration = (config.oauthStateDurationMinutes || 10) * 60; // Default to 10 minutes
const isSecure = process.env.NODE_ENV === 'production';
const isSecure = process.env.NODE_ENV === 'production' &&
event.node.req.headers['x-forwarded-proto'] === 'https';
// Store state in session cookie
// IMPORTANT: This cookie is ONLY for the login flow (CSRF protection).
+3 -1
View File
@@ -189,7 +189,9 @@ export default defineEventHandler(async (event) => {
if (primaryGroup) params.append('groups', primaryGroup);
// Backend API URL - adjust this to match your backend
const backendUrl = `http://10.10.123.140:8089/api/v1/permission?${params.toString()}`;
const config = useRuntimeConfig();
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const backendUrl = `${apiBase}/permission?${params.toString()}`;
try {
console.log(`📡 Fetching permissions from: ${backendUrl}`);
+37
View File
@@ -0,0 +1,37 @@
import { defineEventHandler, proxyRequest } from 'h3'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const targetBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'
// Extract path suffix after /klinik-api
const suffix = event.path.replace(/^\/klinik-api/, '')
const targetUrl = `${targetBase}${suffix}`
// Clone request headers and overwrite Host/Origin/Referer with backend-whitelisted values
const headers = { ...event.node.req.headers } as Record<string, string>
// Extract backend target host
try {
const urlObj = new URL(targetBase)
headers.host = urlObj.host
} catch (e) {
headers.host = '10.10.123.140:8089'
}
// Spoof the origin and referer to match the server's trusted/whitelisted client origin
headers.origin = 'http://10.10.150.175:3000'
headers.referer = 'http://10.10.150.175:3000/'
console.log(`[Proxy Klinik-API] Forwarding ${event.node.req.method} to: ${targetUrl}`)
console.log(`[Proxy Klinik-API] Headers - Host: ${headers.host}, Origin: ${headers.origin}`)
try {
return await proxyRequest(event, targetUrl, {
headers
})
} catch (error: any) {
console.error(`[Proxy Klinik-API] Error forwarding to ${targetUrl}:`, error)
throw error
}
})
+37
View File
@@ -0,0 +1,37 @@
import { defineEventHandler, proxyRequest } from 'h3'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const targetBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1'
// Extract path suffix after /stats-api
const suffix = event.path.replace(/^\/stats-api/, '')
const targetUrl = `${targetBase}${suffix}`
// Clone request headers and overwrite Host/Origin/Referer with backend-whitelisted values
const headers = { ...event.node.req.headers } as Record<string, string>
// Extract backend target host
try {
const urlObj = new URL(targetBase)
headers.host = urlObj.host
} catch (e) {
headers.host = '10.10.123.135:8084'
}
// Spoof the origin and referer to match the server's trusted/whitelisted client origin
headers.origin = 'http://10.10.150.175:3000'
headers.referer = 'http://10.10.150.175:3000/'
console.log(`[Proxy Stats-API] Forwarding ${event.node.req.method} to: ${targetUrl}`)
console.log(`[Proxy Stats-API] Headers - Host: ${headers.host}, Origin: ${headers.origin}`)
try {
return await proxyRequest(event, targetUrl, {
headers
})
} catch (error: any) {
console.error(`[Proxy Stats-API] Error forwarding to ${targetUrl}:`, error)
throw error
}
})
+37
View File
@@ -0,0 +1,37 @@
import { defineEventHandler, proxyRequest } from 'h3'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const targetBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1'
// Extract path suffix after /visit-api
const suffix = event.path.replace(/^\/visit-api/, '')
const targetUrl = `${targetBase}${suffix}`
// Clone request headers and overwrite Host/Origin/Referer with backend-whitelisted values
const headers = { ...event.node.req.headers } as Record<string, string>
// Extract backend target host
try {
const urlObj = new URL(targetBase)
headers.host = urlObj.host
} catch (e) {
headers.host = '10.10.123.135:8084'
}
// Spoof the origin and referer to match the server's trusted/whitelisted client origin
headers.origin = 'http://10.10.150.175:3000'
headers.referer = 'http://10.10.150.175:3000/'
console.log(`[Proxy Visit-API] Forwarding ${event.node.req.method} to: ${targetUrl}`)
console.log(`[Proxy Visit-API] Headers - Host: ${headers.host}, Origin: ${headers.origin}`)
try {
return await proxyRequest(event, targetUrl, {
headers
})
} catch (error: any) {
console.error(`[Proxy Visit-API] Error forwarding to ${targetUrl}:`, error)
throw error
}
})
+4 -1
View File
@@ -80,11 +80,14 @@ export const useDoctorStore = defineStore('doctor', () => {
let data;
let lastError;
const config = useRuntimeConfig();
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
// Retry loop
for (let i = 0; i <= retries; i++) {
try {
data = await $fetch(
`http://10.10.123.140:8089/api/v1/dokter/${idklinik}`,
`${apiBase}/dokter/${idklinik}`,
{ timeout: 5000 }
);
lastError = null;
+4 -1
View File
@@ -10,7 +10,10 @@ export const usePermissionStore = defineStore("permission", {
const parts = path.split("/").filter(Boolean);
const group = parts[1] || "";
const role = parts.at(-1)?.toLowerCase() || "";
const url = `http://10.10.123.140:8089/api/permission?roles=${role}&groups=${group}`;
const config = useRuntimeConfig();
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const origin = new URL(apiBase).origin;
const url = `${origin}/api/permission?roles=${role}&groups=${group}`;
const { data } = await useFetch(url);
this.data = data.value;
},
+62 -11
View File
@@ -705,7 +705,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
try {
console.log(`🔄 [queueStore] Fetching patients for loket ${loketId}...`);
const response = await fetch(`http://10.10.123.140:8089/api/v1/loket/${loketId}`);
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/loket/${loketId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -1704,7 +1705,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient is called
try {
fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1815,7 +1817,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient is called
try {
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1873,7 +1876,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient finishes at loket
try {
fetch('http://10.10.123.140:8089/api/v1/tiket/selesai', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
fetch(`${apiBase}/tiket/selesai`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -1942,7 +1946,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API and WAIT for response
try {
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
@@ -2004,7 +2009,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API and WAIT for response
try {
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
@@ -2056,7 +2062,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API to revert status to sedang diproses (status 8)
try {
fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -2116,7 +2123,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// POST to external API when patient is being processed (sedang diproses)
try {
fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
fetch(`${apiBase}/tiket/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -2678,7 +2686,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
case "selesai":
// Send API call to finish endpoint
try {
const apiUrl = 'http://10.10.123.135:8084/api/v1/visit/status/finish';
const visitApiBase = config.public.externalApiBaseUrl || 'http://10.10.123.135:8084/api/v1';
const apiUrl = `${visitApiBase}/visit/status/finish`;
const requestBody = {
patient_visit_healthcare_service_id: patient.healthcareServiceId,
visit_code: patient.barcode || patient.visitCode,
@@ -2712,11 +2721,51 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
message = `Pasien ${pCode} selesai diproses`;
break;
case "terlambat":
// Send API call to update status to 33 (TR PEMERIKSAAN)
try {
const visitApiBase = '/visit-api';
const apiUrl = `${visitApiBase}/visit/status/finish`;
const requestBody = {
patient_visit_healthcare_service_id: patient.healthcareServiceId,
visit_code: patient.barcode || patient.visitCode,
visit_status_id: [33]
};
console.log('📤 [queueStore] Sending terlambat status to API:', requestBody);
await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
} catch (error) {
console.error('❌ [queueStore] Error calling terlambat API:', error);
}
allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "terlambat" };
currentProcessingPatient.value[storageKey] = null;
message = `Pasien ${pCode} ditandai terlambat`;
break;
case "pending":
// Send API call to update status to 32 (PE PEMERIKSAAN)
try {
const visitApiBase = '/visit-api';
const apiUrl = `${visitApiBase}/visit/status/finish`;
const requestBody = {
patient_visit_healthcare_service_id: patient.healthcareServiceId,
visit_code: patient.barcode || patient.visitCode,
visit_status_id: [32]
};
console.log('📤 [queueStore] Sending pending status to API:', requestBody);
await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
} catch (error) {
console.error('❌ [queueStore] Error calling pending API:', error);
}
allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: "pending" };
currentProcessingPatient.value[storageKey] = null;
message = `Pasien ${pCode} di-pending`;
@@ -2987,7 +3036,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
console.log('📤 [queueStore] API Body:', body);
// 3. Call API
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/generate', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/tiket/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -3088,7 +3138,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
idklinikstatus2: "2"
};
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/checkin', {
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/tiket/checkin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
+3 -1
View File
@@ -336,7 +336,9 @@ export const useRuangStore = defineStore('ruang', () => {
const allClinics = clinicStore.clinics;
// 2. Fetch room data from new API
const response = await fetch('http://10.10.123.140:8089/api/v1/loket/ruang');
const config = useRuntimeConfig();
const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1';
const response = await fetch(`${apiBase}/loket/ruang`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}