Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
dtd
/
civitas.ui
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 704d29ca
authored
Apr 29, 2025
by
Samuel Taniel Mulyadi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
done token
1 parent
a364264c
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
1558 additions
and
847 deletions
components/beranda/UserJadwal.vue
components/beranda/UserLib.vue
components/beranda/UserLog.vue
components/beranda/UserRiwayat.vue
composables/useApi.ts
composables/useAuthFetch.ts
layouts/components/NavbarTokenExpiredTime.vue
nuxt.config.ts
pages/login-v4.vue
pages/login-v5.vue
pages/login-v6.vue
pages/login.vue
pages/pages/authentication/login-v1.vue
pages/pages/authentication/login-v2.vue
plugins/keycloak.ts
views/dstipro/beranda/UserProfileHeader.vue
views/dstipro/beranda/authentication/AuthProvider.vue
views/dstipro/beranda/keamanan/index.vue
views/dstipro/beranda/profile/About.vue
views/dstipro/beranda/riwayat/index.vue
views/dstipro/database/dosen.vue
views/dstipro/staf/OurStaffTable.vue
components/beranda/UserJadwal.vue
View file @
704d29c
...
...
@@ -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
...
...
components/beranda/UserLib.vue
View file @
704d29c
<
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
>
components/beranda/UserLog.vue
View file @
704d29c
<
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
...
...
components/beranda/UserRiwayat.vue
View file @
704d29c
...
...
@@ -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
||
{})
...
...
composables/useApi.ts
View file @
704d29c
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
,
}
}
composables/useAuthFetch.ts
View file @
704d29c
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
}
`
,
}
},
})
}
layouts/components/NavbarTokenExpiredTime.vue
View file @
704d29c
...
...
@@ -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
...
...
nuxt.config.ts
View file @
704d29c
...
...
@@ -88,6 +88,7 @@ export default defineNuxtConfig({
'@themeConfig'
:
[
'../themeConfig.ts'
],
'@layouts/*'
:
[
'../@layouts/*'
],
'@layouts'
:
[
'../@layouts'
],
'@composables'
:
[
'../@composables'
],
'@core/*'
:
[
'../@core/*'
],
'@core'
:
[
'../@core'
],
'@images/*'
:
[
'../assets/images/*'
],
...
...
pages/login-v4.vue
View file @
704d29c
...
...
@@ -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
>
...
...
pages/login-v5.vue
View file @
704d29c
<
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'
...
...
pages/login-v6.vue
View file @
704d29c
<
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
authTheme
Img
=
authLoginLogo
;
const
authTheme
Bg
=
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-b
ottom
:
20px
;
inline-size
:
120px
;
margin-b
lock-end
:
20px
;
}
/* Title */
h1
{
font-size
:
24px
;
color
:
#000
;
font-size
:
24px
;
text-align
:
center
;
}
.login-button
:hover
{
background-color
:
#
E6C
200
;
background-color
:
#
e6c
200
;
}
@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
>
pages/login.vue
View file @
704d29c
...
...
@@ -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
,
...
...
pages/pages/authentication/login-v1.vue
View file @
704d29c
<
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
)
...
...
pages/pages/authentication/login-v2.vue
View file @
704d29c
<
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
,
...
...
plugins/keycloak.ts
View file @
704d29c
...
...
@@ -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
.
t
okenParsed
?.
exp
??
0
const
refreshTokenExp
=
keycloakInstance
.
refreshT
okenParsed
?.
exp
??
0
if
(
refreshT
okenExp
<=
now
)
{
console
.
warn
(
'
Refresh t
oken expired. Logging out...'
)
await
keycloakInstance
.
logout
({
redirectUri
:
`
${
window
.
location
.
origin
}
/login`
})
return
const
tokenExp
=
keycloakInstance
.
refreshT
okenParsed
?.
exp
??
0
const
tokenParsed
=
keycloakInstance
.
t
okenParsed
?.
exp
??
0
if
(
t
okenExp
<=
now
)
{
console
.
warn
(
'
T
oken 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'
)
...
...
views/dstipro/beranda/UserProfileHeader.vue
View file @
704d29c
<
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>
...
...
views/dstipro/beranda/authentication/AuthProvider.vue
View file @
704d29c
<
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
>
views/dstipro/beranda/keamanan/index.vue
View file @
704d29c
...
...
@@ -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
)
...
...
views/dstipro/beranda/profile/About.vue
View file @
704d29c
<
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>
...
...
views/dstipro/beranda/riwayat/index.vue
View file @
704d29c
...
...
@@ -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
)
{
err
or
.
value
=
err
.
message
||
'Terjadi kesalahan saat mengambil data'
err
.
value
=
err
.
message
||
'Terjadi kesalahan saat mengambil data'
}
finally
{
loading
.
value
=
false
...
...
views/dstipro/database/dosen.vue
View file @
704d29c
<
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>
<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
>
...
...
views/dstipro/staf/OurStaffTable.vue
View file @
704d29c
<
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>
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment