Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
dtd
/
civitas.ui
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 1710b41f
authored
Apr 22, 2025
by
Nabiilah Putri Safa
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
edit log
1 parent
17be0b75
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
455 additions
and
332 deletions
components/beranda/UserLog.vue
views/dstipro/beranda/keamanan/index.vue
components/beranda/UserLog.vue
View file @
1710b41
<
script
setup
lang=
"ts"
>
import
{
onMounted
,
ref
,
watchEffect
}
from
'vue'
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStore'
// Store Keycloak
const
keycloakStore
=
useKeycloakStore
()
// Data dan state
const
items
=
ref
<
any
[]
>
([])
const
loading
=
ref
(
false
)
const
searchQuery
=
ref
(
''
)
// Fungsi ambil tanggal dari shift_start atau shift_end
const
getTanggal
=
(
waktu
:
string
)
=>
(
waktu
?
waktu
.
split
(
' '
)[
0
]
:
'-'
)
// Fungsi ambil jam dari start_time atau end_time, kasih "-" kalau kosong
const
getJam
=
(
waktu
:
string
)
=>
{
if
(
!
waktu
)
return
'-'
return
waktu
.
length
>=
5
?
waktu
.
slice
(
0
,
5
)
:
waktu
}
// Fungsi ambil nama hari dari tanggal
const
getNamaHari
=
(
tanggal
:
string
)
=>
{
if
(
!
tanggal
||
tanggal
===
'-'
)
return
'-'
const
date
=
new
Date
(
tanggal
)
if
(
isNaN
(
date
.
getTime
()))
return
'-'
return
new
Intl
.
DateTimeFormat
(
'id-ID'
,
{
weekday
:
'long'
}).
format
(
date
)
}
const
getJadwalShift
=
(
start
:
string
,
end
:
string
)
=>
{
const
jamStart
=
start
&&
start
.
length
>=
5
?
start
.
slice
(
0
,
5
)
:
'-'
const
jamEnd
=
end
&&
end
.
length
>=
5
?
end
.
slice
(
0
,
5
)
:
'-'
return
`
${
jamStart
}
-
${
jamEnd
}
`
<
script
lang=
"ts"
setup
>
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
const
keycloakStore
=
useKeycloakStore
();
const
isAuthenticated
=
computed
(()
=>
keycloakStore
.
authenticated
);
interface
ShiftData
{
shift_date
:
string
;
shift_start
:
string
;
shift_end
:
string
;
shift
:
string
;
start_time
?:
string
;
end_time
?:
string
;
}
const
getStatus
=
(
start
:
string
|
undefined
,
end
:
string
|
undefined
)
=>
{
if
(
!
start
&&
!
end
)
return
'Tidak Ada'
if
(
!
start
||
!
end
)
return
'Belum Hitung'
return
'On Time'
}
// Header tabel
const
logHeaders
=
[
{
title
:
'TANGGAL'
,
key
:
'tanggal'
},
{
title
:
'NAMA HARI'
,
key
:
'namaHari'
},
{
title
:
'SHIFT'
,
key
:
'shift'
},
{
title
:
'JADWAL SHIFT'
,
key
:
'jadwalShift'
},
{
title
:
'MULAI AKTUAL'
,
key
:
'start_time'
},
{
title
:
'SELESAI AKTUAL'
,
key
:
'end_time'
},
{
title
:
'STATUS'
,
key
:
'status'
},
]
// Fungsi ambil data dari API
async
function
getData
()
{
loading
.
value
=
true
items
.
value
=
[]
const
shifts
=
ref
<
ShiftData
[]
>
([]);
const
loading
=
ref
(
false
);
const
error
=
ref
(
""
);
const
searchQuery
=
ref
(
""
);
const
headersShift
=
[
{
title
:
"Tanggal"
,
key
:
"shift_date"
,
sortable
:
true
},
{
title
:
"Nama Hari"
,
key
:
"day_name"
,
sortable
:
false
},
{
title
:
"Shift"
,
key
:
"shift"
,
sortable
:
false
},
{
title
:
"Jadwal Shift"
,
key
:
"shift_schedule"
,
sortable
:
false
},
{
title
:
"Mulai Aktual"
,
key
:
"start_time"
,
sortable
:
false
},
{
title
:
"Selesai Aktual"
,
key
:
"end_time"
,
sortable
:
false
},
{
title
:
"Status"
,
key
:
"status"
,
sortable
:
false
},
];
async
function
fetchShiftData
()
{
loading
.
value
=
true
;
error
.
value
=
""
;
try
{
const
apiEndpoint
=
'https://api.ui.ac.id/my/hr/attendance'
const
apiEndpoint
=
`https://api.ui.ac.id/my/hr/attendance`
;
const
response
=
await
fetch
(
apiEndpoint
,
{
headers
:
{
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
})
})
;
if
(
!
response
.
ok
)
throw
new
Error
(
'Gagal mengambil data'
)
const
dataku
=
await
response
.
json
()
if
(
!
response
.
ok
)
{
throw
new
Error
(
"Gagal fetch data"
);
}
// Tambahkan properti tanggal, namaHari, mulai aktual, dan selesai aktual ke setiap item
items
.
value
=
dataku
.
map
((
item
:
any
)
=>
{
const
tanggal
=
getTanggal
(
item
.
shift_start
)
||
getTanggal
(
item
.
shift_end
)
const
data
=
await
response
.
json
();
return
{
shifts
.
value
=
data
.
map
((
item
:
any
)
=>
({
...
item
,
tanggal
,
namaHari
:
getNamaHari
(
tanggal
),
jadwalShift
:
getJadwalShift
(
item
.
shift_start
,
item
.
shift_end
),
start_time
:
getJam
(
item
.
start_time
),
end_time
:
getJam
(
item
.
end_time
),
status
:
getStatus
(
item
.
start_time
,
item
.
end_time
),
}
})
}
catch
(
err
)
{
console
.
error
(
'Gagal mengambil data:'
,
err
)
}
finally
{
loading
.
value
=
false
shift_start
:
`
${
item
.
shift_date
}
${
item
.
shift_start
||
"00:00"
}
`
,
shift_end
:
`
${
item
.
shift_date
}
${
item
.
shift_end
||
"00:00"
}
`
,
}))
.
sort
((
a
,
b
)
=>
new
Date
(
b
.
shift_date
).
getTime
()
-
new
Date
(
a
.
shift_date
).
getTime
());
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
message
||
"Terjadi kesalahan saat mengambil data"
;
}
finally
{
loading
.
value
=
false
;
}
}
// Fetch data saat mounted
function
getDayName
(
date
:
string
)
{
const
days
=
[
"Minggu"
,
"Senin"
,
"Selasa"
,
"Rabu"
,
"Kamis"
,
"Jumat"
,
"Sabtu"
];
const
dayIndex
=
new
Date
(
date
).
getDay
();
return
days
[
dayIndex
];
}
const
resolveDayColor
=
(
day
:
string
)
=>
{
if
(
day
===
"Sabtu"
||
day
===
"Minggu"
)
return
"error"
;
};
function
getHour
(
time
:
string
|
undefined
)
{
if
(
!
time
)
return
"-"
;
const
parts
=
time
.
split
(
" "
);
return
parts
.
length
===
2
?
parts
[
1
]
:
time
;
}
function
getStatus
(
start
:
string
|
undefined
,
end
:
string
|
undefined
)
{
if
(
!
start
&&
!
end
)
return
"Tidak Ada"
;
if
(
!
start
||
!
end
)
return
"Belum Hitung"
;
return
"On Time"
;
}
const
resolveStatusColor
=
(
status
:
string
)
=>
{
if
(
status
===
"On Time"
)
return
"success"
;
if
(
status
===
"Belum Hitung"
)
return
"primary"
;
if
(
status
===
"Tidak Ada"
)
return
"error"
;
};
const
filteredShifts
=
computed
(()
=>
{
if
(
!
searchQuery
.
value
)
return
shifts
.
value
;
return
shifts
.
value
.
filter
((
shift
)
=>
{
const
dayName
=
getDayName
(
shift
.
shift_date
);
const
status
=
getStatus
(
shift
.
start_time
,
shift
.
end_time
);
return
(
shift
.
shift_date
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
dayName
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
shift
.
shift
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
shift
.
start_time
?.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
shift
.
end_time
?.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
status
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
);
});
});
onMounted
(()
=>
{
keycloakStore
.
refresh
()
getData
()
})
// Auto refresh data saat token berubah
watchEffect
(
async
()
=>
{
if
(
!
keycloakStore
.
accessToken
)
return
await
getData
()
})
const
filteredItems
=
computed
(()
=>
{
if
(
!
searchQuery
.
value
)
return
items
.
value
const
query
=
searchQuery
.
value
.
toLowerCase
()
return
items
.
value
.
filter
(
item
=>
logHeaders
.
some
(
header
=>
item
[
header
.
key
]
&&
String
(
item
[
header
.
key
]).
toLowerCase
().
includes
(
query
),
),
)
})
fetchShiftData
();
});
</
script
>
<
template
>
<VCard
title=
"Log Absen"
class=
"recentnamaHariCard"
<AppCardActions
:title=
"`Log Absen`"
class=
"jadwalShift"
action-collapsed
action-remove
>
<!-- Search Input -->
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<VTextField
v-model=
"searchQuery"
...
...
@@ -145,36 +132,101 @@ const filteredItems = computed(() => {
outlined
/>
</div>
<VDataTable
:headers=
"logHeaders"
:items=
"filteredItems"
hide-default-footer
fixed-header
item-value=
"tanggal"
:sort-by=
"['tanggal']"
:sort-asc=
"[true]"
:headers=
"headersShift"
:items=
"filteredShifts"
:loading=
"loading"
loading-text=
"Memuat data..."
>
<template
#
item
.
namaHari=
"
{ item }">
<!-- Tanggal -->
<template
#
item
.
shift_date=
"
{ item }">
{{
item
.
shift_date
}}
</
template
>
<!-- Nama Hari -->
<
template
#
item
.
day_name=
"{ item }"
>
<VChip
:color=
"
item.namaHari === 'Sabtu' || item.namaHari === 'Minggu' ? 'error' : 'default'
"
:class=
"`text-$
{
item.namaHari === 'Sabtu' || item.namaHari === 'Minggu' ? 'error' : 'default'
}`"
:color=
"
resolveDayColor(getDayName(item.shift_date))
"
:class=
"`text-$
{
resolveDayColor(getDayName(item.shift_date))
}`"
size="small"
class="font-weight-medium"
>
{{
item
.
namaHari
}}
{{
getDayName
(
item
.
shift_date
)
}}
</VChip>
</
template
>
<!-- Jadwal Shift -->
<
template
#
item
.
shift_schedule=
"{ item }"
>
{{
`${getHour(item.shift_start)
}
- ${getHour(item.shift_end)
}
`
}}
<
/template
>
<!--
Mulai
Aktual
-->
<
template
#
item
.
start_time
=
"
{
item
}"
>
{{
getHour
(
item
.
start_time
)
}}
<
/template
>
<!--
Selesai
Aktual
-->
<
template
#
item
.
end_time
=
"
{
item
}"
>
{{
getHour
(
item
.
end_time
)
}}
<
/template
>
<!--
Status
-->
<
template
#
item
.
status
=
"
{
item
}
">
<VChip
:color=
"
item.status === 'Tidak Ada' ? 'error' : 'primary'
"
:class=
"`text-$
{
item.status === 'Tidak Ada' ? 'error' : 'primary'
}`"
:color="
resolveStatusColor
(
getStatus
(
item
.
start_time
,
item
.
end_time
))
"
:class="`text-${
resolveStatusColor(getStatus(item.start_time, item.end_time))
}
`"
size="
small
"
class="
font
-
weight
-
medium
"
>
{{
item
.
status
}}
{{
getStatus
(
item
.
start_time
,
item
.
end_time
)
}}
<
/VChip
>
<
/template
>
<
/VDataTable
>
</
VCard
>
<
/
AppCardActions
>
<
/template
>
<
style
lang
=
"scss"
>
.
jadwalShift
{
.
v
-
table
{
&--
density
-
default
{
.
v
-
table__wrapper
{
table
{
thead
{
th
{
background
-
color
:
#
f5f5f5
;
border
-
block
-
end
:
2
px
solid
#
ddd
;
color
:
#
2
c2c2c
;
font
-
weight
:
600
;
padding
-
block
:
12
px
;
padding
-
inline
:
1.5
em
;
}
}
tbody
{
tr
{
td
{
border
-
block
-
end
:
1
px
solid
#
eee
;
min
-
block
-
size
:
auto
;
padding
-
block
:
8
px
;
padding
-
inline
:
1
em
;
vertical
-
align
:
top
;
&
.
vti
-
table__td
--
Jadwal
{
color
:
#
4
a4a4a
;
font
-
weight
:
500
;
}
}
}
}
}
}
}
}
}
.
search
-
container
{
display
:
flex
;
justify
-
content
:
flex
-
end
;
}
<
/style
>
views/dstipro/beranda/keamanan/index.vue
View file @
1710b41
...
...
@@ -2,231 +2,302 @@
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
const
keycloakStore
=
useKeycloakStore
();
const
isAuthenticated
=
computed
(()
=>
keycloakStore
.
authenticated
);
interface
ShiftData
{
shift_start
:
string
;
shift_end
:
string
;
shift
:
string
;
start_time
?:
string
;
end_time
?:
string
;
}
const
shifts
=
ref
<
ShiftData
[]
>
([]);
const
loading
=
ref
(
false
);
const
error
=
ref
(
""
);
const
searchQuery
=
ref
(
""
);
const
headersShift
=
[
{
title
:
"Tanggal"
,
key
:
"shift_start"
,
sortable
:
true
},
{
title
:
"Nama Hari"
,
key
:
"day_name"
,
sortable
:
false
},
{
title
:
"Shift"
,
key
:
"shift"
,
sortable
:
false
},
{
title
:
"Jadwal Shift"
,
key
:
"shift_schedule"
,
sortable
:
false
},
{
title
:
"Mulai Aktual"
,
key
:
"start_time"
,
sortable
:
false
},
{
title
:
"Selesai Aktual"
,
key
:
"end_time"
,
sortable
:
false
},
{
title
:
"Status"
,
key
:
"status"
,
sortable
:
false
},
const
isCurrentPasswordVisible
=
ref
(
false
);
const
isNewPasswordVisible
=
ref
(
false
);
const
isConfirmPasswordVisible
=
ref
(
false
);
const
currentPassword
=
ref
(
""
);
const
newPassword
=
ref
(
""
);
const
newPasswordError
=
ref
(
""
);
const
confirmPassword
=
ref
(
""
);
const
isSubmitting
=
ref
(
false
);
watch
(
newPassword
,
(
newValue
)
=>
{
if
(
currentPassword
.
value
&&
newValue
===
currentPassword
.
value
)
{
newPasswordError
.
value
=
"Kata sandi baru tidak boleh sama dengan kata sandi lama."
;
}
else
{
newPasswordError
.
value
=
""
;
}
});
const
passwordRequirements
=
[
"Panjang minimal 8 karakter, maksimal 20 karakter"
,
"Minimal satu karakter huruf besar"
,
"Minimal satu angka"
,
"Minimal satu simbol, atau karakter spasi"
,
];
// Aturan Validasi
const
oldPasswordRules
=
[
(
v
:
string
)
=>
!!
v
||
"Konfirmasi kata sandi diperlukan"
,
];
async
function
fetchShiftData
()
{
loading
.
value
=
true
;
error
.
value
=
""
;
const
passwordRules
=
[
(
v
:
string
)
=>
!!
v
||
"Kata sandi diperlukan"
,
(
v
:
string
)
=>
v
.
length
>=
8
||
"Kata sandi minimal terdiri dari 8 karakter"
,
(
v
:
string
)
=>
/
[
a-z
]
/
.
test
(
v
)
||
"Kata sandi setidaknya mengandung satu huruf kecil"
,
(
v
:
string
)
=>
/
[
A-Z
]
/
.
test
(
v
)
||
"Kata sandi setidaknya mengandung satu huruf besar"
,
(
v
:
string
)
=>
/
\d
/
.
test
(
v
)
||
"Kata sandi setidaknya berisi satu angka"
,
(
v
:
string
)
=>
/
[
!"#$%&'()*+,-.
/
:;<=>?@[
\\\]
^_`{|}~
]
/
.
test
(
v
)
||
"Kata sandi setidaknya mengandung satu simbol atau spasi"
,
];
const
confirmPasswordRules
=
[
(
v
:
string
)
=>
!!
v
||
"Konfirmasi kata sandi diperlukan"
,
(
v
:
string
)
=>
v
===
newPassword
.
value
||
"Kata sandi tidak cocok"
,
];
// Generate Password
function
generatePassword
(
length
:
number
=
10
):
string
{
const
lowercase
=
"abcdefghijklmnopqrstuvwxyz"
;
const
uppercase
=
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
;
const
numbers
=
"0123456789"
;
const
symbols
=
` !"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`
;
// At least one character from each required category
const
mandatoryCharacters
=
[
lowercase
[
Math
.
floor
(
Math
.
random
()
*
lowercase
.
length
)],
uppercase
[
Math
.
floor
(
Math
.
random
()
*
uppercase
.
length
)],
numbers
[
Math
.
floor
(
Math
.
random
()
*
numbers
.
length
)],
symbols
[
Math
.
floor
(
Math
.
random
()
*
symbols
.
length
)],
];
// Combine all character sets into one charset
const
allCharacters
=
lowercase
+
uppercase
+
numbers
+
symbols
;
const
remainingLength
=
length
-
mandatoryCharacters
.
length
;
let
password
=
mandatoryCharacters
;
// Fill the rest of the password with random characters from the combined charset
for
(
let
i
=
0
;
i
<
remainingLength
;
i
++
)
{
const
randomChar
=
allCharacters
[
Math
.
floor
(
Math
.
random
()
*
allCharacters
.
length
)];
password
.
push
(
randomChar
);
}
// Shuffle the password to ensure randomness
password
=
password
.
sort
(()
=>
Math
.
random
()
-
0.5
);
// Return the password as a string
return
password
.
join
(
""
);
}
// fungsi untuk encoding Base64
function
toBase64
(
str
:
string
)
{
return
btoa
(
unescape
(
encodeURIComponent
(
str
)));
}
// Set Password
async
function
setPassword
()
{
isSubmitting
.
value
=
true
;
if
(
isEmpty
(
currentPassword
.
value
))
{
alert
(
"Kata Sandi Lama tidak boleh kosong"
);
return
;
}
// Buat objek data dengan password yang sudah di-encoding
const
requestData
=
{
oldPassword
:
toBase64
(
currentPassword
.
value
),
newPassword
:
toBase64
(
newPassword
.
value
),
};
if
(
newPassword
.
value
.
length
<
8
||
!
/
[
a-z
]
/
.
test
(
newPassword
.
value
)
||
!
/
\d
/
.
test
(
newPassword
.
value
)
||
!
/
[
!"#$%&'()*+,-.
/
:;<=>?@[
\]
^_`{|}~
]
/
.
test
(
newPassword
.
value
)
)
{
alert
(
"Kata Sandi harus memiliki minimal 8 karakter, setidaknya satu huruf besar, satu angka, dan satu karakter khusus."
);
return
;
}
if
(
newPassword
.
value
===
currentPassword
.
value
)
{
alert
(
"Kata sandi baru tidak boleh sama dengan kata sandi lama."
);
return
;
}
if
(
newPassword
.
value
!==
confirmPassword
.
value
)
{
alert
(
"Kata Sandi Baru dan Konfirmasi Kata Sandi tidak cocok."
);
return
;
}
try
{
const
apiEndpoint
=
`https://api.ui.ac.id/my/hr/attendance`
;
// Panggil API untuk mengganti password
const
apiEndpoint
=
`https://api.ui.ac.id/my/pw`
;
const
response
=
await
fetch
(
apiEndpoint
,
{
method
:
"POST"
,
headers
:
{
"Content-Type"
:
"application/json"
,
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
body
:
JSON
.
stringify
(
requestData
),
});
// Check for HTTP status code 401 which indicates old password is incorrect
if
(
response
.
status
===
401
)
{
throw
new
Error
(
"Kata sandi lama tidak sesuai"
);
}
if
(
!
response
.
ok
)
{
throw
new
Error
(
"Gagal fetch data"
);
const
errorData
=
await
response
.
json
();
throw
new
Error
(
errorData
.
message
||
"Gagal mengubah password"
);
}
const
data
=
await
response
.
json
();
shifts
.
value
=
data
;
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
message
||
"Terjadi kesalahan saat mengambil data"
;
// Jika berhasil, tampilkan pesan dan reset form
alert
(
"Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru."
);
currentPassword
.
value
=
""
;
newPassword
.
value
=
""
;
confirmPassword
.
value
=
""
;
}
catch
(
error
)
{
// Handle error
if
(
error
instanceof
Error
)
{
alert
(
error
.
message
);
}
else
{
alert
(
"Terjadi kesalahan saat mengubah kata sandi"
);
}
}
finally
{
load
ing
.
value
=
false
;
isSubmitt
ing
.
value
=
false
;
}
}
// function getDayName(date: string) {
// const options: Intl.DateTimeFormatOptions = { weekday: "long" };
// return new Date(date).toLocaleDateString("en-US", options);
// }
function
getDayName
(
date
:
string
)
{
c
onst
days
=
[
"Minggu"
,
"Senin"
,
"Selasa"
,
"Rabu"
,
"Kamis"
,
"Jumat"
,
"Sabtu"
]
;
const
dayIndex
=
new
Date
(
date
).
getDay
()
;
return
days
[
dayIndex
]
;
// Reset input
c
urrentPassword
.
value
=
""
;
newPassword
.
value
=
""
;
confirmPassword
.
value
=
""
;
}
</
script
>
const
resolveDayColor
=
(
day
:
string
)
=>
{
if
(
day
===
"Sabtu"
||
day
===
"Minggu"
)
return
"error"
;
};
function
getHour
(
time
:
string
|
undefined
)
{
if
(
!
time
)
return
"-"
;
const
[
date
,
hour
]
=
time
.
split
(
" "
);
return
hour
;
}
<
template
>
<VRow>
<!-- SECTION: Change Password -->
<VCol
cols=
"12"
>
<VCard>
<VCardItem
class=
"pb-6"
>
<VCardTitle>
Ganti Kata Sandi
</VCardTitle>
</VCardItem>
<VForm>
<VCardText
class=
"pt-0"
>
<!-- 👉 Current Password -->
<VRow>
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 current password -->
<VTextField
v-model=
"currentPassword"
:type=
"isCurrentPasswordVisible ? 'text' : 'password'"
:maxlength=
"20"
:append-inner-icon=
"
isCurrentPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
"
autocomplete=
"on"
label=
"Kata Sandi Lama"
@
click:append-inner=
"
isCurrentPasswordVisible = !isCurrentPasswordVisible
"
:rules=
"oldPasswordRules"
clearable
/>
</VCol>
</VRow>
function
getStatus
(
start
:
string
|
undefined
,
end
:
string
|
undefined
)
{
if
(
!
start
&&
!
end
)
return
"Tidak Ada"
;
if
(
!
start
||
!
end
)
return
"Belum Hitung"
;
return
"On Time"
;
}
<!-- 👉 New Password -->
<VRow>
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 new password -->
<VTextField
v-model=
"newPassword"
:type=
"isNewPasswordVisible ? 'text' : 'password'"
:maxlength=
"20"
:append-inner-icon=
"
isNewPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
"
label=
"Kata Sandi Baru"
autocomplete=
"on"
@
click:append-inner=
"
isNewPasswordVisible = !isNewPasswordVisible
"
:rules=
"passwordRules"
:error-messages=
"newPasswordError"
clearable
/>
</VCol>
const
resolveStatusColor
=
(
status
:
string
)
=>
{
if
(
status
===
"On Time"
)
return
"success"
;
if
(
status
===
"Belum Hitung"
)
return
"primary"
;
if
(
status
===
"Tidak Ada"
)
return
"error"
;
};
const
filteredShifts
=
computed
(()
=>
{
if
(
!
searchQuery
.
value
)
return
shifts
.
value
;
return
shifts
.
value
.
filter
((
shift
)
=>
{
const
dayName
=
getDayName
(
shift
.
shift_start
.
split
(
" "
)[
0
]);
const
status
=
getStatus
(
shift
.
start_time
,
shift
.
end_time
);
return
(
shift
.
shift_start
.
toLowerCase
()
.
includes
(
searchQuery
.
value
.
toLowerCase
())
||
dayName
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
shift
.
shift
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
shift
.
start_time
?.
toLowerCase
()
.
includes
(
searchQuery
.
value
.
toLowerCase
())
||
shift
.
end_time
?.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
||
status
.
toLowerCase
().
includes
(
searchQuery
.
value
.
toLowerCase
())
);
});
});
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 confirm password -->
<VTextField
v-model=
"confirmPassword"
:type=
"isConfirmPasswordVisible ? 'text' : 'password'"
:maxlength=
"20"
:append-inner-icon=
"
isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
"
autocomplete=
"on"
label=
"Konfirmasi Kata Sandi"
@
click:append-inner=
"
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
:rules=
"confirmPasswordRules"
clearable
/>
</VCol>
</VRow>
</VCardText>
onMounted
(()
=>
{
fetchShiftData
();
});
</
script
>
<!-- 👉 Password Requirements -->
<VCardText>
<h6
class=
"text-h6 text-medium-emphasis mt-1"
>
Persyaratan Kata Sandi:
</h6>
<
template
>
<AppCardActions
:title=
"`Log Absen`"
class=
"jadwalShift"
action-collapsed
action-remove
>
<!-- Search Input -->
<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
/>
</div>
<VDataTable
:headers=
"headersShift"
:items=
"filteredShifts"
:loading=
"loading"
loading-text=
"Memuat data..."
>
<template
#
item
.
shift_start=
"
{ item }">
{{
item
.
shift_start
.
split
(
" "
)[
0
]
}}
</
template
>
<
template
#
item
.
day_name=
"{ item }"
>
<VChip
:color=
"resolveDayColor(getDayName(item.shift_start.split(' ')[0]))"
:class=
"`text-$
{resolveDayColor(
getDayName(item.shift_start.split(' ')[0])
)}`"
size="small"
class="font-weight-medium"
>
{{
getDayName
(
item
.
shift_start
.
split
(
" "
)[
0
])
}}
</VChip>
</
template
>
<
template
#
item
.
shift_schedule=
"{ item }"
>
{{
`${getHour(item.shift_start)
}
- ${getHour(item.shift_end)
}
`
}}
<
/template
>
<
template
#
item
.
start_time
=
"
{
item
}"
>
{{
item
.
start_time
?
getHour
(
item
.
start_time
)
:
"-"
}}
<
/template
>
<
template
#
item
.
end_time
=
"
{
item
}"
>
{{
item
.
end_time
?
getHour
(
item
.
end_time
)
:
"-"
}}
<
/template
>
<!--
Status
-->
<
template
#
item
.
status
=
"
{
item
}
">
<VChip
:color="
resolveStatusColor
(
getStatus
(
item
.
start_time
,
item
.
end_time
))
"
:class="`text-${resolveStatusColor(
getStatus(item.start_time, item.end_time)
)
}
`"
size="
small
"
class="
font
-
weight
-
medium
"
>
{{
getStatus
(
item
.
start_time
,
item
.
end_time
)
}}
<
/VChip
>
<
/template
>
<
/VDataTable
>
<
/AppCardActions
>
<
/template
>
<VList>
<VListItem
v-for=
"(item, index) in passwordRequirements"
:key=
"index"
class=
"px-0 mt-n4 mb-n2"
>
<template
#
prepend
>
<VIcon
size=
"8"
icon=
"ri-circle-fill"
color=
"rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity))"
/>
</
template
>
<VListItemTitle
class=
"text-medium-emphasis text-wrap"
>
{{ item }}
</VListItemTitle>
</VListItem>
</VList>
<
style
lang
=
"scss"
>
.
jadwalShift
{
.
v
-
table
{
&--
density
-
default
{
.
v
-
table__wrapper
{
table
{
thead
{
th
{
background
-
color
:
#
f5f5f5
;
border
-
block
-
end
:
2
px
solid
#
ddd
;
color
:
#
2
c2c2c
;
font
-
weight
:
600
;
padding
-
block
:
12
px
;
padding
-
inline
:
1.5
em
;
}
}
tbody
{
tr
{
td
{
border
-
block
-
end
:
1
px
solid
#
eee
;
min
-
block
-
size
:
auto
;
padding
-
block
:
8
px
;
padding
-
inline
:
1
em
;
vertical
-
align
:
top
;
&
.
vti
-
table__td
--
Jadwal
{
color
:
#
4
a4a4a
;
font
-
weight
:
500
;
}
}
}
}
}
}
}
}
}
<!-- 👉 Action Buttons -->
<div
class=
"d-flex flex-wrap gap-4"
>
<VBtn
@
click=
"setPassword"
:disabled=
"isSubmitting"
>
{{
isSubmitting ? "Sedang Memproses..." : "Simpan Perubahan"
}}
</VBtn>
.
search
-
container
{
display
:
flex
;
justify
-
content
:
flex
-
end
;
}
<
/style
>
<VBtn
type=
"reset"
color=
"secondary"
variant=
"outlined"
>
Reset
</VBtn>
<VBtn
color=
"secondary"
variant=
"outlined"
@
click=
"
newPassword = generatePassword();
confirmPassword = newPassword;
"
>
Generate Kata Sandi
</VBtn
>
</div>
</VCardText>
</VForm>
</VCard>
</VCol>
<!-- !SECTION -->
</VRow>
</template>
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment