first commit

This commit is contained in:
ryan
2026-02-06 14:22:35 +07:00
commit bd70440c71
185 changed files with 69518 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
// Helper function untuk API calls - langsung ke backend Go tanpa melalui Next.js API routes
// Semua platform (Web, Electron, Mobile) langsung memanggil backend Go
// Detect if running in Electron
const isElectron = typeof window !== 'undefined' &&
((window as any).__ELECTRON__ === true ||
(window as any).electron !== undefined ||
(window as any).process?.type === 'renderer' ||
navigator.userAgent.toLowerCase().includes('electron'));
// Detect if running in Capacitor (mobile app)
const isCapacitor = typeof window !== 'undefined' && (window as any).Capacitor !== undefined;
// Get API base URL based on platform - selalu ke backend Go langsung
export const getApiBaseUrl = (): string => {
if (isElectron) {
// Electron app: direct backend call
const electronApiUrl = typeof window !== 'undefined' ? (window as any).__API_URL__ : undefined;
const envApiUrl = process.env.NEXT_PUBLIC_API_URL;
const defaultApiUrl = "http://31.97.109.192:8082";
const finalUrl = electronApiUrl || envApiUrl || defaultApiUrl;
// Debug logging
console.log('🔧 API URL Configuration:', { isElectron, electronApiUrl, envApiUrl, defaultApiUrl, finalUrl });
return finalUrl;
} else if (isCapacitor) {
// Mobile app: direct backend call (Android emulator)
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://31.97.109.192:8082";
console.log('🔧 Mobile API URL:', apiUrl);
return apiUrl;
} else {
// Web browser: direct backend call (tidak lagi melalui Next.js API routes)
const windowApiUrl = typeof window !== 'undefined' ? (window as any).__API_URL__ : undefined;
const envApiUrl = process.env.NEXT_PUBLIC_API_URL;
const defaultApiUrl = "http://localhost:8081";
const apiUrl = windowApiUrl || envApiUrl || defaultApiUrl;
// Debug logging
console.log('🔧 Web API URL Configuration:', { windowApiUrl, envApiUrl, defaultApiUrl, finalUrl: apiUrl });
return apiUrl;
}
};
// Export a resolved base URL for consumers that need it
export const API_BASE_URL = getApiBaseUrl();
// Generic fetch function - selalu memanggil backend Go langsung
export async function apiFetch<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ data?: T; error?: string; status: number }> {
const API_BASE = getApiBaseUrl();
// Build URL - selalu ke backend Go langsung
// Remove /api prefix jika ada, karena endpoint backend Go tidak menggunakan prefix /api
const cleanEndpoint = endpoint.startsWith("/api/")
? endpoint.substring(4)
: endpoint.startsWith("/")
? endpoint
: `/${endpoint}`;
const url = `${API_BASE}${cleanEndpoint}`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
let response: Response;
try {
response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...options.headers,
},
});
clearTimeout(timeoutId);
} catch (fetchError) {
clearTimeout(timeoutId);
if (fetchError instanceof Error) {
if (fetchError.name === 'AbortError') {
return { error: "Request timeout - Server tidak merespon dalam 30 detik", status: 408 };
}
if (fetchError.message.includes("fetch") || fetchError.message.includes("ECONNREFUSED") || fetchError.message.includes("ENOTFOUND") || fetchError.message.includes("CORS")) {
return { error: `Tidak dapat terhubung ke server backend (${API_BASE}). Pastikan backend server berjalan dan dapat diakses.`, status: 0 };
}
}
return { error: fetchError instanceof Error ? fetchError.message : "Terjadi kesalahan yang tidak diketahui", status: 500 };
}
// Handle response
let data: any;
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
try {
const text = await response.text();
if (!text || text.trim() === '') {
return { error: "Empty response from server", status: response.status };
}
data = JSON.parse(text);
} catch (jsonError) {
const text = await response.text().catch(() => "Unable to read response");
return { error: `Invalid JSON response: ${text.substring(0, 200)}`, status: response.status };
}
} else {
const text = await response.text().catch(() => "Unable to read response");
return { error: text || "Request failed", status: response.status };
}
if (!response.ok) {
return { error: data?.message || data?.error || `HTTP ${response.status}: ${response.statusText}`, status: response.status };
}
return { data, status: response.status };
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("fetch") || error.message.includes("ECONNREFUSED") || error.message.includes("ENOTFOUND") || error.message.includes("CORS")) {
return { error: `Tidak dapat terhubung ke server backend (${API_BASE}). Pastikan backend server berjalan di ${API_BASE} dan dapat diakses.`, status: 0 };
}
}
return { error: error instanceof Error ? error.message : "Terjadi kesalahan yang tidak diketahui", status: 500 };
}
}
// Type definitions
export interface Dokter {
id: number;
nama: string;
email: string;
}
export interface Ruangan {
id: number;
nama: string;
}
export interface ICD9 {
kode: string;
deskripsi: string;
}
export interface ICD10 {
kode: string;
deskripsi: string;
}
export interface TarifData {
kode: string;
deskripsi: string;
harga: number;
}
export interface TarifBPJSRawatInap {
kode: string;
deskripsi: string;
tarif: number;
}
export interface TarifBPJSRawatJalan {
kode: string;
deskripsi: string;
tarif: number;
}
export interface BillingRequest {
nama_pasien: string;
id_pasien?: number;
jenis_kelamin: string;
usia: number;
ruangan: string;
kelas: string;
nama_dokter: string[];
tindakan_rs: string[];
billing_sign: string;
tanggal_masuk: string;
tanggal_keluar: string;
icd9: string[];
icd10: string[];
cara_bayar: string;
total_tarif_rs: number;
total_klaim_bpjs?: number; // ← Added: Baseline BPJS claim from FE
id_dpjp?: number; // ← Added: Doctor In Charge (DPJP) ID
}
export interface LoginResponse {
token: string;
dokter?: Dokter;
admin?: { id: number; nama_admin: string };
}
export interface CloseBilling{
id_billing: number;
tanggal_keluar: string;
}
// API Functions
export async function getDokter() {
return apiFetch<Dokter[]>("/dokter", { method: "GET" });
}
export async function CloseBilling() {
return apiFetch<CloseBilling[]>("/billing/close", { method: "post" });
}
export async function getRuangan() {
return apiFetch<Ruangan[]>("/ruangan", { method: "GET" });
}
export async function getICD9() {
return apiFetch<ICD9[]>("/icd9", { method: "GET" });
}
export async function getICD10() {
return apiFetch<ICD10[]>("/icd10", { method: "GET" });
}
export async function getTarifRumahSakit() {
return apiFetch<TarifData[]>("/tarifRS", { method: "GET" });
}
export async function getTarifBPJSRawatInap() {
return apiFetch<TarifBPJSRawatInap[]>("/tarifBPJSRawatInap", { method: "GET" });
}
export async function getTarifBPJSRawatJalan() {
return apiFetch<TarifBPJSRawatJalan[]>("/tarifBPJSRawatJalan", { method: "GET" });
}
export async function getTarifBPJSInacbgRI() {
return apiFetch<TarifBPJSRawatInap[]>("/tarifBPJSRawatInap", { method: "GET" });
}
export async function getTarifBPJSInacbgRJ() {
return apiFetch<TarifBPJSRawatJalan[]>("/tarifBPJSRawatJalan", { method: "GET" });
}
export async function searchPasien(nama: string) {
return apiFetch(`/pasien/search?nama=${encodeURIComponent(nama)}`, { method: "GET" });
}
export async function createBilling(billingData: BillingRequest) {
return apiFetch("/billing", {
method: "POST",
body: JSON.stringify(billingData)
});
}
export async function getAllBilling() {
return apiFetch("/admin/billing", { method: "GET" });
}
export async function getRiwayatBilling() {
return apiFetch("/admin/riwayat-pasien-all", { method: "GET" }); // return apiFetch("/admin/riwayat-billing", { method: "GET" });
}
export async function editINACBG(id: number) {
return apiFetch(`/admin/inacbg`, { method: "PUT" });
}
export async function getallbilingaktif() {
return apiFetch("/billing/aktif/all", { method: "GET" });
}
export async function getBillingAktifByNama(nama: string) {
return apiFetch(`/billing/aktif?nama_pasien=${encodeURIComponent(nama)}`, { method: "GET" });
}
export async function loginDokter(credentials: { email: string; password: string }) {
return apiFetch<LoginResponse>("/login", {
method: "POST",
body: JSON.stringify(credentials)
});
}
export async function loginAdmin(credentials: { nama_admin: string; password: string }) {
return apiFetch<LoginResponse>("/admin/login", {
method: "POST",
body: JSON.stringify(credentials)
});
}
+519
View File
@@ -0,0 +1,519 @@
// API Configuration and Utilities
// Auto-detect platform: use Next.js API routes for web, direct backend calls for mobile/Electron
// Detect if running in Capacitor (mobile app)
const isCapacitor = typeof window !== 'undefined' &&
(window as any).Capacitor !== undefined;
// Detect if running in Electron (desktop app)
const isElectron = typeof window !== 'undefined' &&
(window as any).electron !== undefined;
// All platforms: use direct backend URL
const getApiBaseUrl = (): string => {
if (isCapacitor) {
// Mobile app: direct backend call
return process.env.NEXT_PUBLIC_API_URL || "http://31.97.109.192:8082";
} else if (isElectron) {
// Electron desktop app: direct backend call
return process.env.NEXT_PUBLIC_API_URL || "http://31.97.109.192:8082";
} else {
// Web browser: direct backend call
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api";
}
};
const API_BASE_URL = getApiBaseUrl();
interface ApiResponse<T> {
data?: T;
error?: string;
message?: string;
status: number;
}
interface FetchOptions extends RequestInit {
timeout?: number;
}
// Generic API fetch function
export async function apiRequest<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<ApiResponse<T>> {
const { timeout = 10000, ...fetchOptions } = options;
// Build URL based on platform
let url: string;
// All platforms use direct backend call
const cleanEndpoint = endpoint.startsWith("/api/")
? endpoint.substring(4) // Remove "/api"
: endpoint.startsWith("/")
? endpoint
: `/${endpoint}`;
url = `${API_BASE_URL}${cleanEndpoint}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
...fetchOptions.headers,
},
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
error:
errorData.message ||
errorData.error ||
`HTTP ${response.status}: ${response.statusText}`,
status: response.status,
};
}
const data = await response.json();
return {
data,
status: response.status,
};
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error) {
if (error.name === "AbortError") {
return {
error:
"Request timeout - Server tidak merespon dalam waktu yang ditentukan",
status: 408,
};
}
// Network errors
if (error.message.includes("fetch")) {
return {
error: `Tidak dapat terhubung ke server (${API_BASE_URL}). Pastikan backend server berjalan.`,
status: 0,
};
}
}
return {
error:
error instanceof Error
? error.message
: "Terjadi kesalahan yang tidak diketahui",
status: 500,
};
}
}
// Specific API functions for Tarif Rumah Sakit
export interface TarifData {
KodeRS: string;
Deskripsi: string;
Harga: number;
Kategori: string;
}
export interface TarifQueryParams {
kategori?: string;
search?: string;
page?: number;
limit?: number;
}
export async function getTarifRumahSakit(
params: TarifQueryParams = {}
): Promise<ApiResponse<TarifData[]>> {
const queryParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
queryParams.append(key, value.toString());
}
});
const endpoint = `/api/tarifRS${queryParams.toString() ? "?" + queryParams.toString() : ""
}`;
return apiRequest<TarifData[]>(endpoint);
}
export async function createTarifRumahSakit(
data: TarifData
): Promise<ApiResponse<TarifData>> {
return apiRequest<TarifData>("/api/tarifRS", {
method: "POST",
body: JSON.stringify(data),
});
}
export async function updateTarifRumahSakit(
kodeRS: string,
data: Partial<TarifData>
): Promise<ApiResponse<TarifData>> {
return apiRequest<TarifData>(`/api/tarifRS/${kodeRS}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
export async function deleteTarifRumahSakit(
kodeRS: string
): Promise<ApiResponse<void>> {
return apiRequest<void>(`/api/tarifRS/${kodeRS}`, {
method: "DELETE",
});
}
// Health check function
export async function checkBackendHealth(): Promise<
ApiResponse<{ status: string; message: string }>
> {
// Health check can use any endpoint, using login as it's lightweight
return apiRequest("/api/login", {
method: "POST",
body: JSON.stringify({ email: "healthcheck", password: "healthcheck" }),
timeout: 3000,
});
}
// ============ LOGIN API ============
export interface LoginRequest {
email: string;
password: string;
}
export interface LoginResponse {
status: string;
token: string;
dokter: {
id: number;
nama: string;
ksm: string;
email: string;
};
}
export async function loginDokter(
credentials: LoginRequest
): Promise<ApiResponse<LoginResponse>> {
// Validate credentials before sending
if (!credentials.email || !credentials.password) {
return {
error: "Email/username dan password harus diisi",
status: 400,
};
}
// Ensure credentials are properly formatted
const payload = {
email: credentials.email.trim(),
password: credentials.password.trim(),
};
// Ensure body is not empty
const bodyString = JSON.stringify(payload);
if (!bodyString || bodyString === "{}") {
return {
error: "Payload tidak valid",
status: 400,
};
}
return apiRequest<LoginResponse>("/api/login", {
method: "POST",
body: bodyString,
});
}
// Admin login request/response types
export interface AdminLoginRequest {
nama_admin: string;
password: string;
}
export interface AdminLoginResponse {
status: string;
token: string;
admin: {
id: number;
nama_admin: string;
id_ruangan: string;
};
}
export async function loginAdmin(
credentials: AdminLoginRequest
): Promise<ApiResponse<AdminLoginResponse>> {
// Validate credentials before sending
if (!credentials.nama_admin || !credentials.password) {
return {
error: "Username dan password harus diisi",
status: 400,
};
}
// Ensure credentials are properly formatted
const payload = {
Nama_Admin: credentials.nama_admin.trim(),
Password: credentials.password.trim(),
};
// Ensure body is not empty
const bodyString = JSON.stringify(payload);
if (!bodyString || bodyString === "{}") {
return {
error: "Payload tidak valid",
status: 400,
};
}
return apiRequest<AdminLoginResponse>("/api/admin/login", {
method: "POST",
body: bodyString,
});
}
// ============ DOKTER API ============
export interface Dokter {
ID_Dokter: number;
Nama_Dokter: string;
KSM: string;
Email_UB: string;
Email_Pribadi: string;
Status: string;
}
export async function getDokter(): Promise<ApiResponse<Dokter[]>> {
return apiRequest<Dokter[]>("/api/dokter");
}
// ============ RUANGAN API ============
export interface Ruangan {
ID_Ruangan: string;
Jenis_Ruangan: string;
Nama_Ruangan: string;
Keterangan: string;
Kategori_ruangan: string;
}
export async function getRuangan(): Promise<ApiResponse<Ruangan[]>> {
return apiRequest<Ruangan[]>("/api/ruangan");
}
// ============ ICD9 API ============
export interface ICD9 {
Kode_ICD9: string;
Prosedur: string;
Versi: string;
}
export async function getICD9(): Promise<ApiResponse<ICD9[]>> {
return apiRequest<ICD9[]>("/api/icd9");
}
// ============ ICD10 API ============
export interface ICD10 {
Kode_ICD10: string;
Diagnosa: string;
Versi: string;
}
export async function getICD10(): Promise<ApiResponse<ICD10[]>> {
return apiRequest<ICD10[]>("/api/icd10");
}
// ============ TARIF BPJS API ============
export interface TarifBPJSRawatInap {
KodeINA: string;
Deskripsi: string;
Kelas1: number;
Kelas2: number;
Kelas3: number;
}
export interface TarifBPJSRawatJalan {
KodeINA: string;
Deskripsi: string;
TarifINACBG: number;
tarif_inacbg?: number; // Backend sends this field name
}
export async function getTarifBPJSRawatInap(): Promise<
ApiResponse<TarifBPJSRawatInap[]>
> {
return apiRequest<TarifBPJSRawatInap[]>("/api/tarifBPJSRawatInap");
}
export async function getTarifBPJSRawatInapByKode(
kode: string
): Promise<ApiResponse<TarifBPJSRawatInap>> {
return apiRequest<TarifBPJSRawatInap>(`/api/tarifBPJS/${kode}`);
}
export async function getTarifBPJSRawatJalan(): Promise<
ApiResponse<TarifBPJSRawatJalan[]>
> {
return apiRequest<TarifBPJSRawatJalan[]>("/api/tarifBPJSRawatJalan");
}
export async function getTarifBPJSRawatJalanByKode(
kode: string
): Promise<ApiResponse<TarifBPJSRawatJalan>> {
return apiRequest<TarifBPJSRawatJalan>(`/api/tarifBPJSRawatJalan/${kode}`);
}
// ============ TARIF RS API (already exists, but adding detail function) ============
export async function getTarifRSByKode(
kode: string
): Promise<ApiResponse<TarifData>> {
return apiRequest<TarifData>(`/api/tarifRS/${kode}`);
}
export async function getTarifRSByKategori(
kategori: string
): Promise<ApiResponse<TarifData[]>> {
// Backend uses path parameter: /tarifRSByKategori/:kategori
return apiRequest<TarifData[]>(`/api/tarifRSByKategori/${encodeURIComponent(kategori)}`);
}
// ============ PASIEN API ============
export interface Pasien {
ID_Pasien: number;
Nama_Pasien: string;
Jenis_Kelamin: string;
Usia: number;
Ruangan: string;
Kelas: string;
}
export async function getPasienById(
id: number
): Promise<ApiResponse<{ message: string; data: Pasien }>> {
return apiRequest<{ message: string; data: Pasien }>(`/api/pasien/${id}`);
}
export async function searchPasien(
nama: string
): Promise<ApiResponse<{ status: string; data: Pasien[] }>> {
return apiRequest<{ status: string; data: Pasien[] }>(
`/api/pasien/search?nama=${encodeURIComponent(nama)}`
);
}
// ============ BILLING API ============
export interface BillingRequest {
nama_dokter: string[];
nama_pasien: string;
id_pasien?: number;
jenis_kelamin: string;
usia: number;
ruangan: string;
kelas: string;
tindakan_rs: string[];
billing_sign?: string | null;
tanggal_masuk?: string;
tanggal_keluar?: string;
icd9: string[];
icd10: string[];
cara_bayar: string;
total_tarif_rs: number;
total_klaim_bpjs?: number; // ← Added: Baseline BPJS claim amount from FE
}
export interface BillingResponse {
status: string;
message: string;
data: {
pasien: Pasien;
billing: {
ID_Billing: number;
ID_Pasien: number;
Total_Tarif_RS: number;
[key: string]: any;
};
tindakan_rs: any[];
icd9: any[];
icd10: any[];
};
}
export async function createBilling(
data: BillingRequest
): Promise<ApiResponse<BillingResponse>> {
return apiRequest<BillingResponse>("/api/billing", {
method: "POST",
body: JSON.stringify(data),
});
}
export interface BillingAktifResponse {
status: string;
message: string;
data: {
billing: any;
tindakan_rs: any[];
icd9: any[];
icd10: any[];
};
}
export async function getBillingAktifByNama(
namaPasien: string
): Promise<ApiResponse<BillingAktifResponse>> {
return apiRequest<BillingAktifResponse>(
`/api/billing/aktif?nama_pasien=${encodeURIComponent(namaPasien)}`
);
}
// ============ ADMIN BILLING API ============
export async function getAllBilling(): Promise<
ApiResponse<{ status: string; data: any[] }>
> {
return apiRequest<{ status: string; data: any[] }>("/api/admin/billing");
}
export async function getBillingById(
id: number
): Promise<ApiResponse<any>> {
return apiRequest<any>(`/api/admin/billing/${id}`);
}
export async function getRuanganDenganPasien(): Promise<
ApiResponse<any[]>
> {
return apiRequest<any[]>("/api/admin/ruangan-dengan-pasien");
}
export interface PostINACBGRequest {
id_billing: number;
tipe_inacbg: string;
kode_inacbg: string[];
total_klaim: number;
billing_sign: string;
tanggal_keluar: string;
}
export async function postINACBGAdmin(
data: PostINACBGRequest
): Promise<ApiResponse<{ status: string; message: string }>> {
return apiRequest<{ status: string; message: string }>("/api/admin/inacbg", {
method: "POST",
body: JSON.stringify(data),
});
}
// API_BASE_URL is now "/api" for Next.js API routes
export { API_BASE_URL };