Commit 9d2adbc9 by Nabiilah Putri Safa

Merge branch 'master' into 'naput'

# Conflicts:
#   pages/login.vue
2 parents 584c5823 1069681d
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
min-block-size: 100vh; /* Ensures the element covers the full viewport height */ min-block-size: 100vh; /* Ensures the element covers the full viewport height */
padding-block-start: 10px; padding-block-start: 10px;
// margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin // margin: 5% 5%;marginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmarginmargin
} }
.main-component-margin { .main-component-margin {
...@@ -206,12 +206,11 @@ body { ...@@ -206,12 +206,11 @@ body {
.logo-image-component-login { .logo-image-component-login {
position: static; position: static;
display: flex; display: flex;
display: block;
overflow: hidden; overflow: hidden;
flex-flow: row; flex-flow: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
grid-gap: 0; gap: 0;
padding-block: 0%; padding-block: 0%;
padding-inline: 0; padding-inline: 0;
} }
...@@ -385,6 +384,7 @@ body { ...@@ -385,6 +384,7 @@ body {
position: static; position: static;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
flex-direction: column;
flex-flow: column; flex-flow: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
...@@ -398,6 +398,7 @@ body { ...@@ -398,6 +398,7 @@ body {
object-fit: fill; object-fit: fill;
padding-block: 10%; padding-block: 10%;
padding-inline: 5%; padding-inline: 5%;
text-align: center;
@media (min-width: 768px) { @media (min-width: 768px) {
inline-size: 80%; inline-size: 80%;
...@@ -443,9 +444,9 @@ body { ...@@ -443,9 +444,9 @@ body {
.image-10-3 { .image-10-3 {
margin-block: 80px; margin-block: 80px;
margin-inline: 0; margin-inline: 0;
max-inline-size: 50px; max-inline-size: 80px;
padding-block: 80px; padding-block: 80px;
padding-inline: 30px; padding-inline: 100px;
} }
.logo-login-component-3 { .logo-login-component-3 {
...@@ -482,3 +483,44 @@ body { ...@@ -482,3 +483,44 @@ body {
inline-size: 100%; inline-size: 100%;
} }
} }
// login main
.card-main-login-component4 {
position: static;
display: grid;
overflow: hidden;
flex-flow: row;
align-items: center;
justify-content: center;
border-radius: 1.7rem;
backdrop-filter: blur(5px);
background-color: #fff0;
background-color: white;
box-shadow: 0 12px 16px #0003;
color: #fefce8;
grid-auto-columns: 1fr;
grid-gap: 16px 0;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
margin-inline: 0%;
outline-offset: 0;
padding-block: 0;
padding-inline: 0%;
transform: translate(0);
}
.logo-login-component4 {
position: static;
display: none;
overflow: hidden;
align-items: center;
justify-content: center;
background-color: #ffdc01;
block-size: 100%;
color: #fefce8;
inline-size: 100%;
@media (min-width: 768px) {
display: flex;
}
}
...@@ -9,12 +9,6 @@ defineProps<{ ...@@ -9,12 +9,6 @@ defineProps<{
navItems: HorizontalNavItems navItems: HorizontalNavItems
}>() }>()
// const props = defineProps<{
// navItems: HorizontalNavItems
// }>()
// console.log(props.navItems) // ✅ Now this will work
const configStore = useLayoutConfigStore() const configStore = useLayoutConfigStore()
</script> </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,
building: jadwal.NM_GED,
})),
)
}
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,
building: item.NM_GED,
}))
}
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"
>
<VCard 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">
<i class="ri-time-line" /> {{ 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 class="building">
{{ item.building }}
</div>
</div>
</div>
</div>
</VCard>
</VCard>
</template>
<style scoped>
.timetable {
display: flex;
flex-direction: column;
padding: 0 !important;
margin-block: 0;
margin-block-end: 20px;
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);
padding-inline: 5px;
text-align: center;
}
.time-label,
.day-header {
padding: 10px;
font-size: 14px;
text-align: center;
}
.course-title {
display: flex;
flex-wrap: wrap;
border-block-end: 1px solid;
font-size: 12px;
font-weight: bold;
gap: 4px;
inline-size: 100%; /* Ensures the border spans the entire container */
margin-block-end: 5px;
padding-block-end: 5px;
}
.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;
padding-inline: 5px;
}
.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;
outline: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
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 */
text-overflow: ellipsis; /* Adds "..." when text is too long */
word-break: break-word; /* Allows text to wrap naturally */
}
.time {
font-weight: normal;
text-align: start; /* Aligns time to the left */
}
.room {
overflow: hidden;
font-weight: normal;
text-align: end; /* Aligns room to the right */
text-overflow: ellipsis;
}
.building {
font-weight: normal;
opacity: 0.8;
text-align: start; /* Aligns room to the right */
}
@media screen and (max-width: 1200px) {
.course-title {
border-block-end: 0 solid;
}
.course-header {
overflow-x: hidden; /* Hides horizontal overflow */
text-overflow: ellipsis; /* Adds "..." when text is too long */
word-break: break-word; /* Allows text to wrap naturally */
}
.building,
.time {
display: none;
}
}
@media screen and (max-width: 900px) {
.course-title {
border-block-end: 0 solid;
}
.room {
text-align: start;
}
}
@media screen and (max-width: 600px) {
.room {
display: none;
}
}
</style>
<!-- eslint-disable @typescript-eslint/no-unused-vars --> <!-- eslint-disable @typescript-eslint/no-unused-vars -->
<script setup lang="ts"> <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 authV2LoginBgYellowDark from '@images/dstipro/auth-bg-yellow-dark.png'
import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png' import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png' import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import { useKeycloakStore } from '@core/stores/keycloakStore' import keycloakInstance from '@/keycloak'
import SSOButton from '@/views/dstipro/beranda/authentication/SSOButton.vue'
const keycloakStore = useKeycloakStore() function login() {
const data = ref<Record<string, any> | null>(null) keycloakInstance.login({
const error = ref<Record<string, any> | null>(null) redirectUri: `${window.location.origin}/naputpro/beranda/profile`,
})
}
onMounted(() => {
if (keycloakInstance.authenticated)
router.push('/naputpro/beranda/profile')
})
const authThemeImg = useGenerateImageVariant( const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight, authV2LoginLogoLight,
...@@ -31,77 +32,11 @@ const authThemeBg = useGenerateImageVariant( ...@@ -31,77 +32,11 @@ const authThemeBg = useGenerateImageVariant(
authV2LoginBgYellowDark, authV2LoginBgYellowDark,
) )
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({ definePageMeta({
layout: 'blank', layout: 'blank',
unauthenticatedOnly: true, 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> </script>
<template> <template>
...@@ -117,7 +52,6 @@ const onSubmit = () => { ...@@ -117,7 +52,6 @@ const onSubmit = () => {
<div class="logo-login-component-3"> <div class="logo-login-component-3">
<VImg <VImg
:src="authThemeImg" :src="authThemeImg"
max-height="100"
class="image-10-3 lazyload" class="image-10-3 lazyload"
/> />
</div> </div>
...@@ -126,7 +60,15 @@ const onSubmit = () => { ...@@ -126,7 +60,15 @@ const onSubmit = () => {
cols="12" cols="12"
class="text-center" class="text-center"
> >
<SSOButton /> <VBtn
color="warning"
@click="login"
>
<VIcon
start
icon="ri-login-box-line"
/> Login SSO
</VBtn>
</VCol> </VCol>
</div> </div>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import { VForm } from 'vuetify/components/VForm' 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 authV2LoginBgDark from '@images/dstipro/auth-bg-dark.png'
import authV2LoginBgLight from '@images/dstipro/auth-bg-light.png' import authV2LoginBgLight from '@images/dstipro/auth-bg-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png' import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
import { useKeycloakStore } from '@core/stores/keycloakStore'
const keycloakStore = useKeycloakStore()
const authThemeImg = useGenerateImageVariant( const authThemeImg = useGenerateImageVariant(
authV2LoginLogoLight, authV2LoginLogoLight,
...@@ -28,8 +21,6 @@ const authThemeBg = useGenerateImageVariant( ...@@ -28,8 +21,6 @@ const authThemeBg = useGenerateImageVariant(
authV2LoginBgDark, authV2LoginBgDark,
) )
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({ definePageMeta({
layout: 'blank', layout: 'blank',
unauthenticatedOnly: true, unauthenticatedOnly: true,
......
<script setup lang="ts"> <script setup lang="ts">
import { VForm } from 'vuetify/components/VForm' 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 authV2LoginBgYellowDark from '@images/dstipro/auth-bg-yellow-dark.png'
import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png' import authV2LoginBgYellowLight from '@images/dstipro/auth-bg-yellow-light.png'
import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png' import authV2LoginLogoLight from '@images/dstipro/ui-logo-light.png'
import { useKeycloakStore } from '@core/stores/keycloakStore' import { useKeycloakStore } from '@core/stores/keycloakStore'
import AuthProvider from '@/views/dstipro/beranda/authentication/AuthProvider.vue'
const keycloakStore = useKeycloakStore() const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null) const data = ref<Record<string, any> | null>(null)
...@@ -30,8 +27,6 @@ const authThemeBg = useGenerateImageVariant( ...@@ -30,8 +27,6 @@ const authThemeBg = useGenerateImageVariant(
authV2LoginBgYellowDark, authV2LoginBgYellowDark,
) )
const authThemeMask = useGenerateImageVariant(authV2MaskLight, authV2MaskDark)
definePageMeta({ definePageMeta({
layout: 'blank', layout: 'blank',
unauthenticatedOnly: true, 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>
...@@ -10,15 +10,12 @@ const authThemeImg = authLoginLogo ...@@ -10,15 +10,12 @@ const authThemeImg = authLoginLogo
const authThemeBg = authLoginBg const authThemeBg = authLoginBg
const keycloakStore = useKeycloakStore() const keycloakStore = useKeycloakStore()
const data = ref<Record<string, any> | null>(null)
const error = ref<Record<string, any> | null>(null)
definePageMeta({ definePageMeta({
layout: 'blank', layout: 'blank',
unauthenticatedOnly: true, unauthenticatedOnly: true,
}) })
</script> </script>
<template> <template>
...@@ -27,8 +24,8 @@ definePageMeta({ ...@@ -27,8 +24,8 @@ definePageMeta({
class="body-2" class="body-2"
> >
<section class="global-main-login-component"> <section class="global-main-login-component">
<div class="card-main-login-component"> <div class="card-main-login-component4">
<div class="logo-login-component"> <div class="logo-login-component4">
<VImg <VImg
:src="authThemeImg" :src="authThemeImg"
max-width="650" max-width="650"
...@@ -58,52 +55,4 @@ definePageMeta({ ...@@ -58,52 +55,4 @@ definePageMeta({
<style lang="scss"> <style lang="scss">
@use "@core/scss/template/pages/page-auth"; @use "@core/scss/template/pages/page-auth";
</style>
.form-login-component { \ No newline at end of file
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
// kanan putih
.card-main-login-component {
position: static;
display: grid;
overflow: hidden;
flex-flow: row;
align-items: center;
justify-content: center;
border-radius: 1.7rem;
backdrop-filter: blur(5px);
background-color: #fff0;
background-color: white;
box-shadow: 0 12px 16px #0003;
color: #fefce8;
grid-auto-columns: 1fr;
grid-gap: 16px 0;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
margin-inline: 0%;
outline-offset: 0;
padding-block: 0;
padding-inline: 0%;
transform: translate(0);
}
.logo-login-component {
position: static;
display: none;
overflow: hidden;
align-items: center;
justify-content: center;
background-color: #ffdc01;
block-size: 100%;
color: #fefce8;
inline-size: 100%;
@media (min-width: 768px) {
display: flex;
}
}
</style>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
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 UserBerita from '@/components/beranda/UserBerita.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'
import UserJadwal from '@/components/beranda/UserJadwal.vue'
import UserRiwayat from '@/components/beranda/UserRiwayat.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 UserKeamanan from '@/views/dstipro/beranda/keamanan/index.vue'
import UserPeta from '@/views/dstipro/beranda/peta/index.vue' import UserPeta from '@/views/dstipro/beranda/peta/index.vue'
...@@ -36,6 +37,7 @@ const tabs = [ ...@@ -36,6 +37,7 @@ const tabs = [
{ 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: 'Log Absen', icon: 'ri-history-line', tab: 'log' }, { title: 'Log Absen', icon: 'ri-history-line', tab: 'log' },
{ title: 'Peta', icon: 'ri-history-line', tab: 'peta' }, { title: 'Peta', icon: 'ri-history-line', tab: 'peta' },
...@@ -105,6 +107,11 @@ const navigateTab = (tab: string) => { ...@@ -105,6 +107,11 @@ const navigateTab = (tab: string) => {
<UserRiwayat /> <UserRiwayat />
</VWindowItem> </VWindowItem>
<!-- Jadwal -->
<VWindowItem value="jadwal">
<UserJadwal />
</VWindowItem>
<!-- Log Absen --> <!-- Log Absen -->
<VWindowItem value="log"> <VWindowItem value="log">
<UserLog /> <UserLog />
......
...@@ -37,26 +37,38 @@ const items = ref<Record<string, any>>({}) ...@@ -37,26 +37,38 @@ const items = ref<Record<string, any>>({})
const loading = ref(false) const loading = ref(false)
const errordata = ref('') const errordata = ref('')
const isBirthday = ref(false) const isBirthday = ref(false)
const parkirItems = ref(null)
async function getData() { async function getData() {
loading.value = true loading.value = true
errordata.value = '' errordata.value = ''
try { try {
const apiEndpoint = 'https://api.ui.ac.id/me' const apiEndpoints = [
'https://api.ui.ac.id/me',
const response = await fetch(apiEndpoint, { 'https://api.ui.ac.id/my/parkir/aktif',
headers: { ]
Authorization: `Bearer ${keycloakStore.accessToken}`,
}, const responses = await Promise.all(apiEndpoints.map(endpoint =>
}) fetch(endpoint, {
headers: {
if (!response.ok) Authorization: `Bearer ${keycloakStore.accessToken}`,
throw new Error('Gagal mengambil data') },
const dataku = await response.json() }),
))
for (const response of responses) {
if (!response.ok)
throw new Error('Gagal mengambil data')
}
const [dataku, parkirData] = await Promise.all(responses.map(res => res.json()))
items.value = dataku items.value = dataku
// parkirItems.value = parkirData.data || {} // Store parkir data separately
parkirItems.value = parkirData.data && Object.keys(parkirData.data).length ? parkirData.data : null
} }
catch (err: any) { catch (err) {
errordata.value = err.message || 'Terjadi kesalahan saat mengambil data' errordata.value = err.message || 'Terjadi kesalahan saat mengambil data'
} }
finally { finally {
...@@ -64,6 +76,32 @@ async function getData() { ...@@ -64,6 +76,32 @@ async function getData() {
} }
} }
const daysLeft = computed(() => {
if (!parkirItems.value)
return 0
const endDate = new Date(parkirItems.value.date_end_service)
const now = new Date()
return Math.max(0, Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)))
})
const parkirStatus = computed(() => {
return daysLeft.value > 0 ? 'Parkir Aktif' : 'Parkir Non Aktif'
})
const progress = computed(() => {
if (!parkirItems.value)
return 0
const endDate = new Date(parkirItems.value.date_end_service)
const startDate = new Date(parkirItems.value.date_start_service)
const now = new Date()
// Reverse the calculation: 100% when starting, 0% when ending
return Math.max(0, Math.min(100, ((endDate - now) / (endDate - startDate)) * 100))
})
// Fetch data from API // Fetch data from API
onMounted(() => { onMounted(() => {
getData() getData()
...@@ -79,8 +117,6 @@ onMounted(() => { ...@@ -79,8 +117,6 @@ onMounted(() => {
location: 'Madinah', location: 'Madinah',
profileImg, profileImg,
} }
console.log('Profile Header Data:', profileHeaderData.value)
}) })
// Menentukan nilai tanggal lahir berdasarkan kondisi `civitas` // Menentukan nilai tanggal lahir berdasarkan kondisi `civitas`
...@@ -143,6 +179,10 @@ const ulangTahunBerikutnya = computed(() => { ...@@ -143,6 +179,10 @@ const ulangTahunBerikutnya = computed(() => {
return `${diff.months()} bulan, ${diff.days()} hari` return `${diff.months()} bulan, ${diff.days()} hari`
}) })
const goToParking = () => {
window.open('https://parkir.ui.ac.id/apps/site/index', '_blank')
}
</script> </script>
<template> <template>
...@@ -199,6 +239,57 @@ const ulangTahunBerikutnya = computed(() => { ...@@ -199,6 +239,57 @@ const ulangTahunBerikutnya = computed(() => {
</div> </div>
<div <div
v-if="parkirItems"
class="d-flex align-center gap-x-2"
>
<VIcon
size="24"
icon="ri-car-line"
/>
<VBtn
variant="tonal"
:color="daysLeft > 0 ? 'primary' : 'error'"
@click="goToParking"
>
{{ parkirStatus }}
<VTooltip
activator="parent"
location="top"
>
<div class="d-flex flex-column p-2">
<div class="d-flex align-center gap-x-2">
<VIcon
size="20"
icon="ri-calendar-line"
/>
<span>Mulai: {{ parkirItems.date_start_service }}</span>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="20"
icon="ri-calendar-check-line"
/>
<span>Berakhir: {{ parkirItems.date_end_service }}</span>
</div>
<div class="d-flex align-center gap-x-2">
<VIcon
size="20"
icon="ri-time-line"
/>
<span>{{ daysLeft }} hari tersisa</span>
</div>
<VProgressLinear
:model-value="progress"
color="primary"
height="6"
rounded
/>
</div>
</VTooltip>
</VBtn>
</div>
<div
v-if="!isBirthday" v-if="!isBirthday"
class="d-flex align-center gap-x-2" class="d-flex align-center gap-x-2"
> >
......
...@@ -8,6 +8,11 @@ function login() { ...@@ -8,6 +8,11 @@ function login() {
}) })
} }
onMounted(() => {
if (keycloakInstance.authenticated)
router.push('/naputpro/beranda/profile')
})
const { global } = useTheme() const { global } = useTheme()
const authProviders = [ 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!