Commit 7067e9e7 by Samuel Taniel Mulyadi

Merge branch 'sam' into 'master'

Sam

See merge request !17
2 parents 28ea9917 8b75d2d6
......@@ -75,7 +75,7 @@
min-block-size: 100vh; /* Ensures the element covers the full viewport height */
padding-block-start: 10px;
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
}
.main-component-margin {
......@@ -115,9 +115,9 @@ body {
.form-component-login {
position: relative;
z-index: 1;
display: block;
display: flex; /* changed from block to flex */
overflow: hidden;
flex-flow: row;
flex-direction: column; /* or row depending on layout */
align-items: center;
justify-content: center;
border-radius: 1.7rem;
......@@ -128,6 +128,7 @@ body {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
margin-inline: 20% 0%;
min-block-size: 300px;
min-inline-size: 345px;
outline: 0.5px solid #747474;
outline-offset: 0;
......@@ -169,11 +170,13 @@ body {
font-family: "Open Sans", sans-serif;
font-size: 14px;
grid-gap: 0.7rem;
inline-size: 100%;
margin-block: 1%;
margin-inline: 20px;
object-fit: fill;
padding-block: 10%;
padding-inline: 5%;
padding-inline: 35px;
}
.heading-8 {
......
......@@ -243,7 +243,7 @@ function getColorClass(index: number) {
v-else
class="timetable"
>
<div class="scrollable">
<div class="scrollable2">
<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-title2 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 {
.building2 {
font-weight: normal;
opacity: 0.8;
text-align: start; /* Aligns room to the right */
......
<script setup lang="ts">
import { onMounted } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
<script lang="ts" setup>
import { useKeycloakStore } from '@core/stores/keycloakStore'
import { computed, onMounted, ref } from 'vue'
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 (!response.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 Datatable -->
<!-- Data Table -->
<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: 56px;
}
}
}
}
}
}
}
}
.search-container {
......
......@@ -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">
......
<script setup lang="ts">
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import AuthProvider from '@/views/pages/authentication/AuthProvider.vue'
import authLoginBg from '/assets/images/naput/bg-blue-yellow.png'
import authLoginLogo from '/assets/images/naput/illust-login.png'
const authThemeImg = authLoginLogo
const authThemeBg = authLoginBg
const keycloakStore = useKeycloakStore()
definePageMeta({
layout: 'blank',
unauthenticatedOnly: true,
})
</script>
<template>
<body
:style="{ backgroundImage: `url(${authThemeBg})` }"
class="body-2"
>
<section class="global-main-login-component">
<div class="card-main-login-component4">
<div class="logo-login-component4">
<VImg
:src="authThemeImg"
max-width="650"
max-height="300"
class="image-10 lazyload"
/>
</div>
<div class="form-login-component">
<VCol cols="12">
<img
src="/assets/images/naput/logo-ui-hitam.png"
width="100"
height="auto"
class="mb-0"
>
</VCol>
<VCol cols="12">
<AuthProvider />
</VCol>
</div>
</div>
</section>
</body>
<!-- </VRow> -->
</template>
<style lang="scss">
@use "@core/scss/template/pages/page-auth";
</style>
<script setup lang="ts">
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import ImgDark from '@images/dstipro/auth-bg-dark.png'
import ImgLight from '@images/dstipro/auth-bg-light.png'
import authLoginBg from '/assets/images/naput/bg-blue-yellow.png'
import authLoginLogo from '/assets/images/naput/illust-login.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'
const authThemeImg = authLoginLogo
const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight,
authV2LoginLogoLight,
)
const authThemeBg = authLoginBg
const authThemeColor = useGenerateColorVariant(
'#FFFFFF',
'#303030',
)
const keycloakStore = useKeycloakStore()
const authThemeBgElement = useGenerateImageVariant(
ImgLight,
ImgDark,
)
const authThemeBg = useGenerateImageVariant(
authV2LoginBgLight,
authV2LoginBgDark,
)
definePageMeta({
layout: 'blank',
......@@ -19,38 +35,61 @@ definePageMeta({
</script>
<template>
<VRow
no-gutters
class="auth-wrapper d-flex align-center"
>
<body
:style="{ backgroundImage: `url(${authThemeBg})` }"
class="body-2"
class="body-4"
>
<section class="global-main-login-component">
<div class="card-main-login-component4">
<div class="logo-login-component4">
<div class="main-component-margin">
<section class="main-component-login">
<div
:style="{
backgroundImage: `linear-gradient(rgba(${authThemeColorRGB}, 0.5), rgba(${authThemeColorRGB}, 0.5)), url(${authThemeBgElement})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundBlendMode: 'normal, normal',
width: '100%',
opacity: 1,
}"
class="form-component-login"
>
<div class="combine-contact4_content">
<h2 class="heading-8">
SSO
</h2>
<h2 class="text-h5 heading-7">
Single Sign On
</h2>
<AuthProvider />
</div>
</div>
<div
class="logo-component-login"
:style="{
backgroundImage: `url(${ImgDark})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
width: '100%',
}"
>
<div class="logo-image-component-login">
<VImg
:src="authThemeImg"
max-width="650"
max-height="300"
max-height="100"
class="image-10 lazyload"
/>
</div>
<div class="form-login-component">
<VCol cols="12">
<img
src="/assets/images/naput/logo-ui-hitam.png"
width="100"
height="auto"
class="mb-0"
>
</VCol>
<VCol cols="12">
<AuthProvider />
</VCol>
</div>
</div>
</section>
</div>
</body>
<!-- </VRow> -->
</VRow>
</template>
<style lang="scss">
......
<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: 56px;
}
}
}
}
}
}
}
}
.search-container {
......
......@@ -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)
......@@ -20,12 +26,59 @@ onMounted(() => {
<template>
<VBtn
class="font-weight-bold"
color="#FFDC01"
class="sso-btn font-weight-medium"
color="#3d3d3d"
block
type="submit"
rounded="xl"
@click="login"
>
Login SSO
Sign in SSO
</VBtn>
<VBtn
class="google-btn font-weight-medium"
color="#3d3d3d"
block
type="submit"
rounded="xl"
variant="outlined"
style="margin-block-start: 5px;"
@click="loginGoogle"
>
<template #prepend>
<VImg
src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg"
alt="Google"
max-width="20"
class="mr-2"
/>
</template>
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>
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!