Commit 704d29ca by Samuel Taniel Mulyadi

done token

1 parent a364264c
......@@ -14,19 +14,16 @@ const currentTime = ref(new Date())
// Function to check if the current time is within a schedule item range
const { api } = useCivitasApi()
async function getData() {
loading.value = true
schedule.value = []
try {
const response = await fetch('https://api.ui.ac.id/my/ac', {
headers: { Authorization: `Bearer ${keycloakStore.accessToken}` },
})
if (!response.ok)
throw new Error(`Error: ${response.statusText}`)
const { response, error } = await api.user.getRiwayatAkademik()
const data = await response.json()
const data = response.value?.value // ✅ UNWRAP the inner ref
if (keycloakStore.civitas === 'mahasiswa') {
// Get the latest semester
......
<script setup lang="ts">
import { useKeycloakStore } from "@/@core/stores/keycloakStore";
import { ref, computed, onMounted } from "vue";
import { computed, onMounted, ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
// Store Keycloak
const keycloakStore = useKeycloakStore();
const keycloakStore = useKeycloakStore()
// Data dan state
const items = ref<any[]>([]);
const loading = ref(false);
const searchQuery = ref("");
const items = ref<any[]>([])
const loading = ref(false)
const searchQuery = ref('')
// Header tabel
const logHeaders = [
{ title: "KOLEKSI", key: "koleksi" },
{ title: "TANGGAL PEMINJAMAN", key: "tglpinjam" },
{ title: "TANGGAL PENGEMBALIAN", key: "tglkembali" },
{ title: "DIPINJAM", key: "dipinjam" },
{ title: "PERPANJANGAN", key: "perpanjangan" },
{ title: "DENDA", key: "denda" },
{ title: "DENDA TOTAL", key: "dendatotal" },
];
{ title: 'KOLEKSI', key: 'koleksi' },
{ title: 'TANGGAL PEMINJAMAN', key: 'tglpinjam' },
{ title: 'TANGGAL PENGEMBALIAN', key: 'tglkembali' },
{ title: 'DIPINJAM', key: 'dipinjam' },
{ title: 'PERPANJANGAN', key: 'perpanjangan' },
{ title: 'DENDA', key: 'denda' },
{ title: 'DENDA TOTAL', key: 'dendatotal' },
]
const { api } = useCivitasApi()
// Fungsi ambil data
async function getData() {
loading.value = true;
loading.value = true
try {
const apiEndpoint = "https://api.ui.ac.id/my/lib";
const response = await fetch(apiEndpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
if (!response.ok) throw new Error("Gagal mengambil data");
const dataku = await response.json();
const { response, error } = await api.user.getLib()
const dataku = response.value?.value // ✅ UNWRAP the inner ref
// dataku adalah array, tiap elemen punya properti root-level
const requiredKeys = [
"denda",
"dendatotal",
"dipinjam",
"koleksi",
"perpanjangan",
"tglkembali",
"tglpinjam",
"username",
];
'denda',
'dendatotal',
'dipinjam',
'koleksi',
'perpanjangan',
'tglkembali',
'tglpinjam',
'username',
]
// ambil hanya objek yang memiliki semua key di root
items.value = Array.isArray(dataku)
? dataku.filter((r: any) => requiredKeys.every(k => k in r))
: [];
} catch (err) {
console.error("Gagal mengambil data:", err);
} finally {
loading.value = false;
: []
}
catch (err) {
console.error('Gagal mengambil data:', err)
}
finally {
loading.value = false
}
}
// Panggil saat komponen mount
onMounted(() => {
getData();
});
getData()
})
// Filter hasil pencarian
const filteredItems = computed(() => {
if (!searchQuery.value) return items.value;
const q = searchQuery.value.toLowerCase();
if (!searchQuery.value)
return items.value
const q = searchQuery.value.toLowerCase()
return items.value.filter(item =>
logHeaders.some(
h => item[h.key] && String(item[h.key]).toLowerCase().includes(q)
)
);
});
h => item[h.key] && String(item[h.key]).toLowerCase().includes(q),
),
)
})
</script>
<template>
<AppCardActions
:title="`Status Peminjaman Buku`"
title="Status Peminjaman Buku"
class="jadwalShift"
action-collapsed
action-remove
......@@ -104,9 +104,7 @@ const filteredItems = computed(() => {
item-value="tglpinjam"
:sort-by="['tglpinjam']"
:sort-asc="[true]"
></VDataTable>
/>
</AppCardActions>
</template>
......@@ -153,4 +151,4 @@ const filteredItems = computed(() => {
display: flex;
justify-content: flex-end;
}
</style>
\ No newline at end of file
</style>
<script lang="ts" setup>
import { useKeycloakStore } from "@core/stores/keycloakStore";
import { useKeycloakStore } from '@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore();
const isAuthenticated = computed(() => keycloakStore.authenticated);
const keycloakStore = useKeycloakStore()
const isAuthenticated = computed(() => keycloakStore.authenticated)
interface ShiftData {
shift_date: string;
shift_start: string;
shift_end: string;
shift: string;
start_time?: string;
end_time?: string;
shift_date: string
shift_start: string
shift_end: string
shift: string
start_time?: string
end_time?: string
}
const shifts = ref<ShiftData[]>([]);
const loading = ref(false);
const error = ref("");
const searchQuery = ref("");
const shifts = ref<ShiftData[]>([])
const loading = ref(false)
const error = ref('')
const searchQuery = ref('')
const headersShift = [
{ title: "Tanggal", key: "shift_date", sortable: true },
{ title: "Nama Hari", key: "day_name", sortable: false },
{ title: "Shift", key: "shift", sortable: false },
{ title: "Jadwal Shift", key: "shift_schedule", sortable: false },
{ title: "Mulai Aktual", key: "start_time", sortable: false },
{ title: "Selesai Aktual", key: "end_time", sortable: false },
{ title: "Status", key: "status", sortable: false },
];
{ title: 'Tanggal', key: 'shift_date', sortable: true },
{ title: 'Nama Hari', key: 'day_name', sortable: false },
{ title: 'Shift', key: 'shift', sortable: false },
{ title: 'Jadwal Shift', key: 'shift_schedule', sortable: false },
{ title: 'Mulai Aktual', key: 'start_time', sortable: false },
{ title: 'Selesai Aktual', key: 'end_time', sortable: false },
{ title: 'Status', key: 'status', sortable: false },
]
const { api } = useCivitasApi()
async function fetchShiftData() {
loading.value = true;
error.value = "";
loading.value = true
error.value = ''
try {
const apiEndpoint = `https://api.ui.ac.id/my/hr/attendance`;
const response = await fetch(apiEndpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
if (!response.ok) {
throw new Error("Gagal fetch data");
}
// const apiEndpoint = `https://api.ui.ac.id/my/hr/attendance`;
// const response = await fetch(apiEndpoint, {
// headers: {
// Authorization: `Bearer ${keycloakStore.accessToken}`,
// },
// });
// if (!response.ok) {
// throw new Error("Gagal fetch data");
// }
const data = await response.json();
const { response, error } = await api.user.getAttendance()
const data = response.value?.value // ✅ UNWRAP the inner ref
shifts.value = data
.map((item: any) => ({
...item,
shift_start: `${item.shift_date} ${item.shift_start || "00:00"}`,
shift_end: `${item.shift_date} ${item.shift_end || "00:00"}`,
shift_start: `${item.shift_date} ${item.shift_start || '00:00'}`,
shift_end: `${item.shift_date} ${item.shift_end || '00:00'}`,
}))
.sort((a, b) => new Date(b.shift_date).getTime() - new Date(a.shift_date).getTime());
} catch (err: any) {
error.value = err.message || "Terjadi kesalahan saat mengambil data";
} finally {
loading.value = false;
.sort((a, b) => new Date(b.shift_date).getTime() - new Date(a.shift_date).getTime())
}
catch (err: any) {
error.value = err.message || 'Terjadi kesalahan saat mengambil data'
}
finally {
loading.value = false
}
}
function getDayName(date: string) {
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
const dayIndex = new Date(date).getDay();
return days[dayIndex];
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']
const dayIndex = new Date(date).getDay()
return days[dayIndex]
}
const resolveDayColor = (day: string) => {
if (day === "Sabtu" || day === "Minggu") return "error";
};
if (day === 'Sabtu' || day === 'Minggu')
return 'error'
}
function getHour(time: string | undefined) {
if (!time) return "-";
const parts = time.split(" ");
return parts.length === 2 ? parts[1] : time;
if (!time)
return '-'
const parts = time.split(' ')
return parts.length === 2 ? parts[1] : time
}
function getStatus(start: string | undefined, end: string | undefined) {
if (!start && !end) return "Tidak Ada";
if (!start || !end) return "Belum Hitung";
return "On Time";
if (!start && !end)
return 'Tidak Ada'
if (!start || !end)
return 'Belum Hitung'
return 'On Time'
}
const resolveStatusColor = (status: string) => {
if (status === "On Time") return "success";
if (status === "Belum Hitung") return "primary";
if (status === "Tidak Ada") return "error";
};
if (status === 'On Time')
return 'success'
if (status === 'Belum Hitung')
return 'primary'
if (status === 'Tidak Ada')
return 'error'
}
const filteredShifts = computed(() => {
if (!searchQuery.value) return shifts.value;
if (!searchQuery.value)
return shifts.value
return shifts.value.filter((shift) => {
const dayName = getDayName(shift.shift_date);
const status = getStatus(shift.start_time, shift.end_time);
return shifts.value.filter(shift => {
const dayName = getDayName(shift.shift_date)
const status = getStatus(shift.start_time, shift.end_time)
return (
shift.shift_date.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
dayName.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
shift.shift.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
shift.start_time?.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
shift.end_time?.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
status.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
});
shift.shift_date.toLowerCase().includes(searchQuery.value.toLowerCase())
|| dayName.toLowerCase().includes(searchQuery.value.toLowerCase())
|| shift.shift.toLowerCase().includes(searchQuery.value.toLowerCase())
|| shift.start_time?.toLowerCase().includes(searchQuery.value.toLowerCase())
|| shift.end_time?.toLowerCase().includes(searchQuery.value.toLowerCase())
|| status.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
})
onMounted(() => {
fetchShiftData();
});
fetchShiftData()
})
</script>
<template>
<AppCardActions
:title="`Log Absen`"
title="Log Absen"
class="jadwalShift"
action-collapsed
action-remove
......
......@@ -38,6 +38,7 @@ function resolveStatusColor(status: string) {
default: return 'default'
}
}
const { api } = useCivitasApi()
async function getData() {
loading.value = true
......@@ -45,15 +46,8 @@ async function getData() {
items.value = []
try {
const res = await fetch('https://api.ui.ac.id/my/ac', {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
})
if (!res.ok)
throw new Error('Gagal mengambil data')
const raw = await res.json()
const { response, error } = await api.user.getRiwayatAkademik()
const raw = response.value?.value // ✅ UNWRAP the inner ref
items.value = raw.map((item: any, idx: number) => {
const expandedFiltered = Object.values(item.IRS || {})
......
import { defu } from 'defu'
import type { UseFetchOptions } from 'nuxt/app'
import type { Ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import { useAuthFetch } from '@/composables/useAuthFetch'
import keycloakInstance from '@/keycloak'
export const useApi: typeof useFetch = <T>(url: MaybeRefOrGetter<string>, options: UseFetchOptions<T> = {}) => {
const config = useRuntimeConfig()
const accessToken = useCookie('accessToken')
const BASE_URL = 'https://api.ui.ac.id'
const keycloakStore = useKeycloakStore()
const defaults: UseFetchOptions<T> = {
baseURL: config.public.apiBaseUrl,
key: toValue(url),
headers: accessToken.value ? { Authorization: `Bearer ${accessToken.value}` } : {},
}
// --- Interface Definitions ---
interface PasswordPayload {
oldPassword: string
newPassword: string
}
// for nice deep defaults, please use unjs/defu
const params = defu(options, defaults)
interface ApiErrorDetail {
message: string
details?: any
}
return useFetch(url, params)
interface ApiErrorResponse {
success: false
code: number
error: ApiErrorDetail
message?: string
}
// import { defu } from 'defu'
// import { destr } from 'destr'
// import type { UseFetchOptions } from 'nuxt/app'
// export const useApi = <T>(url: MaybeRefOrGetter<string>, options: UseFetchOptions<T> = {}) => {
// const config = useRuntimeConfig()
// const accessToken = useCookie('accessToken')
// const defaults: UseFetchOptions<T> = {
// baseURL: config.public.apiBaseUrl || '/api',
// key: toValue(url),
// headers: {
// Accept: 'application/json',
// ...(accessToken.value ? { Authorization: `Bearer ${accessToken.value}` } : {}),
// },
// onResponse({ response }) {
// // Parse response with destr (like Vite version)
// try {
// response._data = destr(response._data)
// }
// catch (error) {
// console.error('Failed to parse response:', error)
// }
// },
// onRequest({ options }) {
// // Similar to `beforeFetch`
// if (accessToken.value) {
// options.headers = {
// ...options.headers,
// Authorization: `Bearer ${accessToken.value}`,
// }
// }
// },
// }
// // Merge user options with defaults
// const params = defu(options, defaults)
// return useFetch<T>(url, params)
// }
export function useCivitasApi() {
const loading = ref(false)
const error: Ref<string | null> = ref(null)
const errorResponse: Ref<ApiErrorResponse | null> = ref(null)
const fetchWithAuth = async <T = any>(
endpoint: string,
options: Record<string, any> = {},
) => {
loading.value = true
error.value = null
errorResponse.value = null
const url = `${BASE_URL}${
endpoint.startsWith('/') ? endpoint : `/${endpoint}`
}`
const responseData: Ref<T | null> = ref(null)
const errorRef: Ref<any | null> = ref(null)
const currentTime = Math.floor(Date.now() / 1000) // Current time in seconds
const tokenExp = keycloakInstance.tokenParsed?.exp ?? 0 // Token expiration time in seconds
const timeRemaining = tokenExp - currentTime // Time remaining in seconds
if (timeRemaining < 5) {
console.log('Token hampir expired, mencoba refresh...')
await keycloakStore.updateToken() // Refresh the token if it's about to expire
}
try {
const { data, error: fetchError } = await useAuthFetch<
T | ApiErrorResponse
>(url, options)
responseData.value = data as Ref<T | null>
errorRef.value = fetchError
// so up until this is work
if (fetchError.value) {
const errorData = fetchError.value.data as ApiErrorResponse
errorResponse.value = errorData
if (errorData?.error?.message) {
error.value = errorData.error.message
}
else if (errorData?.message) {
error.value = errorData.message
}
else {
error.value
= fetchError.value.message || 'An error occurred during fetch'
}
console.error('API call error details:', fetchError.value)
}
}
catch (err: any) {
error.value
= err.message || 'An unexpected network or setup error occurred'
console.error('API call exception:', err)
errorResponse.value = {
success: false,
code: 0,
error: { message: error.value || 'Unknown error' },
}
errorRef.value = { message: error.value }
}
finally {
loading.value = false
}
return {
response: responseData,
error: errorRef,
fetchError: error,
errorResponse,
}
}
// Grouped API endpoints
const api = {
staf: {
getStafData: (stafNIP: number) =>
fetchWithAuth(`/staf/${stafNIP}`),
},
dosen: {
getListDosen: () =>
fetchWithAuth('/listdosen'),
},
user: {
getRiwayatAkademik: () => fetchWithAuth('/my/ac'),
getProfile: () => fetchWithAuth('/me'),
getParkirAktif: () => fetchWithAuth('/my/parkir/aktif'),
getAttendance: () => fetchWithAuth('/my/hr/attendance'),
getLib: () => fetchWithAuth('/my/lib'),
changePassword: (payload: PasswordPayload) =>
fetchWithAuth('/my/pw', {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
}),
// getPublicPhoto: (kodeIdentitas: number) =>
// fetchWithAuth(`/public/photo/${kodeIdentitas}.jpg`),
},
}
return {
api,
loading,
error,
errorResponse,
}
}
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
export async function useAuthFetch<T>(url: string, options?: RequestInit): Promise<T> {
const keycloakStore = useKeycloakStore()
import { useFetch } from '#app'
// Ensure token is valid before making the request
if (keycloakStore.isTokenExpired) {
console.log('Token hampir expired, mencoba refresh...')
await keycloakStore.updateToken()
}
export function useAuthFetch<T>(url: string, options?: any) {
const keycloakStore = useKeycloakStore()
const response = await fetch(url, {
return useFetch<T>(url, {
...options,
headers: {
...(options?.headers || {}),
'Authorization': `Bearer ${keycloakStore.accessToken}`,
'Content-Type': 'application/json',
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
})
async beforeRequest({ options }: { options: any }) {
// Cek apakah token hampir expired (less than 5 seconds remaining)
const currentTime = Math.floor(Date.now() / 1000) // Current time in seconds
const tokenExp = keycloakStore.keycloakInstance.tokenParsed?.exp ?? 0 // Token expiration time in seconds
if (!response.ok)
throw new Error(`HTTP error! Status: ${response.status}`)
const timeRemaining = tokenExp - currentTime // Time remaining in seconds
return response.json() as Promise<T>
if (timeRemaining < 5) {
console.log('Token hampir expired, mencoba refresh...')
await keycloakStore.updateToken() // Refresh the token if it's about to expire
}
// Set ulang Authorization header setelah refresh token
options.headers = {
...options.headers,
Authorization: `Bearer ${keycloakStore.accessToken}`,
}
},
})
}
......@@ -7,15 +7,10 @@ const keycloakStore = useKeycloakStore()
const authenticated = computed(() => keycloakStore.authenticated)
const now = ref(Math.floor(Date.now() / 1000))
const tokenLifetime = ref(0)
let timer: ReturnType<typeof setInterval>
onMounted(() => {
// Set tokenLifetime only once
if (keycloakInstance.tokenParsed?.exp && keycloakInstance.tokenParsed?.iat)
tokenLifetime.value = keycloakInstance.refreshTokenParsed.exp - keycloakInstance.refreshTokenParsed.iat
timer = setInterval(() => {
now.value = Math.floor(Date.now() / 1000)
}, 1000)
......@@ -27,11 +22,17 @@ onUnmounted(() => {
const computedExpIn = computed(() => {
return authenticated.value && keycloakInstance.refreshTokenParsed?.exp
? Math.max(keycloakInstance.refreshTokenParsed.exp - now.value, 0)
: 0
? keycloakInstance.refreshTokenParsed.exp - now.value
: null
})
const formattedExpIn = computed(() => {
if (computedExpIn.value === null)
return '--:--'
if (computedExpIn.value < 0)
return 'Expired'
const minutes = Math.floor(computedExpIn.value / 60)
const seconds = computedExpIn.value % 60
......
......@@ -88,6 +88,7 @@ export default defineNuxtConfig({
'@themeConfig': ['../themeConfig.ts'],
'@layouts/*': ['../@layouts/*'],
'@layouts': ['../@layouts'],
'@composables': ['../@composables'],
'@core/*': ['../@core/*'],
'@core': ['../@core'],
'@images/*': ['../assets/images/*'],
......
......@@ -98,16 +98,16 @@ const onSubmit = () => {
}
// Watch perubahan accessToken, panggil ulang useAuthFetch
watchEffect(async () => {
if (!keycloakStore.accessToken)
return // Hindari fetch jika token masih kosong
console.log('Fetching data dengan token baru...')
// watchEffect(async () => {
// if (!keycloakStore.accessToken)
// return // Hindari fetch jika token masih kosong
// console.log('Fetching data dengan token baru...')
const { data: newData, error: newError } = await useAuthFetch('https://api.ui.ac.id/my/ac/st')
// const { data: newData, error: newError } = await useAuthFetch('https://api.ui.ac.id/my/ac/st')
data.value = newData.value || null
error.value = newError.value || null
})
// data.value = newData.value || null
// error.value = newError.value || null
// })
</script>
<template>
......
<script setup lang="ts">
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import authLoginBg from '/assets/images/naput/bg-blue-yellow.png'
import authLoginLogo from '/assets/images/naput/illust-login.png'
......
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import type { VForm } from 'vuetify/components/VForm'
import authV2MaskDark from '@images/pages/mask-v2-dark.png'
import authV2MaskLight from '@images/pages/mask-v2-light.png'
import { useKeycloakStore } from "@/@core/stores/keycloakStore"
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import authLoginLogo from "/assets/images/naput/illust-login.png"
import authLoginBg from "/assets/images/naput/rektorat-bg.png"
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import authLoginLogo from '/assets/images/naput/illust-login.png'
import authLoginBg from '/assets/images/naput/rektorat-bg.png'
const authThemeImg = authLoginLogo
const authThemeImg = authLoginLogo;
const authThemeBg = authLoginBg
const authThemeBg = authLoginBg;
const keycloakStore = useKeycloakStore();
const data = ref<Record<string, any> | null>(null);
const error = ref<Record<string, any> | null>(null);
const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
const error = ref<Record<string, any> | null>(null)
const { signIn, data: sessionData } = useAuth()
......@@ -51,97 +47,92 @@ const credentials = ref({
})
const rememberMe = ref(false)
</script>
<template>
<div class="container">
<div class="left"></div>
<div class="left" />
<div class="right">
<img src="/assets/images/naput/logo-ui-hitam.png" alt="Logo UI" class="logo" />
<img
src="/assets/images/naput/logo-ui-hitam.png"
alt="Logo UI"
class="logo"
>
<VCol
cols="5"
class="text-center"
>
<AuthProvider/>
cols="5"
class="text-center"
>
<AuthProvider />
</VCol>
</div>
</div>
</template>
<style scoped>
.login-button {
margin-top: 20px;
background-color: #FFD700;
padding: 12px 24px;
font-size: 18px;
border: none;
border-radius: 8px;
background-color: #ffd700;
color: white;
cursor: pointer;
font-size: 18px;
font-weight: bold;
margin-block-start: 20px;
padding-block: 12px;
padding-inline: 24px;
transition: background-color 0.2s;
color: white;
}
.container {
display: flex;
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
padding: 0;
margin: 0;
block-size: 100vh;
font-family: Arial, sans-serif;
}
/* Left Side */
.left {
flex: 1;
background: url(/assets/images/naput/rbg.png) no-repeat center center;
background: url("/assets/images/naput/rbg.png") no-repeat center center;
background-size: cover;
}
/* Right Side */
.right {
flex: 1;
background-color: #FDF8E7;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 240px;
justify-content: center;
background-color: #fdf8e7;
gap: 20px; /* Menambah jarak antar elemen */
padding-block-start: 240px;
}
/* Logo */
.logo {
width: 120px;
margin-bottom: 20px;
inline-size: 120px;
margin-block-end: 20px;
}
/* Title */
h1 {
font-size: 24px;
color: #000;
font-size: 24px;
text-align: center;
}
.login-button:hover {
background-color: #E6C200;
background-color: #e6c200;
}
@media (max-width: 768px) {
.container {
justify-content: center;
align-items: center;
background: url(/assets/images/naput/rektorat-ipad-bg.png) no-repeat center center;
justify-content: center;
background: url("/assets/images/naput/rektorat-ipad-bg.png") no-repeat center center;
background-size: cover;
}
......@@ -150,36 +141,33 @@ h1 {
}
.right {
background-color: rgba(253, 248, 231, 0.9);
padding: 40px;
border-radius: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
max-width: 350px;
width: 90%;
height: auto;
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
padding: 40px;
border-radius: 20px;
background-color: rgba(253, 248, 231, 90%);
block-size: auto;
box-shadow: 0 4px 10px rgba(0, 0, 0, 10%);
inline-size: 90%;
max-inline-size: 350px;
}
}
@media (max-width: 480px) {
.container {
background: url(/assets/images/naput/rektorat-iphone-bg.png) no-repeat center center;
background: url("/assets/images/naput/rektorat-iphone-bg.png") no-repeat center center;
background-size: cover;
}
.right {
padding: 30px;
max-width: 320px;
width: 90%;
inline-size: 90%;
max-inline-size: 320px;
}
.logo {
width: 90px;
inline-size: 90px;
}
}
</style>
......@@ -5,7 +5,7 @@ import ImgLight from '@images/dstipro/auth-bg-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import authV2LoginBgDark from '@images/naput/header-bg-dark.png'
import authV2LoginBgLight from '@images/naput/header-bg.png'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight,
......
<script setup lang="ts">
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { themeConfig } from '@themeConfig'
......@@ -7,6 +6,7 @@ import miscMaskDark from '@images/misc/misc-mask-dark.png'
import miscMaskLight from '@images/misc/misc-mask-light.png'
import tree1 from '@images/misc/tree1.png'
import tree3 from '@images/misc/tree3.png'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
......
<script setup lang="ts">
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import { VNodeRenderer } from '@layouts/components/VNodeRenderer'
import { themeConfig } from '@themeConfig'
......@@ -10,6 +9,7 @@ import authV2LoginIllustrationDark from '@images/pages/auth-v2-login-illustratio
import authV2LoginIllustrationLight from '@images/pages/auth-v2-login-illustration-light.png'
import authV2MaskDark from '@images/pages/mask-v2-dark.png'
import authV2MaskLight from '@images/pages/mask-v2-light.png'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeImg = useGenerateImageVariant(
authV2LoginIllustrationLight,
......
......@@ -8,7 +8,7 @@ export default defineNuxtPlugin(async nuxtApp => {
const authenticated = await keycloakInstance.init({
onLoad: 'check-sso',
checkLoginIframe: true,
checkLoginIframeInterval: 5, // seconds
checkLoginIframeInterval: 5,
})
keycloakStore.authenticated = authenticated
......@@ -17,36 +17,22 @@ export default defineNuxtPlugin(async nuxtApp => {
keycloakStore.refresh()
console.log('User is authenticated')
setInterval(async () => {
setInterval(() => {
const now = Math.floor(Date.now() / 1000)
const accessTokenExp = keycloakInstance.tokenParsed?.exp ?? 0
const refreshTokenExp = keycloakInstance.refreshTokenParsed?.exp ?? 0
if (refreshTokenExp <= now) {
console.warn('Refresh token expired. Logging out...')
await keycloakInstance.logout({ redirectUri: `${window.location.origin}/login` })
return
const tokenExp = keycloakInstance.refreshTokenParsed?.exp ?? 0
const tokenParsed = keycloakInstance.tokenParsed?.exp ?? 0
if (tokenExp <= now) {
console.warn('Token expired. Logging out...')
keycloakInstance.logout({
redirectUri: `${window.location.origin}/`,
})
}
// Refresh the token if the access token is close to expiring (e.g., within 60 seconds)
if (accessTokenExp - now < 60) {
try {
const refreshed = await keycloakInstance.updateToken(60)
if (refreshed) {
console.log('Access token refreshed')
keycloakStore.refresh() // update store data if necessary
}
else {
console.log('Access token still valid, no need to refresh')
}
}
catch (err) {
console.error('Failed to refresh token', err)
await keycloakInstance.logout({ redirectUri: `${window.location.origin}/login` })
}
else {
console.log('Token expires in:', tokenParsed - now, 'seconds')
console.log('Token expires in:', tokenExp - now, 'seconds')
}
}, 10_000) // check every 10 seconds
}, 10_000)
}
else {
console.log('User is not authenticated')
......
<script lang="ts" setup>
import type { ProfileHeader } from '@db/dstipro/profile/types'
import profileImg from '@images/avatars/avatar-1.png'
// import coverImg from '@images/pages/user-profile-header-bg.png'
import coverImgLight from '@images/naput/header-bg.png'
import coverImgDark from '@images/naput/header-bg-dark.png'
import coverImgLight from '@images/naput/header-bg.png'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import PersonAvatar from '@/layouts/components/PersonAvatar.vue'
......@@ -16,7 +17,6 @@ const profileHeaderData = ref<ProfileHeader | null>(null)
const coverImg = useGenerateImageVariant(coverImgLight, coverImgDark)
// const userImg = computed(() => profileHeaderData.profileImg || profileImg)
const keycloakStore = useKeycloakStore()
......@@ -45,44 +45,73 @@ const loading = ref(false)
const errordata = ref('')
const isBirthday = ref(false)
const parkirItems = ref(null)
const { api } = useCivitasApi()
async function getData() {
loading.value = true
errordata.value = ''
try {
const apiEndpoints = [
'https://api.ui.ac.id/me',
'https://api.ui.ac.id/my/parkir/aktif',
]
const responses = await Promise.all(apiEndpoints.map(endpoint =>
fetch(endpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
}),
))
for (const response of responses) {
if (!response.ok)
throw new Error('Gagal mengambil data')
}
const { response: profileResponse, error: profileError } = await api.user.getProfile()
const { response: parkirResponse, error: parkirError } = await api.user.getParkirAktif()
// Unwrap actual values using consistent helper
const profileData = unwrapResponseJson(profileResponse)
const parkirData = unwrapResponseJson(parkirResponse)
const [dataku, parkirData] = await Promise.all(responses.map(res => res.json()))
if (profileData)
items.value = profileData
else
throw new Error('Data profil tidak ditemukan')
items.value = dataku
// Assuming parkirData.data is a reactive array
if (Array.isArray(parkirData?.data) && parkirData.data.length === 0)
parkirItems.value = null // Set to null if the array is empty
else
parkirItems.value = parkirData?.data || null // If not empty, set the data, otherwise null
// parkirItems.value = parkirData.data || {} // Store parkir data separately
parkirItems.value = parkirData.data && Object.keys(parkirData.data).length ? parkirData.data : null
console.log(parkirItems.value) // Check the result
}
catch (err) {
errordata.value = err.message || 'Terjadi kesalahan saat mengambil data'
catch (err: any) {
errordata.value = err?.message || 'Terjadi kesalahan saat mengambil data'
console.error('Gagal mengambil data:', errordata.value)
}
finally {
loading.value = false
}
}
function unwrapResponseJson(refObj: any) {
// Check if the object is a Vue ref and unwrap its value if it is
const isRef = (val: any) => val && typeof val === 'object' && val.__v_isRef
let data = refObj
while (isRef(data))
data = data.value
// Try parsing if the result is a string JSON
if (typeof data === 'string') {
try {
return JSON.parse(data)
}
catch {
throw new Error('Format string JSON tidak valid')
}
}
// If it's an object, we return it after removing any potential circular references
if (typeof data === 'object') {
// If data is an empty object or array, return null
if (Object.keys(data).length === 0)
return null
return JSON.parse(JSON.stringify(data)) // Avoid Vue reactivity here if needed
}
// If data is not valid or recognized, throw an error
throw new Error('Data tidak ditemukan atau format tidak dikenali')
}
const daysLeft = computed(() => {
if (!parkirItems.value)
return 0
......@@ -220,14 +249,13 @@ function openDialog() {
>
<div class="d-flex h-0">
<VAvatar
rounded="circle"
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<PersonAvatar style="block-size: 120px; inline-size: 120px;" />
</VAvatar>
rounded="circle"
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<PersonAvatar style="block-size: 120px; inline-size: 120px;" />
</VAvatar>
</div>
<div class="user-profile-info w-100 mt-16 pt-6 pt-sm-0 mt-sm-0">
......@@ -384,19 +412,23 @@ function openDialog() {
</div>
<!-- tombol test aksi ultah -->
<!-- <div class="d-flex align-center gap-x-2">
<!--
<div class="d-flex align-center gap-x-2">
<VBtn @click="isBirthday = true">
<VIcon
size="24"
icon="ri-hand-heart-line"
/>
<VIcon
size="24"
icon="ri-hand-heart-line"
/>
</VBtn>
</div> -->
</div>
-->
</div>
<!-- <VBtn prepend-icon="ri-user-follow-line">
<!--
<VBtn prepend-icon="ri-user-follow-line">
Connected
</VBtn> -->
</VBtn>
-->
<VBtn
class="bg-error"
......@@ -409,80 +441,82 @@ function openDialog() {
</div>
</VCardText>
<!-- <VCardText
<!--
<VCardText
v-else
class="d-flex align-bottom flex-sm-row flex-column justify-center gap-x-6"
>
>
<div class="d-flex h-0">
<VAvatar
rounded
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<VImg
:src="profileHeaderData.profileImg"
height="120"
width="120"
/>
</VAvatar>
<VAvatar
rounded
size="130"
:image="profileHeaderData.profileImg"
class="user-profile-avatar mx-auto"
>
<VImg
:src="profileHeaderData.profileImg"
height="120"
width="120"
/>
</VAvatar>
</div>
<div class="user-profile-info w-100 mt-16 pt-6 pt-sm-0 mt-sm-0">
<h4 class="text-h4 text-center text-sm-start mb-2">
{{ profileHeaderData.fullName }}
</h4>
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-4">
<div class="d-flex flex-wrap justify-center justify-sm-start flex-grow-1 gap-6">
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-palette-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.designation }}
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-map-pin-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.location }}
</div>
</div>
<h4 class="text-h4 text-center text-sm-start mb-2">
{{ profileHeaderData.fullName }}
</h4>
<div class="d-flex align-center justify-center justify-sm-space-between flex-wrap gap-4">
<div class="d-flex flex-wrap justify-center justify-sm-start flex-grow-1 gap-6">
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-palette-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.designation }}
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-calendar-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.joiningDate }}
</div>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-map-pin-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.location }}
</div>
</div>
<VBtn
class="bg-error"
prepend-icon="ri-user-follow-line"
>
Not Connected
</VBtn>
<div class="d-flex align-center gap-x-2">
<VIcon
size="24"
icon="ri-calendar-line"
/>
<div class="text-body-1 font-weight-medium">
{{ profileHeaderData.joiningDate }}
</div>
</div>
</div>
<VBtn
class="bg-warning"
prepend-icon="ri-login-box-line"
@click="login"
>
Login
</VBtn>
</div>
<VBtn
class="bg-error"
prepend-icon="ri-user-follow-line"
>
Not Connected
</VBtn>
<VBtn
class="bg-warning"
prepend-icon="ri-login-box-line"
@click="login"
>
Login
</VBtn>
</div>
</div>
</VCardText> -->
</VCardText>
-->
</VCard>
</template>
......
<script setup lang="ts">
import { useTheme } from 'vuetify'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router' // Import Vue Router
import keycloakInstance from '@/keycloak'
const router = useRouter() // Inisialisasi router
function login() {
keycloakInstance.login({
redirectUri: `${window.location.origin}/profile`,
})
}
function loginGoogle() {
keycloakInstance.login({
redirectUri: `${window.location.origin}/profile`, idpHint: 'google',
})
}
// Cek apakah user sudah login saat komponen dimuat
onMounted(() => {
if (keycloakInstance.authenticated)
router.push('/profile')
})
const { global } = useTheme()
const authProviders = [
{
icon: 'bxl-facebook',
color: '#4267b2',
colorInDark: '#4267b2',
},
{
icon: 'bxl-twitter',
color: '#1da1f2',
colorInDark: '#1da1f2',
},
{
icon: 'bxl-github',
color: '#272727',
colorInDark: '#fff',
},
{
icon: 'bxl-google',
color: '#db4437',
colorInDark: '#db4437',
},
]
</script>
<template>
<VBtn
v-for="link in authProviders"
:key="link.icon"
:icon="link.icon"
variant="text"
size="small"
:color="global.name.value === 'dark' ? link.colorInDark : link.color"
/>
<VBtn
color="warning"
class="sso-btn font-weight-medium"
color="#3d3d3d"
prepend-icon="ri-key-fill"
block
type="submit"
rounded="xl"
@click="login"
>
<VIcon
start
icon="ri-login-box-line"
/> Login SSO
Single Sign On
</VBtn>
<VBtn
class="google-btn font-weight-medium"
color="#3d3d3d"
prepend-icon="ri-google-fill"
block
type="submit"
rounded="xl"
variant="outlined"
style="margin-block-start: 5px;"
@click="loginGoogle"
>
Sign in with Google
</VBtn>
</template>
<style scoped>
/* Use :deep() correctly to apply styles to the Vuetify button */
.v-btn.sso-btn {
background-color: #535666 !important; /* semi-transparent dark */
transition: background-color 0.3s ease, transform 0.2s ease;
}
.v-btn.sso-btn:hover {
background-color: #3d3e50 !important;
transform: translateY(-2px);
}
/* Google button */
.v-btn.google-btn {
border: 2px solid #a3a3a3;
background-color: rgba(255, 255, 255, 80%) !important;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.v-btn.google-btn:hover {
background-color: rgba(61, 61, 61, 100%);
transform: translateY(-2px);
}
</style>
......@@ -92,6 +92,8 @@ function toBase64(str: string) {
return btoa(unescape(encodeURIComponent(str)))
}
const { api } = useCivitasApi()
// Set Password
async function setPassword() {
isSubmitting.value = true
......@@ -135,16 +137,18 @@ async function setPassword() {
try {
// Panggil API untuk mengganti password
const apiEndpoint = 'https://api.ui.ac.id/my/pw'
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${keycloakStore.accessToken}`,
},
body: JSON.stringify(requestData),
})
// const apiEndpoint = 'https://api.ui.ac.id/my/pw'
// const response = await fetch(apiEndpoint, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': `Bearer ${keycloakStore.accessToken}`,
// },
// body: JSON.stringify(requestData),
// })
const { response } = await api.user.changePassword(requestData)
// Check for HTTP status code 401 which indicates old password is incorrect
if (response.status === 401)
......
<script lang="ts" setup>
import { useKeycloakStore } from "@core/stores/keycloakStore";
import type { ProfileTab } from '@db/dstipro/profile/types';
import { useKeycloakStore } from '@core/stores/keycloakStore'
import type { ProfileTab } from '@db/dstipro/profile/types'
// State management
const props = defineProps<Props>()
const keycloakStore = useKeycloakStore()
const keycloakStore = useKeycloakStore();
// Gunakan computed agar selalu reaktif
const isAuthenticated = computed(() => keycloakStore.authenticated);
const isAuthenticated = computed(() => keycloakStore.authenticated)
// const isAuthenticated = keycloakStore.authenticated;
interface Props {
data: ProfileTab
}
// State management
const props = defineProps<Props>();
const items = ref<any[]>([]);
const loading = ref(false);
const error = ref('');
const items = ref<any[]>([])
const loading = ref(false)
// const error = ref('')
const showFull = ref({
ssouser: false,
nim: false,
......@@ -24,39 +28,64 @@ const showFull = ref({
mobile: false,
emailui: false,
emaillain: false,
});
})
const isTooltipKontakVisible = ref(false)
const { api } = useCivitasApi()
const error = ref('')
async function getData() {
loading.value = true;
error.value = '';
loading.value = true
error.value = ''
try {
const apiEndpoint = `https://api.ui.ac.id/me`;
const response = await fetch(apiEndpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
const { response } = await api.user.getProfile()
if (!response.ok) throw new Error('Gagal mengambil data');
const dataku = await response.json();
items.value = dataku;
const actualData = unwrapResponseJson(response)
} catch (err: any) {
error.value = err.message || 'Terjadi kesalahan saat mengambil data';
} finally {
loading.value = false;
Object.assign(items.value, actualData)
}
catch (err: any) {
error.value = err?.message || 'Terjadi kesalahan saat mengambil data'
console.error('Gagal mengambil data:', error.value)
}
finally {
loading.value = false
}
}
function unwrapResponseJson(refObj: any) {
const isRef = (val: any) => val && typeof val === 'object' && val.__v_isRef
let data = refObj
while (isRef(data))
data = data.value
// Coba parse jika hasil akhirnya adalah string JSON
if (typeof data === 'string') {
try {
return JSON.parse(data)
}
catch {
throw new Error('Format string JSON tidak valid')
}
}
if (typeof data === 'object')
return JSON.parse(JSON.stringify(data))
throw new Error('Data tidak ditemukan atau format tidak dikenali')
}
// Fetch data from API
onMounted(() => {
getData();
});
getData()
})
// Fungsi untuk menyembunyikan sebagian data
function maskData(isidata: string, visibleChars = 3): string {
return isidata.slice(0, visibleChars) + '*'.repeat(Math.max(0, isidata.length - visibleChars));
return isidata.slice(0, visibleChars) + '*'.repeat(Math.max(0, isidata.length - visibleChars))
}
</script>
......@@ -66,122 +95,243 @@ function maskData(isidata: string, visibleChars = 3): string {
<div class="text-body-2 text-disabled mb-3">
TENTANG
</div>
<div v-if="isAuthenticated && keycloakStore.civitas !== 'mahasiswa'" class="d-flex flex-column gap-y-2">
<div
v-if="isAuthenticated && keycloakStore.civitas !== 'mahasiswa'"
class="d-flex flex-column gap-y-2"
>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-pass-valid-line" size="24" />
<div class="font-weight-medium">NIP:</div>
<div class="text-truncate">{{ showFull.nip ? items?.nip : maskData(items?.nip ?? '') }}
<VIcon :icon="showFull.nip ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.nip = !showFull.nip" />
<VIcon
icon="ri-pass-valid-line"
size="24"
/>
<div class="font-weight-medium">
NIP:
</div>
<div class="text-truncate">
{{ showFull.nip ? items?.nip : maskData(items?.nip ?? '') }}
<VIcon
:icon="showFull.nip ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.nip = !showFull.nip"
/>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-user-line" size="24" />
<div class="font-weight-medium">Nama:</div>
<VIcon
icon="ri-user-line"
size="24"
/>
<div class="font-weight-medium">
Nama:
</div>
<div>{{ items.employee_display_name }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-star-line" size="24" />
<div class="font-weight-medium">Username:</div>
<div class="text-truncate">{{ showFull.ssouser ? items?.sso_username : maskData(items?.sso_username ?? '') }}
<VIcon :icon="showFull.ssouser ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.ssouser = !showFull.ssouser" />
<VIcon
icon="ri-star-line"
size="24"
/>
<div class="font-weight-medium">
Username:
</div>
<div class="text-truncate">
{{ showFull.ssouser ? items?.sso_username : maskData(items?.sso_username ?? '') }}
<VIcon
:icon="showFull.ssouser ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.ssouser = !showFull.ssouser"
/>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-pass-valid-line" size="24" />
<div class="font-weight-medium">NIK:</div>
<div>{{ showFull.nik ? items?.nik : maskData(items?.nik ?? '') }}
<VIcon :icon="showFull.nik ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.nik = !showFull.nik" />
<VIcon
icon="ri-pass-valid-line"
size="24"
/>
<div class="font-weight-medium">
NIK:
</div>
<div>
{{ showFull.nik ? items?.nik : maskData(items?.nik ?? '') }}
<VIcon
:icon="showFull.nik ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.nik = !showFull.nik"
/>
</div>
</div>
</div>
<div v-if="isAuthenticated && keycloakStore.civitas == 'mahasiswa'" class="d-flex flex-column gap-y-2">
<div
v-if="isAuthenticated && keycloakStore.civitas == 'mahasiswa'"
class="d-flex flex-column gap-y-2"
>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-pass-valid-line" size="24" />
<div class="font-weight-medium">NIM:</div>
<div class="text-truncate">{{ showFull.nim ? items?.KD_MHS : maskData(items?.KD_MHS ?? '') }}
<VIcon :icon="showFull.nim ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.nim = !showFull.nim" />
<VIcon
icon="ri-pass-valid-line"
size="24"
/>
<div class="font-weight-medium">
NIM:
</div>
<div class="text-truncate">
{{ showFull.nim ? items?.KD_MHS : maskData(items?.KD_MHS ?? '') }}
<VIcon
:icon="showFull.nim ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.nim = !showFull.nim"
/>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-user-line" size="24" />
<div class="font-weight-medium">Nama:</div>
<VIcon
icon="ri-user-line"
size="24"
/>
<div class="font-weight-medium">
Nama:
</div>
<div>{{ items.NM_MHS }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-star-line" size="24" />
<div class="font-weight-medium">Username:</div>
<div class="text-truncate">{{ showFull.ssouser ? items?.USERNAME : maskData(items?.USERNAME ?? '') }}
<VIcon :icon="showFull.ssouser ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.ssouser = !showFull.ssouser" />
<VIcon
icon="ri-star-line"
size="24"
/>
<div class="font-weight-medium">
Username:
</div>
<div class="text-truncate">
{{ showFull.ssouser ? items?.USERNAME : maskData(items?.USERNAME ?? '') }}
<VIcon
:icon="showFull.ssouser ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.ssouser = !showFull.ssouser"
/>
</div>
</div>
</div>
<div v-if="isAuthenticated && keycloakStore.civitas !== 'mahasiswa'" class="d-flex flex-column gap-y-2">
<v-divider class="mt-2"></v-divider>
<div
v-if="isAuthenticated && keycloakStore.civitas !== 'mahasiswa'"
class="d-flex flex-column gap-y-2"
>
<VDivider class="mt-2" />
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-flag-line" size="24" />
<div class="font-weight-medium">Status:</div>
<VIcon
icon="ri-flag-line"
size="24"
/>
<div class="font-weight-medium">
Status:
</div>
<div>{{ items.status }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-flag-line" size="24" />
<div class="font-weight-medium">Status Pegawai:</div>
<VIcon
icon="ri-flag-line"
size="24"
/>
<div class="font-weight-medium">
Status Pegawai:
</div>
<div>{{ items.employment_status }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-flag-line" size="24" />
<div class="font-weight-medium">Kategori:</div>
<VIcon
icon="ri-flag-line"
size="24"
/>
<div class="font-weight-medium">
Kategori:
</div>
<div>{{ items.employee_category }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-id-card-line" size="24" />
<div class="font-weight-medium">Jabatan:</div>
<VIcon
icon="ri-id-card-line"
size="24"
/>
<div class="font-weight-medium">
Jabatan:
</div>
<div>{{ items.jabatan_pekerjaan }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-id-card-line" size="24" />
<div class="font-weight-medium">Unit Kerja:</div>
<VIcon
icon="ri-id-card-line"
size="24"
/>
<div class="font-weight-medium">
Unit Kerja:
</div>
<div>{{ items.organization_unit }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-id-card-line" size="24" />
<div class="font-weight-medium">Tgl Masuk:</div>
<VIcon
icon="ri-id-card-line"
size="24"
/>
<div class="font-weight-medium">
Tgl Masuk:
</div>
<div>{{ items.join_date }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-id-card-line" size="24" />
<div class="font-weight-medium">Tgl Pensiun:</div>
<VIcon
icon="ri-id-card-line"
size="24"
/>
<div class="font-weight-medium">
Tgl Pensiun:
</div>
<div>{{ items.tgl_akhir_pegawai }}</div>
</div>
</div>
<div v-if="isAuthenticated && items.employee_category == 'DOSEN'" class="d-flex flex-column gap-y-2">
<v-divider class="mt-2"></v-divider>
<div
v-if="isAuthenticated && items.employee_category == 'DOSEN'"
class="d-flex flex-column gap-y-2"
>
<VDivider class="mt-2" />
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-sticky-note-line" size="24" />
<div class="font-weight-medium">NIDN:</div>
<VIcon
icon="ri-sticky-note-line"
size="24"
/>
<div class="font-weight-medium">
NIDN:
</div>
<div>{{ items.nidn }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-sticky-note-line" size="24" />
<div class="font-weight-medium">Google Scholar ID:</div>
<VIcon
icon="ri-sticky-note-line"
size="24"
/>
<div class="font-weight-medium">
Google Scholar ID:
</div>
<div>{{ items.google_scholar_id }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-sticky-note-line" size="24" />
<div class="font-weight-medium">Scopus ID:</div>
<VIcon
icon="ri-sticky-note-line"
size="24"
/>
<div class="font-weight-medium">
Scopus ID:
</div>
<div>{{ items.scopus_id }}</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-sticky-note-line" size="24" />
<div class="font-weight-medium">Sinta ID:</div>
<VIcon
icon="ri-sticky-note-line"
size="24"
/>
<div class="font-weight-medium">
Sinta ID:
</div>
<div>{{ items.sinta_id }}</div>
</div>
</div>
......@@ -192,22 +342,37 @@ function maskData(isidata: string, visibleChars = 3): string {
</div>
<div class="d-flex flex-column gap-y-2">
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-shield-star-line" size="24" />
<div class="font-weight-medium">Jabatan:</div>
<VIcon
icon="ri-shield-star-line"
size="24"
/>
<div class="font-weight-medium">
Jabatan:
</div>
<div>
{{ items.posisi_struktural }}
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-shield-star-line" size="24" />
<div class="font-weight-medium">Tgl Mulai:</div>
<VIcon
icon="ri-shield-star-line"
size="24"
/>
<div class="font-weight-medium">
Tgl Mulai:
</div>
<div>
{{ items.tgl_awal_posisi_struktural }}
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-shield-star-line" size="24" />
<div class="font-weight-medium">Tgl Akhir:</div>
<VIcon
icon="ri-shield-star-line"
size="24"
/>
<div class="font-weight-medium">
Tgl Akhir:
</div>
<div>
{{ items.tgl_akhir_posisi_struktural }}
</div>
......@@ -217,65 +382,122 @@ function maskData(isidata: string, visibleChars = 3): string {
<div class="text-body-2 text-disabled mt-6 mb-3">
KONTAK
<VTooltip :model-value="isTooltipKontakVisible" location="top">
<VTooltip
:model-value="isTooltipKontakVisible"
location="top"
>
<template #activator="{ props }">
<VIcon v-bind="props" icon="ri-question-line" color="primary" />
<VIcon
v-bind="props"
icon="ri-question-line"
color="primary"
/>
</template>
<span>Jika ada data yang tidak lengkap <br />harap dilengkapi di {{ (keycloakStore.civitas == 'mahasiswa') ?
'SIAK-NG' :
'HRIS-UI' }}</span>
<span>Jika ada data yang tidak lengkap <br>harap dilengkapi di {{ (keycloakStore.civitas == 'mahasiswa')
? 'SIAK-NG'
: 'HRIS-UI' }}</span>
</VTooltip>
</div>
<div v-if="isAuthenticated && keycloakStore.civitas !== 'mahasiswa'" class="d-flex flex-column gap-y-2">
<div
v-if="isAuthenticated && keycloakStore.civitas !== 'mahasiswa'"
class="d-flex flex-column gap-y-2"
>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-smartphone-line" size="24" />
<div class="font-weight-medium">Mobile:</div>
<div class="text-truncate">{{ showFull.mobile ? items?.phone : maskData(items?.phone ?? '', 3) }}
<VIcon :icon="showFull.mobile ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.mobile = !showFull.mobile" />
<VIcon
icon="ri-smartphone-line"
size="24"
/>
<div class="font-weight-medium">
Mobile:
</div>
<div class="text-truncate">
{{ showFull.mobile ? items?.phone : maskData(items?.phone ?? '', 3) }}
<VIcon
:icon="showFull.mobile ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.mobile = !showFull.mobile"
/>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-mail-open-line" size="24" />
<div class="font-weight-medium">Email UI:</div>
<div class="text-truncate">{{ showFull.emailui ? items?.email : maskData(items?.email ?? '', 3)
<VIcon
icon="ri-mail-open-line"
size="24"
/>
<div class="font-weight-medium">
Email UI:
</div>
<div class="text-truncate">
{{ showFull.emailui ? items?.email : maskData(items?.email ?? '', 3)
}}
<VIcon :icon="showFull.emailui ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.emailui = !showFull.emailui" />
<VIcon
:icon="showFull.emailui ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.emailui = !showFull.emailui"
/>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-mail-open-line" size="24" />
<div class="font-weight-medium">Email Lain:</div>
<div class="text-truncate">{{ showFull.emaillain ? 'nama.anda@gmail.com' : maskData('nama.anda@gmail.com', 3)
<VIcon
icon="ri-mail-open-line"
size="24"
/>
<div class="font-weight-medium">
Email Lain:
</div>
<div class="text-truncate">
{{ showFull.emaillain ? 'nama.anda@gmail.com' : maskData('nama.anda@gmail.com', 3)
}}
<VIcon :icon="showFull.emaillain ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.emaillain = !showFull.emaillain" />
<VIcon
:icon="showFull.emaillain ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.emaillain = !showFull.emaillain"
/>
</div>
</div>
</div>
<div v-if="isAuthenticated && keycloakStore.civitas == 'mahasiswa'" class="d-flex flex-column gap-y-2">
<div
v-if="isAuthenticated && keycloakStore.civitas == 'mahasiswa'"
class="d-flex flex-column gap-y-2"
>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-mail-open-line" size="24" />
<div class="font-weight-medium">Email UI:</div>
<div class="text-truncate">{{ showFull.emailui ? items?.EMAIL_UI : maskData(items?.EMAIL_UI ?? '', 3)
<VIcon
icon="ri-mail-open-line"
size="24"
/>
<div class="font-weight-medium">
Email UI:
</div>
<div class="text-truncate">
{{ showFull.emailui ? items?.EMAIL_UI : maskData(items?.EMAIL_UI ?? '', 3)
}}
<VIcon :icon="showFull.emailui ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.emailui = !showFull.emailui" />
<VIcon
:icon="showFull.emailui ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.emailui = !showFull.emailui"
/>
</div>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon icon="ri-mail-open-line" size="24" />
<div class="font-weight-medium">Email Lain:</div>
<div class="text-truncate">{{ showFull.emaillain ? 'nama.anda@gmail.com' : maskData('nama.anda@gmail.com', 3)
<VIcon
icon="ri-mail-open-line"
size="24"
/>
<div class="font-weight-medium">
Email Lain:
</div>
<div class="text-truncate">
{{ showFull.emaillain ? 'nama.anda@gmail.com' : maskData('nama.anda@gmail.com', 3)
}}
<VIcon :icon="showFull.emaillain ? 'ri-eye-off-line' : 'ri-eye-line'" class="cursor-pointer"
@click="showFull.emaillain = !showFull.emaillain" />
<VIcon
:icon="showFull.emaillain ? 'ri-eye-off-line' : 'ri-eye-line'"
class="cursor-pointer"
@click="showFull.emaillain = !showFull.emaillain"
/>
</div>
</div>
</div>
</VCardText>
</VCard>
</template>
......
......@@ -8,7 +8,8 @@ const isAuthenticated = computed(() => keycloakStore.authenticated)
const items = ref<any[]>([])
const expandedRows = ref<any[]>([])
const loading = ref(false)
const error = ref('')
// const error = ref('')
const searchQuery = ref('')
// Headers
......@@ -39,21 +40,23 @@ function resolveStatusColor(status: string) {
}
}
const { api } = useCivitasApi()
async function getData() {
loading.value = true
error.value = ''
items.value = []
try {
const res = await fetch('https://api.ui.ac.id/my/ac', {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
})
// const response = await fetch('https://api.ui.ac.id/my/ac', {
// headers: {
// Authorization: `Bearer ${keycloakStore.accessToken}`,
// },
// })
const { response, error } = await api.user.getProfile()
if (!res.ok)
throw new Error('Gagal mengambil data')
const raw = await res.json()
const raw = response.value?.value // ✅ UNWRAP the inner ref
items.value = raw.map((item: any, idx: number) => {
const expandedFiltered = Object.values(item.IRS || {})
......@@ -75,7 +78,7 @@ async function getData() {
}).filter(item => item.expanded.length > 0) // Optionally remove parent rows with no expanded items
}
catch (err: any) {
error.value = err.message || 'Terjadi kesalahan saat mengambil data'
err.value = err.message || 'Terjadi kesalahan saat mengambil data'
}
finally {
loading.value = false
......
<script setup lang="ts">
import { useKeycloakStore } from '@/@core/stores/keycloakStore';
import type { SalesDetails } from '@db/pages/datatable/types';
import type { SalesDetails } from '@db/pages/datatable/types'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore()
const { api } = useCivitasApi()
const keycloakStore = useKeycloakStore();
// Gunakan computed agar selalu reaktif
const isAuthenticated = computed(() => keycloakStore.authenticated);
const isAuthenticated = computed(() => keycloakStore.authenticated)
const { data: productList } = await useApi<SalesDetails[]>('pages/datatable')
......@@ -83,108 +85,110 @@ const headersapi: Header[] = [
{ title: 'Nama', key: 'nama', sortable: true },
{ title: 'Kode Fakultas', key: 'org', sortable: true },
{ title: 'Aksi', key: 'operation', align: 'start', sortable: false },
];
const items = ref<JsonDosen[]>([]);
const searchText = ref('');
const loading = ref(false);
const error = ref('');
const dialogView = ref(false);
const dialogEdit = ref(false);
const dialogDelete = ref(false);
const selectedDosen = ref<JsonDosen | null>(null);
const dialogSetPassword = ref(false);
const newPassword = ref('');
const confirmPassword = ref('');
const isPasswordNewVisible = ref(false);
const isPasswordKonfVisible = ref(false);
]
const items = ref<JsonDosen[]>([])
const searchText = ref('')
const loading = ref(false)
const dialogView = ref(false)
const dialogEdit = ref(false)
const dialogDelete = ref(false)
const selectedDosen = ref<JsonDosen | null>(null)
const dialogSetPassword = ref(false)
const newPassword = ref('')
const confirmPassword = ref('')
const isPasswordNewVisible = ref(false)
const isPasswordKonfVisible = ref(false)
// Interface
interface JsonDosen {
nip: string;
nama: string;
org: string;
nip: string
nama: string
org: string
}
// Filter Data Berdasarkan Pencarian
const filteredItems = computed(() => {
if (!searchText.value) return items.value;
const query = searchText.value.toLowerCase();
return items.value.filter((item) =>
[item.nip, item.nama].some((field) =>
field.toLowerCase().includes(query)
)
);
});
if (!searchText.value)
return items.value
const query = searchText.value.toLowerCase()
return items.value.filter(item =>
[item.nip, item.nama].some(field =>
field.toLowerCase().includes(query),
),
)
})
// Ambil Data dari API
onMounted(() => {
getData();
});
getData()
})
async function getData() {
loading.value = true;
error.value = '';
loading.value = true
error.value = ''
try {
const response = await fetch('https://api.ui.ac.id/listdosen', {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
if (!response.ok) throw new Error('Gagal mengambil data dosen');
const data = await response.json();
items.value = data;
} catch (err: any) {
error.value = err.message || 'Terjadi kesalahan';
} finally {
loading.value = false;
const { response, error } = await api.dosen.getListDosen()
const data = response.value?.value // ✅ UNWRAP the inner ref
items.value = data
}
catch (err: any) {
err.value = err.message || 'Terjadi kesalahan'
}
finally {
loading.value = false
}
}
// Buka Dialog View
function viewDosen(dosen: JsonDosen) {
selectedDosen.value = dosen;
dialogView.value = true;
selectedDosen.value = dosen
dialogView.value = true
}
// Buka Dialog Edit
function editDosen(dosen: JsonDosen) {
selectedDosen.value = { ...dosen }; // Buat salinan data dosen
dialogEdit.value = true;
selectedDosen.value = { ...dosen } // Buat salinan data dosen
dialogEdit.value = true
}
// Simpan Perubahan
function saveChanges() {
const updatedIndex = items.value.findIndex(
(item) => item.nip === selectedDosen.value?.nip
);
if (updatedIndex !== -1) {
items.value[updatedIndex] = { ...selectedDosen.value };
}
dialogEdit.value = false;
item => item.nip === selectedDosen.value?.nip,
)
if (updatedIndex !== -1)
items.value[updatedIndex] = { ...selectedDosen.value }
dialogEdit.value = false
}
// Buka Dialog Delete
function confirmDelete(dosen: JsonDosen) {
selectedDosen.value = dosen;
dialogDelete.value = true;
selectedDosen.value = dosen
dialogDelete.value = true
}
// Hapus Data
function deleteDosen() {
const deleteIndex = items.value.findIndex(
(item) => item.nip === selectedDosen.value?.nip
);
if (deleteIndex !== -1) {
items.value.splice(deleteIndex, 1);
}
dialogDelete.value = false;
item => item.nip === selectedDosen.value?.nip,
)
if (deleteIndex !== -1)
items.value.splice(deleteIndex, 1)
dialogDelete.value = false
}
// Buka Dialog Password
function viewSetPassword(dosen: JsonDosen) {
selectedDosen.value = dosen;
dialogSetPassword.value = true;
selectedDosen.value = dosen
dialogSetPassword.value = true
}
const passwordRequirements = [
......@@ -202,19 +206,19 @@ const passwordRules = [
(v: string) => /[A-Z]/.test(v) || 'Password must contain at least one uppercase letter',
(v: string) => /\d/.test(v) || 'Password must contain at least one number',
(v: string) => /[ !"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]/.test(v) || 'Password must contain at least one symbol or whitespace',
];
]
const confirmPasswordRules = [
(v: string) => !!v || 'Confirm password is required',
(v: string) => v === newPassword.value || 'Passwords do not match',
];
]
// Generate Password
function generatePassword(length: number = 10): string {
const lowercase = "abcdefghijklmnopqrstuvwxyz";
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const numbers = "0123456789";
const symbols = ` !"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`;
const lowercase = 'abcdefghijklmnopqrstuvwxyz'
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const numbers = '0123456789'
const symbols = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
// At least one character from each required category
const mandatoryCharacters = [
......@@ -222,46 +226,49 @@ function generatePassword(length: number = 10): string {
uppercase[Math.floor(Math.random() * uppercase.length)],
numbers[Math.floor(Math.random() * numbers.length)],
symbols[Math.floor(Math.random() * symbols.length)],
];
]
// Combine all character sets into one charset
const allCharacters = lowercase + uppercase + numbers + symbols;
const remainingLength = length - mandatoryCharacters.length;
let password = mandatoryCharacters;
const allCharacters = lowercase + uppercase + numbers + symbols
const remainingLength = length - mandatoryCharacters.length
let password = mandatoryCharacters
// Fill the rest of the password with random characters from the combined charset
for (let i = 0; i < remainingLength; i++) {
const randomChar = allCharacters[Math.floor(Math.random() * allCharacters.length)];
password.push(randomChar);
const randomChar = allCharacters[Math.floor(Math.random() * allCharacters.length)]
password.push(randomChar)
}
// Shuffle the password to ensure randomness
password = password.sort(() => Math.random() - 0.5);
password = password.sort(() => Math.random() - 0.5)
// Return the password as a string
return password.join('');
return password.join('')
}
// Set Password
function setPassword() {
if (newPassword.value.length < 8 || !/[a-z]/.test(newPassword.value) || !/\d/.test(newPassword.value) || !/[ !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(newPassword.value)) {
alert("Password harus memiliki minimal 8 karakter, setidaknya satu huruf kecil, satu angka, dan satu karakter khusus.");
return;
alert('Password harus memiliki minimal 8 karakter, setidaknya satu huruf kecil, satu angka, dan satu karakter khusus.')
return
}
if (newPassword.value !== confirmPassword.value) {
alert("Password dan konfirmasi password tidak cocok.");
return;
alert('Password dan konfirmasi password tidak cocok.')
return
}
// Simulasi pengubahan password
// Di sini Anda bisa menambahkan logika untuk mengupdate password ke server
alert(`Password atas nama ${selectedDosen.value?.nama} sudah diubah`);
alert(`Password atas nama ${selectedDosen.value?.nama} sudah diubah`)
// Reset input
newPassword.value = '';
confirmPassword.value = '';
dialogSetPassword.value = false;
newPassword.value = ''
confirmPassword.value = ''
dialogSetPassword.value = false
}
</script>
......@@ -269,21 +276,45 @@ function setPassword() {
<div v-if="isAuthenticated">
<VCardText>
<VRow>
<VCol cols="12" offset-md="8" md="4" sm="12">
<VTextField v-model="searchText" label="Search" placeholder="Search ..." append-inner-icon="ri-search-line"
clearable @click:clear="searchText = ''" hide-details dense outlined />
<VCol
cols="12"
offset-md="8"
md="4"
sm="12"
>
<VTextField
v-model="searchText"
label="Search"
placeholder="Search ..."
append-inner-icon="ri-search-line"
clearable
hide-details
dense
outlined
@click:clear="searchText = ''"
/>
</VCol>
</VRow>
</VCardText>
<!-- 👉 Data Table API -->
<VDataTable :headers="headersapi" :items="filteredItems || []" :search="searchText" :items-per-page="10"
class="text-no-wrap">
<VDataTable
:headers="headersapi"
:items="filteredItems || []"
:search="searchText"
:items-per-page="10"
class="text-no-wrap"
>
<!-- Foto -->
<template #item.foto="{ item }">
<div class="d-flex align-center">
<div>
<VImg rounded="circle" :src="`https://api.ui.ac.id/public/photo/${item.nip}.jpg`" height="40" width="40" />
<VImg
rounded="circle"
:src="`https://api.ui.ac.id/public/photo/${item.nip}.jpg`"
height="40"
width="40"
/>
</div>
</div>
</template>
......@@ -291,7 +322,10 @@ function setPassword() {
<!-- Nama -->
<template #item.nama="{ item }">
<div class="d-flex align-center">
<VIcon size="18" class="bg-success rounded-0">
<VIcon
size="18"
class="bg-success rounded-0"
>
ri-user-follow-fill
</VIcon> &nbsp;
<span class="d-block font-weight-medium text-truncate text-high-emphasis">{{ item.nama }}</span>
......@@ -301,16 +335,36 @@ function setPassword() {
<!-- Aksi -->
<template #item.operation="{ item }">
<div class="d-flex align-center">
<VBtn color="primary" size="small" icon @click="viewSetPassword(item)">
<VBtn
color="primary"
size="small"
icon
@click="viewSetPassword(item)"
>
<VIcon icon="ri-lock-line" />
</VBtn>
<VBtn color="info" size="small" icon @click="viewDosen(item)">
<VBtn
color="info"
size="small"
icon
@click="viewDosen(item)"
>
<VIcon icon="ri-eye-line" />
</VBtn>
<VBtn color="warning" size="small" icon @click="editDosen(item)">
<VBtn
color="warning"
size="small"
icon
@click="editDosen(item)"
>
<VIcon icon="ri-pencil-line" />
</VBtn>
<VBtn color="error" size="small" icon @click="confirmDelete(item)">
<VBtn
color="error"
size="small"
icon
@click="confirmDelete(item)"
>
<VIcon icon="ri-delete-bin-line" />
</VBtn>
</div>
......@@ -318,17 +372,34 @@ function setPassword() {
</VDataTable>
<!-- Dialog untuk Set Password -->
<VDialog v-model="dialogSetPassword" persistent :max-width="$vuetify.display.smAndDown ? 'auto' : 400">
<DialogCloseBtn variant="text" size="default"
@click="dialogSetPassword = false; newPassword = ''; confirmPassword = '';" />
<VDialog
v-model="dialogSetPassword"
persistent
:max-width="$vuetify.display.smAndDown ? 'auto' : 400"
>
<DialogCloseBtn
variant="text"
size="default"
@click="dialogSetPassword = false; newPassword = ''; confirmPassword = '';"
/>
<VCard class="pa-sm-5 pa-3">
<VCardTitle class="text-center pb-5 text-h4">Set Password</VCardTitle>
<VCardTitle class="text-center pb-5 text-h4">
Set Password
</VCardTitle>
<VCardText>
<VRow>
<VCol cols="12" md="12" class="mb-n3">
<VAlert color="primary" variant="tonal" class="mb-3">
<VCol
cols="12"
md="12"
class="mb-n3"
>
<VAlert
color="primary"
variant="tonal"
class="mb-3"
>
<p class="mb-0">
<strong>NIP:</strong> {{ selectedDosen?.nip }}
</p>
......@@ -337,20 +408,36 @@ function setPassword() {
</p>
</VAlert>
<VTextField v-model="newPassword" label="Password Baru" :maxlength="20"
<VTextField
v-model="newPassword"
label="Password Baru"
:maxlength="20"
:type="isPasswordNewVisible ? 'text' : 'password'"
:append-inner-icon="isPasswordNewVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
@click:append-inner="isPasswordNewVisible = !isPasswordNewVisible" :rules="passwordRules" clearable
outlined dense />
:rules="passwordRules"
clearable
outlined
dense
@click:append-inner="isPasswordNewVisible = !isPasswordNewVisible"
/>
</VCol>
<VCol cols="12" md="12">
<VTextField v-model="confirmPassword" label="Konfirmasi Password" :maxlength="20"
<VCol
cols="12"
md="12"
>
<VTextField
v-model="confirmPassword"
label="Konfirmasi Password"
:maxlength="20"
:type="isPasswordKonfVisible ? 'text' : 'password'"
:append-inner-icon="isPasswordKonfVisible ? 'ri-eye-off-line' : 'ri-eye-line'"
@click:append-inner="isPasswordKonfVisible = !isPasswordKonfVisible" :rules="confirmPasswordRules"
clearable outlined dense />
:rules="confirmPasswordRules"
clearable
outlined
dense
@click:append-inner="isPasswordKonfVisible = !isPasswordKonfVisible"
/>
</VCol>
</VRow>
<VRow class="pl-3">
<h6 class="text-h6 text-body-2 mt-1">
......@@ -358,10 +445,17 @@ function setPassword() {
</h6>
<VList>
<VListItem v-for="(item, index) in passwordRequirements" :key="index" class="px-0 mt-n4 mb-n2">
<VListItem
v-for="(item, index) in passwordRequirements"
:key="index"
class="px-0 mt-n4 mb-n2"
>
<template #prepend>
<VIcon size="8" icon="ri-circle-fill"
color="rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))" />
<VIcon
size="8"
icon="ri-circle-fill"
color="rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))"
/>
</template>
<VListItemTitle class="text-body-2 text-wrap">
{{ item }}
......@@ -372,123 +466,273 @@ function setPassword() {
</VCardText>
<VCardActions class="ma-n3">
<VCol cols="12" class="d-flex flex-wrap justify-center gap-2">
<VBtn class="bg-warning" color="white" variant="tonal" @click="setPassword">Save</VBtn>
<VBtn color="primary" variant="outlined"
@click="dialogSetPassword = false; newPassword = ''; confirmPassword = '';">Cancel</VBtn>
<VBtn color="secondary" variant="outlined"
@click="newPassword = generatePassword(); confirmPassword = newPassword">Generate Password</VBtn>
<VCol
cols="12"
class="d-flex flex-wrap justify-center gap-2"
>
<VBtn
class="bg-warning"
color="white"
variant="tonal"
@click="setPassword"
>
Save
</VBtn>
<VBtn
color="primary"
variant="outlined"
@click="dialogSetPassword = false; newPassword = ''; confirmPassword = '';"
>
Cancel
</VBtn>
<VBtn
color="secondary"
variant="outlined"
@click="newPassword = generatePassword(); confirmPassword = newPassword"
>
Generate Password
</VBtn>
</VCol>
</VCardActions>
</VCard>
</VDialog>
<!-- Dialog untuk Detail -->
<VDialog v-model="dialogView" persistent :max-width="$vuetify.display.smAndDown ? 'auto' : 600">
<VDialog
v-model="dialogView"
persistent
:max-width="$vuetify.display.smAndDown ? 'auto' : 600"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn variant="text" size="default" @click="dialogView = false" />
<DialogCloseBtn
variant="text"
size="default"
@click="dialogView = false"
/>
<VCard class="pa-sm-5 pa-3">
<VCardTitle class="text-center pb-5 text-h4">Detail Dosen</VCardTitle>
<VCardTitle class="text-center pb-5 text-h4">
Detail Dosen
</VCardTitle>
<VCardText>
<div class="d-flex align-center">
<VImg rounded="circle" v-if="selectedDosen"
:src="`https://api.ui.ac.id/public/photo/${selectedDosen.nip}.jpg`" height="150" width="150"
class="mb-4" />
<VImg
v-if="selectedDosen"
rounded="circle"
:src="`https://api.ui.ac.id/public/photo/${selectedDosen.nip}.jpg`"
height="150"
width="150"
class="mb-4"
/>
</div>
<VAlert color="primary" variant="tonal">
<p class="mb-0"><strong>NIP:</strong> {{ selectedDosen?.nip }}</p>
<p class="mb-0"><strong>Nama:</strong> {{ selectedDosen?.nama }}</p>
<p class="mb-0"><strong>Kode Fakultas:</strong> {{ selectedDosen?.org }}</p>
<VAlert
color="primary"
variant="tonal"
>
<p class="mb-0">
<strong>NIP:</strong> {{ selectedDosen?.nip }}
</p>
<p class="mb-0">
<strong>Nama:</strong> {{ selectedDosen?.nama }}
</p>
<p class="mb-0">
<strong>Kode Fakultas:</strong> {{ selectedDosen?.org }}
</p>
</VAlert>
</VCardText>
<VCardActions class="ma-n3">
<!-- <VSpacer /> -->
<!-- 👉 Close -->
<VCol cols="12" class="d-flex flex-wrap justify-center gap-2">
<VBtn class="bg-info" color="white" variant="tonal" @click="dialogView = false">Close</VBtn>
<VCol
cols="12"
class="d-flex flex-wrap justify-center gap-2"
>
<VBtn
class="bg-info"
color="white"
variant="tonal"
@click="dialogView = false"
>
Close
</VBtn>
</VCol>
</VCardActions>
</VCard>
</VDialog>
<!-- Dialog untuk Edit -->
<VDialog v-model="dialogEdit" persistent :max-width="$vuetify.display.smAndDown ? 'auto' : 600">
<VDialog
v-model="dialogEdit"
persistent
:max-width="$vuetify.display.smAndDown ? 'auto' : 600"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn variant="text" size="default" @click="dialogEdit = false" />
<DialogCloseBtn
variant="text"
size="default"
@click="dialogEdit = false"
/>
<VCard class="pa-sm-5 pa-3">
<VCardTitle class="text-center pb-5 text-h4">Edit Dosen</VCardTitle>
<VCardTitle class="text-center pb-5 text-h4">
Edit Dosen
</VCardTitle>
<VCardText>
<VRow>
<VCol cols="12" md="12">
<VTextField v-model="selectedDosen.nama" label="Nama" outlined dense />
<VCol
cols="12"
md="12"
>
<VTextField
v-model="selectedDosen.nama"
label="Nama"
outlined
dense
/>
</VCol>
<VCol cols="12" md="12">
<VTextField v-model="selectedDosen.org" label="Kode Fakultas" outlined dense />
<VCol
cols="12"
md="12"
>
<VTextField
v-model="selectedDosen.org"
label="Kode Fakultas"
outlined
dense
/>
</VCol>
</VRow>
</VCardText>
<VCardActions class="ma-n3">
<!-- <VSpacer /> -->
<VCol cols="12" class="d-flex flex-wrap justify-center gap-2">
<VBtn class="bg-warning" color="white" variant="tonal" @click="saveChanges">Save</VBtn>
<VBtn color="primary" variant="outlined" @click="dialogEdit = false">Cancel</VBtn>
<VCol
cols="12"
class="d-flex flex-wrap justify-center gap-2"
>
<VBtn
class="bg-warning"
color="white"
variant="tonal"
@click="saveChanges"
>
Save
</VBtn>
<VBtn
color="primary"
variant="outlined"
@click="dialogEdit = false"
>
Cancel
</VBtn>
</VCol>
</VCardActions>
</VCard>
</VDialog>
<!-- Dialog untuk Konfirmasi Delete -->
<VDialog v-model="dialogDelete" persistent :max-width="$vuetify.display.smAndDown ? 'auto' : 400">
<VDialog
v-model="dialogDelete"
persistent
:max-width="$vuetify.display.smAndDown ? 'auto' : 400"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn variant="text" size="default" @click="dialogDelete = false" />
<DialogCloseBtn
variant="text"
size="default"
@click="dialogDelete = false"
/>
<VCard class="pa-sm-5 pa-3">
<VCardTitle class="text-center pb-5 text-h4">Konfirmasi Hapus</VCardTitle>
<VCardTitle class="text-center pb-5 text-h4">
Konfirmasi Hapus
</VCardTitle>
<VCardText class="text-justify">
<p>Apakah Anda yakin ingin menghapus dosen dengan nama "{{ selectedDosen?.nama }}"?</p>
</VCardText>
<VCardActions class="ma-n3">
<!-- <VSpacer /> -->
<VCol cols="12" class="d-flex flex-wrap justify-center gap-2">
<VBtn class="bg-error" color="white" variant="tonal" @click="deleteDosen">Hapus</VBtn>
<VBtn color="primary" variant="outlined" @click="dialogDelete = false">Batal</VBtn>
<VCol
cols="12"
class="d-flex flex-wrap justify-center gap-2"
>
<VBtn
class="bg-error"
color="white"
variant="tonal"
@click="deleteDosen"
>
Hapus
</VBtn>
<VBtn
color="primary"
variant="outlined"
@click="dialogDelete = false"
>
Batal
</VBtn>
</VCol>
</VCardActions>
</VCard>
</VDialog>
</div>
<div v-else>
<VCardText>
<VRow>
<VCol cols="12" md="7" sm="12">
<VBtn v-if="isAuthenticated == false" class="bg-error ml-2 mr-2" prepend-icon="ri-user-follow-line">
<VCol
cols="12"
md="7"
sm="12"
>
<VBtn
v-if="isAuthenticated == false"
class="bg-error ml-2 mr-2"
prepend-icon="ri-user-follow-line"
>
Not Connected
</VBtn>
</VCol>
<VCol cols="12" md="5" sm="12">
<VTextField v-model="search" label="Search" placeholder="Search ..." append-inner-icon="ri-search-line"
single-line hide-details dense outlined />
<VCol
cols="12"
md="5"
sm="12"
>
<VTextField
v-model="search"
label="Search"
placeholder="Search ..."
append-inner-icon="ri-search-line"
single-line
hide-details
dense
outlined
/>
</VCol>
</VRow>
</VCardText>
<!-- 👉 Data Table -->
<VDataTable :headers="headers" :items="productList || []" :search="search" :items-per-page="10"
class="text-no-wrap">
<VDataTable
:headers="headers"
:items="productList || []"
:search="search"
:items-per-page="10"
class="text-no-wrap"
>
<!-- product -->
<template #item.product.name="{ item }">
<div class="d-flex align-center">
<div>
<VImg :src="item.product.image" height="40" width="40" />
<VImg
:src="item.product.image"
height="40"
width="40"
/>
</div>
<div class="d-flex flex-column ms-3">
<span class="d-block font-weight-medium text-truncate text-high-emphasis">{{ item.product.name }}</span>
......@@ -500,9 +744,18 @@ function setPassword() {
<!-- category -->
<template #item.product.category="{ item }">
<div class="d-flex align-center">
<VAvatar v-for="(category, index) in categoryIconFilter(item.product.category)" :key="index" size="26"
:color="category.color" variant="tonal">
<VIcon size="18" :color="category.color" class="rounded-0">
<VAvatar
v-for="(category, index) in categoryIconFilter(item.product.category)"
:key="index"
size="26"
:color="category.color"
variant="tonal"
>
<VIcon
size="18"
:color="category.color"
class="rounded-0"
>
{{ category.icon }}
</VIcon>
</VAvatar>
......@@ -513,10 +766,19 @@ function setPassword() {
<!-- buyer -->
<template #item.buyer.name="{ item }">
<div class="d-flex align-center">
<VAvatar size="1.875rem" :color="!item.buyer.avatar ? 'primary' : undefined"
:variant="!item.buyer.avatar ? 'tonal' : undefined">
<VImg v-if="item.buyer.avatar" :src="item.buyer.avatar" />
<span v-else class="text-sm">{{ item.buyer.name.slice(0, 2).toUpperCase() }}</span>
<VAvatar
size="1.875rem"
:color="!item.buyer.avatar ? 'primary' : undefined"
:variant="!item.buyer.avatar ? 'tonal' : undefined"
>
<VImg
v-if="item.buyer.avatar"
:src="item.buyer.avatar"
/>
<span
v-else
class="text-sm"
>{{ item.buyer.name.slice(0, 2).toUpperCase() }}</span>
</VAvatar>
<span class="text-no-wrap font-weight-medium text-high-emphasis ms-2">{{ item.buyer.name }}</span>
</div>
......@@ -535,15 +797,22 @@ function setPassword() {
<!-- Status -->
<template #item.status="{ item }">
<VChip :color="resolveStatusColor(item.payment.status)"
:class="`text-${resolveStatusColor(item.payment.status)}`" size="small" class="font-weight-medium">
<VChip
:color="resolveStatusColor(item.payment.status)"
:class="`text-${resolveStatusColor(item.payment.status)}`"
size="small"
class="font-weight-medium"
>
{{ item.payment.status }}
</VChip>
</template>
<!-- Delete -->
<template #item.delete="{ item }">
<IconBtn size="small" @click="deleteItem(item.product.id)">
<IconBtn
size="small"
@click="deleteItem(item.product.id)"
>
<VIcon icon="ri-delete-bin-line" />
</IconBtn>
</template>
......
<script setup lang="ts">
import { useKeycloakStore } from '@/@core/stores/keycloakStore';
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
const props = defineProps<Props>()
const { api } = useCivitasApi()
const keycloakStore = useKeycloakStore()
const keycloakStore = useKeycloakStore();
// Gunakan computed agar selalu reaktif
const isAuthenticated = computed(() => keycloakStore.authenticated);
const isAuthenticated = computed(() => keycloakStore.authenticated)
interface Props {
searchQuery: string
}
const props = defineProps<Props>()
// List NIP Staf DSTI
const listStaf = [
{ nama: 'Agus Mulyana', nip: '141113024' },
......@@ -22,33 +25,33 @@ const listStaf = [
{ nama: 'Muh. Bahrian Shalat', nip: '198309102022073001' },
{ nama: 'M. Budi Utama', nip: '198502232009121002' },
{ nama: 'Renia Fathiayusa', nip: '141113007' },
];
]
const loading = ref(false);
const detailedStaff = ref<any[]>([]);
const selectedStaff = ref<any>(null); // Untuk staff yang dipilih untuk View/Delete
const isViewDialogOpen = ref(false);
const isDeleteDialogOpen = ref(false);
const loading = ref(false)
const detailedStaff = ref<any[]>([])
const selectedStaff = ref<any>(null) // Untuk staff yang dipilih untuk View/Delete
const isViewDialogOpen = ref(false)
const isDeleteDialogOpen = ref(false)
// Fungsi untuk mendapatkan data lengkap staf
async function fetchDetailedStaff() {
loading.value = true;
detailedStaff.value = [];
loading.value = true
detailedStaff.value = []
for (const staf of listStaf) {
try {
const hrisEndpoint = `https://api.ui.ac.id/staf/${staf.nip}`;
const response = await fetch(hrisEndpoint, {
headers: {
Authorization: `Bearer ${keycloakStore.accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Gagal mengambil data untuk NIP: ${staf.nip}`);
}
const data = await response.json();
// const hrisEndpoint = `https://api.ui.ac.id/staf/${staf.nip}`;
// const response = await fetch(hrisEndpoint, {
// headers: {
// Authorization: `Bearer ${keycloakStore.accessToken}`,
// },
// });
const stafNIP = staf.nip
const { response, error } = await api.staf.getStafData(stafNIP)
const data = response.value?.value // ✅ UNWRAP the inner ref
detailedStaff.value.push({
...staf,
kategori: data.employee_category || '-',
......@@ -56,9 +59,10 @@ async function fetchDetailedStaff() {
jabatan: data.jabatan_pekerjaan || '-',
unit: data.organization_unit || '-',
posisi: data.posisi || '-',
});
} catch (error) {
console.error(error);
})
}
catch (error) {
console.error(error)
detailedStaff.value.push({
...staf,
kategori: 'Error fetching data',
......@@ -66,16 +70,16 @@ async function fetchDetailedStaff() {
jabatan: '-',
unit: '-',
posisi: '-',
});
})
}
}
loading.value = false;
loading.value = false
}
onMounted(() => {
fetchDetailedStaff();
});
fetchDetailedStaff()
})
// Daftar header tabel
const headers = [
......@@ -85,39 +89,41 @@ const headers = [
{ title: 'Unit Organisasi', key: 'unit', sortable: true },
{ title: 'Posisi', key: 'posisi', sortable: true },
{ title: 'Aksi', key: 'actions', align: 'center', sortable: false },
];
]
// Filter data berdasarkan query pencarian dari props
const filteredStaff = computed(() => {
if (!props.searchQuery) return detailedStaff.value;
const query = props.searchQuery.toLowerCase();
if (!props.searchQuery)
return detailedStaff.value
const query = props.searchQuery.toLowerCase()
return detailedStaff.value.filter(
(staff) =>
staff.nama.toLowerCase().includes(query) ||
staff.nip.includes(query) ||
staff.unit.toLowerCase().includes(query) ||
staff.posisi.toLowerCase().includes(query)
);
});
staff =>
staff.nama.toLowerCase().includes(query)
|| staff.nip.includes(query)
|| staff.unit.toLowerCase().includes(query)
|| staff.posisi.toLowerCase().includes(query),
)
})
// Fungsi untuk membuka dialog View
function openViewDialog(staff: any) {
selectedStaff.value = staff;
isViewDialogOpen.value = true;
selectedStaff.value = staff
isViewDialogOpen.value = true
}
// Fungsi untuk membuka dialog Delete
function openDeleteDialog(staff: any) {
selectedStaff.value = staff;
isDeleteDialogOpen.value = true;
selectedStaff.value = staff
isDeleteDialogOpen.value = true
}
// Fungsi untuk menghapus staf
function deleteStaff() {
if (selectedStaff.value) {
detailedStaff.value = detailedStaff.value.filter((staff) => staff.nip !== selectedStaff.value.nip);
}
isDeleteDialogOpen.value = false;
if (selectedStaff.value)
detailedStaff.value = detailedStaff.value.filter(staff => staff.nip !== selectedStaff.value.nip)
isDeleteDialogOpen.value = false
}
</script>
......@@ -128,28 +134,54 @@ function deleteStaff() {
<!-- Header -->
<div class="d-flex justify-space-between align-center flex-wrap gap-4 mb-6">
<div>
<h5 class="text-h5">Our Staff</h5>
<div class="text-body-1">From All Direktorat</div>
<h5 class="text-h5">
Our Staff
</h5>
<div class="text-body-1">
From All Direktorat
</div>
</div>
</div>
<!-- Tabel Staf -->
<VDataTable :headers="headers" :items="filteredStaff" :loading="loading" item-value="nip" class="elevation-1"
:items-per-page="10">
<VDataTable
:headers="headers"
:items="filteredStaff"
:loading="loading"
item-value="nip"
class="elevation-1"
:items-per-page="10"
>
<!-- Kolom Foto -->
<template #item.foto="{ item }">
<img :src="item.foto" alt="Foto Staf" v-if="item.foto !== '-'"
style="border-radius: 50%; block-size: 50px; inline-size: 50px;" />
<img
v-if="item.foto !== '-'"
:src="item.foto"
alt="Foto Staf"
style="border-radius: 50%; block-size: 50px; inline-size: 50px;"
>
<span v-else>Tidak Ada Foto</span>
</template>
<!-- Kolom Aksi -->
<template #item.actions="{ item }">
<div class="d-flex gap-2 justify-center">
<VBtn icon size="small" variant="outlined" color="primary" @click="() => openViewDialog(item)">
<VBtn
icon
size="small"
variant="outlined"
color="primary"
@click="() => openViewDialog(item)"
>
<VIcon icon="ri-eye-line" />
</VBtn>
<VBtn icon size="small" variant="outlined" color="error" @click="() => openDeleteDialog(item)">
<VBtn
icon
size="small"
variant="outlined"
color="error"
@click="() => openDeleteDialog(item)"
>
<VIcon icon="ri-delete-bin-line" />
</VBtn>
</div>
......@@ -159,25 +191,43 @@ function deleteStaff() {
</VCard>
<!-- Dialog View -->
<VDialog v-model="isViewDialogOpen" persistent :max-width="$vuetify.display.smAndDown ? 'auto' : 500">
<VDialog
v-model="isViewDialogOpen"
persistent
:max-width="$vuetify.display.smAndDown ? 'auto' : 500"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn variant="text" size="default" @click="isViewDialogOpen = false" />
<DialogCloseBtn
variant="text"
size="default"
@click="isViewDialogOpen = false"
/>
<VCard class="pa-sm-5 pa-3">
<VCardTitle>
<h5 class="text-h5 mb-1">
<VIcon size="18" class="mb-1 bg-primary rounded-0">
<VIcon
size="18"
class="mb-1 bg-primary rounded-0"
>
ri-user-follow-fill
</VIcon> {{ selectedStaff?.nama || 'Staff Details' }}
</h5>
</VCardTitle>
<VCardText>
<div class="pa-2">
<VImg :src="selectedStaff?.foto" alt="Foto" class="cursor-pointer"
style="inline-size: 100%; max-block-size: 200px;" />
<VImg
:src="selectedStaff?.foto"
alt="Foto"
class="cursor-pointer"
style="inline-size: 100%; max-block-size: 200px;"
/>
</div>
<div class="mb-2">
<VAlert color="primary" variant="tonal">
<VAlert
color="primary"
variant="tonal"
>
<p class="mb-0">
<strong>NIP:</strong> {{ selectedStaff?.nip }}
</p>
......@@ -191,23 +241,53 @@ function deleteStaff() {
</div>
</VCardText>
<VCardActions>
<VBtn variant="tonal" class="bg-primary" color="white" @click="isViewDialogOpen = false">Close</VBtn>
<VBtn
variant="tonal"
class="bg-primary"
color="white"
@click="isViewDialogOpen = false"
>
Close
</VBtn>
</VCardActions>
</VCard>
</VDialog>
<!-- Dialog Delete -->
<VDialog v-model="isDeleteDialogOpen" persistent :max-width="$vuetify.display.smAndDown ? 'auto' : 400">
<VDialog
v-model="isDeleteDialogOpen"
persistent
:max-width="$vuetify.display.smAndDown ? 'auto' : 400"
>
<!-- 👉 dialog close btn -->
<DialogCloseBtn variant="text" size="default" @click="isDeleteDialogOpen = false" />
<DialogCloseBtn
variant="text"
size="default"
@click="isDeleteDialogOpen = false"
/>
<VCard class="pa-sm-5 pa-3">
<VCardTitle class="text-center pb-5 text-h4">Delete Staff</VCardTitle>
<VCardText class="text-justify">Are you sure you want to delete <strong>{{ selectedStaff?.nama }}</strong>?
<VCardTitle class="text-center pb-5 text-h4">
Delete Staff
</VCardTitle>
<VCardText class="text-justify">
Are you sure you want to delete <strong>{{ selectedStaff?.nama }}</strong>?
</VCardText>
<VCardActions>
<VBtn variant="tonal" class="bg-error" color="white" @click="deleteStaff">Delete</VBtn>
<VBtn variant="outlined" @click="isDeleteDialogOpen = false">Cancel</VBtn>
<VBtn
variant="tonal"
class="bg-error"
color="white"
@click="deleteStaff"
>
Delete
</VBtn>
<VBtn
variant="outlined"
@click="isDeleteDialogOpen = false"
>
Cancel
</VBtn>
</VCardActions>
</VCard>
</VDialog>
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!