Merge pull request 'fix(payperiodpicker, expensedialog): change date picker display from dialog to menu, add transition animations for expense dialog form, move update dialog form into expense item.' (#24) from dev/nicolas/timesheet-gui-refactor into main

Reviewed-on: Targo/targo_frontend#24
This commit is contained in:
Nicolas 2025-11-12 15:29:16 -05:00
commit ce2fd3e024
15 changed files with 386 additions and 364 deletions

View File

@ -39,7 +39,7 @@ body.body--dark {
}
.frosted-glass {
background-color: #FFFA !important;
background-color: #0008 !important;
backdrop-filter: blur(5px);
}

View File

@ -16,13 +16,13 @@ $primary : #30303A;
$secondary : #DAE0E7;
$accent : #0c9a3b;
$dark-shadow-color : #00220f;
$dark-shadow-color : #173625;
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.2);
$elevation-dark-ambient : rgba($dark-shadow-color, 0.2);
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.5);
$elevation-dark-ambient : rgba($dark-shadow-color, 0.3);
$dark-shadow-2 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient;
$dark-shadow-2 : 2px 3px $elevation-dark-umbra, 2px 3px 6px $elevation-dark-penumbra, 2px 3px 14px $elevation-dark-ambient;
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
$input-text-color : #455A64;

View File

@ -1,6 +1,9 @@
<script setup lang="ts">
<script
setup
lang="ts"
>
import { computed, ref } from 'vue';
import { date} from 'quasar';
import { date } from 'quasar';
import { useTimesheetStore } from 'src/stores/timesheet-store';
const NEXT = 1;
@ -10,18 +13,18 @@
const timesheet_store = useTimesheetStore();
const is_showing_calendar_picker = ref(false);
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
const calendar_date = ref(date.formatDate(Date.now(), 'YYYY-MM-DD'));
const is_disabled = computed(() => timesheet_store.pay_period === undefined);
const emit = defineEmits<{
'date-selected': [ value: string ]
'date-selected': [value: string]
'pressed-previous-button': []
'pressed-next-button': []
}>();
const is_previous_pay_period_limit = computed( ()=>
( timesheet_store.pay_period?.pay_year === 2024 &&
timesheet_store.pay_period?.pay_period_no <= 1 ) ?? false
const is_previous_pay_period_limit = computed(() =>
(timesheet_store.pay_period?.pay_year === 2024 &&
timesheet_store.pay_period?.pay_period_no <= 1) ?? false
);
const onDateSelected = (value: string) => {
@ -59,28 +62,30 @@
</script>
<template>
<div class="row" >
<div class="row">
<!-- navigation to previous week -->
<q-btn
push rounded
<q-btn
push
rounded
icon="keyboard_arrow_left"
color="accent"
@click="getPreviousPayPeriod"
:disable="is_previous_pay_period_limit || timesheet_store.is_loading || is_disabled"
class="q-mr-sm q-px-sm"
>
<q-tooltip
<q-tooltip
anchor="top middle"
self="center middle"
class="bg-primary text-uppercase text-weight-bold"
>
{{ $t( 'timesheet.nav_button.previous_week' )}}
>
{{ $t('timesheet.nav_button.previous_week') }}
</q-tooltip>
</q-btn>
<!-- navigation through calendar date picker -->
<q-btn
push rounded
<q-btn
push
rounded
icon="calendar_month"
color="accent"
@click="is_showing_calendar_picker = true"
@ -94,11 +99,29 @@
>
{{ $t('timesheet.nav_button.calendar_date_picker') }}
</q-tooltip>
<!-- date picker calendar -->
<q-menu
anchor="bottom middle"
self="top middle"
:offset="[0, 10]"
class="shadow-24"
>
<q-date
v-model="calendar_date"
color="primary"
today-btn
mask="YYYY-MM-DD"
:options="date => date >= PAY_PERIOD_DATE_LIMIT"
@update:model-value="onDateSelected"
/>
</q-menu>
</q-btn>
<!-- navigation to next week -->
<q-btn
push rounded
<q-btn
push
rounded
icon="keyboard_arrow_right"
color="accent"
@click="getNextPayPeriod"
@ -113,21 +136,4 @@
</q-tooltip>
</q-btn>
</div>
<!-- date picker calendar -->
<q-dialog
v-model="is_showing_calendar_picker"
transition-show="jump-down"
transition-hide="jump-up"
position="top">
<q-date
v-model="calendar_date"
color="primary"
class="q-mt-xl"
today-btn
mask="YYYY-MM-DD"
:options="date => date >= PAY_PERIOD_DATE_LIMIT"
@update:model-value="onDateSelected"
/>
</q-dialog>
</template>

View File

@ -2,17 +2,19 @@
setup
lang="ts"
>
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useExpensesStore } from 'src/stores/expense-store';
import { Expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { date } from 'quasar';
import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useUiStore } from 'src/stores/ui-store';
import { useExpensesStore } from 'src/stores/expense-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
import { Expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
const { t } = useI18n();
const ui_store = useUiStore();
const timesheet_store = useTimesheetStore();
const expenses_store = useExpensesStore();
const expenses_api = useExpensesApi();
@ -23,11 +25,16 @@
const employee_email = inject<string>('employeeEmail');
const rules = useExpenseRules(t);
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
const openDatePicker = () => {
is_navigator_open.value = true;
if (expenses_store.current_expense.date === '') {
expenses_store.current_expense.date = date.formatDate(new Date(), 'YYYY-MM-DD');
if (timesheet_store.pay_period !== undefined) {
expenses_store.current_expense.date = timesheet_store.pay_period.period_start;
}
console.log('current pay period start date: ', period_start_date.value);
console.log('current pay period end date: ', period_end_date.value);
};
const cancelUpdateMode = () => {
@ -40,12 +47,6 @@
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
};
const getExpenseCalendarRange = (current_date: string) => {
const period = timesheet_store.pay_period;
if (period !== undefined) return current_date >= period.period_start && current_date <= period.period_end;
return false;
}
</script>
<template>
@ -55,7 +56,10 @@
flat
@submit.prevent="requestExpenseCreationOrUpdate"
>
<div class="text-uppercase text-weight-medium q-pt-sm q-px-lg">
<div
class="text-uppercase text-weight-medium q-pt-sm q-px-lg q-ma-sm"
:class="expenses_store.mode === 'create' ? '' : 'invisible'"
>
{{ $t('timesheet.expense.add_expense') }}
</div>
<div class="row justify-between items-start rounded-5 q-px-lg q-pb-sm">
@ -66,8 +70,10 @@
outlined
readonly
stack-label
class="col q-px-xs"
color="primary"
class="col q-px-xs"
input-class="text-weight-medium"
input-style="font-size: 1.2em;"
:label="$t('timesheet.expense.date')"
>
<template #prepend>
@ -89,11 +95,17 @@
v-model="expenses_store.current_expense.date"
mask="YYYY-MM-DD"
event-color="accent"
:options="getExpenseCalendarRange"
:options="date => date >= period_start_date && date <= period_end_date"
@update:model-value="is_navigator_open = false"
/>
</q-dialog>
</template>
<template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.date') }}
</span>
</template>
</q-input>
<!-- expenses type selection -->
@ -103,8 +115,8 @@
standout="bg-blue-grey-9"
dense
emit-value
map-options
hide-dropdown-icon
label-slot
class="col q-px-xs"
color="primary"
:label="$t('timesheet.expense.type')"
@ -115,7 +127,26 @@
popup-content-style="border: 2px solid var(--q-accent)"
:rules="[rules.typeRequired]"
:option-label="label => $t(`timesheet.expense.types.${label}`)"
/>
>
<template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.type') }}
</span>
</template>
<template #selected-item="scope">
<div
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
:tabindex="scope.tabindex"
>
<span
style="line-height: 0.9em;"
class="col-auto ellipsis"
>{{ scope.opt.label }}</span>
</div>
</template>
</q-select>
<!-- amount input -->
<div v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')">
@ -207,6 +238,12 @@
color="primary"
/>
</template>
<template #label>
<span class="text-weight-bold text-accent text-uppercase text-caption">
{{ $t('timesheet.expense.hints.attach_file') }}
</span>
</template>
</q-file>
</div>
<div class="col row full-width items-center">

View File

@ -12,6 +12,7 @@
import { useAuthStore } from 'src/stores/auth-store';
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
import { Expense } from 'src/modules/timesheets/models/expense.models';
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
const { expense, horizontal = false } = defineProps<{
expense: Expense;
@ -29,6 +30,7 @@
const background_style = computed(() => deepEqual(expense, expenses_store.current_expense) ? 'border: 3px solid var(--q-accent);' : '');
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
const is_showing_update_form = ref(false);
const requestExpenseDeletion = async () => {
await expenses_api.deleteExpenseById(expense.id);
@ -45,28 +47,27 @@
if (deepEqual(expense, expenses_store.current_expense)) {
expenses_store.mode = 'create';
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
is_showing_update_form.value = false;
return;
}
expenses_store.mode = 'update';
expenses_store.current_expense = expense;
expenses_store.initial_expense = unwrapAndClone(expense);
is_showing_update_form.value = true;
}
</script>
<template>
<transition
enter-active-class="animated pulse"
mode="out-in"
<q-item
:key="refresh_key"
:clickable="horizontal"
class="column col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
:class="background_class + approved_class"
:style="background_style"
@click="onExpenseClicked"
>
<q-item
:key="refresh_key"
:clickable="horizontal"
class="row col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
:class="background_class + approved_class"
:style="background_style"
@click="onExpenseClicked"
>
<div class="row full-width items-center">
<!-- avatar type icon section -->
<q-item-section avatar>
<q-icon
@ -194,6 +195,13 @@
@click.stop="requestExpenseDeletion"
/>
</q-item-section>
</q-item>
</transition>
</div>
<q-slide-transition
@hide="expenses_store.is_hiding_create_form = false"
:duration="200"
>
<ExpenseDialogForm v-if="is_showing_update_form && expenses_store.is_hiding_create_form" />
</q-slide-transition>
</q-item>
</template>

View File

@ -14,9 +14,10 @@
const expenses_list = computed(() => {
if (timesheet_store.timesheets !== undefined) {
return timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.expenses);
const current_expenses = timesheet_store.timesheets.flatMap(week => week.days).flatMap(day => day.expenses);
console.log('current expenses: ', current_expenses);
return current_expenses;
}
return [];
})
</script>
@ -29,10 +30,12 @@
:class="horizontal ? 'row flex-center' : ''"
>
<q-item-label
v-if="expenses_list.length > 0"
class="text-italic q-px-sm"
v-if="expenses_list.length < 1"
class="text-italic text-center q-pa-sm rounded-4"
>
<q-separator spaced />
{{ $t('timesheet.expense.empty_list') }}
<q-separator spaced />
</q-item-label>
<ExpenseDialogListItem

View File

@ -38,20 +38,9 @@
<ExpenseDialogList />
<transition
appear
enter-active-class="animated fadeInDown faster"
leave-active-class="animated fadeOutDown faster"
mode="out-in"
>
<ExpenseDialogForm v-if="!expense_store.current_expense.is_approved" />
<q-icon
v-else
name="block"
color="negative"
size="lg"
/>
</transition>
<q-slide-transition @hide="expense_store.is_hiding_create_form = true" :duration="200">
<ExpenseDialogForm v-if="!expense_store.current_expense.is_approved && expense_store.mode !== 'update' && expense_store.is_hiding_create_form === false" />
</q-slide-transition>
</q-card-section>
</q-card>
</q-dialog>

View File

@ -1,81 +0,0 @@
<script
setup
lang="ts"
>
import { type Shift, SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
const shift = defineModel<Shift>({ required: true });
defineEmits<{
'onCommentBlur': [void];
}>();
</script>
<template>
<div class="row full-width justify-center">
<div class="col-sm-6 col-md-3 row q-mx-xs q-my-none">
<div class="col-auto column items-center">
<span
class="text-caption q-pa-none q-ma-none"
style="line-height: 0.7em; font-size: 0.7em;"
>{{ $t('timesheet.shift.types.REMOTE') }}</span>
<q-toggle
v-model="shift.is_remote"
class="q-pa-none q-ma-none"
/>
</div>
<q-select
v-model="shift.type"
options-dense
:options="SHIFT_TYPES"
:label="$t('timesheet.shift.types.label')"
class="col q-pa-none"
color="primary"
outlined
dense
square
hide-dropdown-icon
emit-value
map-options
/>
</div>
<div class="col-auto row q-mx-xs">
<q-input
v-model="shift.start_time"
:label="$t('timesheet.shift.fields.start')"
outlined
dense
square
inputmode="numeric"
mask="##:##"
class="col-auto q-mx-xs"
/>
<q-input
v-model="shift.end_time"
:label="$t('timesheet.shift.fields.end')"
outlined
dense
square
inputmode="numeric"
mask="##:##"
class="col-auto q-mx-xs"
/>
</div>
<q-input
v-model="shift.comment"
type="textarea"
autogrow
filled
dense
square
:label="$t('timesheet.shift.fields.header_comment')"
:counter="true"
:maxlength="512"
class="col-grow"
/>
</div>
</template>

View File

@ -14,9 +14,9 @@
const ui_store = useUiStore();
interface ShiftOption {
label: string;
label: string;
value: ShiftType;
icon: string;
icon: string;
icon_color: string;
}
@ -85,7 +85,8 @@
<template>
<q-slide-item
right-color="negative"
class="q-my-xs rounded-5 bg-transparent"
class="rounded-5 bg-transparent"
:class="ui_store.is_mobile_mode ? 'q-my-md' : ''"
@right="details => slideDeleteShift(details.reset)"
>
<template
@ -94,172 +95,226 @@
>
<q-icon name="delete" />
</template>
<div
class="row flex-center text-uppercase rounded-5 bg-transparent"
>
<!-- shift type -->
<q-select
ref="select"
v-model="shift_type_selected"
standout="bg-blue-grey-9"
dense
:readonly="shift.is_approved"
:options-dense="!ui_store.is_mobile_mode"
hide-dropdown-icon
:menu-offset="[0, 10]"
menu-anchor="bottom middle"
menu-self="top middle"
:options="SHIFT_OPTIONS"
class="rounded-5 q-mx-xs bg-dark"
:class="(ui_store.is_mobile_mode ? 'col-12 q-mb-xs ' : 'col ')"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect"
@update:model-value="option => shift.type = option.value"
>
<template #selected-item="scope">
<div
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
:tabindex="scope.tabindex"
>
<q-icon
:name="scope.opt.icon"
:color="scope.opt.icon_color"
size="sm"
class="col-auto q-mx-xs"
/>
<span
style="line-height: 0.9em;"
class="col-auto ellipsis"
>{{ scope.opt.label }}</span>
</div>
</template>
</q-select>
<!-- punch in field -->
<q-input
v-model="shift.start_time"
dense
:readonly="shift.is_approved"
type="time"
:standout="$q.dark.isActive ? 'bg-blue-grey-9' : 'bg-blue-grey-1 text-white'"
label-slot
label-color="accent"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
>
<template #label>
<span
class="text-weight-bolder"
style="font-size: 0.95em;"
>{{ $t('shared.misc.in') }}</span>
</template>
</q-input>
<!-- punch out field -->
<q-input
v-model="shift.end_time"
dense
:readonly="shift.is_approved"
type="time"
standout="bg-blue-grey-9"
label-slot
label-color="accent"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
>
<template #label>
<span
class="text-weight-bolder"
style="font-size: 0.95em;"
>{{ $t('shared.misc.out') }}</span>
</template>
</q-input>
<!-- comment and delete buttons -->
<div :class="ui_store.is_mobile_mode ? 'col-12 row' : 'col-auto'">
<q-icon
v-if="shift.type && dense"
:name="shift.comment ? 'comment' : ''"
color="primary"
:size="dense ? 'xs' : 'sm'"
class="col"
/>
<q-btn
v-else
flat
dense
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? 'accent' : 'grey-5'"
class="col"
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
>
<q-popup-edit
v-model="shift.comment"
:title="$t('timesheet.shift.fields.header_comment')"
auto-save
v-slot="scope"
class="bg-dark"
>
<q-input
color="white"
v-model="scope.value"
dense
:readonly="shift.is_approved"
autofocus
counter
bottom-slots
:maxlength="COMMENT_LENGTH_MAX"
class="q-pb-lg"
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
@keyup.enter="scope.set"
>
<template #append>
<q-icon name="edit" />
</template>
<template #counter>
<div class="row flex-center">
<q-space />
<q-knob
:model-value="scope.value?.length"
readonly
:max="COMMENT_LENGTH_MAX"
size="1.6em"
:thickness="0.4"
:color="getCommentCounterColor(scope.value?.length ?? 0)"
track-color="grey-4"
class="col-auto q-mr-xs"
/>
<span
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
>{{ 280 - (scope.value?.length ?? 0) }}</span>
</div>
</template>
</q-input>
</q-popup-edit>
</q-btn>
<q-btn
v-if="!ui_store.is_mobile_mode"
flat
dense
:disable="shift.is_approved"
tabindex="-1"
icon="cancel"
text-color="negative"
class="col"
:class="shift.is_approved ? 'invisible' : ''"
@click="$emit('requestDelete')"
/>
</div>
</div>
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
<div class="row items-center text-uppercase rounded-5 bg-transparent q-mb-xs" :class="ui_store.is_mobile_mode ? 'col' : 'col-4'">
<!-- mobile comment button -->
<q-btn
v-if="ui_store.is_mobile_mode && !dense"
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? 'accent' : 'grey-5'"
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
>
<q-popup-edit
v-model="shift.comment"
:title="$t('timesheet.shift.fields.header_comment')"
auto-save
v-slot="scope"
class="bg-dark"
>
<q-input
color="white"
v-model="scope.value"
dense
:readonly="shift.is_approved"
autofocus
counter
bottom-slots
:maxlength="COMMENT_LENGTH_MAX"
class="q-pb-lg"
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
@keyup.enter="scope.set"
>
<template #append>
<q-icon name="edit" />
</template>
<template #counter>
<div class="row flex-center">
<q-space />
<q-knob
:model-value="scope.value?.length"
readonly
:max="COMMENT_LENGTH_MAX"
size="1.6em"
:thickness="0.4"
:color="getCommentCounterColor(scope.value?.length ?? 0)"
track-color="grey-4"
class="col-auto q-mr-xs"
/>
<span
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
>{{ 280 - (scope.value?.length ?? 0) }}</span>
</div>
</template>
</q-input>
</q-popup-edit>
</q-btn>
<!-- shift type -->
<q-select
ref="select"
v-model="shift_type_selected"
standout="bg-blue-grey-9"
dense
:readonly="shift.is_approved"
:options-dense="!ui_store.is_mobile_mode"
hide-dropdown-icon
:menu-offset="[0, 10]"
menu-anchor="bottom middle"
menu-self="top middle"
:options="SHIFT_OPTIONS"
class="col rounded-5 q-mx-xs bg-dark"
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
popup-content-style="border: 2px solid var(--q-accent)"
@blur="onBlurShiftTypeSelect"
@update:model-value="option => shift.type = option.value"
>
<template #selected-item="scope">
<div
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
:tabindex="scope.tabindex"
>
<q-icon
:name="scope.opt.icon"
:color="scope.opt.icon_color"
size="sm"
class="col-auto q-mx-xs"
/>
<span
style="line-height: 0.9em;"
class="col-auto ellipsis"
>{{ scope.opt.label }}</span>
</div>
</template>
</q-select>
</div>
<div class="col row flex-center text-uppercase rounded-5 bg-transparent q-pa-xs">
<!-- punch in field -->
<q-input
v-model="shift.start_time"
dense
:readonly="shift.is_approved"
type="time"
:standout="$q.dark.isActive ? 'bg-blue-grey-9' : 'bg-blue-grey-1 text-white'"
label-slot
label-color="accent"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
>
<template #label>
<span
class="text-weight-bolder"
style="font-size: 0.95em;"
>{{ $t('shared.misc.in') }}</span>
</template>
</q-input>
<!-- punch out field -->
<q-input
v-model="shift.end_time"
dense
:readonly="shift.is_approved"
type="time"
standout="bg-blue-grey-9"
label-slot
label-color="accent"
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
input-style="font-size: 1.2em;"
class="col rounded-5 bg-dark"
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
>
<template #label>
<span
class="text-weight-bolder"
style="font-size: 0.95em;"
>{{ $t('shared.misc.out') }}</span>
</template>
</q-input>
<!-- comment and delete buttons -->
<div :class="ui_store.is_mobile_mode ? 'col-12 row' : 'col-auto'">
<q-icon
v-if="shift.type && dense"
:name="shift.comment ? 'comment' : ''"
color="primary"
:size="dense ? 'xs' : 'sm'"
class="col"
/>
<!-- desktop comment button -->
<q-btn
v-else-if="!ui_store.is_mobile_mode"
flat
dense
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
:text-color="shift.comment ? 'accent' : 'grey-5'"
class="col"
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
>
<q-popup-edit
v-model="shift.comment"
:title="$t('timesheet.shift.fields.header_comment')"
auto-save
v-slot="scope"
class="bg-dark"
>
<q-input
color="white"
v-model="scope.value"
dense
:readonly="shift.is_approved"
autofocus
counter
bottom-slots
:maxlength="COMMENT_LENGTH_MAX"
class="q-pb-lg"
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
@keyup.enter="scope.set"
>
<template #append>
<q-icon name="edit" />
</template>
<template #counter>
<div class="row flex-center">
<q-space />
<q-knob
:model-value="scope.value?.length"
readonly
:max="COMMENT_LENGTH_MAX"
size="1.6em"
:thickness="0.4"
:color="getCommentCounterColor(scope.value?.length ?? 0)"
track-color="grey-4"
class="col-auto q-mr-xs"
/>
<span
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
>{{ 280 - (scope.value?.length ?? 0) }}</span>
</div>
</template>
</q-input>
</q-popup-edit>
</q-btn>
<q-btn
v-if="!ui_store.is_mobile_mode"
flat
dense
:disable="shift.is_approved"
tabindex="-1"
icon="cancel"
text-color="negative"
class="col"
:class="shift.is_approved ? 'invisible' : ''"
@click="$emit('requestDelete')"
/>
</div>
</div>
</div>
</q-slide-item>
</template>

View File

@ -31,7 +31,7 @@
</script>
<template>
<div class="column justify-center q-py-xs" :class="approved ? 'bg-dark' : ''">
<div class="column justify-center q-py-xs" :class="approved ? '' : ''">
<ShiftListDayRow
v-for="shift, shift_index in day.shifts"
:key="shift_index"

View File

@ -57,17 +57,20 @@
v-if="ui_store.is_mobile_mode"
class="col column full-width"
>
<q-card class="rounded-5 q-my-md" :class="getDayApproval(day) ? 'bg-accent' : 'bg-dark'">
<q-card
class="rounded-10 bg-dark"
:style="ui_store.is_mobile_mode ? (getDayApproval(day) ? 'border: 3px solid var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
>
<q-card-section
class="text-white text-weight-bolder text-uppercase text-h6 q-py-xs"
:class="getDayApproval(day) ? 'bg-dark' : 'bg-accent'"
style="line-height: 1em;"
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
:class="getDayApproval(day) ? 'bg-dark text-accent' : 'bg-primary text-white'"
style="line-height: 1em;"
>
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
weekday: 'long', day: 'numeric', month:
'long'
}) }}</span>
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
weekday: 'long', day: 'numeric', month:
'long'
}) }}</span>
</q-card-section>
<q-card-section
@ -85,7 +88,6 @@
<q-card-actions class="q-pa-none">
<q-btn
v-if="!getDayApproval(day)"
push
square
color="accent"
icon="more_time"
@ -98,9 +100,9 @@
<q-badge
v-if="getDayApproval(day)"
floating
class="bg-secondary q-pa-none rounded-50"
class="transparent q-pa-none rounded-50"
style="transform: translate(15px, -5px);"
>
>
<q-icon
name="verified"
size="5em"
@ -137,7 +139,7 @@
<div class="col-auto self-stretch">
<q-icon
v-if="getDayApproval(day)"
v-if="getDayApproval(day)"
name="verified"
color="white"
size="xl"

View File

@ -65,7 +65,7 @@
v-if="$q.screen.lt.md"
push
rounded
color="primary"
color="accent"
icon="receipt_long"
:label="$t('timesheet.expense.open_btn')"
class="q-mt-sm"

View File

@ -15,7 +15,6 @@ export const ShiftService = {
updateShifts: async (existing_shifts: Shift[]) => {
console.log('sent shifts: ', existing_shifts)
const response = await api.patch(`/shift/update`, existing_shifts);
console.log('API response to existing shifts: ', response.data);
return response;
}
};

View File

@ -20,7 +20,7 @@ export const timesheetService = {
},
getTimesheetsByPayPeriod: async (year: number, period_number: number): Promise<TimesheetResponse> => {
const response = await api.get('timesheets', { params: { year, period_number } });
return response.data;
const response = await api.get(`timesheets/${year}/${period_number}`);
return response.data.data;
},
};

View File

@ -1,22 +1,25 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import { useTimesheetStore } from "src/stores/timesheet-store";
import { Expense } from "src/modules/timesheets/models/expense.models";
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
import { date } from "quasar";
export const useExpensesStore = defineStore('expenses', () => {
const timesheet_store = useTimesheetStore();
const is_open = ref(false);
const is_loading = ref(false);
const is_hiding_create_form = ref(false);
const mode = ref<'create' | 'update' | 'delete'>('create');
const current_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const initial_expense = ref<Expense>(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const open = (): void => {
is_open.value = true;
current_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
initial_expense.value = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
if (timesheet_store.pay_period !== undefined) {
current_expense.value = new Expense(timesheet_store.pay_period.period_start);
initial_expense.value = new Expense(timesheet_store.pay_period.period_start);
}
mode.value = 'create';
}
@ -45,6 +48,7 @@ export const useExpensesStore = defineStore('expenses', () => {
return {
is_open,
is_loading,
is_hiding_create_form,
mode,
current_expense,
initial_expense,