Compare commits

...

3 Commits

7 changed files with 187 additions and 94 deletions

View File

@@ -196,6 +196,7 @@ body {
height: 200px;
}
/*
.list-header {
position: sticky;
top: 0;
@@ -203,7 +204,6 @@ body {
display: grid;
padding: 10px;
background-color: var(--background-body);
/* grid-template-columns: repeat(8, 1fr); */
grid-template-columns: 1fr 1fr 30px [start-enter] 1.5fr 30px 1.5fr[end-exit] 0.5fr 1fr 1fr 2fr 2fr 2fr 2fr;
grid-template-areas: 'dayOfMonth dayOfWeek gap enter rem-x exit alert holiday sickday worked overtime hours balance';
text-align: center;
@@ -218,7 +218,6 @@ body {
display: grid;
padding: 10px;
height: 40px;
/* grid-template-columns: repeat(8, 1fr); */
grid-template-columns: 1fr 1fr 30px [start-enter] 1.5fr 30px 1.5fr[end-exit] 0.5fr 1fr 1fr 2fr 2fr 2fr 2fr;
grid-template-areas: 'dayOfMonth dayOfWeek gap enter rem-x exit alert holiday sickday worked overtime hours balance';
justify-items: center;
@@ -230,6 +229,37 @@ body {
margin-bottom: 5px;
font-size: 18px;
}
*/
.list,
.list-header,
.list-item {
display: grid;
grid-template-columns: 1fr 1fr 30px 50px [start-enter] 1fr 30px 1fr[end-exit] 50px 0.5fr 1fr 1fr 2fr 2fr 2fr 2fr;
grid-template-areas: 'dayOfMonth dayOfWeek gap left-pag enter rem-x exit right-pag alert holiday sickday worked overtime hours balance';
justify-items: center;
align-items: center;
border-radius: 5px;
padding: 10px;
margin-bottom: 5px;
}
/* nagłówek listy */
.list-header {
position: sticky;
top: 0;
z-index: 10;
background-color: var(--background-body);
text-align: center;
box-shadow: 0 1px 5px 1px rgba(0, 0, 0, 0.1);
}
/* pojedynczy element listy */
.list-item {
height: 40px;
background-color: var(--background-card);
font-size: 18px;
box-shadow: 0 0 5px 1px rgba(0, 0, 0, 0.05);
}
.list-grid-dayOfMonth {
grid-area: dayOfMonth;
@@ -237,6 +267,14 @@ body {
.list-grid-dayOfWeek {
grid-area: dayOfWeek;
}
.list-grid-left {
grid-area: left-pag;
justify-self: end;
}
.list-grid-right {
grid-area: right-pag;
justify-self: start;
}
.list-grid-enter {
grid-area: enter;
}

View File

@@ -1,26 +1,52 @@
<template>
<div class="list-item">
<div class="list list-item">
<!-- Day of month -->
<span class="list-grid-dayOfMonth">{{ day.dayOfMonth }}</span>
<!-- Day of week -->
<span class="badge list-grid-dayOfWeek" :class="dayBadge(day)">{{ day.dayOfWeek }}</span>
<!-- Edit button -->
<div class="enter-exit center" v-if="!entryT && !exitT">
<!-- Add button -->
<div class="enter-exit center" v-if="day.entryTime.length === 0">
<button class="button edit-button" @click="addWorkTime">DODAJ</button>
</div>
<!-- Remove button -->
<div class="list-grid-remove center" v-else>
<button class="button remove-button center" @click="removeWorkTime">
<svg-icon type="mdi" :path="mdiCloseCircleOutline" :size="32" />
</button>
</div>
<!-- Left button -->
<div class="list-grid-left" v-if="day.entryTime.length > 1">
<button class="button remove-button center" @click="prevEntry">
<svg-icon type="mdi" :path="mdiArrowLeftDropCircleOutline" :size="32" />
</button>
</div>
<!-- Entry time -->
<TimeStepper class="list-grid-enter" v-if="entryT" v-model:time="entryT" />
<TimeStepper
class="list-grid-enter"
v-if="day.entryTime[currentIndex]"
:time="day.entryTime[currentIndex]"
@update:time="updateEntryTime(currentIndex, $event)"
/>
<!-- Exit time -->
<TimeStepper class="list-grid-exit" v-if="exitT" v-model:time="exitT" />
<TimeStepper
class="list-grid-exit"
v-if="day.exitTime[currentIndex]"
:time="day.exitTime[currentIndex]"
@update:time="updateExitTime(currentIndex, $event)"
/>
<!-- Right button -->
<div class="list-grid-right" v-if="day.entryTime.length > 1">
<button class="button remove-button center" @click="nextEntry">
<svg-icon type="mdi" :path="mdiArrowRightDropCircleOutline" :size="32" />
</button>
</div>
<!-- Alert -->
<div class="list-grid-alert red center">
@@ -82,64 +108,57 @@
<script setup>
import TimeStepper from './TimeStepper.vue'
import TimeLabel from './TimeLabel.vue'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useAttendanceStore } from '@/stores/attendanceStore'
import SvgIcon from '@jamescoyle/vue-icon'
import { mdiAlertCircleOutline, mdiCloseCircleOutline } from '@mdi/js'
import {
mdiAlertCircleOutline,
mdiCloseCircleOutline,
mdiArrowLeftDropCircleOutline,
mdiArrowRightDropCircleOutline,
} from '@mdi/js'
import utils from '@/utils/utils.js'
// const { day } = defineProps({
// day: Object,
// })
const attendanceStore = useAttendanceStore()
const props = defineProps({ index: Number })
const currentIndex = ref(0)
const day = computed(() => attendanceStore.days[props.index])
const isWorkTime = computed(() => Boolean(day.value.workedHours || utils.isValidWorkDay(day.value)))
const entryT = computed({
get() {
return day.value.entryTime
},
set(newVal) {
// console.log('entryT newVal: ', newVal, 'typeof:', typeof newVal)
if (typeof newVal === 'number' && Number.isNaN(newVal)) {
console.log('entryT newVal NaN')
return
}
console.log('neVal: ', newVal)
const workTime = utils.calculateWorkedTime(newVal, day.value.exitTime)
if (workTime > 0) {
day.value.entryTime = newVal
attendanceStore.updateDay(day.value)
}
},
})
const exitT = computed({
get() {
return day.value.exitTime
},
set(newVal) {
if (typeof newVal === 'number' && Number.isNaN(newVal)) {
console.log('exitT newVal NaN')
return
}
console.log('neVal: ', newVal)
const workTime = utils.calculateWorkedTime(day.value.entryTime, newVal)
if (workTime > 0) {
day.value.exitTime = newVal
attendanceStore.updateDay(day.value)
}
},
})
const updateDay = (currentday) => {
attendanceStore.updateDay(currentday)
}
const updateEntryTime = (index, newTime) => {
// Basic validation before updating
if (
day.value.exitTime[index] &&
utils.calculateWorkedTime([newTime], [day.value.exitTime[index]]) < 0
) {
// Maybe show an error to the user
console.error('Entry time cannot be after exit time.')
return
}
day.value.entryTime[index] = newTime
attendanceStore.updateDay(day.value)
}
const updateExitTime = (index, newTime) => {
// Basic validation before updating
// console.log('exitTime: [index] %d', index)
if (
day.value.entryTime[index] &&
utils.calculateWorkedTime([day.value.entryTime[index]], [newTime]) < 0
) {
console.error('Exit time cannot be before entry time.')
return
}
day.value.exitTime[index] = newTime
attendanceStore.updateDay(day.value)
}
const dayBadge = (currentday) => ({
satturday: currentday.dayOfWeek === 'So',
sunday: currentday.dayOfWeek === 'Nd',
@@ -156,16 +175,30 @@ const isfreeDay = (currentday) => ({
})
const addWorkTime = () => {
console.log('click add')
day.value.entryTime = '07:00'
day.value.exitTime = '15:15'
day.value.entryTime.push('07:00')
day.value.exitTime.push('15:15')
// No need to call updateDay here, let the user fill the times.
// Or maybe call it to reflect a change in workedHours if 00:00-00:00 is 0.
attendanceStore.updateDay(day.value)
}
const removeWorkTime = () => {
console.log('click remove')
day.value.entryTime = ''
day.value.exitTime = ''
day.value.entryTime.splice(currentIndex.value, 1)
day.value.exitTime.splice(currentIndex.value, 1)
attendanceStore.updateDay(day.value)
currentIndex.value = 0
}
const prevEntry = () => {
if (currentIndex.value > 0) {
currentIndex.value--
}
}
const nextEntry = () => {
if (currentIndex.value < day.value.entryTime.length - 1) {
currentIndex.value++
}
}
</script>
@@ -173,7 +206,6 @@ const removeWorkTime = () => {
.enter-exit {
grid-column-start: start-enter;
grid-column-end: end-exit;
/* background-color: blueviolet; */
width: 100%;
height: 100%;
}
@@ -195,16 +227,9 @@ const removeWorkTime = () => {
}
.remove-button {
/* width: 30px; */
/* height: 30px; */
/* font-size: 16px; */
/* font-weight: 600; */
/* padding: auto; */
color: var(--gray-bar);
background-color: transparent;
border: none;
/* border-width: 2px; */
/* border-radius: 50%; */
transition:
color 0.3s ease,
border-color 0.3s ease,

View File

@@ -31,7 +31,7 @@ const dropdownRef = ref(null)
const loading = ref(false)
const select = async (newMonth) => {
console.log('newMonth: ', newMonth)
// console.log('newMonth: ', newMonth)
showMenu.value = false
loading.value = true

View File

@@ -64,7 +64,7 @@ const hour = computed({
// console.log('Hour computed. h:', hourVal, 'm: ', m)
if (typeof newVal !== 'number' || isNaN(newVal)) {
console.log('Hour emit NaN')
console.error('Hour emit NaN')
emit('update:time', NaN)
} else {
emit('update:time', `${String(hourVal).padStart(2, '0')}:${m}`)
@@ -84,7 +84,7 @@ const minute = computed({
// console.log('Minute computed. h:', h, 'm: ', minuteVal)
if (typeof newVal !== 'number' || isNaN(newVal)) {
console.log('Minute emit NaN')
console.error('Minute emit NaN')
emit('update:time', NaN)
} else {
emit('update:time', `${h}:${String(minuteVal).padStart(2, '0')}`)

View File

@@ -23,7 +23,7 @@ export const useAttendanceStore = defineStore('attendanceStore', () => {
const holidayCount = computed(() => days.value.filter((d) => d.isHolidayLeave)?.length || 0)
const leaveDayCount = computed(() => {
console.log('wywylanie leaveFayCount')
// console.log('wywylanie leaveFayCount')
return (
workingDays.value -
days.value.filter(
@@ -54,15 +54,25 @@ export const useAttendanceStore = defineStore('attendanceStore', () => {
for (let d = 1; d <= daysInMonth.value; d++) {
const dateStr = `${year.value}-${month.value.toString().padStart(2, '0')}-${d.toString().padStart(2, '0')}`
const dayData = response.days.find((day) => day.check_in.startsWith(dateStr)) || {}
const dayEntries = response.days.filter(
(day) => day.check_in && day.check_in.startsWith(dateStr),
)
// Sort entries by check-in time to ensure correct order
dayEntries.sort((a, b) => new Date(a.check_in) - new Date(b.check_in))
const entryTimes = dayEntries.map((entry) => utils.extractTimeFromDateString(entry.check_in))
const exitTimes = dayEntries.map((entry) => utils.extractTimeFromDateString(entry.check_out))
const attendance_reason_ids = dayEntries.flatMap((entry) => entry.attendance_reason_ids || [])
const isPublicHoliday = response.public_holidays.some((h) => h.date === dateStr)
const alert = dayData.attendance_reason_ids?.length > 0
const alert = attendance_reason_ids.length > 0
const day = {
dayOfMonth: d,
dayOfWeek: utils.getDayOfWeek(dateStr),
entryTime: utils.extractTimeFromDateString(dayData.check_in),
exitTime: utils.extractTimeFromDateString(dayData.check_out),
entryTime: entryTimes,
exitTime: exitTimes,
isSickLeave: false,
isHolidayLeave: false,
isPublicHoliday: isPublicHoliday,

View File

@@ -95,21 +95,34 @@ function isValidWorkDay(day) {
return day.dayOfWeek !== 'So' && day.dayOfWeek !== 'Nd' && !day.isPublicHoliday
}
function calculateWorkedTime(entryTime, exitTime) {
if (!entryTime || !exitTime || typeof entryTime !== 'string' || typeof exitTime !== 'string') {
function calculateWorkedTime(entryTimes, exitTimes) {
if (!entryTimes || !exitTimes || !Array.isArray(entryTimes) || !Array.isArray(exitTimes) || entryTimes.length === 0) {
return 0
}
let totalWorkedMinutes = 0
for (let i = 0; i < entryTimes.length; i++) {
const entryTime = entryTimes[i]
// Use the corresponding exit time, or skip if it doesn't exist
const exitTime = exitTimes[i]
if (!entryTime || !exitTime || typeof entryTime !== 'string' || typeof exitTime !== 'string') {
continue
}
const [eh, em] = entryTime.split(':').map(Number)
const [xh, xm] = exitTime.split(':').map(Number)
if ([eh, em, xh, xm].some((v) => typeof v !== 'number' || isNaN(v))) {
return 0
continue
}
const start = eh * 60 + em
const end = xh * 60 + xm
totalWorkedMinutes += end - start
}
let worked = (end - start) / 60
const worked = totalWorkedMinutes / 60
return parseFloat(worked.toFixed(9))
}
@@ -130,23 +143,30 @@ function floatHoursToHHMM(decimalHours) {
}
function calculateDay(day) {
let exitTime = day.exitTime
// Create a mutable copy of exit times to potentially add the current time.
const exitTimes = [...day.exitTime]
if (!day.exitTime || typeof day.exitTime !== 'string' || day.exitTime.trim() === '') {
// const now = new Date().toLocaleString('pl-PL', { timeZone: 'Europe/Warsaw' })
// If there's one more entry than exits, it means the user is currently clocked in.
if (day.entryTime.length > 0 && day.entryTime.length === day.exitTime.length + 1) {
const now = new Date()
const hh = String(now.getHours()).padStart(2, '0')
const mm = String(now.getMinutes()).padStart(2, '0')
const ss = String(now.getSeconds()).padStart(2, '0')
exitTime = `${hh}:${mm}:${ss}`
// Add the current time as the "exit" for the last entry.
exitTimes.push(`${hh}:${mm}:${ss}`)
}
if (!day.entryTime || typeof day.entryTime !== 'string' || day.entryTime.trim() === '') {
if (day.entryTime.length === 0) {
day.workedHours = 0
} else {
day.workedHours = calculateWorkedTime(day.entryTime, exitTime) - 0.25
day.workedHours = parseFloat(day.workedHours.toFixed(9))
day.exitTime = exitTime
// The -0.25 for a 15-minute break seems to be a business rule.
// It should probably only be subtracted once per day, not per interval.
// Let's subtract it from the total.
let totalHours = calculateWorkedTime(day.entryTime, exitTimes)
if (totalHours > 0) {
totalHours -= 0.25
}
day.workedHours = parseFloat(totalHours.toFixed(9))
}
if (isValidWorkDay(day) && !day.isHolidayLeave && !day.isSickLeave) {

View File

@@ -31,7 +31,7 @@
<div class="card-header">
<h3>Tabela godzin</h3>
</div>
<div class="list-header">
<div class="list list-header">
<span class="list-grid-dayOfMonth">Dzień</span>
<span class="list-grid-dayOfWeek">Dzień tygodnia</span>
<span class="list-grid-enter">Wejscie</span>