refactor(task-modals): updated timeblocks to new local dat format

This commit is contained in:
Matej Glasnák committed 2025-10-14 18:42:07 +02:00
1 parent eaf83bf0cc
commit 0035251080
8 files changed
+1059 -597

No files matched your search

@@ -1,170 +1,244 @@
<template>
<v-row no-gutters>
<v-col cols="5">
<v-sheet class="py-3">
<div class="d-flex align-center">
<v-icon icon="mdi-clock-time-three-outline" size="large"></v-icon>
<p class="mx-3">Odpracovaný čas</p>
</div>
</v-sheet>
</v-col>
<v-row no-gutters>
<v-col cols="5">
<v-sheet class="py-3">
<div class="d-flex align-center">
<v-icon icon="mdi-clock-time-three-outline" size="large"></v-icon>
<p class="mx-3">Odpracovaný čas</p>
</div>
</v-sheet>
</v-col>
<v-col cols="7">
<v-sheet class="py-3">
<p>{{ hours }} h</p>
</v-sheet>
<v-col cols="7">
<v-sheet class="py-3">
<p>{{ hours }} h</p>
</v-sheet>
<v-sheet class="py-3">
<div class="d-flex align-center mb-3">
<v-icon @click="showModal = true" class="add-date cursor-pointer mr-2" icon="mdi-plus" size="medium" />
<p class="task-badge rounded-pill">Pridať záznam</p>
</div>
<v-sheet class="py-3">
<div class="d-flex align-center mb-3">
<v-icon
@click="showModal = true"
class="add-date cursor-pointer mr-2"
icon="mdi-plus"
size="medium"
/>
<p class="task-badge rounded-pill">Pridať záznam</p>
</div>
<template v-if="subtask.timesheets.length">
<div v-for="timesheet in subtask.timesheets" :key="timesheet.id" class="d-flex align-center mb-3">
<v-icon @click.stop="deleteTimesheet(timesheet)" class="circle-border cursor-pointer mr-2" icon="mdi-minus" size="medium" />
<template v-if="subtask.timesheets.length">
<div
v-for="timesheet in subtask.timesheets"
:key="timesheet.id"
class="d-flex align-center mb-3"
>
<v-icon
@click.stop="deleteTimesheet(timesheet)"
class="circle-border cursor-pointer mr-2"
icon="mdi-minus"
size="medium"
/>
<div class="d-flex align-center flex-start width-100">
<p class="task-badge rounded-pill">{{ timesheet.time }}</p>
<!-- <p class="ml-5">{{ timesheet.employee_name }} ({{ moment(timesheet.timestamp).format('D.M.YYYY') }})</p> -->
<p class="ml-5">{{ timesheet.employee_name }} ({{ timesheet.timestamp }})</p>
</div>
</div>
</template>
</v-sheet>
</v-col>
</v-row>
<div class="d-flex align-center flex-start width-100">
<p class="task-badge rounded-pill">{{ timesheet.time }}</p>
<!-- <p class="ml-5">{{ timesheet.employee_name }} ({{ moment(timesheet.timestamp).format('D.M.YYYY') }})</p> -->
<p class="ml-5">
{{ timesheet.employee_name }} ({{ timesheet.timestamp }})
</p>
</div>
</div>
</template>
</v-sheet>
</v-col>
</v-row>
<v-dialog max-width="750" v-model="showModal" persistent>
<v-card>
<div class="d-flex justify-space-between align-center">
<h1>Pridať odpracovaný čas</h1>
<v-dialog max-width="750" v-model="showModal" persistent>
<v-card>
<div class="d-flex justify-space-between align-center">
<h1>Pridať odpracovaný čas</h1>
<v-icon @click.stop="closeModal()" icon="mdi-close" size="large"></v-icon>
</div>
<v-card-actions class="mt-10 d-flex justify-start align-center">
<v-text-field v-model="form.hours" variant="solo" bg-color="white" type="number" :hint="v$.form.hours.$errors ? errorUtils.parseErrors(v$.form.hours.$errors) : null" :class="{ error: v$.form.hours.$errors.length }" class="mb-2" label="Odpracovaný čas" />
</v-card-actions>
<v-btn @click="submit()" class="button primary mt-5">Potvrdiť</v-btn>
</v-card>
</v-dialog>
<v-icon
@click.stop="closeModal()"
icon="mdi-close"
size="large"
></v-icon>
</div>
<v-card-actions class="mt-10 d-flex justify-start align-center">
<v-text-field
v-model="form.hours"
variant="solo"
bg-color="white"
type="number"
:hint="
v$.form.hours.$errors
? errorUtils.parseErrors(v$.form.hours.$errors)
: null
"
:class="{ error: v$.form.hours.$errors.length }"
class="mb-2"
label="Odpracovaný čas"
/>
</v-card-actions>
<v-btn @click="submit()" class="button primary mt-5">Potvrdiť</v-btn>
</v-card>
</v-dialog>
</template>
<script>
import moment from 'moment'
import errorUtils from '@/plugins/lib@auth/_utils/error.utils'
import useVuelidate from '@vuelidate/core'
import { required, numeric, minValue } from '@vuelidate/validators'
import tmUtils from '@/plugins/tm@_utils/tm.utils'
import { mapGetters } from 'vuex'
import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin'
import moment from 'moment';
import errorUtils from '@/plugins/lib@auth/_utils/error.utils';
import useVuelidate from '@vuelidate/core';
import { required, numeric, minValue } from '@vuelidate/validators';
import tmUtils from '@/plugins/tm@_utils/tm.utils';
import { mapGetters } from 'vuex';
import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin';
export default {
mixins: [ modalMixin ],
props: {
hours: {
type: Number,
required: true
},
mixins: [modalMixin],
props: {
hours: {
type: Number,
required: true,
},
subtask: {
type: Object,
required: true
}
},
subtask: {
type: Object,
required: true,
},
},
data() {
return {
moment,
v$: useVuelidate(),
errorUtils,
showModal: false,
form: {
hours: null
}
}
},
data() {
return {
moment,
v$: useVuelidate(),
errorUtils,
showModal: false,
form: {
hours: null,
},
};
},
validations() {
return {
form: {
hours: {required, numeric, minValue: minValue(0.1)}
}
}
},
validations() {
return {
form: {
hours: { required, numeric, minValue: minValue(0.1) },
},
};
},
computed: {
...mapGetters('auth', ['user'])
},
computed: {
...mapGetters('auth', ['user']),
},
methods: {
async submit() {
if (!await this.v$.$validate())
return
methods: {
async submit() {
if (!(await this.v$.$validate())) return;
const subtask = JSON.parse(JSON.stringify(this.subtask))
const subtask = JSON.parse(JSON.stringify(this.subtask));
subtask.timesheets.push({
id: null,
employee_id: this.user.id,
time: typeof this.form.hours == 'string' ? parseFloat(this.form.hours) : this.form.hours
})
subtask.timesheets.push({
id: null,
employee_id: this.user.id,
time:
typeof this.form.hours == 'string'
? parseFloat(this.form.hours)
: this.form.hours,
});
if(subtask.deadline) subtask.deadline = tmUtils.formatDateTimeForBackend(this._mergeDeadlineTimeDate())
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
);
this._update(subtask)
},
this._update(subtask);
},
async deleteTimesheet({ id }) {
const subtask = JSON.parse(JSON.stringify(this.subtask))
subtask.timesheets = subtask.timesheets.filter(timesheet => timesheet.id != id)
if(subtask.deadline) subtask.deadline = tmUtils.formatDateTimeForBackend(this._mergeDeadlineTimeDate())
async deleteTimesheet({ id }) {
const subtask = JSON.parse(JSON.stringify(this.subtask));
subtask.timesheets = subtask.timesheets.filter(
(timesheet) => timesheet.id != id,
);
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
);
this._update(subtask)
},
this._update(subtask);
},
closeModal() {
this.form = {
hours: null
}
closeModal() {
this.form = {
hours: null,
};
this.showModal = false
this.$nextTick(() => this.v$.$reset())
},
this.showModal = false;
this.$nextTick(() => this.v$.$reset());
},
async _update(data) {
try {
data.timesheets = data.timesheets.map((timesheet) => {
delete timesheet.employee_name
return timesheet
})
const { subtask } = await this.$axios.put_auth(`v1/subtasks/${data.id}`, data)
async _update(data) {
try {
const timesheets = data.timesheets.map((timesheet) => {
delete timesheet.employee_name;
return timesheet;
});
this.closeModal()
this.$events.emit('on-subtask-edited', subtask)
} catch (error) {
this.$toast.error(error.message || error)
// Convert timeblocks from internal format to BE format
const timeblocks = (data.timeblocks || [])
.filter((timeblock) => {
// Filter timeblocks that have either old format (date) or new format (start)
return timeblock.date || timeblock.start;
})
.map((timeblock) => {
// If already in BE format (has start field), return as is
if (timeblock.start && !timeblock.date) {
return {
id: timeblock.id || null,
start: timeblock.start,
end: timeblock.end,
is_finished: timeblock.is_finished || 0,
};
}
if (process.env.NODE_ENV === 'development')
console.log('[a-task-spent-time.vue] Error: ', error)
}
},
// Convert from internal format to BE format
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
return {
id: timeblock.id || null,
start: `${dateStr} ${timeblock.starts_at_time}:00`,
end: `${dateStr} ${timeblock.ends_at_time}:00`,
is_finished: timeblock.is_finished || 0,
};
});
_mergeDeadlineTimeDate() {
const date = new Date(this.subtask.deadline)
console.log('a-task-spent-time', this.subtask)
const timeParts = this.subtask._deadline_time.split(':')
const { subtask } = await this.$axios.put_auth(
`v1/subtasks/${data.id}`,
{ ...data, timesheets, timeblocks },
);
date.setHours(parseInt(timeParts[0]))
date.setMinutes(parseInt(timeParts[1]))
this.closeModal();
this.$events.emit('on-subtask-edited', subtask);
} catch (error) {
this.$toast.error(error.message || error);
return date
},
}
}
if (process.env.NODE_ENV === 'development')
console.log('[a-task-spent-time.vue] Error: ', error);
}
},
_mergeDeadlineTimeDate() {
const date = new Date(this.subtask.deadline);
console.log('a-task-spent-time', this.subtask);
const timeParts = this.subtask._deadline_time.split(':');
date.setHours(parseInt(timeParts[0]));
date.setMinutes(parseInt(timeParts[1]));
return date;
},
},
};
</script>
<style lang="sass" scoped>
.width-100
width: 100%
</style>
</style>
@@ -41,10 +41,8 @@
class="task-badge rounded-pill"
:class="{ 'strike-through': timeblock.is_finished }"
>
{{ tmUtils.formatTimeblockDateTime(timeblock) }} -
{{
timeblock.ends_at_time || tmUtils.formatDateTime(timeblock.end)
}}
{{ formatTimeblockStart(timeblock) }} -
{{ formatTimeblockEnd(timeblock) }}
</p>
<v-icon
v-if="!isOffice365Task"
@@ -212,20 +210,35 @@ export default {
},
methods: {
formatTimeblockStart(timeblock) {
// Handle internal format with date and time fields
if (timeblock.date && timeblock.starts_at_time) {
const dateStr = moment(timeblock.date).format('DD.MM.yyyy');
return `${dateStr}, ${timeblock.starts_at_time}`;
}
// Fallback to backend format
if (timeblock.start) {
return tmUtils.formatDateTime(timeblock.start);
}
return '';
},
formatTimeblockEnd(timeblock) {
// Handle internal format with date and time fields
if (timeblock.date && timeblock.ends_at_time) {
return timeblock.ends_at_time;
}
// Fallback to backend format
if (timeblock.end) {
return tmUtils.formatDateTime(timeblock.end);
}
return '';
},
async submit() {
if (!(await this.v$.$validate())) return;
const subtask = JSON.parse(JSON.stringify(this.subtask));
const splittedStartsAtTime = this.form.starts_at_time.split(':');
const splittedEndsAtTime = this.form.ends_at_time.split(':');
const start = moment(this.form.date)
.hours(parseInt(splittedStartsAtTime[0]))
.minutes(parseInt(splittedStartsAtTime[1]))
.toISOString();
const end = moment(this.form.date)
.hours(parseInt(splittedEndsAtTime[0]))
.minutes(parseInt(splittedEndsAtTime[1]))
.toISOString();
if (!subtask?.timeblocks) {
subtask.timeblocks = [];
@@ -237,11 +250,13 @@ export default {
subtask.is_finished = 0;
}
// Add timeblock in internal format
subtask.timeblocks.push({
end: tmUtils.formatDateTimeForBackend(end),
id: null,
date: this.form.date,
starts_at_time: this.form.starts_at_time,
ends_at_time: this.form.ends_at_time,
is_finished: 0,
start: tmUtils.formatDateTimeForBackend(start),
});
if (subtask.deadline)
@@ -256,6 +271,7 @@ export default {
} else {
if (!subtask.id) {
this.$emit('update-unfinished-subtask', subtask);
this.closeModal();
} else {
this._update(subtask);
}
@@ -264,16 +280,6 @@ export default {
async confirmSubmit() {
const subtask = JSON.parse(JSON.stringify(this.subtask));
const splittedStartsAtTime = this.form.starts_at_time.split(':');
const splittedEndsAtTime = this.form.ends_at_time.split(':');
const start = moment(this.form.date)
.hours(parseInt(splittedStartsAtTime[0]))
.minutes(parseInt(splittedStartsAtTime[1]))
.toISOString();
const end = moment(this.form.date)
.hours(parseInt(splittedEndsAtTime[0]))
.minutes(parseInt(splittedEndsAtTime[1]))
.toISOString();
if (!subtask?.timeblocks) {
subtask.timeblocks = [];
@@ -285,11 +291,13 @@ export default {
subtask.is_finished = 0;
}
// Add timeblock in internal format
subtask.timeblocks.push({
end: tmUtils.formatDateTimeForBackend(end),
id: null,
date: this.form.date,
starts_at_time: this.form.starts_at_time,
ends_at_time: this.form.ends_at_time,
is_finished: 0,
start: tmUtils.formatDateTimeForBackend(start),
});
if (subtask.deadline)
@@ -309,20 +317,48 @@ export default {
async toggleTimeblockIsFinished(originalTimeblock) {
// TODO: Pridat odpracovany cas modal?
console.log('toggleTimeblockIsFinished 5', this.subtask);
const subtask = JSON.parse(JSON.stringify(this.subtask));
const timeblock = JSON.parse(JSON.stringify(originalTimeblock));
timeblock.is_finished = timeblock.is_finished == 1 ? 0 : 1;
try {
// Convert timeblock to backend format before sending
const timeblockForBackend = {
id: timeblock.id,
is_finished: timeblock.is_finished,
};
// If timeblock is in internal format, convert it
if (timeblock.date) {
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
timeblockForBackend.start = `${dateStr} ${timeblock.starts_at_time}:00`;
timeblockForBackend.end = `${dateStr} ${timeblock.ends_at_time}:00`;
} else {
// Already in backend format
timeblockForBackend.start = timeblock.start;
timeblockForBackend.end = timeblock.end;
}
const { data: newTimeblock } = await this.$axios.put_auth(
`v1/timeblocks/${timeblock.id}`,
timeblock,
timeblockForBackend,
);
// Convert returned timeblock from backend format to internal format
const startMoment = moment(newTimeblock.start, 'YYYY-MM-DD HH:mm:ss');
const endMoment = moment(newTimeblock.end, 'YYYY-MM-DD HH:mm:ss');
const convertedTimeblock = {
id: newTimeblock.id,
date: startMoment.toDate(),
starts_at_time: startMoment.format('HH:mm'),
ends_at_time: endMoment.format('HH:mm'),
is_finished: newTimeblock.is_finished,
};
subtask.timeblocks = subtask.timeblocks.map((originalTimeblock) => {
if (originalTimeblock.id == newTimeblock.id) return newTimeblock;
if (originalTimeblock.id == convertedTimeblock.id)
return convertedTimeblock;
return originalTimeblock;
});
@@ -365,9 +401,36 @@ export default {
return timesheet;
});
// Convert timeblocks from internal format to BE format
const timeblocks = (data.timeblocks || [])
.filter((timeblock) => {
// Filter timeblocks that have either old format (date) or new format (start)
return timeblock.date || timeblock.start;
})
.map((timeblock) => {
// If already in BE format (has start field), return as is
if (timeblock.start && !timeblock.date) {
return {
id: timeblock.id || null,
start: timeblock.start,
end: timeblock.end,
is_finished: timeblock.is_finished || 0,
};
}
// Convert from internal format to BE format
const dateStr = moment(timeblock.date).format('YYYY-MM-DD');
return {
id: timeblock.id || null,
start: `${dateStr} ${timeblock.starts_at_time}:00`,
end: `${dateStr} ${timeblock.ends_at_time}:00`,
is_finished: timeblock.is_finished || 0,
};
});
const { subtask } = await this.$axios.put_auth(
`v1/subtasks/${data.id}`,
{ ...data, timesheets },
{ ...data, timesheets, timeblocks },
);
this.closeModal();