Commit 7accbf0f by Samuel Taniel Mulyadi

Update filtering dan responsiveness

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