Compare commits
8 Commits
5aeac397a1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e84a700a8b | |||
| 6a70a4a6ae | |||
| bcc56d6dd2 | |||
| e3656203ed | |||
| 54de913c16 | |||
| 0ff842b845 | |||
| 037790a497 | |||
| fb63414d69 |
@@ -1,19 +1,33 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
# Przekieruj /odoo (bez slash) na /odoo/ (z slash)
|
||||
# Przekierowanie bez / na /odoo/
|
||||
location = /odoo {
|
||||
return 301 /odoo/;
|
||||
}
|
||||
|
||||
# Serwowanie plików Vue pod /odoo/
|
||||
# Serwowanie Vue
|
||||
location /odoo/ {
|
||||
alias /usr/share/nginx/html/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /odoo/index.html;
|
||||
}
|
||||
|
||||
# Proxy do backendu (niewidoczny z zewnątrz)
|
||||
# no cache index.html
|
||||
location = /odoo/index.html {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
expires 0;
|
||||
}
|
||||
|
||||
# cache JS/CSS/OBRAZY na długo
|
||||
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
access_log off;
|
||||
expires 1y;
|
||||
alias /usr/share/nginx/html/;
|
||||
}
|
||||
|
||||
# Proxy do backendu
|
||||
location /odoo/api/ {
|
||||
proxy_pass http://backend:8000/;
|
||||
proxy_set_header Host $host;
|
||||
@@ -22,3 +36,4 @@ server {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
980
frontend/package-lock.json
generated
980
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jamescoyle/vue-icon": "^0.1.2",
|
||||
@@ -30,8 +31,10 @@
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-vue": "~10.3.0",
|
||||
"globals": "^16.3.0",
|
||||
"jsdom": "^27.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-vue-devtools": "^8.0.0"
|
||||
"vite-plugin-vue-devtools": "^8.0.0",
|
||||
"vitest": "^4.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/odoo/api', // Twój backend
|
||||
// baseURL: 'http://localhost:8000', // Twój backend
|
||||
})
|
||||
|
||||
// Request interceptor – dodawanie tokena
|
||||
|
||||
@@ -196,14 +196,14 @@ body {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
/*
|
||||
.list-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')}`)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -95,21 +95,40 @@ 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') {
|
||||
return 0
|
||||
}
|
||||
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))) {
|
||||
function calculateWorkedTime(entryTimes, exitTimes) {
|
||||
if (
|
||||
!entryTimes ||
|
||||
!exitTimes ||
|
||||
!Array.isArray(entryTimes) ||
|
||||
!Array.isArray(exitTimes) ||
|
||||
entryTimes.length === 0
|
||||
) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const start = eh * 60 + em
|
||||
const end = xh * 60 + xm
|
||||
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]
|
||||
|
||||
let worked = (end - start) / 60
|
||||
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))) {
|
||||
continue
|
||||
}
|
||||
|
||||
const start = eh * 60 + em
|
||||
const end = xh * 60 + xm
|
||||
totalWorkedMinutes += end - start
|
||||
}
|
||||
|
||||
const worked = totalWorkedMinutes / 60
|
||||
return parseFloat(worked.toFixed(9))
|
||||
}
|
||||
|
||||
@@ -130,23 +149,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.
|
||||
day.exitTime.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, day.exitTime)
|
||||
if (totalHours > 0) {
|
||||
totalHours -= 0.25
|
||||
}
|
||||
day.workedHours = parseFloat(totalHours.toFixed(9))
|
||||
}
|
||||
|
||||
if (isValidWorkDay(day) && !day.isHolidayLeave && !day.isSickLeave) {
|
||||
|
||||
117
frontend/src/utils/utils.spec.js
Normal file
117
frontend/src/utils/utils.spec.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import utils from './utils.js'
|
||||
|
||||
const { calculateDay } = utils
|
||||
|
||||
describe('calculateDay', () => {
|
||||
let day
|
||||
|
||||
beforeEach(() => {
|
||||
day = {
|
||||
entryTime: [],
|
||||
exitTime: [],
|
||||
isHolidayLeave: false,
|
||||
isSickLeave: false,
|
||||
dayOfWeek: 'Pn', // Monday
|
||||
isPublicHoliday: false,
|
||||
workedHours: 0,
|
||||
overtime: 0,
|
||||
}
|
||||
})
|
||||
|
||||
it('should calculate worked hours and overtime for a standard workday', () => {
|
||||
day.entryTime = ['08:00:00']
|
||||
day.exitTime = ['16:30:00'] // 8.5 hours of work
|
||||
const totalHours = calculateDay(day)
|
||||
|
||||
// 8.5 hours - 0.25 break = 8.25 workedHours
|
||||
// 8.25 workedHours - 8 standard hours = 0.25 overtime
|
||||
// totalHours should be workedHours, so 8.25
|
||||
expect(day.workedHours).toBe(8.25)
|
||||
expect(day.overtime).toBe(0.25)
|
||||
expect(totalHours).toBe(8.25)
|
||||
})
|
||||
|
||||
it('should handle multiple entries and exits', () => {
|
||||
day.entryTime = ['08:00:00', '13:00:00']
|
||||
day.exitTime = ['12:00:00', '16:30:00'] // 4h + 3.5h = 7.5h
|
||||
const totalHours = calculateDay(day)
|
||||
|
||||
// 7.5 hours - 0.25 break = 7.25 workedHours
|
||||
// 7.25 workedHours - 8 standard hours = -0.75 overtime
|
||||
expect(day.workedHours).toBe(7.25)
|
||||
expect(day.overtime).toBe(-0.75)
|
||||
expect(totalHours).toBe(7.25)
|
||||
})
|
||||
|
||||
it('should calculate hours correctly when currently clocked in', () => {
|
||||
// Mock Date for consistent results
|
||||
const now = new Date('2025-10-29T12:00:00')
|
||||
vi.setSystemTime(now)
|
||||
|
||||
day.entryTime = ['09:00:00']
|
||||
day.exitTime = [] // Clocked in
|
||||
const totalHours = calculateDay(day)
|
||||
|
||||
// 9:00 to 12:00 is 3 hours
|
||||
// 3 hours - 0.25 break = 2.75 workedHours
|
||||
expect(day.workedHours).toBe(2.75)
|
||||
expect(day.overtime).toBe(2.75 - 8) // -5.25
|
||||
expect(totalHours).toBe(2.75)
|
||||
expect(day.exitTime[0]).toBe('12:00:00')
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should return 0 for a day with no entries', () => {
|
||||
const totalHours = calculateDay(day)
|
||||
expect(day.workedHours).toBe(0)
|
||||
expect(day.overtime).toBe(-8)
|
||||
expect(totalHours).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle holiday leave', () => {
|
||||
day.isHolidayLeave = true
|
||||
const totalHours = calculateDay(day)
|
||||
expect(day.workedHours).toBe(0)
|
||||
expect(day.overtime).toBe(0) // Overtime is just workedHours on non-work days
|
||||
expect(totalHours).toBe(8) // 0 worked + 8 holiday
|
||||
})
|
||||
|
||||
it('should handle sick leave', () => {
|
||||
day.isSickLeave = true
|
||||
const totalHours = calculateDay(day)
|
||||
expect(day.workedHours).toBe(0)
|
||||
expect(day.overtime).toBe(0)
|
||||
expect(totalHours).toBe(6.4) // 0 worked + 6.4 sick
|
||||
})
|
||||
|
||||
it('should handle sick leave and holiday leave', () => {
|
||||
day.isHolidayLeave = true
|
||||
day.isSickLeave = true
|
||||
const totalHours = calculateDay(day)
|
||||
expect(day.workedHours).toBe(0)
|
||||
expect(day.overtime).toBe(0)
|
||||
expect(totalHours).toBe(14.4) // 0 worked + 8 holiday + 6.4 sick
|
||||
})
|
||||
|
||||
it('should not subtract break time if worked hours are 0', () => {
|
||||
day.entryTime = []
|
||||
day.exitTime = []
|
||||
const totalHours = calculateDay(day)
|
||||
expect(day.workedHours).toBe(0)
|
||||
expect(totalHours).toBe(0)
|
||||
})
|
||||
|
||||
it('should calculate overtime as workedHours on a weekend', () => {
|
||||
day.dayOfWeek = 'So' // Sunday
|
||||
day.entryTime = ['10:00:00']
|
||||
day.exitTime = ['12:00:00'] // 2 hours of work
|
||||
const totalHours = calculateDay(day)
|
||||
|
||||
// 2 hours - 0.25 break = 1.75
|
||||
expect(day.workedHours).toBe(1.75)
|
||||
expect(day.overtime).toBe(1.75) // On non-work days, all worked time is overtime
|
||||
expect(totalHours).toBe(1.75)
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
/// <reference types="vitest" />
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ command }) => ({
|
||||
base: '/odoo/',
|
||||
plugins: [vue(), vueDevTools()],
|
||||
plugins: [vue(), ...(command === 'serve' ? [VueDevTools()] : [])],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user