Commit 777aabba by Samuel Taniel Mulyadi

Merge branch 'staging' into 'master'

Staging

See merge request !3
2 parents d63c4d85 d7f699f9
......@@ -10,12 +10,6 @@ defineProps<{
navItems: HorizontalNavItems
}>()
// const props = defineProps<{
// navItems: HorizontalNavItems
// }>()
// console.log(props.navItems) // ✅ Now this will work
const configStore = useLayoutConfigStore()
</script>
......
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore()
const schedule = ref<any[]>([])
const loading = ref(false)
const daysOfWeek = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat']
const startHour = 7
const endHour = 21
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 data = await response.json()
if (keycloakStore.civitas === 'mahasiswa') {
// Get the latest semester
const latestSemesterData = data.reduce((latest, current) => {
return Number.parseInt(current.SEMESTER) > Number.parseInt(latest.SEMESTER) ? current : latest
}, data[0])
// Map schedule for mahasiswa
schedule.value = Object.values(latestSemesterData.IRS).flatMap(course =>
Object.values(course.JADWAL).map(jadwal => ({
day: jadwal.NM_HARI,
start: jadwal.JAM_MULAI,
end: jadwal.JAM_SELESAI,
course: course.NM_KLS_MK,
lecturer: Object.values(course.PENGAJAR).join(', '), // Join multiple lecturers if exist
room: jadwal.NM_RUANG,
})),
)
}
else if (keycloakStore.civitas === 'dosen') {
// Map schedule for dosen
schedule.value = data.map(item => ({
day: item.NM_HARI,
start: item.JAM_MULAI,
end: item.JAM_SELESAI,
course: item.NM_KLS_MK,
lecturer: item.NAMA_DOSEN,
room: item.NM_RUANG,
}))
}
else if (keycloakStore.civitas === 'staf') {
// Do nothing (empty schedule)
schedule.value = []
}
}
catch (err) {
console.error('Failed to fetch schedule:', err.message)
}
finally {
loading.value = false
}
}
onMounted(() => {
keycloakStore.refresh()
getData()
})
function parseTime(time: string) {
console.log(`Parsing time: ${time}`)
const formattedTime = time.replace('.', ':')
const [hour, minute] = formattedTime.split(':').map(Number)
if (isNaN(hour) || isNaN(minute)) {
console.error(`Invalid time format: ${time}`)
return Number.NaN
}
console.log((hour - startHour) * 60 + minute)
return (hour - startHour) * 60 + minute
}
function calculateRowSpan(start: string, end: string) {
const rowSpan = (parseTime(end) - parseTime(start))
console.log(`RowSpan for ${start} - ${end}:`, rowSpan)
return rowSpan
}
</script>
<template>
<VCard
title="Jadwal Kuliah"
class="timetable-card"
>
<div class="timetable">
<!-- Header Row -->
<div class="grid-header">
<div class="time-label">
Jam
</div>
<div
v-for="day in daysOfWeek"
:key="day"
class="day-header"
>
{{ day }}
</div>
</div>
<!-- Grid Body -->
<div class="grid-body">
<!-- Time Labels -->
<div class="time-column">
<div
v-for="hour in Array.from({ length: endHour - startHour + 1 }, (_, i) => startHour + i)"
:key="hour"
class="time-slot"
:style="{
gridRowStart: (hour - startHour) * 60 + 1, // Now each row is a single minute
gridRowEnd: `span 60`, // Each hour label spans 60 rows
gridColumn: 1, // Stays in the first column
}"
>
{{ hour }}:00
</div>
</div>
<!-- Schedule Grid -->
<div class="schedule-grid">
<div
v-for="item in schedule"
:key="item.course + item.start"
class="schedule-item"
:style="{
gridRowStart: parseTime(item.start) + 1, // Ensure it starts correctly
gridRowEnd: `span ${calculateRowSpan(item.start, item.end)}`,
gridColumn: daysOfWeek.indexOf(item.day) + 1, //its already right because monday starts at 0 so time column +1
}"
>
<div class="course-header">
<span class="time">{{ item.start }} - {{ item.end }}</span>
<span class="room">{{ item.room }}</span>
</div>
<div class="course-title">
<span
v-if="item.course.includes('-')"
class="course-prefix"
>
{{ item.course.split(' - ')[0] }}
</span>
<span class="course-name">
{{ item.course.includes('-') ? item.course.split(' - ')[1] : item.course }}
</span>
</div>
</div>
</div>
</div>
</div>
</VCard>
</template>
<style scoped>
.timetable {
display: flex;
flex-direction: column;
padding: 0 !important;
border-radius: 12px;
margin-block: 0;
margin-inline: 20px; /* Only applies margin to left and right */
max-inline-size: calc(100% - 40px); /* Adjusts width accordingly */
overflow-x: auto;
}
.grid-header {
display: grid;
background-color: rgba(var(--v-global-theme-primary));
color: white;
font-weight: bold;
grid-template-columns: 80px repeat(5, 1fr);
text-align: center;
}
.time-label,
.day-header {
padding: 10px;
font-size: 14px;
text-align: center;
}
.course-title {
display: flex;
flex-wrap: wrap;
font-size: 12px;
font-weight: bold;
gap: 4px;
}
.course-prefix {
color: color-mix(in srgb, rgba(var(--v-global-theme-primary)) 70%, black 30%);
}
.grid-body {
display: grid;
grid-template-columns: 80px auto; /* Time column + Schedule grid */
padding-block: 30px;
}
.time-column {
display: grid;
font-weight: bold;
grid-template-rows: repeat(840, 1px); /* Assuming 07:00 - 21:00, 30-min per row */
text-align: center;
}
.time-slot {
position: relative;
display: flex;
flex-direction: column; /* Stack content vertically */
align-items: center; /* Center horizontally */
justify-content: flex-start; /* Align content to the top */
block-size: 60px; /* 30 min per row */
margin-block-start: -15px;
padding-block-start: 5px; /* Optional: Add some spacing from the top */
}
.schedule-item {
display: flex;
flex-direction: column;
align-items: flex-start; /* Keeps text left-aligned */
justify-content: flex-start; /* Aligns content to the top */
padding: 4px;
border-radius: 6px;
margin: 2px;
background-color: color-mix(in srgb, rgba(var(--v-global-theme-primary)) 70%, white 30%);
color: white;
font-size: 12px;
text-align: start; /* Ensures text is left-aligned */
}
.schedule-grid {
position: relative;
display: grid;
flex: 1;
background-image: repeating-linear-gradient(to bottom, #ccc 0, #ccc 1px, transparent 1px, transparent 60px);
background-size: 100%; /* Ensures lines every 60px */
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(840, 1px); /* 1px per minute */
}
.course-header {
display: flex;
justify-content: space-between; /* Pushes elements to the left & right */
inline-size: 100%; /* Ensures full width */
}
.time {
font-weight: normal;
text-align: start; /* Aligns time to the left */
}
.room {
font-weight: normal;
text-align: end; /* Aligns room to the right */
}
</style>
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
<script setup lang="ts">
import type { VForm } from 'vuetify/components/VForm'
import authV2MaskDark from '@images/dstipro/mask-v2-dark.png'
import authV2MaskLight from '@images/dstipro/mask-v2-light.png'
import authV2LoginBgYellowDark from '@images/dstipro/auth-bg-yellow-dark.png'
import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import { useKeycloakStore } from '@core/stores/keycloakStore'
import SSOButton from '@/views/dstipro/beranda/authentication/SSOButton.vue'
import keycloakInstance from '@/keycloak'
function login() {
keycloakInstance.login({
redirectUri: `${window.location.origin}/naputpro/beranda/profile`,
})
}
const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
const error = ref<Record<string, any> | null>(null)
onMounted(() => {
if (keycloakInstance.authenticated)
router.push('/naputpro/beranda/profile')
})
const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight,
......@@ -31,77 +32,11 @@ const authThemeBg = useGenerateImageVariant(
authV2LoginBgYellowDark,
)
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({
layout: 'blank',
unauthenticatedOnly: true,
})
const isPasswordVisible = ref(false)
const route = useRoute()
const router = useRouter()
const ability = useAbility()
const errors = ref<Record<string, string | undefined>>({
email: undefined,
password: undefined,
})
const refVForm = ref<VForm>()
const credentials = ref({
email: 'admin@demo.com',
password: 'admin',
})
const rememberMe = ref(false)
const login = async () => {
try {
const res = await $api('/auth/login', {
method: 'POST',
body: {
email: credentials.value.email,
password: credentials.value.password,
},
onResponseError({ response }) {
errors.value = response._data.errors
},
})
const { accessToken, userData, userAbilityRules } = res
useCookie('userAbilityRules').value = userAbilityRules
ability.update(userAbilityRules)
useCookie('userData').value = userData
useCookie('accessToken').value = accessToken
console.log('Ini userdata: ', userData)
console.log('Ini accesstoken: ', accessToken)
console.log('Ini route: ', route.query.to)
// Redirect to `to` query if exist or redirect to index route
// ❗ nextTick is required to wait for DOM updates and later redirect
await nextTick(() => {
router.replace(route.query.to ? String(route.query.to) : '/')
})
}
catch (err) {
console.error(err)
}
}
const onSubmit = () => {
refVForm.value?.validate().then(({ valid: isValid }) => {
if (isValid)
login()
})
}
</script>
<template>
......@@ -125,7 +60,15 @@ const onSubmit = () => {
cols="12"
class="text-center"
>
<SSOButton />
<VBtn
color="warning"
@click="login"
>
<VIcon
start
icon="ri-login-box-line"
/> Login SSO
</VBtn>
</VCol>
</div>
</div>
......
<script setup lang="ts">
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import { VForm } from 'vuetify/components/VForm'
import authV2MaskDark from '@images/dstipro/mask-v2-dark.png'
import authV2MaskLight from '@images/dstipro/mask-v2-light.png'
import authV2LoginBgDark from '@images/dstipro/auth-bg-dark.png'
import authV2LoginBgLight from '@images/dstipro/auth-bg-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import { useKeycloakStore } from '@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore()
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight,
......@@ -28,8 +21,6 @@ const authThemeBg = useGenerateImageVariant(
authV2LoginBgDark,
)
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({
layout: 'blank',
unauthenticatedOnly: true,
......
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import authV2MaskDark from '@images/dstipro/mask-v2-dark.png'
import authV2MaskLight from '@images/dstipro/mask-v2-light.png'
import authV2LoginBgYellowDark from '@images/dstipro/auth-bg-yellow-dark.png'
import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import { useKeycloakStore } from '@core/stores/keycloakStore'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
......@@ -30,8 +27,6 @@ const authThemeBg = useGenerateImageVariant(
authV2LoginBgYellowDark,
)
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({
layout: 'blank',
unauthenticatedOnly: true,
......
<script setup lang="ts">
import type { VForm } from 'vuetify/components/VForm'
import authV2MaskDark from '@images/dstipro/mask-v2-dark.png'
import authV2MaskLight from '@images/dstipro/mask-v2-light.png'
import authV2LoginBgYellowDark from '@images/dstipro/auth-bg-yellow-dark.png'
import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import { useKeycloakStore } from '@core/stores/keycloakStore'
import SSOButton from '@/views/dstipro/beranda/authentication/SSOButton.vue'
const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
const error = ref<Record<string, any> | null>(null)
const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight,
authV2LoginLogoLight,
)
const authThemeColor = useGenerateColorVariant(
'#4f4f4f',
'transparent',
)
const authThemeBg = useGenerateImageVariant(
authV2LoginBgYellowLight,
authV2LoginBgYellowDark,
)
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({
layout: 'blank',
unauthenticatedOnly: true,
})
const isPasswordVisible = ref(false)
const route = useRoute()
const router = useRouter()
const ability = useAbility()
const errors = ref<Record<string, string | undefined>>({
email: undefined,
password: undefined,
})
const refVForm = ref<VForm>()
const credentials = ref({
email: 'admin@demo.com',
password: 'admin',
})
const rememberMe = ref(false)
const login = async () => {
try {
const res = await $api('/auth/login', {
method: 'POST',
body: {
email: credentials.value.email,
password: credentials.value.password,
},
onResponseError({ response }) {
errors.value = response._data.errors
},
})
const { accessToken, userData, userAbilityRules } = res
useCookie('userAbilityRules').value = userAbilityRules
ability.update(userAbilityRules)
useCookie('userData').value = userData
useCookie('accessToken').value = accessToken
console.log('Ini userdata: ', userData)
console.log('Ini accesstoken: ', accessToken)
console.log('Ini route: ', route.query.to)
// Redirect to `to` query if exist or redirect to index route
// ❗ nextTick is required to wait for DOM updates and later redirect
await nextTick(() => {
router.replace(route.query.to ? String(route.query.to) : '/')
})
}
catch (err) {
console.error(err)
}
}
const onSubmit = () => {
refVForm.value?.validate().then(({ valid: isValid }) => {
if (isValid)
login()
})
}
</script>
<template>
<!-- <VRow no-gutters class="auth-wrapper d-flex align-center"> -->
<body
:style="{ backgroundImage: `url(${authThemeBg})` }"
class="body-2"
>
<section class="global-main-login-component-3">
<div
:style="{ backgroundColor: `${authThemeColor}` }"
class="card-main-login-component-3"
>
<div class="logo-login-component-3">
<VImg
:src="authThemeImg"
max-height="100"
class="image-10-3 lazyload"
/>
</div>
<div class="form-login-component-3">
<VCol
cols="12"
class="text-center"
>
<SSOButton />
</VCol>
</div>
</div>
</section>
</body>
<!-- </VRow> -->
</template>
<style lang="scss">
@use "@core/scss/template/pages/page-auth";
</style>
......@@ -11,15 +11,12 @@ const authThemeImg = authLoginLogo
const authThemeBg = authLoginBg
const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
const error = ref<Record<string, any> | null>(null)
definePageMeta({
layout: 'blank',
unauthenticatedOnly: true,
})
</script>
<template>
......@@ -62,5 +59,4 @@ definePageMeta({
<style lang="scss">
@use "@core/scss/template/pages/page-auth";
</style>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useKeycloakStore } from '@/@core/stores/keycloakStore'
import UserBerita from '@/components/beranda/UserBerita.vue'
import UserLog from '@/components/beranda/UserLog.vue'
import UserJadwal from '@/components/beranda/UserJadwal.vue'
import UserRiwayat from '@/components/beranda/UserRiwayat.vue'
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import UserKeamanan from '@/views/dstipro/beranda/keamanan/index.vue'
import UserPeta from '@/views/dstipro/beranda/peta/index.vue'
......@@ -35,6 +36,7 @@ const tabs = [
{ 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' },
{ title: 'Log Absen', icon: 'ri-history-line', tab: 'log' },
{ title: 'Peta', icon: 'ri-history-line', tab: 'peta' },
......@@ -102,6 +104,11 @@ const navigateTab = (tab: string) => {
<UserRiwayat />
</VWindowItem>
<!-- Jadwal -->
<VWindowItem value="jadwal">
<UserJadwal />
</VWindowItem>
<!-- Log Absen -->
<VWindowItem value="log">
<UserLog />
......
......@@ -79,8 +79,6 @@ onMounted(() => {
location: 'Madinah',
profileImg,
}
console.log('Profile Header Data:', profileHeaderData.value)
})
// Menentukan nilai tanggal lahir berdasarkan kondisi `civitas`
......
......@@ -8,6 +8,11 @@ function login() {
})
}
onMounted(() => {
if (keycloakInstance.authenticated)
router.push('/naputpro/beranda/profile')
})
const { global } = useTheme()
const authProviders = [
......
<script setup lang="ts">
import keycloakInstance from '@/keycloak'
function login() {
keycloakInstance.login({
redirectUri: `${window.location.origin}/naputpro/beranda/profile`,
})
}
</script>
<template>
<VBtn
color="warning"
@click="login"
>
<VIcon
start
icon="ri-login-box-line"
/> Login SSO
</VBtn>
</template>
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!