first commit

This commit is contained in:
Yusron alamsyah
2026-03-13 10:45:28 +07:00
commit 6bb6a1d430
568 changed files with 51753 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
import { ref } from "vue";
import type { OdontogramData } from "~/types/apps/medical/odontogram";
const STORAGE_KEY = "odontogramData";
const savedData = ref<OdontogramData | null>(null);
function saveData(data: OdontogramData) {
try {
// Convert reactive data to plain JS object before saving
const plainData = JSON.parse(JSON.stringify(data));
console.log("Saving odontogram data to localStorage (plain):", plainData);
localStorage.setItem(STORAGE_KEY, JSON.stringify(plainData));
savedData.value = plainData;
} catch (error) {
console.error("Failed to save odontogram data:", error);
}
}
function loadData(): OdontogramData | null {
try {
const data = localStorage.getItem(STORAGE_KEY);
console.log("Loading odontogram data from localStorage:", data);
if (data) {
const parsed = JSON.parse(data);
if (isOdontogramData(parsed)) {
savedData.value = parsed;
return parsed;
}
}
} catch (error) {
console.error("Failed to load odontogram data:", error);
}
return null;
}
const clearData = () => {
try {
localStorage.removeItem(STORAGE_KEY);
savedData.value = null;
return true;
} catch (error) {
console.error("Error clearing odontogram data:", error);
return false;
}
};
const exportData = (data: OdontogramData) => {
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json"
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `odontogram_${new Date().toISOString().split("T")[0]}.json`;
a.click();
URL.revokeObjectURL(url);
};
const importData = (file: File): Promise<OdontogramData | null> => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (isOdontogramData(data)) {
resolve(data);
} else {
console.error("Imported data is not valid OdontogramData");
resolve(null);
}
} catch (error) {
console.error("Error parsing imported data:", error);
resolve(null);
}
};
reader.readAsText(file);
});
};
function isOdontogramData(data: any): data is OdontogramData {
return (
data &&
typeof data === "object" &&
Array.isArray(data.conditions) &&
typeof data.metadata === "object" &&
data.metadata !== null &&
(data.currentMode === undefined || typeof data.currentMode === "number")
);
}
export function useDataStorage() {
return {
saveData,
loadData,
clearData,
exportData,
importData,
savedData
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+134
View File
@@ -0,0 +1,134 @@
import type { MenuItem } from '~/types/menu'
export const useMenuManagement = () => {
const menuItems = ref<MenuItem[]>([])
const menuOptions = ref<string[]>([])
const references = ref<string[]>([])
const selectedReference = ref('Referensi')
const isLoading = ref(false)
// Load menus from JSON file
const loadMenus = async () => {
isLoading.value = true
try {
const response = await $fetch<{ success: boolean; data: { menus: MenuItem[]; menuOptions: string[]; references: string[] } }>('/api/menus/list')
const data = response.data
menuItems.value = data.menus || []
menuOptions.value = data.menuOptions || []
references.value = data.references || []
} catch (error) {
console.error('Error loading menus:', error)
throw error
} finally {
isLoading.value = false
}
}
// Save single menu item
const saveMenu = async (menuItem: MenuItem) => {
try {
const response = await $fetch<{ success: boolean; data: MenuItem }>('/api/menus/save', {
method: 'POST',
body: { menuItem, reference: selectedReference.value }
})
const data = response.data
// Update local state
const index = menuItems.value.findIndex(item => item.id === menuItem.id)
if (index >= 0) {
menuItems.value[index] = data
} else {
menuItems.value.push(data)
}
return data
} catch (error) {
console.error('Error saving menu:', error)
throw error
}
}
// Delete menu item
const deleteMenu = async (menuId: string) => {
try {
await $fetch('/api/menus/delete', {
method: 'DELETE',
body: { menuId, reference: selectedReference.value }
})
// Remove from local state
const removeFromItems = (items: MenuItem[]): MenuItem[] => {
return items.filter(item => {
if (item.id === menuId) return false
if (item.children) {
item.children = removeFromItems(item.children)
}
return true
})
}
menuItems.value = removeFromItems(menuItems.value)
} catch (error) {
console.error('Error deleting menu:', error)
throw error
}
}
// Update menu order
const updateMenuOrder = async (newOrder: MenuItem[]) => {
try {
await $fetch('/api/menus/reorder', {
method: 'POST',
body: { menus: newOrder, reference: selectedReference.value }
})
menuItems.value = newOrder
} catch (error) {
console.error('Error updating menu order:', error)
throw error
}
}
// Generate Vue page from menu
const generatePage = async (menuItem: MenuItem) => {
try {
const pageData = {
name: menuItem.title.replace(/\s+/g, ''),
path: menuItem.url,
templateType: 'default',
metadata: {
title: menuItem.title,
description: `Page for ${menuItem.title}`,
keywords: [menuItem.title.toLowerCase()]
},
content: {
title: menuItem.title,
icon: menuItem.icon
}
}
await $fetch('/api/pages/generate', {
method: 'POST',
body: pageData
})
return pageData
} catch (error) {
console.error('Error generating page:', error)
throw error
}
}
return {
menuItems: readonly(menuItems),
menuOptions: readonly(menuOptions),
references: readonly(references),
selectedReference,
isLoading: readonly(isLoading),
loadMenus,
saveMenu,
deleteMenu,
updateMenuOrder,
generatePage
}
}
+131
View File
@@ -0,0 +1,131 @@
// composables/usePatientForm.ts
import type { FhirPatient, FhirHumanName } from "~/types/fhir/humanName";
interface PatientFormData {
dataDiri: {
namaLengkap: string;
jenisKelamin: string;
tanggalLahir: string;
nomorIdentitas: string;
jenisIdentitas: string;
fhirName?: FhirHumanName | null;
};
// ... other interfaces
}
export const usePatientForm = () => {
const convertToFhirPatient = (formData: PatientFormData): FhirPatient => {
const fhirPatient: FhirPatient = {
resourceType: "Patient",
identifier: [],
active: true,
name: [],
telecom: [],
gender: undefined,
birthDate: undefined,
address: []
};
// Add parsed FHIR name
if (formData.dataDiri.fhirName) {
fhirPatient.name!.push(formData.dataDiri.fhirName);
}
// Gender mapping
if (formData.dataDiri.jenisKelamin) {
fhirPatient.gender =
formData.dataDiri.jenisKelamin === "L" ? "male" : "female";
}
// Birth date
if (formData.dataDiri.tanggalLahir) {
fhirPatient.birthDate = formData.dataDiri.tanggalLahir;
}
// Identifiers
if (formData.dataDiri.nomorIdentitas) {
fhirPatient.identifier!.push({
use: "official",
type: {
coding: [
{
system: "http://terminology.hl7.org/CodeSystem/v2-0203",
code:
formData.dataDiri.jenisIdentitas === "KTP" ? "NNESP" : "PPN",
display: formData.dataDiri.jenisIdentitas
}
]
},
value: formData.dataDiri.nomorIdentitas
});
}
return fhirPatient;
};
return {
convertToFhirPatient
};
};
interface PersonalInfo {
nik?: string;
fullName?: string;
birthPlace?: string;
birthDate?: string;
gender?: string;
}
interface ContactInfo {
phone?: string;
address?: string;
province?: string;
city?: string;
}
// export const usePatientForm = () => {
// const validatePersonalInfo = (data: PersonalInfo) => {
// const errors: Record<string, string> = {};
// if (!data.nik) errors.nik = "NIK wajib diisi";
// if (!data.fullName) errors.fullName = "Nama lengkap wajib diisi";
// if (!data.birthPlace) errors.birthPlace = "Tempat lahir wajib diisi";
// if (!data.birthDate) errors.birthDate = "Tanggal lahir wajib diisi";
// if (!data.gender) errors.gender = "Jenis kelamin wajib diisi";
// return {
// valid: Object.keys(errors).length === 0,
// errors
// };
// };
// const validateContactInfo = (data: ContactInfo) => {
// const errors: Record<string, string> = {};
// if (!data.phone) errors.phone = "Nomor telepon wajib diisi";
// if (!data.address) errors.address = "Alamat wajib diisi";
// if (!data.province) errors.province = "Provinsi wajib diisi";
// if (!data.city) errors.city = "Kota wajib diisi";
// return {
// valid: Object.keys(errors).length === 0,
// errors
// };
// };
// const generateMRNumber = () => {
// const date = new Date();
// const year = date.getFullYear().toString().substr(-2);
// const month = (date.getMonth() + 1).toString().padStart(2, "0");
// const random = Math.floor(Math.random() * 10000)
// .toString()
// .padStart(4, "0");
// return `MR${year}${month}${random}`;
// };
// return {
// validatePersonalInfo,
// validateContactInfo,
// generateMRNumber
// };
// };
+606
View File
@@ -0,0 +1,606 @@
import type { ComputedRef, Ref } from "vue";
import type { ExtendedUser } from "~/types/auth";
import { computed, ref, watch, readonly } from "vue";
export interface UseUserInfoReturnType {
// State
isAuthenticated: ComputedRef<boolean>;
isLoading: ComputedRef<boolean>;
user: ComputedRef<ExtendedUser | null>;
userRoles: ComputedRef<string[]>;
clientRoles: ComputedRef<string[]>;
accessToken: ComputedRef<string>;
sessionExpires: ComputedRef<string | null>;
isIdle: Ref<boolean>;
// Raw data for debugging
data: Ref<any>;
status: Ref<string>;
// Session data from [...].ts
idToken: ComputedRef<string>;
refreshToken: ComputedRef<string>;
accessTokenPayload: ComputedRef<any>;
expiresAt: ComputedRef<number | null>;
sessionScope: ComputedRef<string>;
jwtToken: ComputedRef<string>;
// Role checking functions
hasRole: (role: string) => boolean;
hasClientRole: (role: string) => boolean;
hasAnyRole: (roles: string[]) => boolean;
isAdmin: ComputedRef<boolean>;
// Session monitoring functions
updateActivity: () => void;
startSessionMonitoring: () => (() => void) | null;
stopSessionMonitoring: () => void;
// Authentication actions
login: (provider?: string) => Promise<void>;
logout: (options?: any) => Promise<void>;
fullLogout: () => Promise<void>;
refresh: () => Promise<void>;
forceRefreshSession: () => Promise<void>;
isKeycloakSessionActive: () => Promise<boolean>;
}
export const useUserInfo = (): UseUserInfoReturnType => {
const { data, status, signIn, signOut, getSession } = useAuth();
// Internal writable refs for manual setting
const internalData = ref(data.value);
const internalStatus = ref(status.value);
// Sync internal refs with useAuth data and status
watch(
() => data.value,
(newVal) => {
internalData.value = newVal;
},
{ immediate: true }
);
watch(
() => status.value,
(newVal) => {
internalStatus.value = newVal;
},
{ immediate: true }
);
// Session monitoring state
const lastActivity = ref(Date.now());
const isIdle = ref(false);
const tabVisible = ref(true);
// Constants
const IDLE_TIMEOUT = 15 * 60 * 1000; // 15 menit
const SESSION_CHECK_INTERVAL = 5 * 60 * 1000; // 5 menit
const TAB_INACTIVE_THRESHOLD = 10 * 60 * 1000; // 10 menit
// Basic computed properties
const isAuthenticated = computed(
() => internalStatus.value === "authenticated"
);
const isLoading = computed(() => internalStatus.value === "loading");
// User info from session
const user: ComputedRef<ExtendedUser | null> = computed(() => {
if (internalData.value?.user) {
return internalData.value.user as ExtendedUser;
}
return null;
});
// Session data dari [...].ts callbacks - INILAH DATA YANG AKAN DITAMPILKAN
const idToken = computed(() => (internalData.value as any)?.id_token || "");
const accessToken = computed(
() => (internalData.value as any)?.access_token || ""
);
const refreshToken = computed(
() => (internalData.value as any)?.refresh_token || ""
);
const accessTokenPayload = computed(
() => (internalData.value as any)?.access_token_payload || null
);
const expiresAt = computed(
() => (internalData.value as any)?.expires_at || null
);
const sessionScope = computed(() => (internalData.value as any)?.scope || "");
const jwtToken = computed(() => (internalData.value as any)?.jwt || "");
// User roles dari session
const userRoles = computed(() => {
return (internalData.value?.user as any)?.roles || [];
});
const clientRoles = computed(() => {
return (internalData.value?.user as any)?.client_roles || [];
});
// Session expires
const sessionExpires = computed(() => data.value?.expires || null);
// Activity tracking functions
const updateActivity = () => {
if (process.client) {
lastActivity.value = Date.now();
isIdle.value = false;
}
};
const checkIdleStatus = () => {
if (!process.client) return;
const now = Date.now();
const timeSinceLastActivity = now - lastActivity.value;
if (
timeSinceLastActivity >= IDLE_TIMEOUT &&
tabVisible.value &&
isAuthenticated.value
) {
isIdle.value = true;
handleIdleTimeout();
}
};
const handleIdleTimeout = async () => {
if (!process.client) return;
try {
const session = await getSession();
if (
session &&
session.expires &&
new Date(session.expires) > new Date()
) {
await navigateTo("/auth/login?reason=idle&continue=true");
} else {
await logout({ reason: "expired" });
}
} catch (error) {
console.error("Idle timeout handling error:", error);
await logout({ reason: "expired" });
}
};
const handleVisibilityChange = async () => {
if (!process.client) return;
tabVisible.value = !document.hidden;
if (!document.hidden) {
updateActivity();
const inactiveTime = Date.now() - lastActivity.value;
if (inactiveTime >= TAB_INACTIVE_THRESHOLD && isAuthenticated.value) {
await handleTabReactivation();
}
}
};
const handleTabReactivation = async () => {
if (!process.client) return;
try {
const session = await getSession();
if (
session &&
session.expires &&
new Date(session.expires) > new Date()
) {
await navigateTo("/auth/login?reason=tab_inactive&continue=true");
} else {
await navigateTo("/auth/login?reason=session_expired");
}
} catch (error) {
console.error("Tab reactivation error:", error);
await navigateTo("/auth/login?reason=error");
}
};
const startSessionMonitoring = (): (() => void) | null => {
if (!process.client) {
console.warn("Session monitoring requires client-side environment");
return null;
}
if (!isAuthenticated.value) {
console.warn("Session monitoring requires authenticated user");
return null;
}
const events = [
"mousedown",
"mousemove",
"keydown",
"scroll",
"touchstart",
"click"
];
const activityHandler = () => updateActivity();
events.forEach((event) => {
document.addEventListener(event, activityHandler, { passive: true });
});
document.addEventListener("visibilitychange", handleVisibilityChange);
const idleInterval = setInterval(checkIdleStatus, 60 * 1000);
const sessionInterval = setInterval(async () => {
if (isAuthenticated.value) {
try {
await getSession();
} catch (error) {
console.error("Session check failed:", error);
clearInterval(sessionInterval);
await logout({ reason: "expired" });
}
}
}, SESSION_CHECK_INTERVAL);
return () => {
events.forEach((event) => {
document.removeEventListener(event, activityHandler);
});
document.removeEventListener("visibilitychange", handleVisibilityChange);
clearInterval(idleInterval);
clearInterval(sessionInterval);
};
};
// **PERBAIKAN: Stop Session Monitoring Function**
const stopSessionMonitoring = (): void => {
// Implementation bisa ditambahkan jika diperlukan
//console.log("Stopping session monitoring...");
};
// Role checking functions
const hasRole = (role: string): boolean => {
return userRoles.value.includes(role);
};
const hasClientRole = (role: string): boolean => {
return clientRoles.value.includes(role);
};
const hasAnyRole = (roles: string[]): boolean => {
return roles.some((role) => userRoles.value.includes(role));
};
const isAdmin = computed(
() => hasRole("admin") || hasRole("super_admin") || hasRole("realm-admin")
);
// Login function
const login = async (provider = "keycloak") => {
try {
const currentUrl = window.location.pathname;
const callbackUrl = currentUrl !== "/auth/login" ? currentUrl : "/";
const result = await signIn(provider, {
callbackUrl,
redirect: true
});
if (result?.error) {
throw new Error(result.error);
}
// **PERBAIKAN: Update activity setelah login berhasil**
updateActivity();
await getSession();
} catch (error) {
throw error;
}
};
// Enhanced Logout function dengan reason support
const logout = async (options?: {
redirect?: string;
clearStorage?: boolean;
confirmDialog?: boolean;
reason?: "manual" | "expired" | "admin" | "idle";
}) => {
const defaultOptions = {
redirect: "/auth/login",
clearStorage: true,
confirmDialog: false,
reason: "idle" as const,
...options
};
try {
//console.log("Logout called with options:", defaultOptions);
// Menghilangkan pemanggilan fullLogout manual sesuai permintaan
if (defaultOptions.clearStorage && process.client) {
//console.log("Clearing localStorage and sessionStorage");
localStorage.removeItem("user-preferences");
localStorage.removeItem("app-state");
sessionStorage.clear();
}
// Memanggil API logout server untuk menghandle logout Keycloak dan pembersihan session
if (process.client) {
// Mengambil keycloakIssuer dan redirectUri dari public runtime config
const config = useRuntimeConfig();
const keycloakIssuer = config.public.keycloakIssuer;
const origin = window.location.origin;
const redirectUri = `${origin}/auth/login`;
let logoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(
redirectUri
)}`;
if (idToken.value) {
logoutUrl += `&id_token_hint=${encodeURIComponent(idToken.value)}`;
}
//console.log("Calling Keycloak logout URL:", logoutUrl);
await fetch("/api/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ id_token_hint: idToken.value })
});
// Bersihkan local storage setelah logout server
//console.log("Clearing localStorage and sessionStorage after server logout");
localStorage.removeItem("user-preferences");
localStorage.removeItem("app-state");
sessionStorage.clear();
}
await signOut({
callbackUrl: `${defaultOptions.redirect}?reason=idle`,
redirect: true
});
} catch (error) {
console.error("Logout failed:", error);
throw new Error("Logout failed. Please try again.");
}
};
// Perbaikan fullLogout - simplified dan menggunakan logout yang sudah ada
const fullLogout = async () => {
try {
console.log("Full logout initiated");
// 1. Stop session monitoring terlebih dahulu
stopSessionMonitoring();
console.log("Session monitoring stopped");
console.log(process);
if (process.client) {
// 2. Clear all storage before logout
console.log("Clearing all storage before logout");
// Clear localStorage
localStorage.removeItem("user-preferences");
localStorage.removeItem("app-state");
localStorage.clear();
// Clear sessionStorage
sessionStorage.clear();
// Log cookies and sessionStorage before clearing
console.log("Cookies before clearing:", document.cookie);
console.log(
"SessionStorage before clearing:",
JSON.stringify(sessionStorage)
);
// Clear cookies if any
const cookies = document.cookie.split(";");
const domain = window.location.hostname;
cookies.forEach((c, index) => {
const eqPos = c.indexOf("=");
const name = eqPos > -1 ? c.substr(0, eqPos).trim() : c.trim();
console.log(`Clearing cookie [${index}]:`, name);
// Clear cookie for current path
document.cookie =
name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
// Clear cookie for current path and domain
document.cookie =
name +
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`;
// Clear cookie for current path and domain with leading dot (for subdomains)
if (domain.indexOf(".") !== -1) {
const domainWithDot = "." + domain;
document.cookie =
name +
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`;
}
// Explicitly clear Keycloak cookies if present
const keycloakCookies = [
"KEYCLOAK_SESSION",
"KEYCLOAK_IDENTITY",
"KEYCLOAK_IDENTITY_LEGACY",
"KEYCLOAK_REMEMBER_ME",
"KEYCLOAK_REMEMBER_ME_LEGACY"
];
if (keycloakCookies.includes(name)) {
console.log(`Explicitly clearing Keycloak cookie: ${name}`);
document.cookie =
name +
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`;
if (domain.indexOf(".") !== -1) {
const domainWithDot = "." + domain;
document.cookie =
name +
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`;
}
}
console.log("Cleared cookie:", name);
});
}
// 3. Call server API to get the correct logout URL
const response = await $fetch("/api/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id_token_hint: idToken.value
})
});
if (response.success && response.logoutUrl && process.client) {
console.log("Redirecting to Keycloak logout:", response.logoutUrl);
localStorage.removeItem("user-preferences");
localStorage.removeItem("app-state");
localStorage.clear();
sessionStorage.clear();
// Append query parameters to logoutUrl
let logoutUrlWithParams = response.logoutUrl;
// 4. Redirect directly to Keycloak logout endpoint with appended params
window.location.href = logoutUrlWithParams;
return;
}
// Fallback to signOut if server API fails
// Clear local data and update status to logged out
if (process.client) {
// Clear localStorage and sessionStorage again to be sure
localStorage.clear();
sessionStorage.clear();
// Reset data and status to reflect logged out state
internalData.value = null;
internalStatus.value = "unauthenticated";
}
return;
} catch (error) {
console.error("Full logout failed:", error);
// Emergency fallback - manual logout to Keycloak
if (process.client) {
try {
console.log("Executing emergency logout");
const config = useRuntimeConfig();
const keycloakIssuer = config.public.keycloakIssuer;
const origin = window.location.origin;
const redirectUri = `${origin}/auth/login?reason=emergency`;
// Clear all storage again to ensure
localStorage.clear();
sessionStorage.clear();
// Build logout URL with client_id as fallback
let emergencyLogoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(
redirectUri
)}`;
// Add id_token_hint if available
if (idToken.value) {
emergencyLogoutUrl += `&id_token_hint=${encodeURIComponent(
idToken.value
)}`;
} else {
// Use client_id as alternative if id_token_hint is missing
const clientId = config.public.keycloakClientId || "your-client-id";
emergencyLogoutUrl += `&client_id=${encodeURIComponent(
String(clientId)
)}`;
}
console.log("Emergency logout URL:", emergencyLogoutUrl);
window.location.href = emergencyLogoutUrl;
} catch (emergencyError) {
console.error("Emergency logout failed:", emergencyError);
// Last resort - force redirect and clear storage
if (process.client) {
localStorage.clear();
sessionStorage.clear();
window.location.href = "/auth/login?reason=force";
}
}
}
}
};
// Force refresh session
const forceRefreshSession = async () => {
try {
await getSession();
} catch (error) {
console.error("Force refresh session error:", error);
}
};
// New method to check if Keycloak session is active
const isKeycloakSessionActive = async (): Promise<boolean> => {
if (!process.client) return false;
try {
const session = await getSession();
if (
session &&
session.expires &&
new Date(session.expires) > new Date()
) {
return true;
}
return false;
} catch (error) {
console.error("Error checking Keycloak session active status:", error);
return false;
}
};
// Add computed property for accessTokenPayload
// **PERBAIKAN: Return semua fungsi yang diperlukan termasuk session monitoring**
return {
// State
isAuthenticated,
isLoading,
user,
userRoles,
clientRoles,
accessToken,
sessionExpires,
isIdle: readonly(isIdle),
// Raw data untuk debugging
data: internalData,
status: internalStatus,
// Session data dari [...].ts
idToken,
refreshToken,
accessTokenPayload,
expiresAt,
sessionScope,
jwtToken,
// Role checking functions
hasRole,
hasClientRole,
hasAnyRole,
isAdmin,
// Session monitoring functions
updateActivity,
startSessionMonitoring,
stopSessionMonitoring,
// Authentication actions
login,
logout,
fullLogout,
refresh: async () => {
await getSession();
},
forceRefreshSession,
isKeycloakSessionActive
};
};
+61
View File
@@ -0,0 +1,61 @@
/**
* Composable untuk menyediakan fungsi dan aturan validasi umum.
*/
export const useValidation = () => {
const required = (value, message = 'Bidang ini wajib diisi.') => {
// Memastikan nilai ada dan bukan string kosong (setelah trim)
return (value !== null && value !== undefined && String(value).trim() !== '') || message;
};
const minNameLength = (value, minLength = 3) => {
if (!value) return true; // Lewati jika kosong (handle oleh 'required')
const length = value.trim().length; // Hitung panjang setelah menghapus spasi di awal/akhir
return length >= minLength || `Panjang minimal adalah ${minLength} karakter.`;
};
const isNumber = (value) => {
if (!value) return true; // Lewati jika kosong (handle oleh 'required')
return /^[0-9]+$/.test(value) || 'Hanya boleh mengandung angka.';
};
const phoneLength = (value) => {
if (!value) return true;
// Hapus karakter non-angka sebelum menghitung panjang
const length = value.replace(/[^0-9]/g, '').length;
return (length >= 9 && length <= 15) || 'Nomor telepon harus antara 9 hingga 15 digit.';
};
const indonesianPhoneFormat = (value) => {
if (!value) return true;
// Pola: Harus dimulai dengan '08', diikuti 7 hingga 13 digit angka
return /^08[0-9]{7,13}$/.test(value) || 'Format nomor telepon tidak valid (Contoh: 08xxxxxxxx).';
};
// Gabungkan semua aturan validasi telepon ke dalam satu objek untuk kemudahan
const phoneRules = [
required,
isNumber,
phoneLength,
indonesianPhoneFormat // Opsional: Hapus komentar jika format 08 wajib
];
const textRules = [
required,
minNameLength
];
const requiredRules = [
required
]
return {
required,
isNumber,
phoneLength,
indonesianPhoneFormat,
phoneRules,
textRules,
requiredRules
};
};