Merge pull request 'dev/nicolas/timesheet-approval-staging-prep' (#36) from dev/nicolas/timesheet-approval-staging-prep into main

Reviewed-on: Targo/targo_frontend#36
This commit is contained in:
Nicolas 2025-12-29 14:15:01 -05:00
commit 6448a62fc5
17 changed files with 379 additions and 309 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View File

@ -1,6 +1,11 @@
export default {
dashboard: {
welcome: "Welcome to the Targo App!",
carousel: {
welcome_title: "Welcome to the new Targo Application!",
welcome_message: "Development is complete and the application is live! Things have remained mostly the same, but with a new coat of paint, a more streamlined user experience, and most importantly, drastically improved security and optimization!",
help_title: "We have a help page!",
help_message: "We did our best to keep the app intuitive with as few clicks and changes as possible, but it's not always perfect! We made this page to explain every part of the app if you any of it ever feels confusing.",
},
},
help: {
label: "Centre d'aide",
@ -339,6 +344,8 @@ export default {
verified: "approved",
unverified: "pending",
inactive: "inactive",
filter_active: "show only active employees",
filter_team: "",
},
tooltip: {
button_detailed_view: "detailed view",

View File

@ -1,6 +1,11 @@
export default {
dashboard: {
welcome: "Bienvenue dans l'application Targo!",
carousel: {
welcome_title: "Bienvenue dans la nouvelle application Targo!",
welcome_message: "Le développement est terminé et l'application est officiellement en ligne! Les fonctionnalités demeurent grandement intactes comparé à l'ancienne version, mise à part une nouvelle couche de peinture, une expérience utilisateur plus intuitive et surtout une sécurité et optimization drastiquement amélioriés!",
help_title: "Nous avons une page d'aide!",
help_message: "Nous avons fait notre possible pour rendre l'application plus intuitive et facile d'accès en suivant les tendances modernes, mais il y a toujours place à l'amélioration! La page d'aide est là pour vous si jamais nous avons raté la cible et qu'une partie du site semble nébuleux.",
},
},
help: {
label: "Centre d'aide",
@ -107,7 +112,7 @@ export default {
},
},
error :{
error: {
not_found_header: "page introuvable",
not_found_description: "Vous avez possiblement entré une mauvaise addresse URL, ou vous n'avez pas accès à cette section du site",
go_back: "retour en arrière",
@ -340,6 +345,8 @@ export default {
verified: "approuvé",
unverified: "à vérifier",
inactive: "inactif",
filter_active: "",
filter_team: "",
},
tooltip: {
button_detailed_view: "vue détaillée",

View File

@ -8,28 +8,26 @@
</script>
<template>
<div class="column text-weight-medium">
<q-separator
color="accent"
size="5px"
class="col-auto"
/>
<div class="column bg-primary text-uppercase">
<div class="col row">
<q-checkbox
v-model="filters.is_showing_inactive"
keep-color
size="lg"
color="accent"
label="show inactive"
class="col"
:class="filters.is_showing_inactive ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'"
/>
<q-checkbox
v-model="filters.is_showing_team_only"
keep-color
size="lg"
val="team"
color="accent"
label="show team only"
class="col"
:class="filters.is_showing_team_only ? 'text-accent text-weight-bolder' : 'text-white text-weight-medium'"
/>
</div>
</div>

View File

@ -2,31 +2,20 @@
setup
lang="ts"
>
import type { TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import type { TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { getHoursMinutesStringFromHoursFloat, getMinutes } from 'src/utils/date-and-time-utils';
const modelApproval = defineModel<boolean>();
const { row, index = 0 } = defineProps<{
row: TimesheetOverview;
row: TimesheetApprovalOverview;
index?: number;
}>();
const emit = defineEmits<{
'clickDetails': [overview: TimesheetOverview];
'clickDetails': [overview: TimesheetApprovalOverview];
'clickApprovalAll' : [is_approved: boolean];
}>();
const getMinutes = (hours: number) => {
const minutes_percent = hours - Math.floor(hours);
const minutes = Math.round(minutes_percent * 60);
return minutes > 1 ? minutes.toString() : '0';
}
const getHoursMinutesString = (hours: number): string => {
const flat_hours = Math.floor(hours);
const minutes = Math.round((hours - flat_hours) * 60);
return `${flat_hours}h ${minutes > 1 ? minutes : ''}`
}
</script>
<template>
@ -37,7 +26,7 @@
>
<q-card
class="rounded-10 shadow-5"
:style="`animation-delay: ${index / 15}s; opacity: ${row.is_active ? '1' : '0.5'}; transform: scale(${row.is_active ? '1' : '0.9'})`"
:style="`animation-delay: ${index / 15}s; opacity: ${row.is_active ? '1' : '0.75'}; transform: scale(${row.is_active ? '1' : '0.9'})`"
>
<!-- Card header with employee name and details button-->
<q-card-section
@ -49,9 +38,9 @@
class="text-h5 text-uppercase text-weight-medium q-mr-xs"
:class="row.is_active ? 'text-accent' : 'text-negative'"
>
{{ row.employee_name.split(' ')[0] }}
{{ row.employee_first_name }}
</span>
<span class="text-uppercase text-weight-light">{{ row.employee_name.split(' ')[1] }}</span>
<span class="text-uppercase text-weight-light">{{ row.employee_last_name }}</span>
</div>
<!-- Buttons to view detailed shifts or view employee timesheet -->
@ -95,7 +84,7 @@
<span
class="text-weight-bolder text-h3 q-py-none"
:class="row.regular_hours > 80 || !row.is_active ? 'text-negative' : ''"
> {{ getHoursMinutesString(row.regular_hours) }} </span>
> {{ getHoursMinutesStringFromHoursFloat(row.regular_hours) }} </span>
<q-separator class="q-mr-sm" />
</div>
@ -116,7 +105,7 @@
<span
class="text-weight-bolder q-pa-none q-mb-xs"
style="font-size: 1.2em; line-height: 1em;"
> {{ getHoursMinutesString(hour_type) }} </span>
> {{ getHoursMinutesStringFromHoursFloat(hour_type) }} </span>
</div>
</div>
</div>

View File

@ -10,24 +10,31 @@
import OverviewListFilters from 'src/modules/timesheet-approval/components/overview-list-filters.vue';
import { computed, ref } from 'vue';
import { useAuthStore } from 'src/stores/auth-store';
import { useTimesheetStore } from 'src/stores/timesheet-store';
import { useTimesheetApprovalApi } from 'src/modules/timesheet-approval/composables/use-timesheet-approval-api';
import { overview_column_names, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { overview_column_names, OverviewColumns, pay_period_overview_columns, PayPeriodOverviewFilters, type TimesheetApprovalOverview } from 'src/modules/timesheet-approval/models/timesheet-overview.models';
import { getHoursMinutesStringFromHoursFloat } from 'src/utils/date-and-time-utils';
const auth_store = useAuthStore();
const timesheet_store = useTimesheetStore();
const timesheet_approval_api = useTimesheetApprovalApi();
const visible_columns = ref<string[]>([
overview_column_names.REGULAR,
overview_column_names.EVENING,
overview_column_names.EMERGENCY,
overview_column_names.SICK,
overview_column_names.VACATION,
overview_column_names.HOLIDAY,
overview_column_names.OVERTIME,
overview_column_names.IS_APPROVED,
const TIME_COLUMNS: OverviewColumns[] = ['REGULAR', 'EVENING', 'EMERGENCY', 'OVERTIME', 'HOLIDAY', 'VACATION'];
const VISIBLE_COLUMNS = ref<OverviewColumns[]>([
'employee_first_name',
'REGULAR',
'EVENING',
'EMERGENCY',
'OVERTIME',
'HOLIDAY',
'VACATION',
'expenses',
'mileage',
'is_approved',
]);
const is_showing_filters = ref(false);
const search_string = ref('');
@ -39,7 +46,7 @@
name_search_string: search_string.value,
});
const onClickedDetails = async (row: TimesheetOverview) => {
const onClickedDetails = async (row: TimesheetApprovalOverview) => {
timesheet_store.current_pay_period_overview = row;
await timesheet_store.getTimesheetsByOptionalEmployeeEmail(row.email);
@ -50,16 +57,23 @@
await timesheet_approval_api.toggleTimesheetsApprovalByEmployeeEmail(email, is_approved);
}
const filterEmployeeRows = (rows: readonly TimesheetOverview[], terms: PayPeriodOverviewFilters): TimesheetOverview[] => {
const filterEmployeeRows = (rows: readonly TimesheetApprovalOverview[], terms: PayPeriodOverviewFilters): TimesheetApprovalOverview[] => {
let result = [...rows];
if (!terms.is_showing_inactive) {
result = result.filter(row => row.is_active);
}
// if (terms.name_search_string) {
// result = result.filter(row => row.employee_name.includes(terms.name_search_string ?? ''));
// }
if (terms.is_showing_team_only) {
result = result.filter(row => row.supervisor !== null && row.supervisor.email === (auth_store.user ? auth_store.user.email : ''));
}
if (terms.name_search_string.length > 0) {
const search_words = terms.name_search_string.trim().split(' ');
search_words.map(word => result = result.filter(row =>
row.employee_first_name.includes(word ?? '') || row.employee_last_name.includes(word ?? '')
));
}
return result;
};
@ -68,15 +82,10 @@
<template>
<div class="q-px-md full-height">
<LoadingOverlay v-model="timesheet_store.is_loading" />
<transition
appear
enter-active-class="animated fadeInUp"
leave-active-class="animated fadeOutDown"
mode="out-in"
>
<q-table
:key="timesheet_store.is_approval_grid_mode ? 'grid' : 'list'"
:visible-columns="visible_columns"
:visible-columns="VISIBLE_COLUMNS"
:rows="overview_rows"
:columns="pay_period_overview_columns"
row-key="email"
@ -91,17 +100,18 @@
card-container-class="justify-center"
class="bg-transparent"
:class="timesheet_store.is_approval_grid_mode ? '' : 'sticky-header-table no-shadow'"
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15"
table-class="q-pa-none q-mx-md rounded-10 bg-dark shadow-15 hide-scrollbar"
:no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.label.loading')"
:style="$q.platform.is.mobile ? '' : 'max-height: 70vh;'"
>
<template #top>
<div class="column full-width">
<div
class="col-auto row items-start full-width q-px-lg"
:class="($q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md') + (timesheet_store.is_approval_grid_mode ? '' : ' q-mb-md')"
:class="$q.platform.is.mobile ? 'column flex-center' : 'row q-mt-md'"
>
<PayPeriodNavigator
@date-selected="timesheet_approval_api.getTimesheetOverviews"
@ -146,7 +156,7 @@
<QTableFilters
v-model:search="search_string"
class="col-auto q-mb-xs"
class="col-auto q-mb-sm"
/>
<q-btn
@ -154,7 +164,7 @@
icon="filter_alt"
color="white"
:label="$q.platform.is.mobile ? '' : $t('shared.label.filter')"
class="col q-ml-sm self-stretch bg-accent"
class="col q-ml-sm self-stretch bg-primary"
style="border-radius: 5px 5px 0 0;"
@click="is_showing_filters = !is_showing_filters"
/>
@ -170,9 +180,9 @@
</q-slide-transition>
<q-separator
color="accent"
color="primary"
size="5px"
class="q-mx-lg"
class="q-mx-lg q-my-none q-pa-none"
/>
</div>
</template>
@ -224,25 +234,33 @@
:color="props.value ? 'white' : 'grey-5'"
class="rounded-5 "
:class="props.value ? 'bg-accent' : ''"
@click.stop="props.row.is_approved = !props.row.is_approved"
@click.stop="onClickApproveAll(props.row.email, props.row.is_approved)"
/>
</transition>
<div v-else-if="props.col.name === 'employee_name'">
<div v-else-if="props.col.name === 'employee_first_name'">
<span class="text-h5 text-uppercase text-accent q-mr-xs">
{{ props.value.split(' ')[0] }}
{{ props.value }}
</span>
<span class="text-uppercase text-weight-light">
{{ props.row.employee_last_name }}
</span>
<span class="text-uppercase text-weight-light">{{ props.value.split(' ')[1]
}}</span>
</div>
<span v-else>{{ props.value }}</span>
<span
v-else
:class="props.col.name === overview_column_names.REGULAR && props.row.overtime > 0 ? 'text-negative text-weight-bolder' : 'text-weight-regular'"
>
{{ TIME_COLUMNS.includes(props.col.name) ?
getHoursMinutesStringFromHoursFloat(props.value) : props.value }}
</span>
</div>
</transition>
</q-td>
</template>
<!-- Template for individual employee cards -->
<template #item="props: { row: TimesheetOverview, rowIndex: number }">
<template #item="props: { row: TimesheetApprovalOverview, rowIndex: number }">
<OverviewListItem
v-model="props.row.is_approved"
:key="props.row.email + timesheet_store.pay_period?.pay_period_no"
@ -267,7 +285,6 @@
</div>
</template>
</q-table>
</transition>
</div>
</template>

View File

@ -5,10 +5,10 @@ export const useTimesheetApprovalApi = () => {
const timesheet_store = useTimesheetStore();
const DATE_REGEX = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
const getTimesheetOverviews = async () => {
const getTimesheetOverviews = async (date?: string) => {
timesheet_store.is_loading = true;
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber();
const success = await timesheet_store.getPayPeriodByDateOrYearAndNumber(date);
if (success) await timesheet_store.getTimesheetOverviews();
timesheet_store.is_loading = false;

View File

@ -1,8 +1,14 @@
import type { QTableColumn } from "quasar";
export class TimesheetOverview {
export class TimesheetApprovalOverview {
email: string;
employee_name: string;
employee_first_name: string;
employee_last_name: string;
supervisor: {
first_name: string;
last_name: string;
email: string;
} | null;
is_active: boolean;
regular_hours: number;
other_hours: {
@ -20,7 +26,9 @@ export class TimesheetOverview {
constructor() {
this.email = '';
this.employee_name = 'John Doe';
this.employee_first_name = 'Unknown';
this.employee_last_name = 'Unknown';
this.supervisor = null;
this.is_active = true;
this.regular_hours = 0;
this.other_hours = {
@ -45,18 +53,21 @@ export interface PayPeriodOverviewResponse {
period_end: string;
payday: string;
label: string;
employees_overview: TimesheetOverview[];
employees_overview: TimesheetApprovalOverview[];
}
export interface PayPeriodOverviewFilters {
is_showing_inactive: boolean;
is_showing_team_only: boolean;
supervisors: string[];
name_search_string: string | number | null;
name_search_string: string;
}
export type OverviewColumns = 'employee_first_name' | 'employee_last_name' | 'email' | 'REGULAR' | 'EVENING' | 'EMERGENCY' | 'SICK' | 'HOLIDAY' | 'VACATION' | 'OVERTIME' | 'expenses' | 'mileage' | 'is_approved' | 'is_active'
export const overview_column_names = {
EMPLOYEE_NAME: 'employee_name',
FIRST_NAME: 'employee_first_name',
LAST_NAME: 'employee_last_name',
EMAIL: 'email',
REGULAR: 'REGULAR',
EVENING: 'EVENING',
@ -73,18 +84,25 @@ export const overview_column_names = {
export const pay_period_overview_columns: QTableColumn[] = [
{
name: overview_column_names.EMPLOYEE_NAME,
name: overview_column_names.FIRST_NAME,
label: 'timesheet_approvals.table.full_name',
align: 'left',
field: 'employee_name',
field: overview_column_names.FIRST_NAME,
sortable: true,
required: true,
},
{
name: overview_column_names.LAST_NAME,
label: 'timesheet_approvals.table.full_name',
align: 'left',
field: 'employee_last_name',
sortable: true,
},
{
name: overview_column_names.EMAIL,
label: 'timesheet_approvals.table.email',
align: 'left',
field: 'email',
field: overview_column_names.EMAIL,
sortable: true,
},
{

View File

@ -67,7 +67,7 @@
/>
<!-- label for approval mode to delimit that this is the timesheet -->
<span class="col-auto text-uppercase text-bold text-h5"> {{ $t('timesheet.page_header') }}</span>
<span v-if="mode === 'approval'" class="col-auto text-uppercase text-bold text-h5"> {{ $t('timesheet.page_header') }}</span>
<q-space v-if="$q.screen.width > $q.screen.height" />

View File

@ -1,7 +1,7 @@
import { api } from "src/boot/axios";
import type { PayPeriod } from "src/modules/shared/models/pay-period.models";
import type { TimesheetResponse } from "src/modules/timesheets/models/timesheet.models";
import type { TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import type { TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
export const timesheetService = {
@ -15,8 +15,8 @@ export const timesheetService = {
return response.data.data;
},
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetOverview[]> => {
const response = await api.get<{ success: boolean, data: TimesheetOverview[], error?: string }>(`pay-periods/${year}/${period_number}/${supervisor_email}`);
getTimesheetOverviewsByPayPeriodAndSupervisorEmail: async (year: number, period_number: number, supervisor_email: string): Promise<TimesheetApprovalOverview[]> => {
const response = await api.get<{ success: boolean, data: TimesheetApprovalOverview[], error?: string }>(`pay-periods/${year}/${period_number}/${supervisor_email}`);
return response.data.data;
},

View File

@ -4,83 +4,105 @@
>
import { ref } from 'vue';
const LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et \
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip \
ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu \
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia \
deserunt mollit anim id est laborum."
const slide = ref<string>('welcome');
</script>
<template>
<q-page
padding
class="q-pa-md row justify-center"
class="q-pa-md justify-center items-stretch"
:class="$q.platform.is.mobile ? 'column' : 'row'"
>
<q-card flat class="column col-9 transparent ">
<div class="col-1"></div>
<!-- left column -->
<div class="column col flex-center q-pa-md">
</div>
<!-- center column -->
<div class="column col-xs-12 col-md-8 col-xl-6 flex-center q-pa-md">
<q-carousel
v-model="slide"
transition-prev="jump-right"
transition-next="jump-left"
swipeable
animated
control-color="accent"
navigation-icon="radio_button_unchecked"
navigation
class="col-5 bg-dark rounded-15 shadow-2"
infinite
:autoplay="9001"
control-color="accent"
class="col-auto bg-dark rounded-15 shadow-18"
:style="$q.platform.is.mobile ? 'height: 60vh;' : 'height: 50vh;'"
>
<!-- welcome slide -->
<q-carousel-slide
name="welcome"
class="column no-wrap flex-center q-pa-none q-pb-xl"
class="q-pa-none q-pb-xl fit"
>
<q-img src="src/assets/line-truck-1.jpg" class="full-height">
<div class="absolute-bottom text-h5">
{{ $t('dashboard.welcome') }}
<div class="column fit">
<q-img
src="src/assets/targo_building.png"
height="30vh"
position="50% 25%"
fit="cover"
class="col-auto"
>
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
{{ $t('dashboard.carousel.welcome_title') }}
</div>
</q-img>
<div class="q-mt-md text-center">
{{ LOREM_IPSUM }}
<div class="column col q-mt-md q-px-md flex-center">
<span class="col-auto">{{ $t('dashboard.carousel.welcome_message') }}</span>
</div>
</div>
</q-carousel-slide>
<!-- help page slide -->
<q-carousel-slide
name="tv"
class="column no-wrap flex-center q-pa-none q-pb-xl"
class="q-pa-none q-pb-xl"
>
<q-icon
name="live_tv"
size="56px"
/>
<div class="q-mt-md text-center">
{{ LOREM_IPSUM }}
<div class="column fit">
<q-img
src="src/assets/targo_help_banner.png"
height="30vh"
position="50% 25%"
fit="none"
class="col-auto"
>
<div class="absolute-bottom text-h6 text-uppercase text-weight-light">
{{ $t('dashboard.carousel.help_title') }}
</div>
</q-carousel-slide>
<q-carousel-slide
name="layers"
class="column no-wrap flex-center q-pa-none q-pb-xl"
>
<q-icon
name="layers"
size="56px"
/>
<div class="q-mt-md text-center">
{{ LOREM_IPSUM }}
</q-img>
<div class="col column justify-center q-mt-md q-px-md">
<span class="col-auto">{{ $t('dashboard.carousel.help_message') }}</span>
</div>
</q-carousel-slide>
<q-carousel-slide
name="map"
class="column no-wrap flex-center q-pa-none q-pb-xl"
>
<q-icon
name="terrain"
size="56px"
/>
<div class="q-mt-md text-center">
{{ LOREM_IPSUM }}
</div>
</q-carousel-slide>
</q-carousel>
</q-card>
<div class="col column">
<span class="col-auto text-h6 text-uppercase"> </span>
</div>
</div>
<!-- right column -->
<div class="column col items-center">
<div
class="col-auto row full-width within-iframe"
:class="$q.platform.is.mobile ? 'justify-center' : 'justify-end q-pl-md'"
style="height: 50vh;"
>
<iframe
title="Environment Canada Weather"
height="400px"
src="https://weather.gc.ca/wxlink/wxlink.html?coords=45.159%2C-73.676&lang=e"
allowtransparency="true"
style="border: 0;"
class="col-auto"
></iframe>
</div>
</div>
</q-page>
</template>

View File

@ -4,7 +4,7 @@
<template>
<q-layout view="hHh lpR fFf">
<q-page-container class="bg-secondary">
<q-page-container class="bg-dark">
<q-page class="row">
<q-img src="src/assets/village.png" fit="cover" :class="$q.screen.lt.md ? 'absolute-bottom' : 'absolute-right'" />
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut" class="col-xs-10 absolute-center">

View File

@ -3,7 +3,7 @@ import { defineStore } from 'pinia';
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/timesheet-approval-service';
import { timesheetService } from 'src/modules/timesheets/services/timesheet-service';
import type { PayPeriodOverviewResponse, TimesheetOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import type { PayPeriodOverviewResponse, TimesheetApprovalOverview } from "src/modules/timesheet-approval/models/timesheet-overview.models";
import type { PayPeriod } from 'src/modules/shared/models/pay-period.models';
import type { Timesheet } from 'src/modules/timesheets/models/timesheet.models';
import type { TimesheetApprovalCSVReportFilters } from 'src/modules/timesheet-approval/models/timesheet-approval-csv-report.models';
@ -16,13 +16,13 @@ export const useTimesheetStore = defineStore('timesheet', () => {
const all_current_shifts = computed(() => timesheets.value.flatMap(week => week.days.flatMap(day => day.shifts)) ?? []);
const initial_timesheets = ref<Timesheet[]>([]);
const pay_period_overviews = ref<TimesheetOverview[]>([]);
const pay_period_overviews = ref<TimesheetApprovalOverview[]>([]);
const pay_period_infos = ref<PayPeriodOverviewResponse>();
const is_report_dialog_open = ref(false);
const is_details_dialog_open = ref(false);
const selected_employee_name = ref<string>();
const current_pay_period_overview = ref<TimesheetOverview>();
const current_pay_period_overview = ref<TimesheetApprovalOverview>();
const is_approval_grid_mode = ref<boolean>(true);
const pay_period_report = ref();

View File

@ -13,3 +13,15 @@ export const getCurrentPayPeriod = (today = new Date()): number => {
return current_period;
}
export const getMinutes = (hours: number) => {
const minutes_percent = hours - Math.floor(hours);
const minutes = Math.round(minutes_percent * 60);
return minutes > 1 ? minutes.toString() : '0';
}
export const getHoursMinutesStringFromHoursFloat = (hours: number): string => {
const flat_hours = Math.floor(hours);
const minutes = Math.round((hours - flat_hours) * 60);
return `${flat_hours}h ${minutes > 1 ? minutes : ''}`
}