refactor(task-modal): enhance timeblock handling and improve date formatting utilities

This commit is contained in:
Matej Glasnák committed 2025-10-14 18:04:00 +02:00
1 parent 61fc55d313
commit eaf83bf0cc
2 files changed
+404 -268

No files matched your search

+38 -25
View File
@@ -1,32 +1,45 @@
import moment from 'moment' import moment from 'moment';
export default { export default {
enrichEmployees(employees) { enrichEmployees(employees) {
return employees.map(employee => ({ return employees.map((employee) => ({
...employee, ...employee,
_fullName: `${ employee.name } ${ employee.surname }`, _fullName: `${employee.name} ${employee.surname}`,
_initials: employee.name.charAt(0) + employee.surname.charAt(0) _initials: employee.name.charAt(0) + employee.surname.charAt(0),
})) }));
}, },
formatDateTime(date) { formatDateTime(date) {
moment(date) moment(date);
return moment(date).format('DD.MM.yyyy, HH:mm') return moment(date).format('DD.MM.yyyy, HH:mm');
}, },
formatDateTimeForBackend(date) { formatTimeblockDateTime(timeblock) {
return moment(date).format('YYYY-MM-DD HH:mm:ss') // Handle new format with separate 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 old format
if (timeblock.start) {
return this.formatDateTime(timeblock.start);
}
return '';
},
formatDate(date) { formatDateTimeForBackend(date) {
return moment(date).format('DD.MM.yyyy') return moment(date).format('YYYY-MM-DD HH:mm:ss');
}, },
isBefore(date, compareTo = null) { formatDate(date) {
return moment(date).isBefore(moment(compareTo)) return moment(date).format('DD.MM.yyyy');
}, },
isAfter(date, compareTo = moment()) { isBefore(date, compareTo = null) {
return moment(date).isAfter(moment(compareTo)) return moment(date).isBefore(moment(compareTo));
} },
}
isAfter(date, compareTo = moment()) {
return moment(date).isAfter(moment(compareTo));
},
};
@@ -1,306 +1,429 @@
<template> <template>
<v-row no-gutters> <v-row no-gutters>
<v-col cols="5"> <v-col cols="5">
<v-sheet class="py-3"> <v-sheet class="py-3">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon icon="mdi-calendar-week" size="large"></v-icon> <v-icon icon="mdi-calendar-week" size="large"></v-icon>
<p class="mx-3">Trvanie</p> <p class="mx-3">Trvanie</p>
</div> </div>
</v-sheet> </v-sheet>
</v-col> </v-col>
<v-col cols="7"> <v-col cols="7">
<v-sheet class="py-3"> <v-sheet class="py-3">
<div class="d-flex align-center mb-3" v-if="!isOffice365Task"> <div class="d-flex align-center mb-3" v-if="!isOffice365Task">
<v-icon @click="showModal = true" class="add-date cursor-pointer mr-2" icon="mdi-plus" size="medium" /> <v-icon
<p class="task-badge rounded-pill">Pridať záznam</p> @click="showModal = true"
</div> class="add-date cursor-pointer mr-2"
icon="mdi-plus"
size="medium"
/>
<p class="task-badge rounded-pill">Pridať záznam</p>
</div>
<div v-for="(timeblock, index) in timeblocks" :key="timeblock.id" class="d-flex align-center mb-3"> <div
<v-icon v-if="!isOffice365Task" @click.stop="subtask.id ? deleteTimeblock(timeblock) : deleteTimeblockWithoutId(index)" class="circle-border cursor-pointer mr-2" icon="mdi-minus" size="medium" /> v-for="(timeblock, index) in timeblocks"
<p class="task-badge rounded-pill" :class="{'strike-through': timeblock.is_finished}">{{ tmUtils.formatDateTime(timeblock.start) }} - {{ tmUtils.formatDateTime(timeblock.end) }}</p> :key="timeblock.id"
<v-icon v-if="!isOffice365Task" v-show="subtask.id" @click.stop="toggleTimeblockIsFinished(timeblock)" class="circle-border cursor-pointer ml-2" :class="{'completed': timeblock.is_finished}" icon="mdi-check" size="medium" /> class="d-flex align-center mb-3"
</div> >
</v-sheet> <v-icon
</v-col> v-if="!isOffice365Task"
</v-row> @click.stop="
subtask.id
? deleteTimeblock(timeblock)
: deleteTimeblockWithoutId(index)
"
class="circle-border cursor-pointer mr-2"
icon="mdi-minus"
size="medium"
/>
<p
class="task-badge rounded-pill"
:class="{ 'strike-through': timeblock.is_finished }"
>
{{ tmUtils.formatTimeblockDateTime(timeblock) }} -
{{
timeblock.ends_at_time || tmUtils.formatDateTime(timeblock.end)
}}
</p>
<v-icon
v-if="!isOffice365Task"
v-show="subtask.id"
@click.stop="toggleTimeblockIsFinished(timeblock)"
class="circle-border cursor-pointer ml-2"
:class="{ completed: timeblock.is_finished }"
icon="mdi-check"
size="medium"
/>
</div>
</v-sheet>
</v-col>
</v-row>
<v-dialog max-width="750" v-model="showModal" persistent> <v-dialog max-width="750" v-model="showModal" persistent>
<v-card> <v-card>
<div class="d-flex justify-space-between align-center"> <div class="d-flex justify-space-between align-center">
<h1>Pridať dátum</h1> <h1>Pridať dátum</h1>
<v-icon @click.stop="closeModal()" icon="mdi-close" size="large"></v-icon> <v-icon
</div> @click.stop="closeModal()"
icon="mdi-close"
size="large"
></v-icon>
</div>
<v-card-actions class="mt-10 d-flex flex-column modal-content"> <v-card-actions class="mt-10 d-flex flex-column modal-content">
<Datepicker v-model="form.date" format="dd.MM.yyyy" locale="sk" select-text="Vybrať" cancel-text="Zrušiť" placeholder="Dátum" :auto-apply="true" :enable-time-picker="false" class="mb-5 modal-input" :class="{error: v$.form.date.$errors.length}" /> <Datepicker
<v-text-field v-model="form.starts_at_time" type="time" variant="solo" bg-color="white" :hint="v$.form.starts_at_time.$errors ? errorUtils.parseErrors(v$.form.starts_at_time.$errors) : null" :class="{ error: v$.form.starts_at_time.$errors.length }" class="mb-1 modal-input" label="Začiatok" /> v-model="form.date"
<v-text-field v-model="form.ends_at_time" type="time" variant="solo" bg-color="white" :hint="v$.form.ends_at_time.$errors ? errorUtils.parseErrors(v$.form.ends_at_time.$errors) : null" :class="{ error: v$.form.ends_at_time.$errors.length }" class="modal-input" label="Koniec" /> format="dd.MM.yyyy"
</v-card-actions> locale="sk"
select-text="Vybrať"
cancel-text="Zrušiť"
placeholder="Dátum"
:auto-apply="true"
:enable-time-picker="false"
class="mb-5 modal-input"
:class="{ error: v$.form.date.$errors.length }"
/>
<v-text-field
v-model="form.starts_at_time"
type="time"
variant="solo"
bg-color="white"
:hint="
v$.form.starts_at_time.$errors
? errorUtils.parseErrors(v$.form.starts_at_time.$errors)
: null
"
:class="{ error: v$.form.starts_at_time.$errors.length }"
class="mb-1 modal-input"
label="Začiatok"
/>
<v-text-field
v-model="form.ends_at_time"
type="time"
variant="solo"
bg-color="white"
:hint="
v$.form.ends_at_time.$errors
? errorUtils.parseErrors(v$.form.ends_at_time.$errors)
: null
"
:class="{ error: v$.form.ends_at_time.$errors.length }"
class="modal-input"
label="Koniec"
/>
</v-card-actions>
<v-btn @click="submit()" class="button primary mt-5">Potvrdiť</v-btn> <v-btn @click="submit()" class="button primary mt-5">Potvrdiť</v-btn>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-dialog v-model="visible" max-width="750" persistent> <v-dialog v-model="visible" max-width="750" persistent>
<v-card> <v-card>
<div class="d-flex justify-space-between align-center"> <div class="d-flex justify-space-between align-center">
<h1 class="text-red">Ste si istý, že chcete pridať timeblock po deadline <br> <b class="text-black">z mastertasku {{ subtask?.project_code }} ?</b></h1> <h1 class="text-red">
Ste si istý, že chcete pridať timeblock po deadline <br />
<b class="text-black">z mastertasku {{ subtask?.project_code }} ?</b>
</h1>
<v-icon <v-icon
@click.stop="visible = false" @click.stop="visible = false"
icon="mdi-close" icon="mdi-close"
size="large" size="large"
class="mb-15" class="mb-15"
></v-icon> ></v-icon>
</div> </div>
<v-card-actions class="mt-10 d-flex justify-start align-center mobile-view"> <v-card-actions
<v-btn class="button primary half" variant="flat" @click.stop="confirmSubmit()">ÁNO</v-btn> class="mt-10 d-flex justify-start align-center mobile-view"
<v-btn class="button primary half outline" @click.stop="visible = false">ZRUŠIŤ</v-btn> >
</v-card-actions> <v-btn
</v-card> class="button primary half"
</v-dialog> variant="flat"
@click.stop="confirmSubmit()"
>ÁNO</v-btn
>
<v-btn class="button primary half outline" @click.stop="visible = false"
>ZRUŠIŤ</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>
</template> </template>
<script> <script>
import tmUtils from '@/plugins/tm@_utils/tm.utils' import tmUtils from '@/plugins/tm@_utils/tm.utils';
import errorUtils from '@/plugins/lib@auth/_utils/error.utils' import errorUtils from '@/plugins/lib@auth/_utils/error.utils';
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators' import { required } from '@vuelidate/validators';
import Datepicker from '@vuepic/vue-datepicker' import Datepicker from '@vuepic/vue-datepicker';
import moment from 'moment' import moment from 'moment';
import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin' import modalMixin from '@/plugins/tm@modal/_mixins/modal.mixin';
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex';
export default { export default {
mixins: [ modalMixin ], mixins: [modalMixin],
emits: ['update-subtask'], emits: ['update-subtask'],
components: { components: {
Datepicker Datepicker,
}, },
props: { props: {
timeblocks: { timeblocks: {
type: Array, type: Array,
required: true required: true,
}, },
subtask: { subtask: {
type: Object, type: Object,
required: true required: true,
} },
}, },
data() { data() {
return { return {
v$: useVuelidate(), v$: useVuelidate(),
errorUtils, errorUtils,
tmUtils, tmUtils,
showModal: false, showModal: false,
visible: false, visible: false,
form: { form: {
date: null, date: null,
starts_at_time: '09:00', starts_at_time: '09:00',
ends_at_time: '16:00' ends_at_time: '16:00',
} },
} };
}, },
validations() { validations() {
return { return {
form: { form: {
date: { required }, date: { required },
starts_at_time: { required }, starts_at_time: { required },
ends_at_time: { required } ends_at_time: { required },
} },
} };
}, },
computed: { computed: {
...mapGetters('tm', ['mastertasks']), ...mapGetters('tm', ['mastertasks']),
isOffice365Task() { isOffice365Task() {
return this.subtask.type == 'office365' return this.subtask.type == 'office365';
} },
}, },
methods: { methods: {
async submit() { async submit() {
if (!await this.v$.$validate()) if (!(await this.v$.$validate())) return;
return
const subtask = JSON.parse(JSON.stringify(this.subtask)) const subtask = JSON.parse(JSON.stringify(this.subtask));
const splittedStartsAtTime = this.form.starts_at_time.split(':') const splittedStartsAtTime = this.form.starts_at_time.split(':');
const splittedEndsAtTime = this.form.ends_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 start = moment(this.form.date)
const end = moment(this.form.date).hours(parseInt(splittedEndsAtTime[0])).minutes(parseInt(splittedEndsAtTime[1])).toISOString() .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 = [] } if (!subtask?.timeblocks) {
if (!subtask?.timesheets) { subtask.timesheets = [] } subtask.timeblocks = [];
if (!subtask?.is_finished) { subtask.is_finished = 0 } }
if (!subtask?.timesheets) {
subtask.timesheets = [];
}
if (!subtask?.is_finished) {
subtask.is_finished = 0;
}
subtask.timeblocks.push({ subtask.timeblocks.push({
end: tmUtils.formatDateTimeForBackend(end), end: tmUtils.formatDateTimeForBackend(end),
id: null, id: null,
is_finished: 0, is_finished: 0,
start: tmUtils.formatDateTimeForBackend(start) start: tmUtils.formatDateTimeForBackend(start),
}) });
if(subtask.deadline) subtask.deadline = tmUtils.formatDateTimeForBackend(this._mergeDeadlineTimeDate()) if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
);
const isAfterDeadline = this.isAfterDeadline() const isAfterDeadline = this.isAfterDeadline();
if (isAfterDeadline) { if (isAfterDeadline) {
this.visible = true this.visible = true;
} else { } else {
if (!subtask.id) { if (!subtask.id) {
this.$emit('update-unfinished-subtask', subtask) this.$emit('update-unfinished-subtask', subtask);
} else { } else {
this._update(subtask) this._update(subtask);
} }
} }
}, },
async confirmSubmit() { async confirmSubmit() {
const subtask = JSON.parse(JSON.stringify(this.subtask)) const subtask = JSON.parse(JSON.stringify(this.subtask));
const splittedStartsAtTime = this.form.starts_at_time.split(':') const splittedStartsAtTime = this.form.starts_at_time.split(':');
const splittedEndsAtTime = this.form.ends_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 start = moment(this.form.date)
const end = moment(this.form.date).hours(parseInt(splittedEndsAtTime[0])).minutes(parseInt(splittedEndsAtTime[1])).toISOString() .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 = [] } if (!subtask?.timeblocks) {
if (!subtask?.timesheets) { subtask.timesheets = [] } subtask.timeblocks = [];
if (!subtask?.is_finished) { subtask.is_finished = 0 } }
if (!subtask?.timesheets) {
subtask.timesheets = [];
}
if (!subtask?.is_finished) {
subtask.is_finished = 0;
}
subtask.timeblocks.push({ subtask.timeblocks.push({
end: tmUtils.formatDateTimeForBackend(end), end: tmUtils.formatDateTimeForBackend(end),
id: null, id: null,
is_finished: 0, is_finished: 0,
start: tmUtils.formatDateTimeForBackend(start) start: tmUtils.formatDateTimeForBackend(start),
}) });
if(subtask.deadline) subtask.deadline = tmUtils.formatDateTimeForBackend(this._mergeDeadlineTimeDate()) if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
);
if (!subtask.id) { if (!subtask.id) {
this.$emit('update-unfinished-subtask', subtask) this.$emit('update-unfinished-subtask', subtask);
this.closeModal() this.closeModal();
return return;
} }
this.visible = false this.visible = false;
this._update(subtask) this._update(subtask);
}, },
async toggleTimeblockIsFinished(originalTimeblock) { async toggleTimeblockIsFinished(originalTimeblock) {
// TODO: Pridat odpracovany cas modal? // TODO: Pridat odpracovany cas modal?
console.log('toggleTimeblockIsFinished 5', this.subtask) console.log('toggleTimeblockIsFinished 5', this.subtask);
const subtask = JSON.parse(JSON.stringify(this.subtask)) const subtask = JSON.parse(JSON.stringify(this.subtask));
const timeblock = JSON.parse(JSON.stringify(originalTimeblock)) const timeblock = JSON.parse(JSON.stringify(originalTimeblock));
timeblock.is_finished = timeblock.is_finished == 1 ? 0 : 1 timeblock.is_finished = timeblock.is_finished == 1 ? 0 : 1;
try { try {
const {data: newTimeblock} = await this.$axios.put_auth(`v1/timeblocks/${timeblock.id}`, timeblock) const { data: newTimeblock } = await this.$axios.put_auth(
`v1/timeblocks/${timeblock.id}`,
timeblock,
);
subtask.timeblocks = subtask.timeblocks.map(originalTimeblock => { subtask.timeblocks = subtask.timeblocks.map((originalTimeblock) => {
if (originalTimeblock.id == newTimeblock.id) if (originalTimeblock.id == newTimeblock.id) return newTimeblock;
return newTimeblock
return originalTimeblock return originalTimeblock;
}) });
this.$events.emit('on-subtask-edited', subtask) this.$events.emit('on-subtask-edited', subtask);
this.$emit('update-subtask', subtask) this.$emit('update-subtask', subtask);
if (timeblock.is_finished) if (timeblock.is_finished)
this.$events.emit('show-task-add-time-modal', { subtask }) this.$events.emit('show-task-add-time-modal', { subtask });
} catch (error) { } catch (error) {
this.$toast.error(error.message || error) this.$toast.error(error.message || error);
if (process.env.NODE_ENV === 'development') if (process.env.NODE_ENV === 'development')
console.log('[a-task-timeblocks.vue] Error: ', error) console.log('[a-task-timeblocks.vue] Error: ', error);
} }
}, },
async deleteTimeblock({ id }) { async deleteTimeblock({ id }) {
const subtask = JSON.parse(JSON.stringify(this.subtask)) const subtask = JSON.parse(JSON.stringify(this.subtask));
subtask.timeblocks = subtask.timeblocks.filter(timeblock => timeblock.id != id) subtask.timeblocks = subtask.timeblocks.filter(
if(subtask.deadline) subtask.deadline = tmUtils.formatDateTimeForBackend(this._mergeDeadlineTimeDate()) (timeblock) => timeblock.id != id,
);
if (subtask.deadline)
subtask.deadline = tmUtils.formatDateTimeForBackend(
this._mergeDeadlineTimeDate(),
);
this._update(subtask) this._update(subtask);
}, },
deleteTimeblockWithoutId(i) { deleteTimeblockWithoutId(i) {
this.$emit('delete-timeblock') this.$emit('delete-timeblock');
}, },
async _update(data) { async _update(data) {
try { try {
const timesheets = data.timesheets.map((timesheet) => { const timesheets = data.timesheets.map((timesheet) => {
delete timesheet.employee_name delete timesheet.employee_name;
return timesheet return timesheet;
}) });
const { subtask } = await this.$axios.put_auth(`v1/subtasks/${data.id}`, {...data, timesheets}) const { subtask } = await this.$axios.put_auth(
`v1/subtasks/${data.id}`,
{ ...data, timesheets },
);
this.closeModal() this.closeModal();
this.$events.emit('on-subtask-edited', subtask) this.$events.emit('on-subtask-edited', subtask);
} catch (error) { } catch (error) {
this.$toast.error(error.message || error) this.$toast.error(error.message || error);
if (process.env.NODE_ENV === 'development') if (process.env.NODE_ENV === 'development')
console.log('[a-task-timeblocks.vue] Error: ', error) console.log('[a-task-timeblocks.vue] Error: ', error);
} }
}, },
isAfterDeadline() { isAfterDeadline() {
const deadline = this.getDeadline(this.subtask) const deadline = this.getDeadline(this.subtask);
if (!deadline) if (!deadline) return;
return
const mDate = moment(this.form.date) const mDate = moment(this.form.date);
const mDeadline = moment(deadline) const mDeadline = moment(deadline);
const [hours, minutes] = this.form.ends_at_time.split(':').map(Number) const [hours, minutes] = this.form.ends_at_time.split(':').map(Number);
mDate.hours(hours) mDate.hours(hours);
mDate.minutes(minutes) mDate.minutes(minutes);
return mDate.isAfter(mDeadline) return mDate.isAfter(mDeadline);
}, },
getDeadline(subtask) { getDeadline(subtask) {
if (subtask?.deadline) if (subtask?.deadline) return subtask.deadline;
return subtask.deadline
return subtask.master_task_deadline return subtask.master_task_deadline;
}, },
_mergeDeadlineTimeDate() { _mergeDeadlineTimeDate() {
const date = new Date(this.subtask.deadline) const date = new Date(this.subtask.deadline);
const timeParts = this.subtask._deadline_time.split(':') const timeParts = this.subtask._deadline_time.split(':');
date.setHours(parseInt(timeParts[0])) date.setHours(parseInt(timeParts[0]));
date.setMinutes(parseInt(timeParts[1])) date.setMinutes(parseInt(timeParts[1]));
return date return date;
}, },
closeModal() { closeModal() {
this.form = { this.form = {
date: null, date: null,
starts_at_time: '09:00', starts_at_time: '09:00',
ends_at_time: '16:00' ends_at_time: '16:00',
} };
this.showModal = false this.showModal = false;
this.$nextTick(() => this.v$.$reset()) this.$nextTick(() => this.v$.$reset());
} },
} },
} };
</script> </script>
<style lang="sass"> <style lang="sass">