refactor(approvals): so many changes that are difficult to keep track of. Work on more integration between approvals and timesheet, add list view to approvals, etc
This commit is contained in:
parent
dc615340bc
commit
7f43341629
|
|
@ -25,7 +25,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body.body--dark {
|
body.body--dark {
|
||||||
--q-secondary: #0f1114;
|
--q-secondary: #2b2f34;
|
||||||
color: $grey-2;
|
color: $grey-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,3 +33,7 @@ body.body--dark {
|
||||||
--q-dark: #FFF;
|
--q-dark: #FFF;
|
||||||
color: $blue-grey-8;
|
color: $blue-grey-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shift-highlight {
|
||||||
|
background: #0195462a;
|
||||||
|
}
|
||||||
|
|
@ -16,16 +16,16 @@ $primary : #019547;
|
||||||
$secondary : #DAE0E7;
|
$secondary : #DAE0E7;
|
||||||
$accent : #AAD5C4;
|
$accent : #AAD5C4;
|
||||||
|
|
||||||
$dark-shadow-color : #019547;
|
$dark-shadow-color : #00220f;
|
||||||
|
|
||||||
$elevation-dark-umbra : rgba($dark-shadow-color, 0.4);
|
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
||||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0);
|
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.2);
|
||||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0);
|
$elevation-dark-ambient : rgba($dark-shadow-color, 0.2);
|
||||||
|
|
||||||
$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 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient;
|
||||||
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
||||||
|
|
||||||
$dark : #333;
|
$dark : #42444b;
|
||||||
$dark-page : #343434;
|
$dark-page : #343434;
|
||||||
|
|
||||||
$positive : #21ba45;
|
$positive : #21ba45;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { RouteNames } from 'src/router/router-constants';
|
import { RouteNames } from 'src/router/router-constants';
|
||||||
|
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const uiStore = useUiStore();
|
const uiStore = useUiStore();
|
||||||
|
|
@ -50,7 +51,7 @@
|
||||||
|
|
||||||
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
<!-- Timesheet Validation -- Supervisor and Accounting only -->
|
||||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_APPROVALS)"
|
||||||
v-if="['supervisor', 'accounting'].includes(authStore.user.role)">
|
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="event_available" color="primary" />
|
<q-icon name="event_available" color="primary" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
@ -61,7 +62,7 @@
|
||||||
|
|
||||||
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
<!-- Employee List -- Supervisor, Accounting and HR only -->
|
||||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
<q-item v-ripple clickable side @click="goToPageName(RouteNames.EMPLOYEE_LIST)"
|
||||||
v-if="['supervisor', 'accounting', 'human_resources'].includes(authStore.user.role)">
|
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="view_list" color="primary" />
|
<q-icon name="view_list" color="primary" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
|
|
||||||
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
<!-- Employee Timesheet temp -- Employee, Supervisor, Accounting only -->
|
||||||
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
<q-item v-ripple clickable side @click="goToPageName(RouteNames.TIMESHEET_TEMP)"
|
||||||
v-if="['supervisor', 'accounting', 'employee'].includes(authStore.user.role)">
|
v-if="CAN_APPROVE_PAY_PERIODS.includes(authStore.user.role)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="punch_clock" color="primary" />
|
<q-icon name="punch_clock" color="primary" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { UserRole } from 'src/modules/shared/models/user.models';
|
||||||
import { useAuthApi } from '../composables/use-auth-api';
|
import { useAuthApi } from '../composables/use-auth-api';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const auth_api = useAuthApi();
|
const auth_api = useAuthApi();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const setBypassUser = (bypassRole: string) => {
|
const setBypassUser = (bypassRole: UserRole) => {
|
||||||
auth_api.setUser(bypassRole);
|
auth_api.setUser(bypassRole);
|
||||||
|
|
||||||
router.push({ name: 'dashboard' }).catch( err => {
|
router.push({ name: 'dashboard' }).catch( err => {
|
||||||
|
|
@ -19,7 +20,7 @@
|
||||||
<q-card-section class="q-pa-sm text-uppercase text-center"> impersonate </q-card-section>
|
<q-card-section class="q-pa-sm text-uppercase text-center"> impersonate </q-card-section>
|
||||||
<q-card-actions vertical>
|
<q-card-actions vertical>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-for="role, index in [ 'supervisor', 'accounting', 'human_resources', 'employee' ]"
|
v-for="role, index in UserRole"
|
||||||
:key="index"
|
:key="index"
|
||||||
push
|
push
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { useAuthStore } from "../../../stores/auth-store";
|
import { useAuthStore } from "../../../stores/auth-store";
|
||||||
|
import type { UserRole } from "src/modules/shared/models/user.models";
|
||||||
|
|
||||||
export const useAuthApi = () => {
|
export const useAuthApi = () => {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
authStore.login();
|
authStore.login();
|
||||||
};
|
};
|
||||||
|
|
@ -21,7 +20,7 @@ export const useAuthApi = () => {
|
||||||
return authStore.isAuthorizedUser;
|
return authStore.isAuthorizedUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUser = (bypassRole: string) => {
|
const setUser = (bypassRole: UserRole) => {
|
||||||
authStore.setUser(bypassRole);
|
authStore.setUser(bypassRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
// const getEmployeeAvatar = (first_name: string, last_name: string) => {
|
// const getEmployeeAvatar = (first_name: string, last_name: string) => {
|
||||||
// // add logic here to see if user has an avatar image and return that instead of initials
|
// // add logic here to see if user has an avatar image and return that instead of initials
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const { row } = defineProps<{
|
const { row } = defineProps<{
|
||||||
row: EmployeeListTableItem
|
row: EmployeeProfile
|
||||||
}>()
|
}>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
onProfileClick: [email: string]
|
onProfileClick: [email: string]
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
import { useEmployeeListApi } from 'src/modules/employee-list/composables/use-employee-api';
|
||||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import SupervisorCrewTableItem from './supervisor-crew-table-item.vue';
|
import EmployeeListTableItem from 'src/modules/employee-list/components/employee-list-table-item.vue';
|
||||||
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
|
|
||||||
import type { QTableColumn } from 'quasar';
|
import type { QTableColumn } from 'quasar';
|
||||||
|
|
||||||
const employee_list_api = useEmployeeListApi();
|
const employee_list_api = useEmployeeListApi();
|
||||||
|
|
@ -17,7 +16,7 @@
|
||||||
const is_grid_mode = ref(true);
|
const is_grid_mode = ref(true);
|
||||||
const pagination = ref({ rowsPerPage: 0 });
|
const pagination = ref({ rowsPerPage: 0 });
|
||||||
|
|
||||||
const employee_list_columns = computed((): QTableColumn<EmployeeListTableItem>[] => [
|
const employee_list_columns = computed((): QTableColumn<EmployeeProfile>[] => [
|
||||||
{name: 'first_name', label: t('employee_list.table.first_name'), field: 'first_name', align: 'left'},
|
{name: 'first_name', label: t('employee_list.table.first_name'), field: 'first_name', align: 'left'},
|
||||||
{name: 'last_name', label: t('employee_list.table.last_name'), field: 'last_name', align: 'left'},
|
{name: 'last_name', label: t('employee_list.table.last_name'), field: 'last_name', align: 'left'},
|
||||||
{name: 'email', label: t('employee_list.table.email'), field: 'email', align: 'left'},
|
{name: 'email', label: t('employee_list.table.email'), field: 'email', align: 'left'},
|
||||||
|
|
@ -49,7 +48,7 @@
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
class="q-pa-md bg-transparent"
|
class="q-pa-md bg-transparent"
|
||||||
:class="is_grid_mode ? '': 'my-sticky-header-table'"
|
:class="is_grid_mode ? '': 'sticky-header-table'"
|
||||||
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
|
:table-class="$q.dark.isActive ? 'q-px-md q-py-none q-mx-md rounded-10 bg-dark' : 'q-px-md q-py-none q-mx-md rounded-10 bg-white'"
|
||||||
color="primary"
|
color="primary"
|
||||||
table-header-class="text-primary text-uppercase"
|
table-header-class="text-primary text-uppercase"
|
||||||
|
|
@ -62,7 +61,7 @@
|
||||||
@row-click="() => console.log('click!')"
|
@row-click="() => console.log('click!')"
|
||||||
>
|
>
|
||||||
<template v-slot:item="props">
|
<template v-slot:item="props">
|
||||||
<SupervisorCrewTableItem :row="props.row"/>
|
<EmployeeListTableItem :row="props.row"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:top>
|
<template v-slot:top>
|
||||||
|
|
@ -126,7 +125,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
.my-sticky-header-table
|
.sticky-header-table
|
||||||
thead tr:first-child th
|
thead tr:first-child th
|
||||||
background-color: var(--q-dark)
|
background-color: var(--q-dark)
|
||||||
margin-top: none
|
margin-top: none
|
||||||
14
src/modules/employee-list/services/employee-list-service.ts
Normal file
14
src/modules/employee-list/services/employee-list-service.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
|
export const EmployeeListService = {
|
||||||
|
getEmployeeList: async (): Promise<EmployeeProfile[]> => {
|
||||||
|
const response = await api.get<EmployeeProfile[]>('/employees/employee-list')
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
|
||||||
|
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
// /* eslint-disable */
|
|
||||||
import { api } from 'src/boot/axios';
|
|
||||||
import type { EmployeeListTableItem } from '../types/employee-list-table-interface';
|
|
||||||
import type { EmployeeProfile } from '../types/employee-profile-interface';
|
|
||||||
|
|
||||||
|
|
||||||
export const EmployeeListService = {
|
|
||||||
getEmployeeList: async (): Promise<EmployeeListTableItem[]> => {
|
|
||||||
const response = await api.get<EmployeeListTableItem[]>('/employees/employee-list')
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getEmployeeDetails: async (email: string): Promise<EmployeeProfile> => {
|
|
||||||
const response = await api.get<EmployeeProfile>('employees/profile/' + email);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export interface EmployeeListTableItem {
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
email: string;
|
|
||||||
supervisor_full_name: string | null;
|
|
||||||
company_name: number;
|
|
||||||
job_title: string;
|
|
||||||
};
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
||||||
import ProfileSelectField from 'src/modules/profile/components/shared/profile-panel-select-field.vue';
|
import ProfileSelectField from 'src/modules/profile/components/shared/profile-panel-select-field.vue';
|
||||||
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
const { employeeProfile } = defineProps<{
|
const { employeeProfile } = defineProps<{
|
||||||
employeeProfile: EmployeeProfile;
|
employeeProfile: EmployeeProfile;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
import ProfileInputField from 'src/modules/profile/components/shared/profile-panel-input-field.vue';
|
||||||
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
const { employeeProfile } = defineProps<{
|
const { employeeProfile } = defineProps<{
|
||||||
employeeProfile: EmployeeProfile;
|
employeeProfile: EmployeeProfile;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import PanelInfoEmployee from 'src/modules/profile/components/employee/profile-panel-info-employee.vue';
|
import PanelInfoEmployee from 'src/modules/profile/components/employee/profile-panel-info-employee.vue';
|
||||||
import PanelPreferences from 'src/modules/profile/components/shared/profile-panel-preferences.vue';
|
import PanelPreferences from 'src/modules/profile/components/shared/profile-panel-preferences.vue';
|
||||||
import ProfileTabMenuTemplate from 'src/modules/profile/components/shared/profile-tab-menu-template.vue';
|
import ProfileTabMenuTemplate from 'src/modules/profile/components/shared/profile-tab-menu-template.vue';
|
||||||
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
import { default_employee_profile, type EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
const PanelNames = {
|
const PanelNames = {
|
||||||
PERSONAL_INFO: 'personal_info',
|
PERSONAL_INFO: 'personal_info',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,26 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: UserRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
ADMIN = 'ADMIN',
|
||||||
|
SUPERVISOR = 'SUPERVISOR',
|
||||||
|
HR = 'HR',
|
||||||
|
ACCOUNTING = 'ACCOUNTING',
|
||||||
|
EMPLOYEE = 'EMPLOYEE',
|
||||||
|
DEALER = 'DEALER',
|
||||||
|
CUSTOMER = 'CUSTOMER',
|
||||||
|
GUEST = 'GUEST',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CAN_APPROVE_PAY_PERIODS: UserRole[] = [
|
||||||
|
UserRole.ADMIN,
|
||||||
|
UserRole.SUPERVISOR,
|
||||||
|
UserRole.HR,
|
||||||
|
UserRole.ACCOUNTING,
|
||||||
|
]
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
|
const all_days_dates = timesheet_store.pay_period_details.weeks.flatMap(week => Object.values(week.shifts))
|
||||||
|
|
||||||
const all_costs = all_days.map(day => day.total_expenses);
|
const all_costs = all_days.map(day => day.total_expenses);
|
||||||
|
console.log('costs, ', all_costs);
|
||||||
const all_mileage = all_days.map(day => day.total_mileage);
|
const all_mileage = all_days.map(day => day.total_mileage);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@
|
||||||
ChartJS.defaults.maintainAspectRatio = false;
|
ChartJS.defaults.maintainAspectRatio = false;
|
||||||
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
|
||||||
|
|
||||||
const { pay_period_details } = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const hours_worked_labels = ref<string[]>([]);
|
const hours_worked_labels = ref<string[]>([]);
|
||||||
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
const hours_worked_dataset = ref<ChartDataset<'bar'>[]>([]);
|
||||||
|
|
||||||
const getHoursWorkedData = (): ChartData<'bar'> => {
|
const getHoursWorkedData = (): ChartData<'bar'> => {
|
||||||
|
|
||||||
const all_days = pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
const all_days = timesheet_store.pay_period_details.weeks.flatMap( week => Object.values(week.shifts));
|
||||||
const datasetConfig = [
|
const datasetConfig = [
|
||||||
{
|
{
|
||||||
key: 'regular_hours',
|
key: 'regular_hours',
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@
|
||||||
shift_type_totals.value = [{
|
shift_type_totals.value = [{
|
||||||
data: [
|
data: [
|
||||||
current_pay_period_overview.regular_hours,
|
current_pay_period_overview.regular_hours,
|
||||||
current_pay_period_overview.evening_hours,
|
current_pay_period_overview.other_hours.evening_hours,
|
||||||
current_pay_period_overview.emergency_hours,
|
current_pay_period_overview.other_hours.emergency_hours,
|
||||||
current_pay_period_overview.overtime_hours,
|
current_pay_period_overview.other_hours.overtime_hours,
|
||||||
],
|
],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
colors.getPaletteColor('green-5'), // Regular
|
colors.getPaletteColor('green-5'), // Regular
|
||||||
|
|
@ -39,9 +39,9 @@
|
||||||
|
|
||||||
shift_type_labels.value = [
|
shift_type_labels.value = [
|
||||||
current_pay_period_overview.regular_hours.toString() + 'h',
|
current_pay_period_overview.regular_hours.toString() + 'h',
|
||||||
current_pay_period_overview.evening_hours.toString() + 'h',
|
current_pay_period_overview.other_hours.evening_hours.toString() + 'h',
|
||||||
current_pay_period_overview.emergency_hours.toString() + 'h',
|
current_pay_period_overview.other_hours.emergency_hours.toString() + 'h',
|
||||||
current_pay_period_overview.overtime_hours.toString() + 'h',
|
current_pay_period_overview.other_hours.overtime_hours.toString() + 'h',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,44 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { ref } from 'vue';
|
import { provide, ref } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
||||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
||||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
||||||
|
|
||||||
defineProps<{
|
const { employeeEmail } = defineProps<{
|
||||||
employeeEmail: string;
|
employeeEmail: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
const dialog_model = defineModel<boolean>('dialog', { default: false });
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const render_key = ref(1);
|
||||||
|
|
||||||
// const timesheet_store = useTimesheetStore();
|
provide('employeeEmail', employeeEmail);
|
||||||
const is_showing_graph = ref(true);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="dialog_model"
|
v-model="dialog_model"
|
||||||
full-width
|
full-width
|
||||||
|
full-height
|
||||||
transition-show="jump-down"
|
transition-show="jump-down"
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
|
@show="render_key += 1"
|
||||||
>
|
>
|
||||||
<!-- loader -->
|
<!-- loader -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated faster zoomIn"
|
||||||
|
leave-active-class="animated faster zoomOut"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
<q-card
|
<q-card
|
||||||
v-if="timesheet_store.is_loading"
|
v-if="timesheet_store.is_loading"
|
||||||
class="column flex-center text-center"
|
class="column flex-center text-center"
|
||||||
|
style="width: 50vw !important; max-height: 50vh !important;"
|
||||||
>
|
>
|
||||||
<q-spinner
|
<q-spinner
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -39,90 +48,64 @@
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
<div class="col-auto text-primary text-h6 text-weight-bold text-center ">
|
||||||
{{ $t('shared.loading') }}
|
{{ $t('shared.label.loading') }}
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
</transition>
|
||||||
|
|
||||||
<q-card
|
<q-card
|
||||||
v-else
|
v-if="!timesheet_store.is_loading"
|
||||||
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
|
||||||
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
:style="$q.screen.lt.md ? '' : 'width: 60vw !important;'"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- employee name -->
|
<!-- employee name -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
class="text-h5 text-weight-bolder text-center text-primary q-pa-none text-uppercase col-auto"
|
class="text-h5 text-weight-bolder text-center bg-primary q-pa-none text-uppercase text-white col-auto"
|
||||||
>
|
>
|
||||||
<span>{{ timesheet_store.pay_period_details.employee_full_name }}</span>
|
<span>{{ timesheet_store.pay_period_details.employee_full_name }}</span>
|
||||||
|
|
||||||
<q-separator
|
|
||||||
spaced
|
|
||||||
size="2px"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-card-actions
|
|
||||||
align="center"
|
|
||||||
class="q-pa-none"
|
|
||||||
>
|
|
||||||
<q-btn-toggle
|
|
||||||
v-model="is_showing_graph"
|
|
||||||
color="white"
|
|
||||||
text-color="primary"
|
|
||||||
toggle-color="primary"
|
|
||||||
:options="[
|
|
||||||
{ icon: 'bar_chart', value: true },
|
|
||||||
{ icon: 'edit', value: false },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- employee timesheet for supervisor editting -->
|
<!-- employee pay period details using chart -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
v-if="!is_showing_graph"
|
:horizontal="!$q.screen.lt.md"
|
||||||
class="q-pa-none"
|
class=" col-auto q-px-sm no-wrap"
|
||||||
>
|
>
|
||||||
|
<DetailedDialogChartHoursWorked
|
||||||
|
:key="render_key"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailedDialogChartShiftTypes
|
||||||
|
:key="render_key + 1"
|
||||||
|
class="col-2 q-ma-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DetailedDialogChartExpenses
|
||||||
|
:key="render_key + 2"
|
||||||
|
class="col"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="col-auto">
|
||||||
|
<q-separator />
|
||||||
|
<ExpenseCrudDialogList
|
||||||
|
horizontal
|
||||||
|
:employee-email="employeeEmail"
|
||||||
|
/>
|
||||||
|
<q-separator />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
<!-- list of shifts -->
|
<!-- list of shifts -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
:horizontal="$q.screen.gt.sm"
|
:horizontal="$q.screen.gt.sm"
|
||||||
class="q-pa-none rounded-10"
|
class="col-auto q-px-sm rounded-5 no-wrap"
|
||||||
>
|
>
|
||||||
<TimesheetWrapper
|
<TimesheetWrapper
|
||||||
dense
|
dense
|
||||||
:employee-email="employeeEmail"
|
:employee-email="employeeEmail"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<!-- employee timesheet details with chart -->
|
|
||||||
<q-card-section
|
|
||||||
v-if="is_showing_graph"
|
|
||||||
class="q-pa-md col column full-width no-wrap"
|
|
||||||
>
|
|
||||||
<q-card-section
|
|
||||||
:horizontal="!$q.screen.lt.md"
|
|
||||||
class="q-pa-none col no-wrap"
|
|
||||||
style="min-height: 300px;"
|
|
||||||
>
|
|
||||||
<DetailedDialogChartHoursWorked class="col-7" />
|
|
||||||
|
|
||||||
<q-separator
|
|
||||||
spaced
|
|
||||||
:vertical="!$q.screen.lt.md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="column col justify-center no-wrap q-pa-none">
|
|
||||||
<DetailedDialogChartShiftTypes class="col-5" />
|
|
||||||
|
|
||||||
<q-separator
|
|
||||||
spaced
|
|
||||||
:vertical="!$q.screen.lt.md"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DetailedDialogChartExpenses class="col" />
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
import type { PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||||
|
|
||||||
const modelApproval = defineModel<boolean>();
|
const modelApproval = defineModel<boolean>();
|
||||||
|
|
@ -6,16 +9,16 @@
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'clickDetails': [overview: PayPeriodOverview];
|
'clickDetails': [overview: PayPeriodOverview];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const stack_label_class = "text-weight-bold text-primary text-uppercase text-caption q-pa-none q-my-none ellipsis";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-px-sm q-pb-sm q-mt-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
|
<div class="q-px-sm q-pb-sm q-mt-sm col-xs-12 col-sm-6 col-md-4 col-lg-4 col-xl-3 grid-style-transition">
|
||||||
<q-card class="rounded-10">
|
<q-card class="rounded-10">
|
||||||
|
|
||||||
<!-- Card header with employee name and details button-->
|
<!-- Card header with employee name and details button-->
|
||||||
<q-card-section horizontal class="q-py-none q-px-sm q-ma-none justify-between items-center">
|
<q-card-section
|
||||||
|
horizontal
|
||||||
|
class="q-py-none q-px-sm q-ma-none justify-between items-center"
|
||||||
|
>
|
||||||
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span>
|
<span class="col text-primary text-h5 text-weight-bolder q-pt-xs"> {{ row.employee_name }} </span>
|
||||||
|
|
||||||
<!-- Buttons to view detailed shifts or view employee timesheet -->
|
<!-- Buttons to view detailed shifts or view employee timesheet -->
|
||||||
|
|
@ -42,35 +45,33 @@
|
||||||
<q-separator size="2px" />
|
<q-separator size="2px" />
|
||||||
|
|
||||||
<!-- Main body of pay period card -->
|
<!-- Main body of pay period card -->
|
||||||
<q-card-section class="q-py-none q-px-sm q-mt-sm q-mb-md">
|
<q-card-section class="q-py-none q-px-sm q-my-sm">
|
||||||
<div class="row no-wrap">
|
<div class="row">
|
||||||
|
|
||||||
<!-- left portion of pay period card -->
|
<!-- left portion of pay period card -->
|
||||||
<div class="col column no-wrap q-px-sm">
|
<div class="col column q-px-sm">
|
||||||
|
|
||||||
<!-- Regular hours segment -->
|
<!-- Regular hours segment -->
|
||||||
<div class="column" :class="$q.screen.lt.md ? 'col' : 'col-8'">
|
<div class="col column">
|
||||||
<span :class="stack_label_class"> {{ $t('shared.shift_type.regular') }} </span>
|
<span class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"> {{ $t('shared.shift_type.regular') }} </span>
|
||||||
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span>
|
<span class="text-weight-bolder text-h3 q-py-none"> {{ row.regular_hours }} </span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator class="q-mx-sm" />
|
<q-separator class="q-mx-sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Other hour types segment -->
|
<!-- Other hour types segment -->
|
||||||
<div class="row q-px-xs">
|
<div class="col-auto row ellipsis q-mt-xs">
|
||||||
<div class="col column no-wrap">
|
<div
|
||||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.evening') }} </span>
|
v-for="hour_type, index in row.other_hours"
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.evening_hours }} </span>
|
:key="index"
|
||||||
</div>
|
class="col-4 column ellipsis"
|
||||||
|
:class="hour_type === 0 ? 'invisible' : ''"
|
||||||
<div class="col column no-wrap">
|
>
|
||||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.emergency') }} </span>
|
<span
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.emergency_hours }} </span>
|
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||||
</div>
|
style="font-size: 0.7em;"
|
||||||
|
> {{ $t(`shared.shift_type.${index.replace('_hours', '')}`) }} </span>
|
||||||
<div class="col column no-wrap">
|
<span
|
||||||
<span :class="stack_label_class" style="font-size: 0.65em;"> {{ $t('shared.shift_type.overtime') }} </span>
|
class="text-weight-bolder q-pa-none q-mb-xs"
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.overtime_hours }} </span>
|
style="font-size: 1.2em; line-height: 1em;"
|
||||||
|
> {{ hour_type }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,19 +84,34 @@
|
||||||
<!-- Right portion of pay period card -->
|
<!-- Right portion of pay period card -->
|
||||||
<div class="col-auto column q-px-sm">
|
<div class="col-auto column q-px-sm">
|
||||||
<div class="col column no-wrap">
|
<div class="col column no-wrap">
|
||||||
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
|
<span
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.expenses }} </span>
|
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||||
|
style="font-size: 0.8em;"
|
||||||
|
> {{ $t('timesheet.expense.types.EXPENSES') }} </span>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder text-h6 q-pa-none"
|
||||||
|
style="line-height: 0.9em;"
|
||||||
|
> {{ row.expenses }} </span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col column no-wrap">
|
<div class="col column no-wrap">
|
||||||
<span :class="stack_label_class" style="font-size: 0.8em;"> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
|
<span
|
||||||
<span class="text-weight-bolder text-h6 q-pa-none" style="line-height: 0.9em;"> {{ row.mileage }} </span>
|
class="text-weight-bold text-primary text-uppercase q-pa-none q-my-none"
|
||||||
|
style="font-size: 0.8em;"
|
||||||
|
> {{ $t('timesheet.expense.types.MILEAGE') }} </span>
|
||||||
|
<span
|
||||||
|
class="text-weight-bolder text-h6 q-pa-none"
|
||||||
|
style="line-height: 0.9em;"
|
||||||
|
> {{ row.mileage }} </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-separator color="primary" size="2px" />
|
<q-separator
|
||||||
|
color="primary"
|
||||||
|
size="2px"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Validate Pay Period section -->
|
<!-- Validate Pay Period section -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,22 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
|
||||||
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
import OverviewListItem from 'src/modules/timesheet-approval/components/overview-list-item.vue';
|
||||||
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
import QTableFilters from 'src/modules/shared/components/q-table-filters.vue';
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
import { pay_period_overview_columns, type PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
import { pay_period_overview_columns, type PayPeriodOverview } from 'src/modules/timesheet-approval/models/pay-period-overview.models';
|
||||||
|
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const timesheet_approval_api = useTimesheetApprovalApi();
|
||||||
|
|
||||||
const filter = ref<string | number | null>('');
|
const filter = ref<string | number | null>('');
|
||||||
|
const is_grid_mode = ref(true);
|
||||||
|
const IS_ABNORMAL_SHIFT = ['OVERTIME', 'EMERGENCY'];
|
||||||
|
const IS_PTO = ['HOLIDAY', 'VACATION', 'SICK'];
|
||||||
|
|
||||||
const employeeEmail = defineModel();
|
const employeeEmail = defineModel();
|
||||||
|
|
||||||
|
|
@ -25,10 +33,24 @@
|
||||||
|
|
||||||
const onClickedDetails = async (employee_email: string, row: PayPeriodOverview) => {
|
const onClickedDetails = async (employee_email: string, row: PayPeriodOverview) => {
|
||||||
employeeEmail.value = employee_email;
|
employeeEmail.value = employee_email;
|
||||||
emit('clickedDetailsButton', employee_email);
|
|
||||||
timesheet_store.current_pay_period_overview = row;
|
timesheet_store.current_pay_period_overview = row;
|
||||||
|
emit('clickedDetailsButton', employee_email);
|
||||||
|
|
||||||
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
await timesheet_store.getPayPeriodDetailsByEmployeeEmail(employee_email);
|
||||||
|
await expenses_store.getPayPeriodExpensesByEmployeeEmail(employee_email);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getListModeTextColor = (type: string): string => {
|
||||||
|
console.log('type: ', type);
|
||||||
|
if (IS_ABNORMAL_SHIFT.includes(type)) {
|
||||||
|
return ' text-negative text-weight-bolder';
|
||||||
|
}
|
||||||
|
else if (IS_PTO.includes(type)) {
|
||||||
|
return ' text-warning text-weight-bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -38,13 +60,16 @@
|
||||||
:columns="pay_period_overview_columns"
|
:columns="pay_period_overview_columns"
|
||||||
row-key="email"
|
row-key="email"
|
||||||
:filter="filter"
|
:filter="filter"
|
||||||
grid
|
:grid="is_grid_mode"
|
||||||
dense
|
dense
|
||||||
hide-pagination
|
hide-pagination
|
||||||
color="primary"
|
color="primary"
|
||||||
:rows-per-page-options="[0]"
|
:rows-per-page-options="[0]"
|
||||||
card-container-class="justify-center"
|
card-container-class="justify-center"
|
||||||
:loading="timesheet_store.is_loading"
|
:loading="timesheet_store.is_loading"
|
||||||
|
class="q-py-md bg-transparent"
|
||||||
|
:class="is_grid_mode ? '' : 'sticky-header-table no-shadow'"
|
||||||
|
table-class="q-pa-none q-py-none q-mx-md rounded-10 bg-dark shadow-4'"
|
||||||
:no-data-label="$t('shared.error.no_data_found')"
|
:no-data-label="$t('shared.error.no_data_found')"
|
||||||
:no-results-label="$t('shared.error.no_search_results')"
|
:no-results-label="$t('shared.error.no_search_results')"
|
||||||
:loading-label="$t('shared.label.loading')"
|
:loading-label="$t('shared.label.loading')"
|
||||||
|
|
@ -54,16 +79,67 @@
|
||||||
class="full-width"
|
class="full-width"
|
||||||
:class="$q.screen.lt.md ? 'text-center' : 'row'"
|
:class="$q.screen.lt.md ? 'text-center' : 'row'"
|
||||||
>
|
>
|
||||||
<PayPeriodNavigator />
|
<PayPeriodNavigator
|
||||||
|
@date-selected="timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<!-- Grid-or-List toggle goes here -->
|
<q-btn-toggle
|
||||||
|
v-model="is_grid_mode"
|
||||||
|
push
|
||||||
|
color="white"
|
||||||
|
text-color="primary"
|
||||||
|
toggle-color="primary"
|
||||||
|
class="q-mr-md"
|
||||||
|
:options="[
|
||||||
|
{ icon: 'grid_view', value: true },
|
||||||
|
{ icon: 'view_list', value: false },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
<QTableFilters v-model="filter" />
|
<QTableFilters v-model="filter" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #header="props">
|
||||||
|
<q-tr
|
||||||
|
:props="props"
|
||||||
|
class="bg-primary"
|
||||||
|
>
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="col.label !== 'timesheet_approvals.table.is_approved'"
|
||||||
|
class="text-uppercase text-weight-bolder text-white"
|
||||||
|
>
|
||||||
|
{{ $t(col.label) }}
|
||||||
|
</span>
|
||||||
|
</q-th>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body-cell="props">
|
||||||
|
<q-td
|
||||||
|
:props="props"
|
||||||
|
class="text-weight-medium"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'"
|
||||||
|
:class="getListModeTextColor(props.col.name)"
|
||||||
|
>{{ props.value }}</span>
|
||||||
|
<q-icon
|
||||||
|
v-if="typeof props.value === 'boolean'"
|
||||||
|
:name="props.value ? 'verified' : 'fiber_manual_record'"
|
||||||
|
:color="props.value ? 'primary' : 'grey-5'"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Template for individual employee cards -->
|
<!-- Template for individual employee cards -->
|
||||||
<template #item="props: { row: PayPeriodOverview, key: string }">
|
<template #item="props: { row: PayPeriodOverview, key: string }">
|
||||||
<OverviewListItem
|
<OverviewListItem
|
||||||
|
|
@ -89,3 +165,22 @@
|
||||||
</q-table>
|
</q-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.sticky-header-table
|
||||||
|
thead tr:first-child th
|
||||||
|
background-color: var(--q-primary)
|
||||||
|
margin-top: none
|
||||||
|
|
||||||
|
thead tr th
|
||||||
|
position: sticky
|
||||||
|
z-index: 1
|
||||||
|
thead tr:first-child th
|
||||||
|
top: 0
|
||||||
|
|
||||||
|
&.q-table--loading thead tr:last-child th
|
||||||
|
top: 48px
|
||||||
|
|
||||||
|
tbody
|
||||||
|
scroll-margin-top: 48px
|
||||||
|
</style>
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { useAuthStore } from "src/stores/auth-store";
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
import type { TimesheetApprovalCSVReportFilters } from "src/modules/timesheet-approval/models/timesheet-approval-csv-report.models";
|
||||||
|
import { NavigatorConstants } from "src/modules/timesheet-approval/models/pay-period-overview.models";
|
||||||
|
|
||||||
export const useTimesheetApprovalApi = () => {
|
export const useTimesheetApprovalApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
|
|
||||||
const getPayPeriodOverviewsByDate = async (date_string: string): Promise<void> => {
|
const getPayPeriodOverviewsByDateOrYearAndNumber = async (date_or_year: string | number, period_number?: number): Promise<void> => {
|
||||||
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_string);
|
let success = false;
|
||||||
|
if (typeof date_or_year === 'string') success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year);
|
||||||
|
else if (typeof date_or_year === 'number' && period_number) success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date_or_year, period_number);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
await timesheet_store.getPayPeriodOverviewsBySupervisorEmail(
|
await timesheet_store.getPayPeriodOverviewsBySupervisorEmail(
|
||||||
|
|
@ -18,6 +21,26 @@ export const useTimesheetApprovalApi = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNextOrPreviousPayPeriodOverview = async (direction: number) => {
|
||||||
|
let new_period_number = timesheet_store.pay_period.pay_period_no + direction;
|
||||||
|
let new_year = timesheet_store.pay_period.pay_year;
|
||||||
|
|
||||||
|
if ( new_period_number > 26 || new_period_number < 1) {
|
||||||
|
new_period_number = 1;
|
||||||
|
new_year += direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
await getPayPeriodOverviewsByDateOrYearAndNumber(new_year, new_period_number);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextPayPeriodOverview = async () => {
|
||||||
|
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.NEXT_PERIOD);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviousPayPeriodOverview = async () => {
|
||||||
|
await getNextOrPreviousPayPeriodOverview(NavigatorConstants.PREVIOUS_PERIOD);
|
||||||
|
};
|
||||||
|
|
||||||
const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number ) => {
|
const getTimesheetApprovalCSVReport = async ( report_filter_company: boolean[], report_filter_type: boolean[], year?: number, period_number?: number ) => {
|
||||||
const [ targo, solucom ] = report_filter_company;
|
const [ targo, solucom ] = report_filter_company;
|
||||||
const [ shifts, expenses, holiday, vacation ] = report_filter_type;
|
const [ shifts, expenses, holiday, vacation ] = report_filter_type;
|
||||||
|
|
@ -34,7 +57,9 @@ export const useTimesheetApprovalApi = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getPayPeriodOverviewsByDate,
|
getPayPeriodOverviewsByDateOrYearAndNumber,
|
||||||
getTimesheetApprovalCSVReport,
|
getTimesheetApprovalCSVReport,
|
||||||
|
getNextPayPeriodOverview,
|
||||||
|
getPreviousPayPeriodOverview,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,15 +1,28 @@
|
||||||
|
import type { QTableColumn } from "quasar";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export enum NavigatorConstants {
|
||||||
|
NEXT_PERIOD = 1,
|
||||||
|
PREVIOUS_PERIOD = -1,
|
||||||
|
}
|
||||||
|
|
||||||
export interface PayPeriodOverview {
|
export interface PayPeriodOverview {
|
||||||
email: string;
|
email: string;
|
||||||
employee_name: string;
|
employee_name: string;
|
||||||
regular_hours: number;
|
regular_hours: number;
|
||||||
|
other_hours: {
|
||||||
evening_hours: number;
|
evening_hours: number;
|
||||||
emergency_hours: number;
|
emergency_hours: number;
|
||||||
overtime_hours: number;
|
overtime_hours: number;
|
||||||
|
sick_hours: number;
|
||||||
|
holiday_hours: number;
|
||||||
|
vacation_hours: number;
|
||||||
|
};
|
||||||
total_hours: number;
|
total_hours: number;
|
||||||
expenses: number;
|
expenses: number;
|
||||||
mileage: number;
|
mileage: number;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface PayPeriodOverviewResponse {
|
export interface PayPeriodOverviewResponse {
|
||||||
pay_period_no: number;
|
pay_period_no: number;
|
||||||
|
|
@ -25,19 +38,25 @@ export const default_pay_period_overview: PayPeriodOverview = {
|
||||||
email: '',
|
email: '',
|
||||||
employee_name: '',
|
employee_name: '',
|
||||||
regular_hours: -1,
|
regular_hours: -1,
|
||||||
|
other_hours: {
|
||||||
evening_hours: -1,
|
evening_hours: -1,
|
||||||
emergency_hours: -1,
|
emergency_hours: -1,
|
||||||
overtime_hours: -1,
|
overtime_hours: -1,
|
||||||
|
sick_hours: -1,
|
||||||
|
holiday_hours: -1,
|
||||||
|
vacation_hours: -1,
|
||||||
|
},
|
||||||
total_hours: -1,
|
total_hours: -1,
|
||||||
expenses: -1,
|
expenses: -1,
|
||||||
mileage: -1,
|
mileage: -1,
|
||||||
is_approved: false
|
is_approved: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pay_period_overview_columns = [
|
export const pay_period_overview_columns: QTableColumn[] = [
|
||||||
{
|
{
|
||||||
name: 'employee_name',
|
name: 'employee_name',
|
||||||
label: 'timesheet_approvals.table.full_name',
|
label: 'timesheet_approvals.table.full_name',
|
||||||
|
align: 'left',
|
||||||
field: 'employee_name',
|
field: 'employee_name',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
|
|
@ -48,27 +67,45 @@ export const pay_period_overview_columns = [
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'regular_hours',
|
name: 'REGULAR',
|
||||||
label: 'shared.shift_type.regular',
|
label: 'shared.shift_type.regular',
|
||||||
field: 'regular_hours',
|
field: 'regular_hours',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'evening_hours',
|
name: 'EVENING',
|
||||||
label: 'shared.shift_type.evening',
|
label: 'shared.shift_type.evening',
|
||||||
field: 'evening_hours',
|
field: row => row.other_hours.evening_hours,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'emergency_hours',
|
name: 'EMERGENCY',
|
||||||
label: 'shared.shift_type.emergency',
|
label: 'shared.shift_type.emergency',
|
||||||
field: 'emergency_hours',
|
field: row => row.other_hours.emergency_hours,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'overtime_hours',
|
name: 'SICK',
|
||||||
|
label: 'shared.shift_type.sick',
|
||||||
|
field: row => row.other_hours.sick_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HOLIDAY',
|
||||||
|
label: 'shared.shift_type.holiday',
|
||||||
|
field: row => row.other_hours.holiday_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'VACATION',
|
||||||
|
label: 'shared.shift_type.vacation',
|
||||||
|
field: row => row.other_hours.vacation_hours,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OVERTIME',
|
||||||
label: 'shared.shift_type.overtime',
|
label: 'shared.shift_type.overtime',
|
||||||
field: 'overtime_hours',
|
field: row => row.other_hours.overtime_hours,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -89,4 +126,4 @@ export const pay_period_overview_columns = [
|
||||||
field: 'is_approved',
|
field: 'is_approved',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
}
|
}
|
||||||
];
|
]
|
||||||
|
|
@ -8,9 +8,11 @@
|
||||||
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { default_expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
import { makeExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { makeExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
const files = defineModel<File[] | null>('files');
|
const files = defineModel<File[] | null>('files');
|
||||||
|
|
@ -35,7 +37,7 @@
|
||||||
<template>
|
<template>
|
||||||
<q-form
|
<q-form
|
||||||
flat
|
flat
|
||||||
v-if="!expenses_store.pay_period_expenses.is_approved"
|
v-if="!timesheet_store.pay_period_details.weeks[0]?.is_approved"
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
>
|
>
|
||||||
<div class="text-subtitle2 q-py-sm">
|
<div class="text-subtitle2 q-py-sm">
|
||||||
|
|
@ -87,7 +89,7 @@
|
||||||
map-options
|
map-options
|
||||||
:label="$t('timesheet.expense.type')"
|
:label="$t('timesheet.expense.type')"
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
:option-label="label => $t(label)"
|
:option-label="label => $t(`timesheet.expense.types.${label}`)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- amount input -->
|
<!-- amount input -->
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,34 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-item class="row justify-between">
|
<q-item class="row justify-between items-center q-pa-none">
|
||||||
<q-item-label
|
<q-item-label
|
||||||
header
|
header
|
||||||
class="text-h6 col-auto"
|
class="text-h6 col q-pa-none"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.expense.title') }}
|
{{ $t('timesheet.expense.title') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-section class="items-center col-auto">
|
<q-item-section
|
||||||
|
no-wrap
|
||||||
|
class="col-auto items-center"
|
||||||
|
>
|
||||||
<q-badge
|
<q-badge
|
||||||
lines="1"
|
outline
|
||||||
class="q-pa-sm q-px-md"
|
class="q-py-xs q-px-md"
|
||||||
:label="$t('timesheet.expense.total_amount') + ': ' + expense_store.pay_period_expenses_totals.amount.toFixed(2)"
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.total_amount') + ': $' + expense_store.pay_period_expenses.total_expense.toFixed(2)"
|
||||||
/>
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
<q-separator spaced />
|
<q-item-section
|
||||||
|
no-wrap
|
||||||
|
class="col-auto items-center"
|
||||||
|
>
|
||||||
<q-badge
|
<q-badge
|
||||||
lines="2"
|
outline
|
||||||
class="q-pa-sm q-px-md"
|
class="q-py-xs q-px-md"
|
||||||
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses_totals.mileage.toFixed(1)"
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.total_mileage') + ': ' + expense_store.pay_period_expenses.total_mileage.toFixed(1) + ' km'"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed, inject, ref } from 'vue';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { getExpenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
import { CAN_APPROVE_PAY_PERIODS } from 'src/modules/shared/models/user.models';
|
||||||
|
|
||||||
|
const { expense, horizontal = false } = defineProps<{
|
||||||
|
expense: Expense;
|
||||||
|
index: number;
|
||||||
|
horizontal?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
|
const is_approved = defineModel<boolean>({ required: true });
|
||||||
|
const is_selected = ref(false);
|
||||||
|
const refresh_key = ref(1);
|
||||||
|
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user.role))
|
||||||
|
|
||||||
|
const expenseItemStyle = computed(() => is_approved.value ? 'border: solid 2px var(--q-primary);' : 'border: solid 2px grey;');
|
||||||
|
const highlightClass = computed(() => (expenses_store.mode === 'update' && is_selected) ? 'bg-accent' : '');
|
||||||
|
const approvedClass = computed(() => horizontal ? ' q-mx-xs q-pa-xs cursor-pointer' : '')
|
||||||
|
|
||||||
|
|
||||||
|
const employeeEmail = inject<string>('employeeEmail') ?? '';
|
||||||
|
|
||||||
|
|
||||||
|
const setExpenseToModify = () => {
|
||||||
|
expenses_store.mode = 'update';
|
||||||
|
expenses_store.current_expense = expense;
|
||||||
|
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestExpenseDeletion = async () => {
|
||||||
|
expenses_store.mode = 'delete';
|
||||||
|
expenses_store.initial_expense = expense;
|
||||||
|
expenses_store.current_expense = default_expense;
|
||||||
|
await expenses_api.deleteExpenseByEmployeeEmail(employeeEmail, expenses_store.initial_expense.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onExpenseClicked() {
|
||||||
|
if (is_authorized_to_approve.value) {
|
||||||
|
is_approved.value = !is_approved.value;
|
||||||
|
refresh_key.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated pulse"
|
||||||
|
mode="out-in"
|
||||||
|
>
|
||||||
|
<q-item
|
||||||
|
:key="refresh_key"
|
||||||
|
:clickable="horizontal"
|
||||||
|
class="row q-mx-xs shadow-2"
|
||||||
|
:style="expenseItemStyle + highlightClass + approvedClass"
|
||||||
|
@click="onExpenseClicked"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="expense.is_approved"
|
||||||
|
class="absolute z-top rounded-20 bg-white q-pa-none"
|
||||||
|
style="transform: translate(-15px, -15px);"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="verified"
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
|
||||||
|
<!-- avatar type icon section -->
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon
|
||||||
|
:name="getExpenseTypeIcon(expense.type)"
|
||||||
|
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<q-badge
|
||||||
|
v-if="expense.type === 'ON_CALL'"
|
||||||
|
floating
|
||||||
|
class="q-pa-none rounded-50 bg-white z-top"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="shield"
|
||||||
|
size="xs"
|
||||||
|
:color="expense.is_approved ? 'primary' : ($q.dark.isActive ? 'blue-grey-2' : 'grey-8')"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
</q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- amount or mileage section -->
|
||||||
|
<q-item-section class="col-auto">
|
||||||
|
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||||
|
<template v-if="typeof expense.mileage === 'number'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
${{ expense.amount.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-else>
|
||||||
|
${{ expense.amount.toFixed(2) }}
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<!-- date label -->
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
lines="1"
|
||||||
|
>
|
||||||
|
<!-- {{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' }) }} -->
|
||||||
|
{{ expense.date }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- attachment file icon -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
color="primary"
|
||||||
|
class="q-mx-lg"
|
||||||
|
icon="attach_file"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- comment section -->
|
||||||
|
<q-item-section
|
||||||
|
v-if="!horizontal"
|
||||||
|
top
|
||||||
|
>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
lines="1"
|
||||||
|
>
|
||||||
|
{{ expense.comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- supervisor comment section -->
|
||||||
|
<q-item-section
|
||||||
|
v-if="expense.supervisor_comment && !horizontal"
|
||||||
|
top
|
||||||
|
>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
v-if="expense.supervisor_comment"
|
||||||
|
caption
|
||||||
|
lines="2"
|
||||||
|
>
|
||||||
|
{{ expense.supervisor_comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section
|
||||||
|
v-if="(!timesheet_store.pay_period_details.weeks[0]?.is_approved && !expense.is_approved) || horizontal"
|
||||||
|
side
|
||||||
|
class="q-pa-none"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
icon="edit"
|
||||||
|
class="q-mb-xs z-top"
|
||||||
|
@click.stop="setExpenseToModify"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="negative"
|
||||||
|
icon="close"
|
||||||
|
class="z-top"
|
||||||
|
@click.stop="requestExpenseDeletion"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
@ -2,35 +2,14 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
|
||||||
import { expenseTypeIcon } from 'src/modules/timesheets/utils/expense.util';
|
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import ExpenseCrudDialogListItem from 'src/modules/timesheets/components/expense-crud-dialog-list-item.vue';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/api/use-expense-api';
|
|
||||||
import { default_expense, type Expense } from 'src/modules/timesheets/models/expense.models';
|
|
||||||
import { computed, inject } from 'vue';
|
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const expenses_api = useExpensesApi();
|
|
||||||
|
|
||||||
const expenses_list = computed(() => timesheet_store.pay_period_details.weeks.flatMap(week =>
|
const { horizontal = false } = defineProps<{
|
||||||
Object.values(week.expenses).flatMap(day => day.expenses)));
|
horizontal?: boolean;
|
||||||
|
}>();
|
||||||
const employee_email = inject('employeeEmail', '');
|
|
||||||
|
|
||||||
const setExpenseToModify = (expense: Expense) => {
|
|
||||||
expenses_store.mode = 'update';
|
|
||||||
expenses_store.current_expense = expense;
|
|
||||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestExpenseDeletion = async (expense: Expense) => {
|
|
||||||
expenses_store.mode = 'delete';
|
|
||||||
expenses_store.initial_expense = expense;
|
|
||||||
expenses_store.current_expense = default_expense;
|
|
||||||
await expenses_api.deleteExpenseByEmployeeEmail(employee_email, expenses_store.initial_expense.date);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -38,6 +17,7 @@
|
||||||
<q-list
|
<q-list
|
||||||
padding
|
padding
|
||||||
class="rounded-borders"
|
class="rounded-borders"
|
||||||
|
:class="horizontal ? 'row justify-center' : ''"
|
||||||
>
|
>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
v-if="expenses_store.pay_period_expenses.expenses.length === 0"
|
||||||
|
|
@ -45,107 +25,15 @@
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.expense.empty_list') }}
|
{{ $t('timesheet.expense.empty_list') }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item
|
|
||||||
style="border: solid 1px lightgrey; border-radius: 7px;"
|
<ExpenseCrudDialogListItem
|
||||||
v-for="(expense, index) in expenses_list"
|
v-for="(expense, index) in expenses_store.pay_period_expenses.expenses"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="q-my-xs shadow-1"
|
v-model="expense.is_approved"
|
||||||
:class="expenses_store.mode === 'update' ? 'bg-accent' : ''"
|
:index="index"
|
||||||
>
|
:expense="expense"
|
||||||
<!-- avatar type icon section -->
|
:horizontal="horizontal"
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon
|
|
||||||
:name="expenseTypeIcon(expense.type)"
|
|
||||||
color="primary"
|
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
|
||||||
<template v-if="typeof expense.mileage === 'number'">
|
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ expense.amount.toFixed(2) }} $
|
|
||||||
</template>
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label v-else>
|
|
||||||
{{ expense.amount.toFixed(2) }} $
|
|
||||||
</q-item-label>
|
|
||||||
|
|
||||||
<!-- date label -->
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
lines="2"
|
|
||||||
>
|
|
||||||
{{ $d(new Date(expense.date), { year: 'numeric', month: 'short', day: 'numeric', weekday: 'short' })
|
|
||||||
}}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- attachment file icon -->
|
|
||||||
<q-item-section side>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="md"
|
|
||||||
color="primary"
|
|
||||||
class="q-mx-lg"
|
|
||||||
icon="attach_file"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- comment section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label lines="1">
|
|
||||||
{{ $t('timesheet.expense.employee_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
caption
|
|
||||||
lines="2"
|
|
||||||
>
|
|
||||||
{{ expense.comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<!-- supervisor comment section -->
|
|
||||||
<q-item-section top>
|
|
||||||
<q-item-label lines="1">
|
|
||||||
{{ $t('timesheet.expense.supervisor_comment') }}
|
|
||||||
</q-item-label>
|
|
||||||
<q-item-label
|
|
||||||
v-if="expense.supervisor_comment"
|
|
||||||
caption
|
|
||||||
lines="2"
|
|
||||||
>
|
|
||||||
{{ expense.supervisor_comment }}
|
|
||||||
</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section
|
|
||||||
v-if="!expenses_store.pay_period_expenses.is_approved && !expense.is_approved"
|
|
||||||
side
|
|
||||||
>
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
color="primary"
|
|
||||||
icon="edit"
|
|
||||||
@click="setExpenseToModify(expense)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
push
|
|
||||||
dense
|
|
||||||
size="xs"
|
|
||||||
color="negative"
|
|
||||||
icon="close"
|
|
||||||
@click="requestExpenseDeletion(expense)"
|
|
||||||
/>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useShiftStore } from 'src/stores/shift-store';
|
||||||
|
import { SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
const shift_store = useShiftStore();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'onCommentBlur': [void];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="col-xs-6 col-sm-4 col-md-3 row q-mx-xs q-my-none">
|
||||||
|
<q-select
|
||||||
|
v-model="shift_store.current_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 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_store.current_shift.is_remote"
|
||||||
|
class="q-pa-none q-ma-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto row q-mx-xs">
|
||||||
|
<q-input
|
||||||
|
v-model="shift_store.current_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_store.current_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_store.current_shift.comment"
|
||||||
|
type="textarea"
|
||||||
|
autogrow
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
square
|
||||||
|
:label="$t('timesheet.shift.fields.header_comment')"
|
||||||
|
:counter="true"
|
||||||
|
:maxlength="512"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -5,10 +5,9 @@
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useShiftStore } from 'src/stores/shift-store';
|
import { useShiftStore } from 'src/stores/shift-store';
|
||||||
import { useShiftApi } from 'src/modules/timesheets/composables/api/use-shift-api';
|
import { useShiftApi } from 'src/modules/timesheets/composables/api/use-shift-api';
|
||||||
import { SHIFT_TYPES } from 'src/modules/timesheets/models/shift.models';
|
|
||||||
|
|
||||||
const { date_iso, mode, current_shift, is_open, close } = useShiftStore();
|
const shift_store = useShiftStore();
|
||||||
const { upsertOrDeleteShiftByEmployeeEmail } = useShiftApi();
|
const shift_api = useShiftApi();
|
||||||
|
|
||||||
const { employeeEmail } = defineProps<{
|
const { employeeEmail } = defineProps<{
|
||||||
employeeEmail: string;
|
employeeEmail: string;
|
||||||
|
|
@ -19,33 +18,38 @@
|
||||||
const conflicts = ref<Array<{ start_time: string; end_time: string; type: string }>>([]);
|
const conflicts = ref<Array<{ start_time: string; end_time: string; type: string }>>([]);
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
mode === 'delete' ||
|
shift_store.mode === 'delete' ||
|
||||||
(current_shift.start_time.trim().length === 5 &&
|
(shift_store.current_shift.start_time.trim().length === 5 &&
|
||||||
current_shift.end_time.trim().length === 5 &&
|
shift_store.current_shift.end_time.trim().length === 5 &&
|
||||||
current_shift.type !== undefined)
|
shift_store.current_shift.type !== undefined)
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model=" is_open"
|
v-model="shift_store.is_open"
|
||||||
persistent
|
persistent
|
||||||
|
full-width
|
||||||
transition-show="fade"
|
transition-show="fade"
|
||||||
transition-hide="fade"
|
transition-hide="fade"
|
||||||
>
|
>
|
||||||
|
|
||||||
<q-card class="q-pa-md">
|
<q-card
|
||||||
<div class="row items-center q-mb-sm">
|
class="q-pa-md rounded-10 shadow-5"
|
||||||
|
:style="$q.screen.gt.sm ? 'max-width: 60vw !important;' : ''"
|
||||||
|
>
|
||||||
|
<q-card-section class="row items-center q-mb-sm q-pa-none">
|
||||||
<q-icon
|
<q-icon
|
||||||
name="schedule"
|
name="schedule"
|
||||||
size="24px"
|
size="24px"
|
||||||
class="q-mr-sm"
|
class="q-mr-sm"
|
||||||
|
color="primary"
|
||||||
/>
|
/>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
{{
|
{{
|
||||||
mode === 'create'
|
shift_store.mode === 'create'
|
||||||
? $t('timesheet.shift.actions.add')
|
? $t('timesheet.shift.actions.add')
|
||||||
: mode === 'update'
|
: shift_store.mode === 'update'
|
||||||
? $t('timesheet.shift.actions.edit')
|
? $t('timesheet.shift.actions.edit')
|
||||||
: $t('timesheet.shift.actions.delete')
|
: $t('timesheet.shift.actions.delete')
|
||||||
}}
|
}}
|
||||||
|
|
@ -55,68 +59,17 @@
|
||||||
outline
|
outline
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{{ date_iso }}
|
{{ shift_store.date_iso }}
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</div>
|
</q-card-section>
|
||||||
|
|
||||||
<q-separator spaced />
|
<q-separator spaced />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="mode !== 'delete'"
|
v-if="shift_store.mode !== 'delete'"
|
||||||
class="column q-gutter-md"
|
class="row no-wrap items-start justify-center"
|
||||||
>
|
>
|
||||||
<div class="row ">
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="current_shift.start_time"
|
|
||||||
:label="$t('timesheet.shift.fields.start')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
inputmode="numeric"
|
|
||||||
mask="##:##"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<q-input
|
|
||||||
v-model="current_shift.end_time"
|
|
||||||
:label="$t('timesheet.shift.fields.end')"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
inputmode="numeric"
|
|
||||||
mask="##:##"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row items-center">
|
|
||||||
<q-select
|
|
||||||
v-model="current_shift.type"
|
|
||||||
options-dense
|
|
||||||
:options="SHIFT_TYPES"
|
|
||||||
:label="$t('timesheet.shift.types.label')"
|
|
||||||
class="col"
|
|
||||||
color="primary"
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
hide-dropdown-icon
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
/>
|
|
||||||
<q-toggle
|
|
||||||
v-model="current_shift.is_remote"
|
|
||||||
:label="$t('timesheet.shift.types.REMOTE')"
|
|
||||||
class="col-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<q-input
|
|
||||||
v-model="current_shift.comment"
|
|
||||||
type="textarea"
|
|
||||||
autogrow
|
|
||||||
filled
|
|
||||||
dense
|
|
||||||
:label="$t('timesheet.shift.fields.header_comment')"
|
|
||||||
:counter="true"
|
|
||||||
:maxlength="512"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -157,15 +110,15 @@
|
||||||
flat
|
flat
|
||||||
color="grey-8"
|
color="grey-8"
|
||||||
:label="$t('timesheet.cancel_button')"
|
:label="$t('timesheet.cancel_button')"
|
||||||
@click="close"
|
@click="shift_store.close"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="save_alt"
|
icon="save_alt"
|
||||||
:label="mode === 'delete' ? $t('timesheet.delete_button') : $t('timesheet.save_button')"
|
:label="shift_store.mode === 'delete' ? $t('timesheet.delete_button') : $t('timesheet.save_button')"
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
:disable="!canSubmit"
|
:disable="!canSubmit"
|
||||||
@click="upsertOrDeleteShiftByEmployeeEmail(employeeEmail)"
|
@click="shift_api.upsertOrDeleteShiftByEmployeeEmail(employeeEmail)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
const is_showing_legend = ref(false);
|
const is_showing_legend = ref(false);
|
||||||
|
|
||||||
const legend: ShiftLegendItem[] = [
|
const legend: ShiftLegendItem[] = [
|
||||||
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR', text_color: 'grey-8' },
|
{ type: 'REGULAR', color: 'secondary', label_type: 'timesheet.shift.types.REGULAR' },
|
||||||
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
{ type: 'EVENING', color: 'warning', label_type: 'timesheet.shift.types.EVENING' },
|
||||||
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
{ type: 'EMERGENCY', color: 'amber-10', label_type: 'timesheet.shift.types.EMERGENCY' },
|
||||||
{ type: 'OVERTIME', color: 'negative', label_type: 'timesheet.shift.types.OVERTIME' },
|
{ type: 'OVERTIME', color: 'negative', label_type: 'timesheet.shift.types.OVERTIME' },
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
dense
|
dense
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col-auto q-ma-sm"
|
class="col-auto q-my-sm"
|
||||||
@click="is_showing_legend = !is_showing_legend"
|
@click="is_showing_legend = !is_showing_legend"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #default>
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="is_showing_legend"
|
v-if="is_showing_legend"
|
||||||
class="q-pa-xs bg-white rounded-5 shadow-2 text-center q-ma-xs"
|
class="q-py-xs bg-white rounded-5 shadow-2 text-center q-my-xs"
|
||||||
>
|
>
|
||||||
<q-badge
|
<q-badge
|
||||||
v-for="shift_type in shift_type_legend"
|
v-for="shift_type in shift_type_legend"
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
:color="shift_type.color"
|
:color="shift_type.color"
|
||||||
:label="shift_type.label"
|
:label="shift_type.label"
|
||||||
:text-color="shift_type.text_color || 'white'"
|
:text-color="shift_type.text_color || 'white'"
|
||||||
class="q-px-md q-py-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
class="q-pa-xs q-mx-xs q-my-none text-uppercase text-weight-bolder justify-center"
|
||||||
style="font-size: 0.8em;"
|
style="font-size: 0.8em;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script
|
||||||
import { computed } from 'vue';
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
import type { Shift } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
||||||
|
const q = useQuasar();
|
||||||
|
|
||||||
const { shift, dense = false } = defineProps<{
|
const { shift, dense = false } = defineProps<{
|
||||||
shift: Shift;
|
shift: Shift;
|
||||||
|
|
@ -20,7 +25,9 @@
|
||||||
})
|
})
|
||||||
const comment_icon = computed(() => (has_comment.value ? 'announcement' : 'chat_bubble_outline'));
|
const comment_icon = computed(() => (has_comment.value ? 'announcement' : 'chat_bubble_outline'));
|
||||||
const comment_color = computed(() => (has_comment.value ? 'primary' : 'grey-8'));
|
const comment_color = computed(() => (has_comment.value ? 'primary' : 'grey-8'));
|
||||||
const hour_font_size = computed(() => dense ? '0.9em' : '1.5em' )
|
const hour_font_size = computed(() => dense ? 'font-size: 1em;' : 'font-size: 1.5em;')
|
||||||
|
const is_hovering = ref(false);
|
||||||
|
const font_color = computed(() => shift.type === 'REGULAR' ? ( q.dark.isActive ? ' text-blue-grey-2' : ' text-grey-8' ) : ' text-white' )
|
||||||
|
|
||||||
const get_shift_color = (type: string): string => {
|
const get_shift_color = (type: string): string => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|
@ -35,14 +42,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const get_text_color = (type: string): string => {
|
|
||||||
switch (type) {
|
|
||||||
case 'REGULAR': return 'grey-8';
|
|
||||||
case '': return 'grey-5';
|
|
||||||
default: return 'white';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClickUpdate = (type: string) => {
|
const onClickUpdate = (type: string) => {
|
||||||
if (type !== '') { emit('request-update', shift) };
|
if (type !== '') { emit('request-update', shift) };
|
||||||
}
|
}
|
||||||
|
|
@ -53,17 +52,33 @@
|
||||||
<template>
|
<template>
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
class="q-py-none q-mx-md text-uppercase text-center items-center rounded-10 cursor-pointer"
|
||||||
:class="shift.type"
|
|
||||||
style="line-height: 1;"
|
style="line-height: 1;"
|
||||||
@click.stop="onClickUpdate(shift.type)"
|
@click.stop="onClickUpdate(shift.type)"
|
||||||
|
@mouseenter="is_hovering = true"
|
||||||
|
@mouseleave="is_hovering = false"
|
||||||
|
>
|
||||||
|
<!-- highlight hovering div -->
|
||||||
|
<transition
|
||||||
|
enter-active-class="animated zoomIn"
|
||||||
|
leave-active-class="animated zoomOut"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="is_hovering"
|
||||||
|
class="absolute shift-highlight full-height full-width no-pointer-events"
|
||||||
|
></div>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
<div class="col row">
|
||||||
|
<!-- punch-in timestamp -->
|
||||||
|
<q-card-section
|
||||||
|
class="col q-pa-none"
|
||||||
|
:class="dense ? 'q-px-xs q-mx-xs' : ''"
|
||||||
>
|
>
|
||||||
<!-- punch-in timestamps -->
|
|
||||||
<q-card-section class="q-pa-none col">
|
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="text-weight-bolder q-pa-xs rounded-5"
|
class="text-weight-bolder q-pa-xs rounded-5"
|
||||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
:class="'bg-' + get_shift_color(shift.type) + font_color"
|
||||||
:style="'font-size: ' + hour_font_size + '; line-height: 80% !important;'"
|
:style="hour_font_size + ' line-height: 80% !important;'"
|
||||||
>
|
>
|
||||||
{{ shift.start_time }}
|
{{ shift.start_time }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
@ -72,7 +87,7 @@
|
||||||
<!-- arrows pointing to punch-out timestamps -->
|
<!-- arrows pointing to punch-out timestamps -->
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="items-center justify-center q-mx-sm col"
|
class="col items-center justify-center q-mx-sm"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="icon_data, index in [
|
v-for="icon_data, index in [
|
||||||
|
|
@ -81,54 +96,53 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="shift.type"
|
v-if="shift.type && !dense"
|
||||||
name="double_arrow"
|
name="double_arrow"
|
||||||
:color="icon_data.color"
|
:color="icon_data.color"
|
||||||
size="24px"
|
:size="dense ? '16px' : '24px'"
|
||||||
:style="icon_data.transform"
|
:style="icon_data.transform"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-primary"
|
||||||
|
> > </span>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<!-- punch-out timestamps -->
|
<!-- punch-out timestamps -->
|
||||||
<q-card-section class="q-pa-none col">
|
<q-card-section
|
||||||
|
class="col q-pa-none"
|
||||||
|
:class="dense ? 'q-px-xs q-mx-xs' : ''"
|
||||||
|
>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="text-weight-bolder text-white q-pa-xs rounded-5"
|
class="text-weight-bolder q-pa-xs rounded-5"
|
||||||
:class="'bg-' + get_shift_color(shift.type) + ' text-' + get_text_color(shift.type)"
|
:class="'bg-' + get_shift_color(shift.type) + font_color"
|
||||||
style="font-size: 1.5em; line-height: 80% !important;"
|
:style="hour_font_size + ' line-height: 80% !important;'"
|
||||||
>
|
>
|
||||||
{{ shift.end_time }}
|
{{ shift.end_time }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
<div class="col-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- comment and expenses buttons -->
|
<q-card-section class="col-auto q-pa-none no-wrap q-mx-xs">
|
||||||
<q-card-section class="col q-pa-none text-right">
|
|
||||||
<!-- comment btn -->
|
<!-- comment btn -->
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="shift.type"
|
v-if="shift.type"
|
||||||
:name="comment_icon"
|
:name="comment_icon"
|
||||||
:color="comment_color"
|
:color="comment_color"
|
||||||
class="q-pa-none q-mx-xs"
|
class="q-pa-none q-mr-xs"
|
||||||
size="sm"
|
:size="dense ? 'xs' : 'sm'"
|
||||||
/>
|
|
||||||
<!-- expenses btn -->
|
|
||||||
<q-btn
|
|
||||||
v-if="shift.type"
|
|
||||||
flat
|
|
||||||
dense
|
|
||||||
color='grey-8'
|
|
||||||
icon="attach_money"
|
|
||||||
class="q-pa-none q-mx-xs"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- delete btn -->
|
<!-- delete btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="shift.type"
|
v-if="shift.type"
|
||||||
push
|
|
||||||
dense
|
dense
|
||||||
size="sm"
|
:size="dense ? 'xs' : 'sm'"
|
||||||
color="red-6"
|
color="negative"
|
||||||
icon="close"
|
icon="clear"
|
||||||
class="q-ml-xs"
|
|
||||||
@click.stop="onClickDelete"
|
@click.stop="onClickDelete"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ import { computed } from 'vue';
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const font_size = computed(() => dense ? '1.5em' : '2.5em')
|
const date_font_size = computed(() => dense ? '1.5em' : '2.5em');
|
||||||
|
const weekday_font_size = computed(() => dense ? '0.55em;' : '0.7em;');
|
||||||
|
const date_box_size = computed(() => dense ? 'width: 50px;' : 'width: 75px;');
|
||||||
|
|
||||||
const get_date_from_short = (short_date: string): Date => {
|
const get_date_from_short = (short_date: string): Date => {
|
||||||
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
|
return new Date(timesheet_store.pay_period.pay_year.toString() + '/' + short_date);
|
||||||
|
|
@ -39,30 +41,31 @@ import { computed } from 'vue';
|
||||||
<div
|
<div
|
||||||
v-for="week, index in timesheet_store.pay_period_details.weeks"
|
v-for="week, index in timesheet_store.pay_period_details.weeks"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="col q-px-xs q-pt-xs q-mx-sm rounded-5"
|
class="column col q-mx-xs"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
v-for="day, day_index in week.shifts"
|
v-for="day, day_index in week.shifts"
|
||||||
:key="day_index + index"
|
:key="day_index + index"
|
||||||
class="row items-center rounded-10 q-mb-xs"
|
class="row col items-center rounded-10 q-my-xs q-pa-xs"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Dates column -->
|
<!-- Dates column -->
|
||||||
<q-card-section class="col-auto q-pa-xs text-white q-mr-md">
|
<q-card-section class="col-auto q-pa-none text-white">
|
||||||
<div
|
<div
|
||||||
class="bg-primary rounded-10 q-pa-xs text-center"
|
class="bg-primary rounded-10 q-pa-xs text-center"
|
||||||
:style="'width: ' + dense? '60px' : '75px;'"
|
:style="date_box_size"
|
||||||
>
|
>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
style="font-size: 0.7em;"
|
v-if="!dense"
|
||||||
|
:style="'font-size: ' + weekday_font_size"
|
||||||
class="text-uppercase"
|
class="text-uppercase"
|
||||||
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
>{{ $d(getDate(day.short_date), { weekday: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
class="text-weight-bolder"
|
class="text-weight-bolder"
|
||||||
:style="'font-size: ' + font_size + '; line-height: 90% !important;'"
|
:style="'font-size: ' + date_font_size + '; line-height: 90% !important;'"
|
||||||
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
>{{ day.short_date.split('/')[1] }}</q-item-label>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
style="font-size: 0.7em;"
|
:style="'font-size: ' + weekday_font_size"
|
||||||
class="text-uppercase"
|
class="text-uppercase"
|
||||||
>{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
>{{ $d(getDate(day.short_date), { month: $q.screen.lt.md ? 'short' : 'long' }) }}</q-item-label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -70,13 +73,15 @@ import { computed } from 'vue';
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<!-- List of shifts column -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col q-pa-none">
|
||||||
<ShiftListHeader v-if="day.shifts.length > 0"/>
|
<ShiftListHeader v-if="day.shifts.length > 0 && !dense"/>
|
||||||
<div
|
<div
|
||||||
v-if="day.shifts.length > 0"
|
v-if="day.shifts.length > 0"
|
||||||
|
class="q-gutter-xs"
|
||||||
>
|
>
|
||||||
<ShiftListRow
|
<ShiftListRow
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
|
:dense="dense"
|
||||||
:shift="shift"
|
:shift="shift"
|
||||||
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
@request-update="value => openUpdate(to_iso_date(day.short_date), value)"
|
||||||
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
@request-delete="value => openDelete(to_iso_date(day.short_date), value)"
|
||||||
|
|
@ -84,12 +89,13 @@ import { computed } from 'vue';
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- add shift btn column -->
|
<!-- add shift btn column -->
|
||||||
<q-card-section class="q-pr-xs col-auto">
|
<q-card-section class="col-auto q-pa-none">
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
|
:dense="dense"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="more_time"
|
icon="more_time"
|
||||||
class="q-pa-sm"
|
:class="dense ? '' : 'q-pa-sm q-mr-sm'"
|
||||||
@click="openCreate(to_iso_date(day.short_date))"
|
@click="openCreate(to_iso_date(day.short_date))"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { is_loading } = useTimesheetStore();
|
const { is_loading } = useTimesheetStore();
|
||||||
const { getPayPeriodDetailsByDate, getPreviousPayPeriodDetails, getNextPayPeriodDetails } = useTimesheetApi();
|
const timesheet_api = useTimesheetApi();
|
||||||
|
|
||||||
provide('employeeEmail', employeeEmail);
|
provide('employeeEmail', employeeEmail);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<template>
|
<template>
|
||||||
<q-card
|
<q-card
|
||||||
flat
|
flat
|
||||||
class="q-mt-md bg-secondary full-width"
|
class="transparent full-width"
|
||||||
>
|
>
|
||||||
<q-inner-loading
|
<q-inner-loading
|
||||||
:showing="is_loading"
|
:showing="is_loading"
|
||||||
|
|
@ -37,19 +37,20 @@
|
||||||
|
|
||||||
<q-card-section
|
<q-card-section
|
||||||
:horizontal="$q.screen.gt.sm"
|
:horizontal="$q.screen.gt.sm"
|
||||||
class="q-px-lg items-center"
|
class="q-px-md items-center"
|
||||||
:class="$q.screen.lt.md ? 'column' : ''"
|
:class="$q.screen.lt.md ? 'column' : ''"
|
||||||
>
|
>
|
||||||
<!-- navigation btn -->
|
<!-- navigation btn -->
|
||||||
<PayPeriodNavigator
|
<PayPeriodNavigator
|
||||||
@date-selected="getPayPeriodDetailsByDate"
|
v-if="!dense"
|
||||||
@pressed-previous-button="getPreviousPayPeriodDetails"
|
@date-selected="timesheet_api.getPayPeriodDetailsByDate(employeeEmail)"
|
||||||
@pressed-next-button="getNextPayPeriodDetails"
|
@pressed-previous-button="timesheet_api.getPreviousPayPeriodDetails(employeeEmail)"
|
||||||
|
@pressed-next-button="timesheet_api.getNextPayPeriodDetails(employeeEmail)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- mobile expenses button -->
|
<!-- mobile expenses button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.screen.lt.md"
|
v-if="$q.screen.lt.md && !dense"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -60,13 +61,13 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- shift's colored legend -->
|
<!-- shift's colored legend -->
|
||||||
<ShiftListLegend :is-loading="false" />
|
<ShiftListLegend v-if="!dense" :is-loading="false" />
|
||||||
|
|
||||||
<q-space />
|
<q-space />
|
||||||
|
|
||||||
<!-- desktop expenses button -->
|
<!-- desktop expenses button -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="$q.screen.gt.sm"
|
v-if="$q.screen.gt.sm && !dense"
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -77,7 +78,7 @@
|
||||||
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section :horizontal="$q.screen.gt.sm">
|
<q-card-section :horizontal="$q.screen.gt.sm" class="bg-secondary q-pa-sm rounded-10">
|
||||||
<ShiftList :dense="dense"/>
|
<ShiftList :dense="dense"/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
import { unwrapAndClone } from "src/utils/unwrap-and-clone";
|
||||||
import { TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
|
// import { TIME_FORMAT_PATTERN } from "src/modules/timesheets/constants/shift.constants";
|
||||||
import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
// import { GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||||
import { useShiftStore } from "src/stores/shift-store";
|
import { useShiftStore } from "src/stores/shift-store";
|
||||||
import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
import { default_shift, type Shift, type UpsertShift } from "src/modules/timesheets/models/shift.models";
|
||||||
import { deepEqual } from "src/utils/deep-equal";
|
import { deepEqual } from "src/utils/deep-equal";
|
||||||
|
|
@ -22,49 +22,49 @@ export const useShiftApi = () => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseHHMM = (s: string): [number, number] => {
|
// const parseHHMM = (s: string): [number, number] => {
|
||||||
const m = /^(\d{2}):(\d{2})$/.exec(s);
|
// const m = TIME_FORMAT_PATTERN.exec(s);
|
||||||
|
|
||||||
if (!m) {
|
// if (!m) {
|
||||||
throw new GenericApiError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.` });
|
// throw new GenericApiError({ status_code: 400, message: `Invalid time format: ${s}. Expected HH:MM.` });
|
||||||
}
|
// }
|
||||||
|
|
||||||
const h = Number(m[1]);
|
// const h = Number(m[1]);
|
||||||
const min = Number(m[2]);
|
// const min = Number(m[2]);
|
||||||
|
|
||||||
if (Number.isNaN(h) || Number.isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) {
|
// if (Number.isNaN(h) || Number.isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) {
|
||||||
throw new GenericApiError({ status_code: 400, message: `Invalid time value: ${s}.` })
|
// throw new GenericApiError({ status_code: 400, message: `Invalid time value: ${s}.` })
|
||||||
}
|
// }
|
||||||
return [h, min];
|
// return [h, min];
|
||||||
};
|
// };
|
||||||
|
|
||||||
const toMinutes = (hhmm: string): number => {
|
// const toMinutes = (hhmm: string): number => {
|
||||||
const [h, m] = parseHHMM(hhmm);
|
// const [h, m] = parseHHMM(hhmm);
|
||||||
return h * 60 + m;
|
// return h * 60 + m;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const validateShift = (shift: Shift, label: 'old_shift' | 'new_shift') => {
|
// const validateShift = (shift: Shift, label: 'old_shift' | 'new_shift') => {
|
||||||
if (!TIME_FORMAT_PATTERN.test(shift.start_time) || !TIME_FORMAT_PATTERN.test(shift.end_time)) {
|
// if (!TIME_FORMAT_PATTERN.test(shift.start_time) || !TIME_FORMAT_PATTERN.test(shift.end_time)) {
|
||||||
throw new GenericApiError({
|
// throw new GenericApiError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: `Invalid time format in ${label}. Expected HH:MM`,
|
// message: `Invalid time format in ${label}. Expected HH:MM, got ${shift.start_time} - ${shift.end_time}`,
|
||||||
context: { [label]: shift }
|
// context: { [label]: shift }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (toMinutes(shift.end_time) <= toMinutes(shift.start_time)) {
|
// if (toMinutes(shift.end_time) <= toMinutes(shift.start_time)) {
|
||||||
throw new GenericApiError({
|
// throw new GenericApiError({
|
||||||
status_code: 400,
|
// status_code: 400,
|
||||||
message: `Invalid time range in ${label}. The End time must be after the Start time`,
|
// message: `Invalid time range in ${label}. The End time must be after the Start time`,
|
||||||
context: { [label]: shift }
|
// context: { [label]: shift }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string): Promise<void> => {
|
||||||
const flat_upsert_shift: UpsertShift = {
|
const flat_upsert_shift: UpsertShift = {
|
||||||
...(deepEqual(shift_store.initial_shift, default_shift) ? { old_shift: unwrapAndClone(shift_store.initial_shift) } : {}),
|
...(deepEqual(shift_store.initial_shift, default_shift) ? undefined : { old_shift: unwrapAndClone(shift_store.initial_shift) }),
|
||||||
...(deepEqual(shift_store.current_shift, default_shift) ? { new_shift: unwrapAndClone(shift_store.current_shift) } : {}),
|
...(deepEqual(shift_store.current_shift, default_shift) ? undefined : { new_shift: unwrapAndClone(shift_store.current_shift) }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalized_upsert_shift: UpsertShift = {
|
const normalized_upsert_shift: UpsertShift = {
|
||||||
|
|
@ -72,8 +72,8 @@ export const useShiftApi = () => {
|
||||||
...(flat_upsert_shift.new_shift ? { new_shift: normalizeShiftPayload(flat_upsert_shift.new_shift) } : {}),
|
...(flat_upsert_shift.new_shift ? { new_shift: normalizeShiftPayload(flat_upsert_shift.new_shift) } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (normalized_upsert_shift.old_shift) validateShift(normalized_upsert_shift.old_shift, 'old_shift');
|
// if (normalized_upsert_shift.old_shift) validateShift(normalized_upsert_shift.old_shift, 'old_shift');
|
||||||
if (normalized_upsert_shift.new_shift) validateShift(normalized_upsert_shift.new_shift, 'new_shift');
|
// if (normalized_upsert_shift.new_shift) validateShift(normalized_upsert_shift.new_shift, 'new_shift');
|
||||||
|
|
||||||
await shift_store.upsertOrDeleteShiftByEmployeeEmail(employee_email, normalized_upsert_shift);
|
await shift_store.upsertOrDeleteShiftByEmployeeEmail(employee_email, normalized_upsert_shift);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
export const TIME_FORMAT_PATTERN = /^(\d{2}:\d{2})?$/;
|
||||||
|
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'PRIME_GARDE';
|
export type ExpenseType = 'PER_DIEM' | 'MILEAGE' | 'EXPENSES' | 'ON_CALL';
|
||||||
|
|
||||||
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'PRIME_GARDE',];
|
export const EXPENSE_TYPE: ExpenseType[] = ['PER_DIEM', 'MILEAGE', 'EXPENSES', 'ON_CALL',];
|
||||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_GARDE',];
|
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'ON_CALL',];
|
||||||
|
|
||||||
export interface Expense {
|
export interface Expense {
|
||||||
date: string;
|
date: string;
|
||||||
|
|
@ -21,9 +21,9 @@ export type ExpenseTotals = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface PayPeriodExpenses {
|
export interface PayPeriodExpenses {
|
||||||
is_approved: boolean;
|
|
||||||
expenses: Expense[];
|
expenses: Expense[];
|
||||||
totals?: ExpenseTotals;
|
total_expense: number;
|
||||||
|
total_mileage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpsertExpense {
|
export interface UpsertExpense {
|
||||||
|
|
@ -40,6 +40,7 @@ export const default_expense: Expense = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const default_pay_period_expenses: PayPeriodExpenses = {
|
export const default_pay_period_expenses: PayPeriodExpenses = {
|
||||||
is_approved: false,
|
|
||||||
expenses: [],
|
expenses: [],
|
||||||
|
total_expense: -1,
|
||||||
|
total_mileage: -1,
|
||||||
}
|
}
|
||||||
|
|
@ -41,8 +41,8 @@ export interface UpsertShift {
|
||||||
|
|
||||||
export const default_shift: Readonly<Shift> = {
|
export const default_shift: Readonly<Shift> = {
|
||||||
date: '',
|
date: '',
|
||||||
start_time: '--:--',
|
start_time: '',
|
||||||
end_time: '--:--',
|
end_time: '',
|
||||||
type: 'REGULAR',
|
type: 'REGULAR',
|
||||||
comment: '',
|
comment: '',
|
||||||
is_approved: false,
|
is_approved: false,
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,12 @@ export const timesheetService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
getExpensesByPayPeriodAndEmployeeEmail: async (email: string, year: string, period_number: string): Promise<PayPeriodExpenses> => {
|
||||||
const response = await api.get(`/expenses/${email}/${year}/${period_number}`);
|
const response = await api.get(`/expenses/list/${email}/${year}/${period_number}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
upsertOrDeleteShiftsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift[], date: string): Promise<PayPeriodDetails> => {
|
upsertOrDeleteShiftsByDateAndEmployeeEmail: async (email: string, payload: UpsertShift): Promise<PayPeriodDetails> => {
|
||||||
const response = await api.put(`/shifts/upsert/${email}/${date}`, payload);
|
const response = await api.put(`/shifts/upsert/${email}`, payload);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@ import type { Expense, ExpenseTotals } from "src/modules/timesheets/models/expen
|
||||||
|
|
||||||
//------------------ normalization / icons ------------------
|
//------------------ normalization / icons ------------------
|
||||||
export const normExpenseType = (type: unknown): string =>
|
export const normExpenseType = (type: unknown): string =>
|
||||||
typeof type === 'string' ? type.trim().toUpperCase() : '';
|
typeof type === 'string' ? type.trim() : '';
|
||||||
|
|
||||||
const icon_map: Record<string,string> = {
|
const icon_map: Record<string,string> = {
|
||||||
MILEAGE: 'time_to_leave',
|
MILEAGE: 'time_to_leave',
|
||||||
EXPENSES: 'receipt_long',
|
EXPENSES: 'receipt_long',
|
||||||
PER_DIEM: 'hotel',
|
PER_DIEM: 'hotel',
|
||||||
PRIME_GARDE: 'admin_panel_settings',
|
ON_CALL: 'phone_android',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const expenseTypeIcon = (type: unknown): string => {
|
export const getExpenseTypeIcon = (type: unknown): string => {
|
||||||
const t = normExpenseType(type);
|
const t = normExpenseType(type);
|
||||||
return (
|
return (
|
||||||
icon_map[t.toLowerCase()] ??
|
icon_map[t] ??
|
||||||
icon_map[t.replace('-','_').toLowerCase()] ??
|
icon_map[t.replace('-','_')] ??
|
||||||
'help_outline'
|
'help_outline'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
18
src/pages/employee-list-page.vue
Normal file
18
src/pages/employee-list-page.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import EmployeeListTable from 'src/modules/employee-list/components/employee-list-table.vue';
|
||||||
|
import EmployeeListAddModifyDialog from 'src/modules/employee-list/components/employee/employee-list-add-modify-dialog.vue';
|
||||||
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<EmployeeListAddModifyDialog />
|
||||||
|
|
||||||
|
<PageHeaderTemplate title="employee_list.page_header" />
|
||||||
|
|
||||||
|
<EmployeeListTable />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue';
|
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue';
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
|
import type { EmployeeProfile } from 'src/modules/employee-list/models/employee-profile.models';
|
||||||
|
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
const employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ];
|
const employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ];
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import SupervisorCrewTable from '../components/supervisor/supervisor-crew-table.vue';
|
|
||||||
import EmployeeListAddModifyDialog from '../components/employee/employee-list-add-modify-dialog.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-page>
|
|
||||||
<EmployeeListAddModifyDialog />
|
|
||||||
<div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder">
|
|
||||||
{{ $t('employee_list.page_header') }}
|
|
||||||
</div>
|
|
||||||
<SupervisorCrewTable />
|
|
||||||
</q-page>
|
|
||||||
</template>
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted( async () => {
|
onMounted( async () => {
|
||||||
await timesheet_approval_api.getPayPeriodOverviewsByDate(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
await timesheet_approval_api.getPayPeriodOverviewsByDateOrYearAndNumber(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,17 @@
|
||||||
<template>
|
<template>
|
||||||
<q-page
|
<q-page
|
||||||
padding
|
padding
|
||||||
class="q-pa-md bg-secondary"
|
class="column q-pa-md bg-secondary flex-center"
|
||||||
>
|
>
|
||||||
<PageHeaderTemplate
|
<PageHeaderTemplate
|
||||||
:title="$t('timesheet.page_header')"
|
:title="'timesheet.page_header'"
|
||||||
:start-date="timesheet_store.pay_period.period_start"
|
:start-date="timesheet_store.pay_period.period_start"
|
||||||
:end-date="timesheet_store.pay_period.period_end"
|
:end-date="timesheet_store.pay_period.period_end"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div :style="$q.screen.gt.sm ? 'width: 70vw': ''">
|
||||||
<TimesheetWrapper :employee-email="user.email" />
|
<TimesheetWrapper :employee-email="user.email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -20,7 +20,7 @@ const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: 'employees',
|
path: 'employees',
|
||||||
name: RouteNames.EMPLOYEE_LIST,
|
name: RouteNames.EMPLOYEE_LIST,
|
||||||
component: () => import('src/pages/supervisor-crew-page.vue'),
|
component: () => import('src/pages/employee-list-page.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'timesheet-temp',
|
path: 'timesheet-temp',
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { AuthService } from "../modules/auth/services/services-auth";
|
import { AuthService } from "../modules/auth/services/services-auth";
|
||||||
import type { User } from "src/modules/shared/models/user.models";
|
import { CAN_APPROVE_PAY_PERIODS, UserRole, type User } from "src/modules/shared/models/user.models";
|
||||||
|
|
||||||
export type CompanyRole = 'guest' | 'supervisor' | 'accounting' | 'human_resources' | 'employee';
|
const TestUsers: Record<UserRole, User> = {
|
||||||
|
[UserRole.ADMIN]: { firstName: 'Alex', lastName: 'Clark', email: 'user1@targointernet.com', role: UserRole.ADMIN },
|
||||||
const TestUsers: Record<CompanyRole, User> = {
|
[UserRole.SUPERVISOR]: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: UserRole.SUPERVISOR },
|
||||||
guest: { firstName: 'Unknown', lastName: 'Unknown', email: 'guest@guest.com', role: 'guest' },
|
[UserRole.HR]: { firstName: 'Avery', lastName: 'Davis', email: 'user5@example.test', role: UserRole.HR },
|
||||||
supervisor: { firstName: 'User', lastName: 'Test', email: 'user@targointernet.com', role: 'supervisor' },
|
[UserRole.ACCOUNTING]: { firstName: 'Avery', lastName: 'Johnson', email: 'user6@example.test', role: UserRole.ACCOUNTING },
|
||||||
accounting: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
[UserRole.EMPLOYEE]: { firstName: 'Alex', lastName: 'Johnson', email: 'user13@example.test', role: UserRole.EMPLOYEE },
|
||||||
human_resources: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
[UserRole.DEALER]: { firstName: 'Dea', lastName: 'Ler', email: 'dealer@example.test', role: UserRole.DEALER },
|
||||||
employee: { firstName: 'Robin', lastName: 'Clark', email: 'user5@example.test', role: 'supervisor' },
|
[UserRole.CUSTOMER]: { firstName: 'Custo', lastName: 'Mer', email: 'customer@example.test', role: UserRole.CUSTOMER },
|
||||||
|
[UserRole.GUEST]: { firstName: 'Guestie', lastName: 'Guesterson', email: 'guest@guest.com', role: UserRole.GUEST },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const user = ref<User>(TestUsers.guest);
|
const user = ref<User>(TestUsers.GUEST);
|
||||||
const authError = ref("");
|
const authError = ref("");
|
||||||
const isAuthorizedUser = computed(() => user.value.role !== 'guest');
|
const isAuthorizedUser = computed(() => CAN_APPROVE_PAY_PERIODS.includes(user.value.role));
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
//TODO: manage customer login process
|
//TODO: manage customer login process
|
||||||
|
|
@ -30,15 +31,15 @@ export const useAuthStore = defineStore('auth', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
user.value = TestUsers.guest;
|
user.value = TestUsers.GUEST;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUser = (bypassRole: string) => {
|
const setUser = (bypassRole: UserRole) => {
|
||||||
if (bypassRole in TestUsers) {
|
if (bypassRole in TestUsers) {
|
||||||
user.value = TestUsers[bypassRole as CompanyRole];
|
user.value = TestUsers[bypassRole];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
user.value = TestUsers.guest;
|
user.value = TestUsers.GUEST;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { EmployeeListService } from "src/modules/employee-list/services/services-employee-list";
|
import { EmployeeListService } from "src/modules/employee-list/services/employee-list-service";
|
||||||
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface";
|
import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/models/employee-profile.models";
|
||||||
import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface";
|
|
||||||
|
|
||||||
export const useEmployeeStore = defineStore('employee', () => {
|
export const useEmployeeStore = defineStore('employee', () => {
|
||||||
const employee = ref<EmployeeProfile>( default_employee_profile );
|
const employee = ref<EmployeeProfile>( default_employee_profile );
|
||||||
const employeeList = ref<EmployeeListTableItem[]>([]);
|
const employeeList = ref<EmployeeProfile[]>([]);
|
||||||
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
|
const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
|
||||||
const isLoadingEmployeeProfile = ref(false);
|
const isLoadingEmployeeProfile = ref(false);
|
||||||
const isLoadingEmployeeList = ref(false);
|
const isLoadingEmployeeList = ref(false);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { computed, ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { default_expense, default_pay_period_expenses, type UpsertExpense, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
import { default_expense, default_pay_period_expenses, type UpsertExpense, type Expense, type PayPeriodExpenses } from "src/modules/timesheets/models/expense.models";
|
||||||
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
import { timesheetService } from "src/modules/timesheets/services/timesheet-service";
|
||||||
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
import { ExpensesApiError, type GenericApiError } from "src/modules/timesheets/models/expense.validation";
|
||||||
import { computeExpenseTotals } from "src/modules/timesheets/utils/expense.util";
|
|
||||||
import type { UpsertAction } from "src/modules/timesheets/models/shift.models";
|
import type { UpsertAction } from "src/modules/timesheets/models/shift.models";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,7 +14,6 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const is_loading = ref(false);
|
const is_loading = ref(false);
|
||||||
const mode = ref<UpsertAction>('create');
|
const mode = ref<UpsertAction>('create');
|
||||||
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
const pay_period_expenses = ref<PayPeriodExpenses>(default_pay_period_expenses);
|
||||||
const pay_period_expenses_totals = computed(() => computeExpenseTotals(pay_period_expenses.value.expenses))
|
|
||||||
const current_expense = ref<Expense>(default_expense);
|
const current_expense = ref<Expense>(default_expense);
|
||||||
const initial_expense = ref<Expense>(default_expense);
|
const initial_expense = ref<Expense>(default_expense);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
|
@ -97,7 +95,6 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
is_loading,
|
is_loading,
|
||||||
mode,
|
mode,
|
||||||
pay_period_expenses,
|
pay_period_expenses,
|
||||||
pay_period_expenses_totals,
|
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
error,
|
error,
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ export const useShiftStore = defineStore('shift', () => {
|
||||||
|
|
||||||
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
|
const upsertOrDeleteShiftByEmployeeEmail = async (employee_email: string, upsert_shift: UpsertShift) => {
|
||||||
const encoded_email = encodeURIComponent(employee_email);
|
const encoded_email = encodeURIComponent(employee_email);
|
||||||
const encoded_date = encodeURIComponent(current_shift.value.date);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await timesheetService.upsertOrDeleteShiftsByDateAndEmployeeEmail(encoded_email, [ upsert_shift, ], encoded_date);
|
const result = await timesheetService.upsertOrDeleteShiftsByDateAndEmployeeEmail(encoded_email, upsert_shift);
|
||||||
timesheet_store.pay_period_details = result;
|
timesheet_store.pay_period_details = result;
|
||||||
|
close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('error doing thing: ', err)
|
console.log('error doing thing: ', err)
|
||||||
// const status_code: number = err?.response?.status ?? 500;
|
// const status_code: number = err?.response?.status ?? 500;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user