penambahan docker dan lain lain untuk penyesuaian docker
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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).
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user