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 7accbf0f
authored
Apr 16, 2025
by
Samuel Taniel Mulyadi
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update filtering dan responsiveness
1 parent
28ea9917
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
280 additions
and
259 deletions
components/beranda/UserJadwal.vue
components/beranda/UserRiwayat.vue
pages/[tab].vue
views/dstipro/beranda/riwayat/index.vue
views/pages/authentication/AuthProvider.vue
components/beranda/UserJadwal.vue
View file @
7accbf0
...
...
@@ -243,7 +243,7 @@ function getColorClass(index: number) {
v-else
class=
"timetable"
>
<div
class=
"scrollable"
>
<div
class=
"scrollable
2
"
>
<table
class=
"w-100 text-left table-schedule fixed-table"
>
<thead>
<tr>
...
...
@@ -279,16 +279,16 @@ function getColorClass(index: number) {
getScheduleByDay(day)[rowIndex - 1].end
}}
</span>
<span
class=
"
room
text-xs"
>
{{ getScheduleByDay(day)[rowIndex - 1].room }}
</span>
<span
class=
"text-xs"
>
{{ getScheduleByDay(day)[rowIndex - 1].room }}
</span>
</div>
<div
class=
"course-title font-semibold text-sm mb-1"
>
<div
class=
"course-title
2
font-semibold text-sm mb-1"
>
<span
v-if=
"getScheduleByDay(day)[rowIndex - 1].course.includes('-')"
style=
" padding-inline-end: 4px;text-decoration: underline;"
>
{{ getScheduleByDay(day)[rowIndex - 1].course.split(' - ')[0] }}
</span>
<span
class=
"
text-sm
"
>
<span
class=
"
course-name
"
>
{{
getScheduleByDay(day)[rowIndex - 1].course.includes('-')
? getScheduleByDay(day)[rowIndex - 1].course.split(' - ')[1]
...
...
@@ -322,6 +322,18 @@ function getColorClass(index: number) {
min-inline-size
:
500px
;
}
.scrollable2
{
overflow
:
auto
hidden
;
/* Optional: you can allow vertical scroll if needed */
box-sizing
:
border-box
;
inline-size
:
100%
;
min-inline-size
:
1000px
;
}
/* Optional: make inner content grow past 700px if needed */
.scrollable2
>
*
{
min-inline-size
:
1000px
;
}
.date-range-box
,
.date-range-box2
{
border-radius
:
5px
;
...
...
@@ -385,21 +397,64 @@ function getColorClass(index: number) {
}
.course-title
{
display
:
flex
;
flex-wrap
:
wrap
;
/* Allow wrapping when needed */
border-block-end
:
1px
solid
;
font-size
:
12px
;
font-weight
:
bold
;
gap
:
4px
;
/* Optional: space between prefix and name */
inline-size
:
100%
;
padding-block-end
:
4px
;
}
.course-title2
{
display
:
flex
;
flex-wrap
:
wrap
;
/* Allow wrapping when needed */
border-block-end
:
1px
solid
;
font-size
:
12px
;
font-weight
:
bold
;
gap
:
4px
;
/* Optional: space between prefix and name */
inline-size
:
100%
;
margin-block-end
:
5px
;
padding-block-end
:
5px
;
text-overflow
:
ellipsis
;
/* Show "..." */
padding-block-end
:
4px
;
}
.course-prefix
{
color
:
color-mix
(
in
srgb
,
rgba
(
var
(
--v-global-theme-primary
))
70%
,
black
30%
);
padding-inline-end
:
4px
;
text-decoration
:
underline
;
}
.course-name
{
display
:
-webkit-box
;
overflow
:
hidden
;
-webkit-box-orient
:
vertical
;
-webkit-line-clamp
:
2
;
text-overflow
:
ellipsis
;
}
.course-header
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
8px
;
/* optional spacing */
inline-size
:
100%
;
}
.time
{
flex-shrink
:
0
;
/* Don't shrink */
font-weight
:
normal
;
text-align
:
start
;
white-space
:
nowrap
;
}
.room
{
overflow
:
hidden
;
flex-grow
:
1
;
flex-shrink
:
1
;
font-weight
:
normal
;
text-align
:
end
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.grid-body
{
display
:
grid
;
grid-template-columns
:
80px
auto
;
/* Time column + Schedule grid */
...
...
@@ -451,31 +506,13 @@ function getColorClass(index: number) {
grid-template-rows
:
repeat
(
840
,
1px
);
/* 1px per minute */
}
.course-header
{
display
:
flex
;
justify-content
:
space-between
;
gap
:
8px
;
/* optional spacing */
inline-size
:
100%
;
}
.time
{
flex-shrink
:
0
;
/* Don't shrink */
font-weight
:
normal
;
text-align
:
start
;
white-space
:
nowrap
;
}
.room
{
overflow
:
hidden
;
flex-grow
:
1
;
flex-shrink
:
1
;
.building
{
font-weight
:
normal
;
text-align
:
end
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
opacity
:
0.8
;
text-align
:
start
;
/* Aligns room to the right */
}
.building
{
.building
2
{
font-weight
:
normal
;
opacity
:
0.8
;
text-align
:
start
;
/* Aligns room to the right */
...
...
components/beranda/UserRiwayat.vue
View file @
7accbf0
<
script
setup
lang=
"ts"
>
import
{
onMounted
}
from
'vu
e'
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStor
e'
<
script
lang=
"ts"
setup
>
import
{
useKeycloakStore
}
from
'@core/stores/keycloakStor
e'
import
{
computed
,
onMounted
,
ref
}
from
'vu
e'
const
data
=
ref
<
Record
<
string
,
any
>
|
null
>
(
null
)
const
error
=
ref
<
Record
<
string
,
any
>
|
null
>
(
null
)
const
keycloakStore
=
useKeycloakStore
(
)
const
isAuthenticated
=
computed
(()
=>
keycloakStore
.
authenticated
)
const
items
=
ref
<
any
[]
>
([])
const
expandedRows
=
ref
<
any
[]
>
([])
const
loading
=
ref
(
false
)
const
error
=
ref
(
''
)
const
searchQuery
=
ref
(
''
)
onMounted
(()
=>
{
keycloakStore
.
refresh
()
})
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'
)
data
.
value
=
newData
.
value
||
null
error
.
value
=
newError
.
value
||
null
})
const
keycloakStore
=
useKeycloakStore
()
// Headers
const
riwayatHeaders
=
[
{
title
:
''
,
key
:
'data-table-expand'
,
sortable
:
false
},
{
title
:
'Tahun'
,
key
:
'THN'
,
sortable
:
false
},
...
...
@@ -45,41 +31,48 @@ const expandedHeaders = [
{
title
:
'Pengajar'
,
key
:
'PENGAJAR'
,
sortable
:
false
},
]
function
resolveStatusColor
(
status
:
string
)
{
switch
(
status
)
{
case
'Aktif'
:
return
'primary'
case
'Lulus'
:
return
'success'
default
:
return
'default'
}
}
async
function
getData
()
{
loading
.
value
=
true
error
.
value
=
''
// Reset items sebelum fetch data baru
items
.
value
=
[]
try
{
const
apiEndpoint
=
'https://api.ui.ac.id/my/ac'
const
response
=
await
fetch
(
apiEndpoint
,
{
const
res
=
await
fetch
(
'https://api.ui.ac.id/my/ac'
,
{
headers
:
{
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
})
if
(
!
res
ponse
.
ok
)
if
(
!
res
.
ok
)
throw
new
Error
(
'Gagal mengambil data'
)
const
dataku
=
await
response
.
json
()
const
raw
=
await
res
.
json
()
items
.
value
=
dataku
.
map
((
item
:
any
)
=>
({
...
item
,
expanded
:
Object
.
values
(
item
.
IRS
||
{}).
map
((
mk
:
any
)
=>
({
items
.
value
=
raw
.
map
((
item
:
any
,
idx
:
number
)
=>
{
const
expandedFiltered
=
Object
.
values
(
item
.
IRS
||
{})
.
filter
((
mk
:
any
)
=>
mk
.
STATUS
!==
'Kosong'
)
// Remove STATUS: Kosong
.
map
((
mk
:
any
)
=>
({
KD_MK
:
mk
.
KD_MK
,
NM_MK
:
mk
.
NM_MK
,
NM_KLS_MK
:
mk
.
NM_KLS_MK
,
STATUS
:
mk
.
STATUS
,
NILAI_HURUF
:
mk
.
NILAI_HURUF
,
PENGAJAR
:
mk
.
PENGAJAR
?
Object
.
values
(
mk
.
PENGAJAR
).
join
(
', '
)
:
'-'
,
})),
}))
// Pastikan data adalah array sebelum dimasukkan ke items
// items.value = Array.isArray(dataku) ? dataku : [];
// items.value = dataku;
return
{
id
:
`
${
item
.
THN
}
-
${
item
.
SEMESTER
}
-
${
item
.
TERM
}
-
${
idx
}
`
,
...
item
,
expanded
:
expandedFiltered
,
}
}).
filter
(
item
=>
item
.
expanded
.
length
>
0
)
// Optionally remove parent rows with no expanded items
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
message
||
'Terjadi kesalahan saat mengambil data'
...
...
@@ -89,74 +82,50 @@ async function getData() {
}
}
// Fetch data from API
onMounted
(()
=>
{
getData
()
})
// Search query state
const
searchQuery
=
ref
(
''
)
// Filter aplikasi berdasarkan pencarian
const
filteredRiwayat
=
computed
(()
=>
{
if
(
!
searchQuery
.
value
)
return
items
.
value
const
query
=
searchQuery
.
value
.
toLowerCase
()
return
items
.
value
.
map
(
item
=>
{
// Filter mata kuliah (MK) berdasarkan NM_MK
const
filteredMK
=
item
.
expanded
.
filter
((
mk
:
any
)
=>
mk
.
NM_MK
.
toLowerCase
().
includes
(
query
),
)
// Jika ada MK yang cocok, tetap tampilkan item tetapi hanya dengan MK yang cocok
if
(
filteredMK
.
length
>
0
)
return
{
...
item
,
expanded
:
filteredMK
}
// Jika tidak ada MK yang cocok, cek apakah tahun, semester, atau status cocok
if
(
[
item
.
THN
,
item
.
SEMESTER
,
item
.
STATUS
].
some
(
field
=>
const
matchParent
=
[
item
.
THN
,
item
.
SEMESTER
,
item
.
STATUS
].
some
(
field
=>
String
(
field
).
toLowerCase
().
includes
(
query
),
)
)
return
item
return
null
// Tidak cocok, hapus dari hasil pencarian
})
.
filter
(
Boolean
)
// Hapus item yang null
if
(
filteredMK
.
length
>
0
||
matchParent
)
{
return
{
...
structuredClone
(
item
),
expanded
:
filteredMK
.
length
>
0
?
filteredMK
:
item
.
expanded
,
}
}
// return items.value.filter((item) =>
// [item.THN, item.SEMESTER, item.STATUS].some((field) =>
// String(field).toLowerCase().includes(query)
// )
// );
return
null
})
.
filter
(
Boolean
)
})
const
resolveStatusColor
=
(
status
:
string
)
=>
{
if
(
status
===
'Aktif'
)
return
'primary'
if
(
status
===
'Lulus'
)
return
'success'
}
</
script
>
<
template
>
<!--
<div
class=
"mb-10"
>
<h1>
Welcome,
{{
keycloakStore
.
name
}}
</h1>
</div>
-->
<VCard
title=
"Riwayat Mata Kuliah"
class=
"riwayatList"
>
<!-- Search
Input
-->
<!-- Search -->
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<VTextField
v-model=
"searchQuery"
label=
"
Search
"
placeholder=
"
Search ...
"
label=
"
Cari Mata Kuliah atau Semester
"
placeholder=
"
Contoh: Pancasila, 2023
"
append-inner-icon=
"ri-search-line"
clearable
single-line
...
...
@@ -166,78 +135,62 @@ const resolveStatusColor = (status: string) => {
/>
</div>
<!--
SECTION Datat
able -->
<!--
Data T
able -->
<VDataTable
v-model:expanded=
"expandedRows"
:headers=
"riwayatHeaders"
:items=
"filteredRiwayat"
item-value=
"id"
hide-default-footer
fixed-header
item-value=
"SEMESTER"
show-expand
:sort-by=
"['SEMESTER']"
:sort-asc=
"[true]"
show-expand
>
<template
#
expanded-row=
"
{ item }">
<tr>
<td
colspan=
"
6
"
>
<td
colspan=
"
100%
"
>
<VDataTable
density=
"compact"
:headers=
"expandedHeaders"
:items=
"item.expanded"
density=
"compact"
class=
"ml-4"
hide-default-footer
/>
</td>
</tr>
</
template
>
<!-- Tahun Ajar -->
<
template
#
item
.
THN=
"{ item }"
>
<div
class=
"d-flex align-center gap-x-3"
>
<div>
<h6
class=
"text-h6 text-no-wrap
"
>
{{
`${Number(item.THN)
}
/${Number(item.THN) + 1
}
`
}}
<h6
class=
"text-h6
"
>
{{
`${item.THN
}
/${Number(item.THN) + 1
}
`
}}
<
/h6
>
<
/div
>
<
/div
>
<
/template
>
<!--
Status
-->
<
template
#
item
.
STATUS
=
"
{
item
}
">
<VChip
:color="
resolveStatusColor
(
item
.
STATUS
)
"
:class="`text-${resolveStatusColor(item.STATUS)
}
`"
size="
small
"
class="
font
-
weight
-
medium
"
size="
small
"
>
{{
item
.
STATUS
}}
<
/VChip
>
<
/template
>
<!--
TODO
Refactor
this
after
vuetify
provides
proper
solution
for
removing
default
footer
-->
<
template
#
bottom
/>
<
/VDataTable
>
<!--
!
SECTION
-->
<
/VCard
>
<
/template
>
<
style
lang
=
"scss"
>
<
style
scoped
lang
=
"scss"
>
.
riwayatList
{
.
v
-
table
{
&--
density
-
default
{
.
v
-
table__wrapper
{
table
{
tbody
{
tr
{
td
{
table
tbody
tr
td
{
block
-
size
:
56
px
;
}
}
}
}
}
}
}
}
.
search
-
container
{
...
...
pages/[tab].vue
View file @
7accbf0
...
...
@@ -2,7 +2,6 @@
import
{
computed
}
from
'vue'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStore'
import
UserBerita
from
'@/components/beranda/UserBerita.vue'
import
UserJadwal
from
'@/components/beranda/UserJadwal.vue'
import
UserLib
from
'@/components/beranda/UserLib.vue'
import
UserLog
from
'@/components/beranda/UserLog.vue'
...
...
@@ -35,6 +34,7 @@ const activeTab = computed({
const
tabs
=
[
{
title
:
'Profil'
,
icon
:
'ri-user-line'
,
tab
:
'profile'
},
{
title
:
'Keamanan'
,
icon
:
'ri-lock-line'
,
tab
:
'keamanan'
},
// { title: 'Berita', icon: 'ri-news-line', tab: 'berita' },
{
title
:
'Riwayat'
,
icon
:
'ri-file-history-line'
,
tab
:
'riwayat'
},
{
title
:
'Jadwal'
,
icon
:
'ri-calendar-line'
,
tab
:
'jadwal'
},
...
...
@@ -46,14 +46,23 @@ const tabs = [
]
// Filter tab berdasarkan civitas
const
filteredTabs
=
computed
(()
=>
tabs
.
filter
(
tab
=>
{
// Jangan sembunyikan "riwayat", hanya "log-absen" untuk mahasiswa
if
(
tab
.
tab
===
'log'
&&
keycloakStore
.
civitas
===
'mahasiswa'
)
return
false
;
return
true
;
})
);
const
tabsByCivitas
:
Record
<
string
,
string
[]
>
=
{
staf
:
[
'profile'
,
'keamanan'
,
'log'
,
'peta'
,
'lib'
],
dosen
:
[
'profile'
,
'keamanan'
,
'jadwal'
,
'log'
,
'peta'
,
'lib'
],
mahasiswa
:
[
'profile'
,
'keamanan'
,
'riwayat'
,
'jadwal'
,
'peta'
,
'lib'
],
}
// Compute allowed tabs for the current user
const
allowedTabs
=
computed
(()
=>
{
const
civitas
=
keycloakStore
.
civitas
return
tabsByCivitas
[
civitas
]
??
[]
})
// Filter tabs based on the allowlist
const
filteredTabs
=
computed
(()
=>
tabs
.
filter
(
tab
=>
allowedTabs
.
value
.
includes
(
tab
.
tab
)),
)
// Fungsi untuk navigasi tab
const
navigateTab
=
(
tab
:
string
)
=>
{
...
...
@@ -103,9 +112,12 @@ const navigateTab = (tab: string) => {
</VWindowItem>
<!-- Berita -->
<!--
<VWindowItem
value=
"berita"
>
<!--
<VWindowItem
value=
"berita"
>
<UserBerita
/>
</VWindowItem>
-->
</VWindowItem>
-->
<!--
<div>
-->
<!-- Riwayat -->
<VWindowItem
value=
"riwayat"
>
...
...
views/dstipro/beranda/riwayat/index.vue
View file @
7accbf0
<
script
lang=
"ts"
setup
>
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
import
{
useKeycloakStore
}
from
'@core/stores/keycloakStore'
import
{
computed
,
onMounted
,
ref
}
from
'vue'
const
keycloakStore
=
useKeycloakStore
();
// Gunakan computed agar selalu reaktif
const
isAuthenticated
=
computed
(()
=>
keycloakStore
.
authenticated
);
// const isAuthenticated = keycloakStore.authenticated;
const
keycloakStore
=
useKeycloakStore
()
const
isAuthenticated
=
computed
(()
=>
keycloakStore
.
authenticated
)
// State management
const
items
=
ref
<
any
[]
>
([]);
const
expandedRows
=
ref
<
any
[]
>
([]);
const
loading
=
ref
(
false
);
const
error
=
ref
(
''
);
const
items
=
ref
<
any
[]
>
([])
const
expandedRows
=
ref
<
any
[]
>
([])
const
loading
=
ref
(
false
)
const
error
=
ref
(
''
)
const
searchQuery
=
ref
(
''
)
//
Project Riwayat Header
//
Headers
const
riwayatHeaders
=
[
{
title
:
""
,
key
:
"data-table-expand"
,
sortable
:
false
},
{
title
:
''
,
key
:
'data-table-expand'
,
sortable
:
false
},
{
title
:
'Tahun'
,
key
:
'THN'
,
sortable
:
false
},
{
title
:
'Term'
,
key
:
'TERM'
,
sortable
:
false
},
{
title
:
'Semester'
,
key
:
'SEMESTER'
,
sortable
:
true
},
...
...
@@ -24,171 +23,174 @@ const riwayatHeaders = [
]
const
expandedHeaders
=
[
{
title
:
"Kode MK"
,
key
:
"KD_MK"
,
sortable
:
true
},
{
title
:
"Nama MK"
,
key
:
"NM_MK"
,
sortable
:
true
},
{
title
:
"Kelas"
,
key
:
"NM_KLS_MK"
,
sortable
:
false
},
{
title
:
"Status"
,
key
:
"STATUS"
,
sortable
:
false
},
{
title
:
"Nilai"
,
key
:
"NILAI_HURUF"
,
sortable
:
true
},
{
title
:
"Pengajar"
,
key
:
"PENGAJAR"
,
sortable
:
false
},
]
;
{
title
:
'Kode MK'
,
key
:
'KD_MK'
,
sortable
:
true
},
{
title
:
'Nama MK'
,
key
:
'NM_MK'
,
sortable
:
true
},
{
title
:
'Kelas'
,
key
:
'NM_KLS_MK'
,
sortable
:
false
},
{
title
:
'Status'
,
key
:
'STATUS'
,
sortable
:
false
},
{
title
:
'Nilai'
,
key
:
'NILAI_HURUF'
,
sortable
:
true
},
{
title
:
'Pengajar'
,
key
:
'PENGAJAR'
,
sortable
:
false
},
]
async
function
getData
()
{
loading
.
value
=
true
;
error
.
value
=
''
;
function
resolveStatusColor
(
status
:
string
)
{
switch
(
status
)
{
case
'Aktif'
:
return
'primary'
case
'Lulus'
:
return
'success'
default
:
return
'default'
}
}
// Reset items sebelum fetch data baru
items
.
value
=
[];
async
function
getData
()
{
loading
.
value
=
true
error
.
value
=
''
items
.
value
=
[]
try
{
const
apiEndpoint
=
`https://api.ui.ac.id/my/ac`
;
const
response
=
await
fetch
(
apiEndpoint
,
{
const
res
=
await
fetch
(
'https://api.ui.ac.id/my/ac'
,
{
headers
:
{
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
})
;
})
if
(
!
response
.
ok
)
throw
new
Error
(
'Gagal mengambil data'
);
const
dataku
=
await
response
.
json
();
if
(
!
res
.
ok
)
throw
new
Error
(
'Gagal mengambil data'
)
const
raw
=
await
res
.
json
()
items
.
value
=
dataku
.
map
((
item
:
any
)
=>
({
...
item
,
expanded
:
Object
.
values
(
item
.
IRS
||
{}).
map
((
mk
:
any
)
=>
({
items
.
value
=
raw
.
map
((
item
:
any
,
idx
:
number
)
=>
{
const
expandedFiltered
=
Object
.
values
(
item
.
IRS
||
{})
.
filter
((
mk
:
any
)
=>
mk
.
STATUS
!==
'Kosong'
)
// Remove STATUS: Kosong
.
map
((
mk
:
any
)
=>
({
KD_MK
:
mk
.
KD_MK
,
NM_MK
:
mk
.
NM_MK
,
NM_KLS_MK
:
mk
.
NM_KLS_MK
,
STATUS
:
mk
.
STATUS
,
NILAI_HURUF
:
mk
.
NILAI_HURUF
,
PENGAJAR
:
mk
.
PENGAJAR
?
Object
.
values
(
mk
.
PENGAJAR
).
join
(
", "
)
:
"-"
,
})),
}));
// Pastikan data adalah array sebelum dimasukkan ke items
// items.value = Array.isArray(dataku) ? dataku : [];
// items.value = dataku;
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
message
||
'Terjadi kesalahan saat mengambil data'
;
}
finally
{
loading
.
value
=
false
;
PENGAJAR
:
mk
.
PENGAJAR
?
Object
.
values
(
mk
.
PENGAJAR
).
join
(
', '
)
:
'-'
,
}))
return
{
id
:
`
${
item
.
THN
}
-
${
item
.
SEMESTER
}
-
${
item
.
TERM
}
-
${
idx
}
`
,
...
item
,
expanded
:
expandedFiltered
,
}
}).
filter
(
item
=>
item
.
expanded
.
length
>
0
)
// Optionally remove parent rows with no expanded items
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
message
||
'Terjadi kesalahan saat mengambil data'
}
finally
{
loading
.
value
=
false
}
}
// Fetch data from API
onMounted
(()
=>
{
getData
();
});
// Search query state
const
searchQuery
=
ref
(
''
);
getData
()
})
// Filter aplikasi berdasarkan pencarian
const
filteredRiwayat
=
computed
(()
=>
{
if
(
!
searchQuery
.
value
)
return
items
.
value
;
const
query
=
searchQuery
.
value
.
toLowerCase
();
if
(
!
searchQuery
.
value
)
return
items
.
value
.
map
((
item
)
=>
{
// Filter mata kuliah (MK) berdasarkan NM_MK
const
filteredMK
=
item
.
expanded
.
filter
((
mk
:
any
)
=>
mk
.
NM_MK
.
toLowerCase
().
includes
(
query
)
);
// Jika ada MK yang cocok, tetap tampilkan item tetapi hanya dengan MK yang cocok
if
(
filteredMK
.
length
>
0
)
{
return
{
...
item
,
expanded
:
filteredMK
};
}
const
query
=
searchQuery
.
value
.
toLowerCase
()
// Jika tidak ada MK yang cocok, cek apakah tahun, semester, atau status cocok
if
(
[
item
.
THN
,
item
.
SEMESTER
,
item
.
STATUS
].
some
((
field
)
=>
String
(
field
).
toLowerCase
().
includes
(
query
)
return
items
.
value
.
map
(
item
=>
{
const
filteredMK
=
item
.
expanded
.
filter
((
mk
:
any
)
=>
mk
.
NM_MK
.
toLowerCase
().
includes
(
query
),
)
const
matchParent
=
[
item
.
THN
,
item
.
SEMESTER
,
item
.
STATUS
].
some
(
field
=>
String
(
field
).
toLowerCase
().
includes
(
query
),
)
)
{
return
item
;
if
(
filteredMK
.
length
>
0
||
matchParent
)
{
return
{
...
structuredClone
(
item
),
expanded
:
filteredMK
.
length
>
0
?
filteredMK
:
item
.
expanded
,
}
}
return
null
;
// Tidak cocok, hapus dari hasil pencarian
return
null
})
.
filter
(
Boolean
);
// Hapus item yang null
// return items.value.filter((item) =>
// [item.THN, item.SEMESTER, item.STATUS].some((field) =>
// String(field).toLowerCase().includes(query)
// )
// );
});
const
resolveStatusColor
=
(
status
:
string
)
=>
{
if
(
status
===
'Aktif'
)
return
'primary'
if
(
status
===
'Lulus'
)
return
'success'
}
.
filter
(
Boolean
)
})
</
script
>
<
template
>
<VCard
title=
"Riwayat Mata Kuliah"
class=
"riwayatList"
>
<!-- Search Input -->
<VCard
title=
"Riwayat Mata Kuliah"
class=
"riwayatList"
>
<!-- Search -->
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<VTextField
v-model=
"searchQuery"
label=
"Search"
placeholder=
"Search ..."
append-inner-icon=
"ri-search-line"
clearable
single-line
hide-details
dense
outlined
/>
<VTextField
v-model=
"searchQuery"
label=
"Cari Mata Kuliah atau Semester"
placeholder=
"Contoh: Pancasila, 2023"
append-inner-icon=
"ri-search-line"
clearable
single-line
hide-details
dense
outlined
/>
</div>
<!-- SECTION Datatable -->
<VDataTable
:headers=
"riwayatHeaders"
:items=
"filteredRiwayat"
hide-default-footer
fixed-header
item-value=
"SEMESTER"
:sort-by=
"['SEMESTER']"
:sort-asc=
"[true]"
v-model:expanded=
"expandedRows"
show-expand
>
<template
v-slot:expanded-row=
"
{ item }">
<!-- Data Table -->
<VDataTable
v-model:expanded=
"expandedRows"
:headers=
"riwayatHeaders"
:items=
"filteredRiwayat"
item-value=
"id"
hide-default-footer
fixed-header
show-expand
:sort-by=
"['SEMESTER']"
:sort-asc=
"[true]"
>
<template
#
expanded-row=
"
{ item }">
<tr>
<td
colspan=
"6"
>
<VDataTable
density=
"compact"
:headers=
"expandedHeaders"
:items=
"item.expanded"
class=
"ml-4"
/>
<td
colspan=
"100%"
>
<VDataTable
:headers=
"expandedHeaders"
:items=
"item.expanded"
density=
"compact"
class=
"ml-4"
hide-default-footer
/>
</td>
</tr>
</
template
>
<!-- Tahun Ajar -->
<
template
#
item
.
THN=
"{ item }"
>
<div
class=
"d-flex align-center gap-x-3"
>
<div>
<h6
class=
"text-h6 text-no-wrap
"
>
{{
Number
(
item
.
THN
)
+
'/'
+
(
Number
(
item
.
THN
)
+
1
)
}}
<h6
class=
"text-h6
"
>
{{
`${item.THN
}
/${Number(item.THN) + 1
}
`
}}
<
/h6
>
<
/div
>
</div>
<
/template
>
<!-- Status -->
<
template
#
item
.
STATUS
=
"
{
item
}
">
<VChip
:color=
"resolveStatusColor(item.STATUS)"
:class=
"`text-$
{resolveStatusColor(item.STATUS)}`" size="small"
class="font-weight-medium">
<VChip
:color="
resolveStatusColor
(
item
.
STATUS
)
"
class="
font
-
weight
-
medium
"
size="
small
"
>
{{
item
.
STATUS
}}
<
/VChip
>
<
/template
>
<!-- TODO Refactor this after vuetify provides proper solution for removing default footer -->
<
template
#
bottom
/>
<
/VDataTable
>
<!-- !SECTION -->
<
/VCard
>
<
/template
>
<
style
lang=
"scss"
>
<
style
scoped
lang
=
"scss"
>
.
riwayatList
{
.v-table
{
&--density-default
{
.
v
-
table__wrapper
{
table
{
tbody
{
tr
{
td
{
table
tbody
tr
td
{
block
-
size
:
56
px
;
}
}
}
}
}
}
}
}
.
search
-
container
{
...
...
views/pages/authentication/AuthProvider.vue
View file @
7accbf0
...
...
@@ -11,6 +11,12 @@ function login() {
})
}
function
loginGoogle
()
{
keycloakInstance
.
login
({
redirectUri
:
`
${
window
.
location
.
origin
}
/profile`
,
idpHint
:
'google'
,
})
}
// Cek apakah user sudah login saat komponen dimuat
onMounted
(()
=>
{
if
(
keycloakInstance
.
authenticated
)
...
...
@@ -28,4 +34,15 @@ onMounted(() => {
>
Login SSO
</VBtn>
<VBtn
class=
"font-weight-bold"
color=
"#FFDC01"
block
type=
"submit"
style=
"margin-block-start: 10px;"
@
click=
"loginGoogle"
>
Login Google
</VBtn>
</
template
>
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