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
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
447 additions
and
324 deletions
components/beranda/UserLog.vue
views/dstipro/beranda/keamanan/index.vue
components/beranda/UserLog.vue
View file @
1710b41
<
script
setup
lang=
"ts"
>
<
script
lang=
"ts"
setup
>
import
{
onMounted
,
ref
,
watchEffect
}
from
'vue'
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
import
{
useKeycloakStore
}
from
'@/@core/stores/keycloakStore'
const
keycloakStore
=
useKeycloakStore
();
// Store Keycloak
const
isAuthenticated
=
computed
(()
=>
keycloakStore
.
authenticated
);
const
keycloakStore
=
useKeycloakStore
()
interface
ShiftData
{
// Data dan state
shift_date
:
string
;
const
items
=
ref
<
any
[]
>
([])
shift_start
:
string
;
const
loading
=
ref
(
false
)
shift_end
:
string
;
const
searchQuery
=
ref
(
''
)
shift
:
string
;
start_time
?:
string
;
// Fungsi ambil tanggal dari shift_start atau shift_end
end_time
?:
string
;
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
}
`
}
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
shifts
=
ref
<
ShiftData
[]
>
([]);
const
logHeaders
=
[
const
loading
=
ref
(
false
);
{
title
:
'TANGGAL'
,
key
:
'tanggal'
},
const
error
=
ref
(
""
);
{
title
:
'NAMA HARI'
,
key
:
'namaHari'
},
const
searchQuery
=
ref
(
""
);
{
title
:
'SHIFT'
,
key
:
'shift'
},
{
title
:
'JADWAL SHIFT'
,
key
:
'jadwalShift'
},
const
headersShift
=
[
{
title
:
'MULAI AKTUAL'
,
key
:
'start_time'
},
{
title
:
"Tanggal"
,
key
:
"shift_date"
,
sortable
:
true
},
{
title
:
'SELESAI AKTUAL'
,
key
:
'end_time'
},
{
title
:
"Nama Hari"
,
key
:
"day_name"
,
sortable
:
false
},
{
title
:
'STATUS'
,
key
:
'status'
},
{
title
:
"Shift"
,
key
:
"shift"
,
sortable
:
false
},
]
{
title
:
"Jadwal Shift"
,
key
:
"shift_schedule"
,
sortable
:
false
},
{
title
:
"Mulai Aktual"
,
key
:
"start_time"
,
sortable
:
false
},
// Fungsi ambil data dari API
{
title
:
"Selesai Aktual"
,
key
:
"end_time"
,
sortable
:
false
},
async
function
getData
()
{
{
title
:
"Status"
,
key
:
"status"
,
sortable
:
false
},
loading
.
value
=
true
];
items
.
value
=
[]
async
function
fetchShiftData
()
{
loading
.
value
=
true
;
error
.
value
=
""
;
try
{
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
,
{
const
response
=
await
fetch
(
apiEndpoint
,
{
headers
:
{
headers
:
{
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
},
},
})
})
;
if
(
!
response
.
ok
)
if
(
!
response
.
ok
)
{
throw
new
Error
(
'Gagal mengambil data'
)
throw
new
Error
(
"Gagal fetch data"
);
const
dataku
=
await
response
.
json
()
}
// Tambahkan properti tanggal, namaHari, mulai aktual, dan selesai aktual ke setiap item
const
data
=
await
response
.
json
();
items
.
value
=
dataku
.
map
((
item
:
any
)
=>
{
const
tanggal
=
getTanggal
(
item
.
shift_start
)
||
getTanggal
(
item
.
shift_end
)
return
{
shifts
.
value
=
data
.
map
((
item
:
any
)
=>
({
...
item
,
...
item
,
tanggal
,
shift_start
:
`
${
item
.
shift_date
}
${
item
.
shift_start
||
"00:00"
}
`
,
namaHari
:
getNamaHari
(
tanggal
),
shift_end
:
`
${
item
.
shift_date
}
${
item
.
shift_end
||
"00:00"
}
`
,
jadwalShift
:
getJadwalShift
(
item
.
shift_start
,
item
.
shift_end
),
}))
start_time
:
getJam
(
item
.
start_time
),
.
sort
((
a
,
b
)
=>
new
Date
(
b
.
shift_date
).
getTime
()
-
new
Date
(
a
.
shift_date
).
getTime
());
end_time
:
getJam
(
item
.
end_time
),
}
catch
(
err
:
any
)
{
status
:
getStatus
(
item
.
start_time
,
item
.
end_time
),
error
.
value
=
err
.
message
||
"Terjadi kesalahan saat mengambil data"
;
}
}
finally
{
})
loading
.
value
=
false
;
}
catch
(
err
)
{
console
.
error
(
'Gagal mengambil data:'
,
err
)
}
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
(()
=>
{
onMounted
(()
=>
{
keycloakStore
.
refresh
()
fetchShiftData
();
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
),
),
)
})
</
script
>
</
script
>
<
template
>
<
template
>
<VCard
<AppCardActions
title=
"Log Absen"
:title=
"`Log Absen`"
class=
"recentnamaHariCard"
class=
"jadwalShift"
action-collapsed
action-remove
>
>
<!-- Search Input -->
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<VTextField
<VTextField
v-model=
"searchQuery"
v-model=
"searchQuery"
...
@@ -145,36 +132,101 @@ const filteredItems = computed(() => {
...
@@ -145,36 +132,101 @@ const filteredItems = computed(() => {
outlined
outlined
/>
/>
</div>
</div>
<VDataTable
<VDataTable
:headers=
"logHeaders"
:headers=
"headersShift"
:items=
"filteredItems"
:items=
"filteredShifts"
hide-default-footer
:loading=
"loading"
fixed-header
loading-text=
"Memuat data..."
item-value=
"tanggal"
:sort-by=
"['tanggal']"
:sort-asc=
"[true]"
>
>
<template
#
item
.
namaHari=
"
{ item }">
<!-- Tanggal -->
<template
#
item
.
shift_date=
"
{ item }">
{{
item
.
shift_date
}}
</
template
>
<!-- Nama Hari -->
<
template
#
item
.
day_name=
"{ item }"
>
<VChip
<VChip
:color=
"
item.namaHari === 'Sabtu' || item.namaHari === 'Minggu' ? 'error' : 'default'
"
:color=
"
resolveDayColor(getDayName(item.shift_date))
"
:class=
"`text-$
{
item.namaHari === 'Sabtu' || item.namaHari === 'Minggu' ? 'error' : 'default'
}`"
:class=
"`text-$
{
resolveDayColor(getDayName(item.shift_date))
}`"
size="small"
size="small"
class="font-weight-medium"
class="font-weight-medium"
>
>
{{
item
.
namaHari
}}
{{
getDayName
(
item
.
shift_date
)
}}
</VChip>
</VChip>
</
template
>
</
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
}
">
<
template
#
item
.
status
=
"
{
item
}
">
<VChip
<VChip
:color=
"
item.status === 'Tidak Ada' ? 'error' : 'primary'
"
:color="
resolveStatusColor
(
getStatus
(
item
.
start_time
,
item
.
end_time
))
"
:class=
"`text-$
{
item.status === 'Tidak Ada' ? 'error' : 'primary'
}`"
:class="`text-${
resolveStatusColor(getStatus(item.start_time, item.end_time))
}
`"
size="
small
"
size="
small
"
class="
font
-
weight
-
medium
"
class="
font
-
weight
-
medium
"
>
>
{{
item
.
status
}}
{{
getStatus
(
item
.
start_time
,
item
.
end_time
)
}}
<
/VChip
>
<
/VChip
>
<
/template
>
<
/template
>
<
/VDataTable
>
<
/VDataTable
>
</
VCard
>
<
/
AppCardActions
>
<
/template
>
<
/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 @@
...
@@ -2,231 +2,302 @@
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
import
{
useKeycloakStore
}
from
"@core/stores/keycloakStore"
;
const
keycloakStore
=
useKeycloakStore
();
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
isCurrentPasswordVisible
=
ref
(
false
);
const
loading
=
ref
(
false
);
const
isNewPasswordVisible
=
ref
(
false
);
const
error
=
ref
(
""
);
const
isConfirmPasswordVisible
=
ref
(
false
);
const
searchQuery
=
ref
(
""
);
const
currentPassword
=
ref
(
""
);
const
newPassword
=
ref
(
""
);
const
headersShift
=
[
const
newPasswordError
=
ref
(
""
);
{
title
:
"Tanggal"
,
key
:
"shift_start"
,
sortable
:
true
},
const
confirmPassword
=
ref
(
""
);
{
title
:
"Nama Hari"
,
key
:
"day_name"
,
sortable
:
false
},
const
isSubmitting
=
ref
(
false
);
{
title
:
"Shift"
,
key
:
"shift"
,
sortable
:
false
},
{
title
:
"Jadwal Shift"
,
key
:
"shift_schedule"
,
sortable
:
false
},
watch
(
newPassword
,
(
newValue
)
=>
{
{
title
:
"Mulai Aktual"
,
key
:
"start_time"
,
sortable
:
false
},
if
(
currentPassword
.
value
&&
newValue
===
currentPassword
.
value
)
{
{
title
:
"Selesai Aktual"
,
key
:
"end_time"
,
sortable
:
false
},
newPasswordError
.
value
=
{
title
:
"Status"
,
key
:
"status"
,
sortable
:
false
},
"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"
,
];
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"
,
];
];
async
function
fetchShiftData
()
{
// Generate Password
loading
.
value
=
true
;
function
generatePassword
(
length
:
number
=
10
):
string
{
error
.
value
=
""
;
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
{
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
,
{
const
response
=
await
fetch
(
apiEndpoint
,
{
method
:
"POST"
,
headers
:
{
headers
:
{
"Content-Type"
:
"application/json"
,
Authorization
:
`Bearer
${
keycloakStore
.
accessToken
}
`
,
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
)
{
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
();
// Jika berhasil, tampilkan pesan dan reset form
shifts
.
value
=
data
;
alert
(
}
catch
(
err
:
any
)
{
"Kata sandi berhasil diubah. Silakan logout dan login kembali dengan kata sandi baru."
error
.
value
=
err
.
message
||
"Terjadi kesalahan saat mengambil data"
;
);
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
{
}
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
)
{
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
[
date
,
hour
]
=
time
.
split
(
" "
);
return
hour
;
}
function
getStatus
(
start
:
string
|
undefined
,
end
:
string
|
undefined
)
{
// Reset input
if
(
!
start
&&
!
end
)
return
"Tidak Ada
"
;
currentPassword
.
value
=
"
"
;
if
(
!
start
||
!
end
)
return
"Belum Hitung
"
;
newPassword
.
value
=
"
"
;
return
"On Time
"
;
confirmPassword
.
value
=
"
"
;
}
}
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
())
);
});
});
onMounted
(()
=>
{
fetchShiftData
();
});
</
script
>
</
script
>
<
template
>
<
template
>
<AppCardActions
<VRow>
:title=
"`Log Absen`"
<!-- SECTION: Change Password -->
class=
"jadwalShift"
<VCol
cols=
"12"
>
action-collapsed
<VCard>
action-remove
<VCardItem
class=
"pb-6"
>
>
<VCardTitle>
Ganti Kata Sandi
</VCardTitle>
<!-- Search Input -->
</VCardItem>
<div
class=
"search-container mb-4 pl-2 pr-2"
>
<VForm>
<VCardText
class=
"pt-0"
>
<!-- 👉 Current Password -->
<VRow>
<VCol
cols=
"12"
md=
"6"
>
<!-- 👉 current password -->
<VTextField
<VTextField
v-model=
"searchQuery"
v-model=
"currentPassword"
label=
"Search"
:type=
"isCurrentPasswordVisible ? 'text' : 'password'"
placeholder=
"Search ..."
:maxlength=
"20"
append-inner-icon=
"ri-search-line"
: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
clearable
single-line
hide-details
dense
outlined
/>
/>
</div>
</VCol>
</VRow>
<VDataTable
<!-- 👉 New Password -->
:headers=
"headersShift"
<VRow>
:items=
"filteredShifts"
<VCol
cols=
"12"
md=
"6"
>
:loading=
"loading"
<!-- 👉 new password -->
loading-text=
"Memuat data..."
<VTextField
>
v-model=
"newPassword"
<template
#
item
.
shift_start=
"
{ item }">
:type=
"isNewPasswordVisible ? 'text' : 'password'"
{{
item
.
shift_start
.
split
(
" "
)[
0
]
}}
:maxlength=
"20"
</
template
>
: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>
<
template
#
item
.
day_name=
"{ item }"
>
<VCol
cols=
"12"
md=
"6"
>
<VChip
<!-- 👉 confirm password -->
:color=
"resolveDayColor(getDayName(item.shift_start.split(' ')[0]))"
<VTextField
:class=
"`text-$
{resolveDayColor(
v-model=
"confirmPassword"
getDayName(item.shift_start.split(' ')[0])
:type=
"isConfirmPasswordVisible ? 'text' : 'password'"
)}`"
:maxlength=
"20"
size="small"
:append-inner-icon=
"
class="font-weight-medium"
isConfirmPasswordVisible ? 'ri-eye-off-line' : 'ri-eye-line'
>
"
{{
getDayName
(
item
.
shift_start
.
split
(
" "
)[
0
])
}}
autocomplete=
"on"
</VChip>
label=
"Konfirmasi Kata Sandi"
</
template
>
@
click:append-inner=
"
isConfirmPasswordVisible = !isConfirmPasswordVisible
"
:rules=
"confirmPasswordRules"
clearable
/>
</VCol>
</VRow>
</VCardText>
<
template
#
item
.
shift_schedule=
"{ item }"
>
<!-- 👉 Password Requirements -->
{{
`${getHour(item.shift_start)
}
- ${getHour(item.shift_end)
}
`
}}
<VCardText>
<
/template
>
<h6
class=
"text-h6 text-medium-emphasis mt-1"
>
Persyaratan Kata Sandi:
</h6>
<
template
#
item
.
start_time
=
"
{
item
}"
>
<VList>
{{
item
.
start_time
?
getHour
(
item
.
start_time
)
:
"-"
}}
<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
>
</
template
>
<VListItemTitle
class=
"text-medium-emphasis text-wrap"
>
{{ item }}
</VListItemTitle>
</VListItem>
</VList>
<
template
#
item
.
end_time
=
"
{
item
}"
>
<!-- 👉 Action Buttons -->
{{
item
.
end_time
?
getHour
(
item
.
end_time
)
:
"-"
}}
<div
class=
"d-flex flex-wrap gap-4"
>
<
/template
>
<VBtn
@
click=
"setPassword"
:disabled=
"isSubmitting"
>
{{
isSubmitting ? "Sedang Memproses..." : "Simpan Perubahan"
}}
</VBtn>
<!--
Status
-->
<VBtn
type=
"reset"
color=
"secondary"
variant=
"outlined"
>
<
template
#
item
.
status
=
"
{
item
}
">
Reset
<VChip
</VBtn>
:color="
resolveStatusColor
(
getStatus
(
item
.
start_time
,
item
.
end_time
))
"
<VBtn
:class="`text-${resolveStatusColor(
color=
"secondary"
getStatus(item.start_time, item.end_time)
variant=
"outlined"
)
}
`"
@
click=
"
size="
small
"
newPassword = generatePassword();
class="
font
-
weight
-
medium
"
confirmPassword = newPassword;
"
>
>
{{
getStatus
(
item
.
start_time
,
item
.
end_time
)
}}
Generate Kata Sandi
</VBtn
<
/VChip
>
>
<
/template
>
</div>
<
/VDataTable
>
</VCardText>
<
/AppCardActions
>
</VForm>
</VCard>
</VCol>
<!-- !SECTION -->
</VRow>
</template>
</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
>
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