Merge branch 'main' of git.targo.ca:Targo/targo_frontend into dev/matthieu/timesheet-form

This commit is contained in:
Matthieu Haineault 2025-09-22 14:21:00 -04:00
commit ceb6313322
47 changed files with 1513 additions and 1329 deletions

109
package-lock.json generated
View File

@ -881,6 +881,45 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@inquirer/external-editor": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.2.tgz",
"integrity": "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^2.1.0",
"iconv-lite": "^0.7.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/external-editor/node_modules/iconv-lite": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@inquirer/figures": { "node_modules/@inquirer/figures": {
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz",
@ -2990,9 +3029,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.11.0", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -3459,9 +3498,9 @@
} }
}, },
"node_modules/chardet": { "node_modules/chardet": {
"version": "0.7.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz",
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -5016,34 +5055,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/external-editor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"dev": true,
"license": "MIT",
"dependencies": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
"tmp": "^0.0.33"
},
"engines": {
"node": ">=4"
}
},
"node_modules/external-editor/node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
"integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"os-tmpdir": "~1.0.2"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/extract-zip": { "node_modules/extract-zip": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@ -5923,16 +5934,16 @@
} }
}, },
"node_modules/inquirer": { "node_modules/inquirer": {
"version": "9.3.7", "version": "9.3.8",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.7.tgz", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz",
"integrity": "sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==", "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@inquirer/external-editor": "^1.0.2",
"@inquirer/figures": "^1.0.3", "@inquirer/figures": "^1.0.3",
"ansi-escapes": "^4.3.2", "ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0", "cli-width": "^4.1.0",
"external-editor": "^3.1.0",
"mute-stream": "1.0.0", "mute-stream": "1.0.0",
"ora": "^5.4.1", "ora": "^5.4.1",
"run-async": "^3.0.0", "run-async": "^3.0.0",
@ -7526,16 +7537,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ospath": { "node_modules/ospath": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
@ -9448,9 +9449,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.3", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -9805,9 +9806,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -105,7 +105,8 @@ export default defineConfig((ctx) => {
notify: { notify: {
color: 'primary', color: 'primary',
avatar: 'https://cdn.quasar.dev/img/boy-avatar.png', avatar: 'https://cdn.quasar.dev/img/boy-avatar.png',
} },
dark: false,
}, },
// iconSet: 'material-icons', // Quasar icon set // iconSet: 'material-icons', // Quasar icon set

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -1,5 +1,5 @@
// app global css in SCSS form // app global css in SCSS form
@each $size in (5, 10, 15, 20, 25) { @each $size in (1, 2, 3, 4, 5, 10, 15, 20, 25, 50, 75, 100) {
.rounded-#{$size} { .rounded-#{$size} {
border-radius: #{$size}px !important; border-radius: #{$size}px !important;
} }
@ -23,3 +23,13 @@
.q-table tbody tr:hover { .q-table tbody tr:hover {
background: #00ff260c; background: #00ff260c;
} }
body.body--dark {
--q-secondary: #0f1114;
color: $grey-2;
}
.body--light {
--q-dark: #FFF;
color: $grey-8;
}

View File

@ -12,19 +12,24 @@
// to match your app's branding. // to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website. // Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary: #019547; $primary : #019547;
$secondary: #DAE0E7; $secondary : #DAE0E7;
$accent: #AAD5C4; $accent : #AAD5C4;
$verdigris: #6EBAB0; $dark-shadow-color : #019547;
$mint: #56B586;
$dark-font: #1f3a1f; $elevation-dark-umbra : rgba($dark-shadow-color, 0.4);
$dark: #000; $elevation-dark-penumbra : rgba($dark-shadow-color, 0);
$dark-page: #323232; $elevation-dark-ambient : rgba($dark-shadow-color, 0);
$positive: #21ba45; $dark-shadow-2 : 0 3px 5px -1px $elevation-dark-umbra, 0 5px 8px $elevation-dark-penumbra, 0 1px 14px $elevation-dark-ambient;
$negative: #e6364b; $layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
$info: #6bb9e7;
$warning: #e4a944; $dark : #333;
$white: white; $dark-page : #343434;
$positive : #21ba45;
$negative : #e6364b;
$info : #6bb9e7;
$warning : #e4a944;
$white : white;

View File

@ -1,299 +1,162 @@
export default { export default {
footerLayout: { employee_list: {
title: `Targo Communications, 2005 - 2025)}. All rights reserved.`, page_header: "Employee Directory",
}, table: {
helpPage: { first_name: "First name",
title_1: 'Contact Us', last_name: "Last name",
title_2: email: "Email",
'Please complete the form below and well get back to you as soon as possible.', phone_number: "Phone number",
fullName: 'Full name*', role: "Role",
email: 'Email address*', supervisor: "Supervisor",
phoneNumber: 'Phone number*', company: "Company",
message:
'How can we help you? Please use this area to provide a detailed message, Thank you!*',
//form validation
fullNameValidation: 'Full name must be filled in.',
emailValidation: 'Email must be a valid email.',
phoneNumberValidation: 'Phone number must be filled in.',
messageValidation: 'Message must be a valid email.',
submit: 'Send',
},
mainLayout: {
backButton: 'Back to home',
clearFilter: 'Clear filter',
},
navBar: {
userMenuHome: 'Homepage',
userMenuEmployeeList: 'Employee Directory',
userMenuShiftValidation: 'Timesheet Approval',
userMenuTimesheetTemp: 'Timesheet',
userMenuProfile: 'Profile',
userMenuHelp: 'Help',
userMenuLogout: 'Log Out',
userMenuTimesheet: 'Timesheet',
userMenuCalendar: 'Calendar',
},
notFoundPage: {
pageText: 'We cannot seem to find the page you are looking for, sorry!',
backButton: 'Take me back!',
},
loginPage: {
title: 'Log in to Targo',
forgotPassword: 'Forgot Password?',
signUp: 'Dont have an account yet?',
email: 'Email',
password: 'Password',
submit: 'Connect',
employeeLoginButton: 'Employee',
facebookLoginButton:'Connect with Facebook',
tooltipComingSoon: 'Coming soon!',
loginOrSeparator: 'OR',
emailValidation: 'Email must be a valid email.',
passwordValidation: 'Password must be a valid email.',
rememberMe: 'Remember me',
},
signUpPage: {
title: 'Create a new account',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phoneNumber: 'Phone number',
password: 'New password',
confirmedPassword: 'Confirm your password',
signIn: 'Already have an account?',
submit: 'Sign up',
firstNameValidation: 'First Name must be filled in.',
lastNameValidation: 'Last Name must be filled in.',
emailValidation: 'Email must be a valid email.',
phoneNumberValidation: 'Phone number must be filled in.',
passwordValidationTitle: 'Password Criteria :',
passwordValidation: 'Password must meet all criteria.',
passwordLengthValidation: 'Must be at least 8 characters long.',
passwordCapitalValidation: 'Must contain at least one capital letter.',
passwordNumberValidation: 'Must contain at least one number.',
passwordSpecialCharacterValidation:
'Must contain at least one special character: !@#$%^&*()-_+=',
confirmPasswordValidation: 'Password must match new Password.',
},
forgotPage: {
title:
'Please enter your email to search for your account and send a verification code.',
email: 'Email',
emailValidation: 'Email must be a valid email.',
submit: 'Send Code',
cancel: 'Cancel',
},
resetPage: {
title: 'Reset your password',
code: 'code',
codeValidation: 'Code must be filled in with 4 digits.',
newPassword: 'New Password',
confirmedPassword: 'Confirm New Password',
newPasswordValidation: 'Password must meet all criteria.',
newPasswordLengthValidation: 'Must be at least 8 characters long.',
newPasswordCapitalValidation: 'Must contain at least one capital letter.',
newPasswordNumberValidation: 'Must contain at least one number.',
newPasswordSpecialCharacterValidation:
'Must contain at least one special character: !@#$%^&*()-_+=',
confirmNewPasswordValidation: 'Password must match new Password.',
submit: 'Send',
cancel: 'Cancel',
},
accountDialog: {
title: 'More',
item_1: 'Language',
item_2: 'Profil',
item_3: 'Log Out',
item_4: 'Time Sheet',
item_5: 'Annual calendar',
},
notificationDialog: {
notice: 'Notice',
markAllRead: 'Mark all read',
deleteAll: 'Delete all',
close: 'Close',
},
profilePage: {
title: 'Profile',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phoneNumber: 'Phone number',
job_title: 'Job title',
company: 'Company',
supervisor: 'Supervisor',
role: 'Role',
address: 'Address',
job_titleValidation: 'Job title must be filled in.',
companyValidation: 'Company must be filled in.',
supervisorValidation: 'Supervisor must be filled in.',
roleValidation: 'Role must be filled in.',
addressValidation: 'Address must be filled in.',
firstNameValidation: 'First Name must be filled in.',
lastNameValidation: 'Last Name must be filled in.',
phoneNumberValidation: 'Phone number must be filled in.',
submit: 'Update Profile',
},
indexAdminPage: {
card_1: 'Administrators',
card_2: 'Technicians',
card_3: 'Dealer',
card_4: 'Customers',
},
usersListPage: {
tableHeader: 'Employee Directory',
searchInput: 'Search',
userListFirstName: 'First name',
userListLastName: 'Last name',
userListEmail: 'Email',
userListPhone: 'Phone number',
userListRole: 'Role',
userListSupervisor: 'Supervisor',
userListCompany: 'Company',
addButton: 'Add Employee',
customer: 'Customer',
dealer: 'Dealer',
employee: 'Employee',
technician: 'Technician',
admin: 'Administrator',
support: 'Support',
},
shared:{
searchBar: 'Search',
loading: 'Obtaining data...',
failedToLoad: 'No data to show',
failedToSearch: 'No data matching search',
languageLabel: 'Language',
},
editUserPage: {
title: 'Edit Account',
passwordTitle: 'Reset Password',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phoneNumber: 'Phone number',
type: 'Select a type',
role: 'Select a role',
job_title: 'Job title',
company: 'Company',
supervisor: 'Supervisor',
isSupervisor: 'Is supervisor',
hours_bank_max: 'Hours bank maximum',
address: 'Address',
verifiedAccountStatus: 'Verified Account',
unVerifiedAccountStatus: 'UnVerified Account',
password: 'New password',
confirmedPassword: 'Confirm your password',
submit: 'Update Account',
//Form Validation
firstNameValidation: 'First Name must be filled in.',
lastNameValidation: 'Last Name must be filled in.',
emailValidation: 'Email must be a valid email.',
phoneNumberValidation: 'Phone number must be filled in.',
typeValidation: 'Type must be filled in.',
roleValidation: 'Role must be filled in.',
job_titleValidation: 'Job title must be filled in.',
companyValidation: 'Company must be filled in.',
supervisorValidation: 'Supervisor must be filled in.',
hours_bank_maxValidation: 'Hours bank maximum must be filled in.',
addressValidation: 'Address must be filled in.',
passwordValidation: 'Password must meet all criteria.',
confirmPasswordValidation: 'Password must match new Password.',
},
addUserPage: {
title: 'Create User',
firstName: 'First name',
lastName: 'Last name',
email: 'Email',
phoneNumber: 'Phone number',
type: 'Select a type',
role: 'Select a role',
job_title: 'Job title',
company: 'Company',
supervisor: 'Supervisor',
isSupervisor: 'Is supervisor',
hours_bank_max: 'Hours bank maximum',
onboarding: 'Onboarding date',
offboarding: 'Offboarding date',
employee_number: 'Employee number (Employer D number)',
regular_hours_day: 'regular number of hours per day',
address: 'Address',
verifiedAccountStatus: 'Verified Account',
unVerifiedAccountStatus: 'UnVerified Account',
password: 'Password',
confirmedPassword: 'Confirm your password',
submit: 'Create',
//Form Validaiton
firstNameValidation: 'First Name must be filled in.',
lastNameValidation: 'Last Name must be filled in.',
emailValidation: 'Email must be a valid email.',
phoneNumberValidation: 'Phone number must be filled in.',
typeValidation: 'Type must be filled in.',
roleValidation: 'Role must be filled in.',
job_titleValidation: 'Job title must be filled in.',
companyValidation: 'Company must be filled in.',
supervisorValidation: 'Supervisor must be filled in.',
hours_bank_maxValidation: 'Hours bank maximum must be filled in.',
onboardingValidation: 'Onboarding date must be filled in.',
employee_numberValidation: 'Employee number must be filled in.',
regular_hours_dayValidation:
'regular number of hours per day must be filled in.',
addressValidation: 'Address must be filled in.',
passwordValidation: 'Password must meet all criteria.',
confirmPasswordValidation: 'Password must match new Password.',
},
pageTitles: {
employeeDirectory: 'Employee Directory',
newUsers: 'New user',
updateUsers: 'Update user',
timeSheets: 'Time sheet',
timeSheetValidations: 'Time sheet approvals',
},
timesheet: {
title:'Timesheet',
date_ranges_to:'to',
days: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
nav_button: {
calendar_date_picker:'Calendar',
current_week:'This week',
next_week:'Next week',
previous_week:'Previous week',
}, },
save_button:'Save', },
cancel_button:'Cancel',
remote_button: 'Remote work', login: {
delete_button: 'Delete', page_header: "account login",
email: "e-mail",
password: "password",
button: {
connect: "connect",
employee: "employee",
facebook:"Facebook",
remember_me: "remember me",
},
tooltip: {
coming_soon: "coming soon!",
},
},
nav_bar: {
home: "homepage",
employee_list: "employee directory",
timesheet_approvals: "timesheet approvals",
timesheet: "timesheet",
profile: "profile",
help: "help",
logout: "log out",
},
profile: {
personal: {
tab_title: "personal",
first_name: "first name",
last_name: "last name",
phone_number: "phone number",
address: "address",
address_hint: "# address, city, region, country",
birthdate: "birthdate",
},
employee: {
tab_title: "career",
email: "e-mail",
job_title: "job title",
company: "company",
supervisor: "supervisor",
hired_date: "hiring date",
},
preferences: {
tab_title: "preferences",
display_options: "display options",
language_options: "language options",
dark_mode: "dark",
light_mode: "light",
},
errors: {
must_enter_birthdate: "You must enter a valid birthdate",
}
},
shared:{
error: {
no_data_found: "no data found",
no_search_results: "no results matching search",
},
label: {
search: "search",
loading: "loading...",
language: "Language",
add: "ajouter",
save: "save",
remove: "remove",
cancel: "cancel",
update: "update",
modify: "modify",
},
misc: {
or: "or",
and: "and",
to: "to",
from: "from",
yes: "yes",
no: "no",
in: "in",
out: "out",
},
shift_type: {
regular: "regular",
evening: "evening",
emergency: "emergency",
overtime: "overtime",
holiday: "holiday",
vacation: "vacation",
sick: "sick",
remote: "remote work",
},
weekday: {
sunday: "dimanche",
monday: "lundi",
tuesday: "mardi",
wednesday: "mercredi",
thursday: "jeudi",
friday: "vendredi",
saturday: "samedi",
},
},
timesheet: {
page_header:"Timesheet",
nav_button: {
calendar_date_picker:"Calendar",
current_week:"This week",
next_week:"Next week",
previous_week:"Previous week",
},
save_button:"Save",
cancel_button:"Cancel",
remote_button: "Remote work",
delete_button: "Delete",
shift: { shift: {
actions: { actions: {
add:'Add Shift', add:"Add Shift",
edit: 'Edit shift', edit: "Edit shift",
delete: 'Delete shift', delete: "Delete shift",
delete_confirmation_msg: 'Do you want to delete this shift completly?', delete_confirmation_msg: "Do you want to delete this shift completly?",
}, },
types: { types: {
label: 'Shift`s Type', label: "Shift`s Type",
EMERGENCY: 'Emergency', EMERGENCY: "Emergency",
EVENING: 'Evening', EVENING: "Evening",
HOLIDAY: 'Holiday', HOLIDAY: "Holiday",
OVERTIME: 'Overtime', OVERTIME: "Overtime",
REGULAR: 'Regular', REGULAR: "Regular",
SICK: 'Sick Leave', SICK: "Sick Leave",
VACATION: 'Vacation', VACATION: "Vacation",
REMOTE: 'Remote work', REMOTE: "Remote work",
}, },
errors: { errors: {
not_found:'Shift not found', not_found:"Shift not found",
overlap:'An overlaps occured between 2 or more shifts', overlap:"An overlaps occured between 2 or more shifts",
invalid:'Invalid shift`s entry', invalid:"Invalid shift`s entry",
unknown:'Unknown error', unknown:"Unknown error",
comment_required:'A comment is required', comment_required:"A comment is required",
comment_too_long:'Your comment is too long', comment_too_long:"Your comment is too long",
}, },
fields: { fields: {
start:'Start (HH:mm)', start:"Start (HH:mm)",
end:'End (HH:mm)', end:"End (HH:mm)",
header_comment:'Shift`s comment', header_comment:"Shift`s comment",
textarea_comment: 'Leave a comment here', textarea_comment: "Leave a comment here",
}, },
}, },
expense: { expense: {
@ -304,140 +167,56 @@ export default {
employee_comment:'Comment', employee_comment:'Comment',
supervisor_comment:'Supervisor note', supervisor_comment:'Supervisor note',
errors: { errors: {
date_required_or_invalid:'the date is missing or invalid', date_required_or_invalid:"the date is missing or invalid",
comment_required:'A comment required', comment_required:"A comment required",
comment_too_long:'Your comment is too long', comment_too_long:"Your comment is too long",
amount_must_be_positive:'the amount cannot be under 0$', amount_must_be_positive:"the amount cannot be under 0$",
mileave_must_be_positive:'the mileage cannot be under 0', mileave_must_be_positive:"the mileage cannot be under 0",
amount_xor_mileage:'you cannot enter an amount and a mileage for the same expense', amount_xor_mileage:"you cannot enter an amount and a mileage for the same expense",
mileage_required_for_type:'you need to enter a value for mileage when you enter an expense of that type', mileage_required_for_type:"you need to enter a value for mileage when you enter an expense of that type",
amount_required_for_type:'you need to enter a value for amount when you enter an expense of that type', amount_required_for_type:"you need to enter a value for amount when you enter an expense of that type",
}, },
hints: { hints: {
amount_or_mileage:'Either amount or mileage, not both', amount_or_mileage:"Either amount or mileage, not both",
comment_required:'A comment required', comment_required:"A comment required",
}, },
mileage:'Mileage', mileage:"mileage",
open_btn:'List of expenses', open_btn:"list of expenses",
title:'List of all expenses', title:"List of all expenses",
total_amount:'Total amount', total_amount:"Total amount",
total_mileage:'Total mileage', total_mileage:"Total mileage",
type:'Type', type:"Type",
types: { types: {
PER_DIEM:'Per Diem', PER_DIEM:"Per Diem",
EXPENSES:'expense', EXPENSES:"expense",
MILEAGE:'mileage', MILEAGE:"mileage",
PRIME_GARDE:'on-call allowance', PRIME_GARDE:"on-call allowance",
}, },
}, },
}, },
timeSheetValidations: {
tableColumnLabelFullname: 'Full name', timesheet_approvals: {
tableColumnLabelEmail: 'email address', page_title: "Validation cartes de temps",
tableColumnLabelRegularHours: 'regular hours', table: {
tableColumnLabelEveningHours: 'evening', full_name: "full name",
tableColumnLabelEmergencyHours: 'emergency', email: "email address",
tableColumnLabelOvertime: 'overtime', expenses: "expenses",
tableColumnLabelExpenses: 'expenses', mileage: "mileage",
tableColumnLabelMileage: 'mileage', verified: "approved",
actionTitle: 'Please save the changes made.', unverified: "pending",
actionButton: 'Save', },
timeSheetStatusVerified: 'approved', chart: {
timeSheetStatusUnverified: 'pending', hours_worked_title: "hours worked",
timeSheetStatusPartial: 'partially approved', expenses_title: "expenses accrued",
timeSheetStatusComplete: 'complete', },
timeSheetStatusEmpty: 'empty', print_report: {
timeSheetStatusBlocked: 'blocked', company: "company",
showAllCheckbox: 'Show all', type: "type",
accumulatedSicknessTotal: 'Accumulated illnesses', shifts: "shifts",
consumedSicknessTotal: 'Consumed with illnesses', expenses: "expenses",
accumulatedVacationTotal: 'Accumulated vacation', },
consumedVacationTotal: 'Consumed with vacation', tooltip: {
maxVacationPerYear: 'Maximum vacation per year', button_detailed_view: "detailed view",
accumulatedSicknessTotalValidation: },
'Accumulated illnesses must be positive.',
consumedSicknessTotalValidation:
'Consumed with illnesses must be positive.',
accumulatedVacationTotalValidation:
'Accumulated vacation must be positive.',
consumedVacationTotalValidation: 'Consumed with vacation must be positive.',
maxVacationPerYearValidation: 'Max Vacation Per Year must be positive.',
resteVacationTotal: 'Rest of vacation',
hoursWorkedChartTitle: 'Hours worked',
hoursWorkedRegular: 'regular',
hoursWorkedEvening: 'evening',
hoursWorkedEmergency: 'emergency',
hoursWorkedOvertime: 'overtime',
tooltipTimeline: 'Daily breakdown',
tooltipTimesheet: 'Open timesheet',
reportFilterCategoryCompany: 'Company',
reportFilterCategoryType: 'Data type',
reportFilterShifts: 'Shifts',
reportFilterExpenses: 'Expenses',
reportFilterHoliday: 'Holiday',
reportFilterVacation: 'Vacation',
},
shiftColumns: {
title: 'shifts',
labelType: 'type',
labelIn: 'start',
labelOut: 'end',
labelComment: 'comment',
labelState: 'state',
labelSupervisorReport: 'supervisor report',
},
expenseColumns: {
title: 'Expenses',
column_1: 'Type',
column_2: 'Amount',
column_3: 'Attachment',
column_4: 'Description',
column_5: 'Status',
column_6: 'Supervisors report',
},
table: {
recordsTitle: 'Records per page:',
noResultsLabel: 'The filter didnt uncover any results',
noDataLabel: 'I didnt find anything for you',
},
autoLogout: {
title: 'Alert',
message_start: 'Attention: You will be automatically logged out in',
message_end: 'seconds if you do not interact with the screen.',
},
weekdays: {
Sunday: ' Sunday',
Monday: 'Monday',
Tuesday: 'Tuesday',
Wednesday: 'Wednesday',
Thursday: 'Thursday',
Friday: 'Friday',
Saturday: 'Saturday',
},
shiftsTemplate: {
tabTitle1: 'Shifts',
tabTitle2: 'Templates for shifts',
saveButton: 'Save',
emptyShiftsMessage: 'No shifts available.',
emptyTemplateMessage: 'No template available.',
selectTemplate: 'Select a template',
selectTemplateNoResult: 'No template available.',
selectDay: 'Day',
startTime: 'Start time',
endTime: 'End time',
templateTitle: 'Title',
templateDescription: 'Description',
createButton: 'Create',
updateButton: 'Update',
deleteButton: 'Delete',
resetButton: 'Reset',
dayValidation: 'Day must be filled in.',
startTimeValidation: 'Start time must be filled in.',
endTimeValidation: 'End time must be filled in.',
startTimeAfterEndTimeValidation:
'The end time cannot be before or equal the start time',
endTimeBeforeStartTimeValidation:
'The end time cannot be before or equal the start time',
existingTimeShift: 'This time is already in use',
}, },
}; };

View File

@ -1,349 +1,162 @@
export default { export default {
accountDialog: { employee_list: {
title: 'Plus', page_header: "Répertoire du personnel",
item_1: 'Langue', table: {
item_2: 'Profile', first_name: "prénom",
item_3: 'Déconnexion', last_name: "nom de famille",
item_4: 'Carte de temps', email: "courriel",
item_5: 'Calendrier annuel', phone_number: "# téléphone",
}, role: "rôle",
addUserPage: { supervisor: "superviseur",
title: 'Créer un utilisateur', company: "Compagnie",
firstName: 'Prénom',
lastName: 'Nom de famille',
email: 'Email',
phoneNumber: 'Numéro de téléphone',
type: 'Choisir un type',
role: 'Choisir un role',
job_title: 'Titre demploi',
company: 'Entreprise',
supervisor: 'Superviseur',
isSupervisor: 'Est un superviseur',
hours_bank_max: 'Maximum de banque dheures ',
onboarding: 'Date dembauche',
offboarding: 'date de départ',
employee_number: 'Numéro demployé (Matricule employeur D)',
regular_hours_day: 'nombre régulier dheures par jour',
address: 'Adresse',
verifiedAccountStatus: 'Compte vérifié',
unVerifiedAccountStatus: 'Compte non vérifié',
password: 'Nouveau mot de passe',
confirmedPassword: 'Confirmez votre mot de passe',
submit: 'Créer',
//Form Validaiton
firstNameValidation: 'Le prénom doit être rempli.',
lastNameValidation: 'Le nom de famille doit être rempli.',
emailValidation: 'Email doit être un e-mail valide.',
phoneNumberValidation: 'Numéro de téléphone doit être rempli.',
typeValidation: 'Type doit être rempli.',
roleValidation: 'Role doit être rempli.',
job_titleValidation: 'Le Titre demploi doit être rempli.',
companyValidation: 'Entreprise doit être rempli.',
supervisorValidation: 'Superviseur doit être rempli.',
hours_bank_maxValidation: 'Maximum de banque dheures doit être rempli.',
onboardingValidation: 'Date dembauche doit être rempli.',
employee_numberValidation: 'Numéro demployé doit être rempli.',
regular_hours_dayValidation:
'nombre régulier dheures par jour doit être rempli.',
addressValidation: 'Adresse doit être rempli.',
passwordValidation: 'Le mot de passe doit répondre à tous les critères.',
confirmPasswordValidation:
'Le mot de passe doit correspondre au nouveau mot de passe.',
},
autoLogout: {
title: 'Alerte',
message_start: 'Attention : vous serez automatiquement déconnecté dans',
message_end: 'secondes si vous ninteragissez pas avec lécran.',
},
weekdays: {
Sunday: 'dimanche',
Monday: 'lundi',
Tuesday: 'mardi',
Wednesday: 'mercredi',
Thursday: 'jeudi',
Friday: 'vendredi',
Saturday: 'samedi',
},
editUserPage: {
title: 'Modifier le compte',
passwordTitle: 'Réinitialiser le mot de passe',
firstName: 'Prénom',
lastName: 'Nom de famille',
email: 'Email',
phoneNumber: 'Numéro de téléphone',
type: 'Choisir un type',
role: 'Choisir un role',
job_title: 'Titre demploi',
company: 'Entreprise',
supervisor: 'Superviseur',
isSupervisor: 'Est un superviseur',
hours_bank_max: 'Maximum de banque dheures ',
address: 'Adresse',
verifiedAccountStatus: 'Compte vérifié',
unVerifiedAccountStatus: 'Compte non vérifié',
password: 'Nouveau mot de passe',
confirmedPassword: 'Confirmez votre mot de passe',
submit: 'Modifier le compte',
//Form Validaiton
firstNameValidation: 'Le prénom doit être rempli.',
lastNameValidation: 'Le nom de famille doit être rempli.',
emailValidation: 'Email doit être un e-mail valide.',
phoneNumberValidation: 'Numéro de téléphone doit être rempli.',
typeValidation: 'Type doit être rempli.',
roleValidation: 'Role doit être rempli.',
job_titleValidation: 'Le Titre demploi doit être rempli.',
companyValidation: 'Entreprise doit être rempli.',
supervisorValidation: 'Superviseur doit être rempli.',
hours_bank_maxValidation: 'Maximum de banque dheures doit être rempli.',
addressValidation: 'Adresse doit être rempli.',
passwordValidation: 'Le mot de passe doit répondre à tous les critères.',
confirmPasswordValidation:
'Le mot de passe doit correspondre au nouveau mot de passe.',
},
expenseColumns: {
title: 'Dépenses',
column_1: 'Type',
column_2: 'Montant',
column_3: 'Attachement',
column_4: 'Description',
column_5: 'État',
column_6: 'Rapport du superviseur',
},
footerLayout: {
title: `Targo Canada, 2005 - 2025. Tous droits réservés.`,
},
forgotPage: {
title:
'Veuillez saisir votre e-mail pour rechercher votre compte et envoyer un code de vérification.',
email: 'Email',
emailValidation: 'Email doit être un e-mail valide.',
submit: 'Envoyer code',
cancel: 'Annuler',
},
helpPage: {
title_1: 'Contactez-nous',
title_2:
'Veuillez remplir le formulaire ci-dessous et nous vous communiquerons dès que possible.',
fullName: 'Nom complet*',
email: 'Adresse e-mail*',
phoneNumber: 'Numéro de téléphone*',
message:
'Comment pouvons-nous vous aider? Sil vous plaît utiliser cette zone pour fournir un message détaillé, Merci!*',
//form validation
fullNameValidation: 'Le nom complet doit être rempli.',
emailValidation: 'Le-mail doit être un e-mail valide.',
phoneNumberValidation: 'Le numéro de téléphone doit être rempli.',
messageValidation: 'Message doit être rempli.',
submit: 'Envoyer',
},
indexAdminPage: {
card_1: 'Administrateurs',
card_2: 'Techniciens',
card_3: 'Marchand',
card_4: 'Clients',
},
loginPage: {
title: 'Se connecter à Targo',
forgotPassword: 'Mot de passe oublié?',
signUp: 'Vous navez pas encore de compte?',
email: 'Email',
password: 'Mot de passe',
submit: 'Connecter',
employeeLoginButton: 'Employé',
facebookLoginButton:'Facebook',
tooltipComingSoon: 'À venir!',
loginOrSeparator: 'OU',
emailValidation: 'Email doit être un e-mail valide.',
passwordValidation: 'Mot de passe doit être rempli.',
rememberMe: 'Rester connecté',
},
mainLayout: {
backButton: 'Retour à la page daccueil',
clearFilter: 'Effacer le filtre',
},
navBar: {
userMenuHome: 'Accueil',
userMenuEmployeeList: 'Répertoire employés',
userMenuShiftValidation: 'Valider les heures',
userMenuTimesheetTemp: 'Carte de temps',
userMenuProfile: 'Profil',
userMenuHelp: 'Aide',
userMenuLogout: 'Déconnexion',
userMenuTimesheet: 'Carte de temps',
userMenuCalendar: 'Calendrier annuel',
},
notFoundPage: {
pageText: 'On ne semble pas trouver la page que vous cherchez, désolé!',
backButton: 'Je veux retourner en arrière!',
},
notificationDialog: {
notice: 'Notification',
markAllRead: 'Marquer tout comme lu',
deleteAll: 'Supprimer tout',
close: 'Fermer',
},
pageTitles: {
employeeDirectory: 'Répertoire des Employés',
newUsers: 'Nouvel utilisateur',
updateUsers: 'Mettre à jour lutilisateur',
timeSheets: 'Carte de temps',
timeSheetValidations: 'Validation cartes de temps',
},
profilePage: {
title: 'Profil',
firstName: 'Prénom',
lastName: 'Nom de famille',
email: 'Email',
phoneNumber: 'Numéro de téléphone',
job_title: 'Titre du poste',
company: 'Entreprise',
supervisor: 'Superviseur',
role: 'Role',
address: 'Adresse',
job_titleValidation: 'Le champ "titre du poste" doit être rempli.',
companyValidation: 'Le champ "entreprise" doit être rempli.',
supervisorValidation: 'Un employé qui na pas le rôle de superviseur doit être attribué à un superviseur.',
roleValidation: 'Le champ "rôle" doit être rempli.',
addressValidation: 'Le champ "adresse" doit être rempli.',
firstNameValidation: 'Le champ "prénom" doit être rempli.',
lastNameValidation: 'Le champ "nom de famille" doit être rempli.',
phoneNumberValidation: 'Le champ "numéro de téléphone" doit être rempli.',
submit: 'Modifier Profil',
},
resetPage: {
title: 'Réinitialiser votre mot de passe',
code: 'code',
codeValidation: 'Le code doit être rempli avec 4 chiffres.',
newPassword: 'Nouveau mot de passe',
confirmedPassword: 'Confirmez votre mot de passe',
newPasswordValidation: 'Le mot de passe doit répondre à tous les critères.',
newPasswordLengthValidation: 'Doit être dau moins 8 caractères de long.',
newPasswordCapitalValidation:
'Doit contenir au moins une lettre majuscule.',
newPasswordNumberValidation: 'Doit contenir au moins un numéro.',
newPasswordSpecialCharacterValidation:
'Doit contenir au moins un caractère spécial : !@#$%^&*()-_+=',
confirmNewPasswordValidation:
'Le mot de passe doit correspondre au nouveau mot de passe.',
submit: 'Envoyer',
cancel: 'Annuler',
},
shared:{
searchBar: 'Rechercher',
loading: 'Téléchargement des données en cours...',
failedToLoad: 'Aucune donnée à afficher',
failedToSearch: 'Aucun résultat de recherche obtenu',
languageLabel: 'Langue',
},
shiftColumns: {
title: 'Quarts de travail',
labelType: 'type',
labelIn: 'entrée',
labelOut: 'sortie',
labelComment: 'commentaire',
labelState: 'état',
labelSupervisorReport: 'rapport du superviseur',
},
shiftsTemplate: {
tabTitle1: 'Quarts de travail',
tabTitle2: 'Modèles de quarts de travail',
saveButton: 'Enregistrer',
emptyShiftsMessage: 'Aucun modèle disponible.',
emptyTemplateMessage: 'Aucun quarts de travail disponible.',
selectTemplate: 'Sélectionnez un modèle',
selectTemplateNoResult: 'Aucun modele disponible.',
selectDay: 'Jour',
startTime: 'Heure de début',
endTime: 'Heure de fin',
templateTitle: 'Titre',
templateDescription: 'Description',
createButton: 'Créer',
updateButton: 'Mettre à jour',
deleteButton: 'Supprimer',
resetButton: 'Réinitialiser',
dayValidation: 'Jour doit être rempli.',
startTimeValidation: 'Heure de début doit être rempli.',
endTimeValidation: 'Heure de fin doit être rempli.',
startTimeAfterEndTimeValidation:
'Lheure de début ne peut pas être après lheure de fin',
endTimeBeforeStartTimeValidation:
'Lheure de fin ne peut pas être précédente à lheure de debut',
existingTimeShift: 'Ce temps est déjà utilisé',
},
signUpPage: {
title: 'Créer un nouveau compte',
firstName: 'Prénom',
lastName: 'Nom de famille',
email: 'Email',
phoneNumber: 'Numéro de téléphone',
password: 'Nouveau mot de passe',
confirmedPassword: 'Confirmez votre mot de passe',
signIn: 'Vous avez déjà un compte?',
submit: 'Sinscrire',
firstNameValidation: 'Le prénom doit être rempli.',
lastNameValidation: 'Le nom de famille doit être rempli.',
emailValidation: 'Email doit être un e-mail valide.',
phoneNumberValidation: 'Numéro de téléphone doit être rempli.',
passwordValidationTitle: 'Critères de mot de passe :',
passwordValidation: 'Le mot de passe doit répondre à tous les critères.',
passwordLengthValidation: 'Doit être dau moins 8 caractères de long.',
passwordCapitalValidation: 'Doit contenir au moins une lettre majuscule.',
passwordNumberValidation: 'Doit contenir au moins un numéro.',
passwordSpecialCharacterValidation:
'Doit contenir au moins un caractère spécial : !@#$%^&*()-_+=',
confirmPasswordValidation:
'Le mot de passe doit correspondre au nouveau mot de passe.',
},
table: {
recordsTitle: 'Enregistrements par page:',
noResultsLabel: 'Le filtre na révélé aucun résultat',
noDataLabel: 'Je nai rien trouvé pour toi',
},
timesheet: {
title:'Carte de temps',
date_ranges_to:'au',
days: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
nav_button: {
calendar_date_picker:'Calendrier',
current_week:'Semaine actuelle',
next_week:'Prochaine semaine',
previous_week:'Semaine précédente',
}, },
save_button:'Enregistrer', },
cancel_button:'Annuler',
remote_button: 'Télétravail', login: {
delete_button: 'Supprimer', page_header: "connexion au compte",
email: "courriel",
password: "mot de passe",
button: {
connect: "connecter",
employee: "employé",
facebook:"Facebook",
remember_me: "rester connecté",
},
tooltip: {
coming_soon: "à venir!",
},
},
nav_bar: {
home: "accueil",
employee_list: "répertoire employés",
timesheet_approvals: "valider les heures",
timesheet: "carte de temps",
profile: "profil",
help: "aide",
logout: "déconnecter",
},
profile: {
personal: {
tab_title: "personnelle",
first_name: "prénom",
last_name: "nom de famille",
phone_number: "numéro de téléphone",
address: "adresse",
address_hint: "# addresse, ville, région, pays",
birthdate: "date de naissance",
},
employee: {
tab_title: "carrière",
email: "courriel",
job_title: "poste",
company: "compagnie",
supervisor: "nom du superviseur",
hired_date: "date d'embauche",
},
preferences: {
tab_title: "préférences",
display_options: "Options d'affichage",
language_options: "Options de langue",
dark_mode: "sombre",
light_mode: "clair",
},
errors: {
must_enter_birthdate: "Vous devez entrer une date de naissance valide",
}
},
shared: {
error: {
no_data_found: 'aucune donnée à afficher',
no_search_results: 'aucun résultat ne correspond à la recherche',
},
label: {
search: 'recherche',
loading: 'chargement en cours...',
language: 'langue',
add: "ajouter",
save: "sauvegarder",
remove: "supprimer",
cancel: "annuler",
update: "mettre à jour",
modify: "modifier",
},
misc: {
or: "ou",
and: "et",
to: "au",
from: "de",
yes: "oui",
no: "non",
in: "entrée",
out: "sortie",
},
shift_type: {
regular: "régulier",
evening: "soir",
emergency: "urgence",
overtime: "supplémentaire",
holiday: "férié",
vacation: "vacances",
sick: "maladie",
remote: "télétravail",
},
weekday: {
sunday: "dimanche",
monday: "lundi",
tuesday: "mardi",
wednesday: "mercredi",
thursday: "jeudi",
friday: "vendredi",
saturday: "samedi",
},
},
timesheet: {
page_header:"Carte de temps",
nav_button: {
calendar_date_picker:"Calendrier",
current_week:"Semaine actuelle",
next_week:"Prochaine semaine",
previous_week:"Semaine précédente",
},
save_button:"Enregistrer",
cancel_button:"Annuler",
remote_button: "Télétravail",
delete_button: "Supprimer",
shift: { shift: {
actions: { actions: {
add:'Ajouter un Quart', add:"Ajouter un Quart",
edit: 'Modifier un Quart', edit: "Modifier un Quart",
delete: 'Supprimer un Quart', delete: "Supprimer un Quart",
delete_confirmation_msg: 'Voulez-vous complètement supprimer ce quart?', delete_confirmation_msg: "Voulez-vous complètement supprimer ce quart?",
}, },
types: { types: {
label: 'Type de Quart', label: "Type de Quart",
EMERGENCY: 'Urgence', EMERGENCY: "Urgence",
EVENING: 'Soir', EVENING: "Soir",
HOLIDAY: 'Férié', HOLIDAY: "Férié",
OVERTIME: 'Supplémentaire', OVERTIME: "Supplémentaire",
REGULAR: 'Régulier', REGULAR: "Régulier",
SICK: 'Maladie', SICK: "Maladie",
VACATION: 'Vacance', VACATION: "Vacance",
REMOTE: 'Télétravail', REMOTE: "Télétravail",
}, },
errors: { errors: {
not_found:'Aucun quart trouvé', not_found:"Aucun quart trouvé",
overlap:'Il y a un chevauchement entre deux ou plusieurs quarts', overlap:"Il y a un chevauchement entre deux ou plusieurs quarts",
invalid:'Entrée du quart invalide', invalid:"Entrée du quart invalide",
unknown:'Erreur inconnue', unknown:"Erreur inconnue",
comment_required:'un commentaire est requis', comment_required:"un commentaire est requis",
comment_too_long:'votre commentaire est trop long', comment_too_long:"votre commentaire est trop long",
}, },
fields: { fields: {
start:'Début (HH:mm)', start:"Début (HH:mm)",
end:'Fin (HH:mm)', end:"Fin (HH:mm)",
header_comment:'Commentaire du Quart', header_comment:"Commentaire du Quart",
textarea_comment: 'Laissez votre commentaire ici', textarea_comment: "Laissez votre commentaire ici",
}, },
}, },
expense: { expense: {
@ -354,92 +167,56 @@ export default {
employee_comment:'Commentaire', employee_comment:'Commentaire',
supervisor_comment:'Note du Superviseur', supervisor_comment:'Note du Superviseur',
errors: { errors: {
date_required_or_invalid:'La date est manquante ou invalide', date_required_or_invalid:"La date est manquante ou invalide",
comment_required:'un commentaire est requis', comment_required:"un commentaire est requis",
comment_too_long:'votre commentaire est trop long', comment_too_long:"votre commentaire est trop long",
amount_must_be_positive:'le montant doit être suppérieur à 0$', amount_must_be_positive:"le montant doit être suppérieur à 0$",
mileave_must_be_positive:'le kilométrage doit être suppérieur à 0', mileave_must_be_positive:"le kilométrage doit être suppérieur à 0",
amount_xor_mileage:'Vous ne pouvez pas saisir un montant et un kilométrage pour une même dépense', amount_xor_mileage:"Vous ne pouvez pas saisir un montant et un kilométrage pour une même dépense",
mileage_required_for_type:'Vous devez entrer une valeur en kilométrage pour ce type de dépense', mileage_required_for_type:"Vous devez entrer une valeur en kilométrage pour ce type de dépense",
amount_required_for_type:'Vous devez entrer une valeur en montant $ pour ce type de dépense', amount_required_for_type:"Vous devez entrer une valeur en montant $ pour ce type de dépense",
}, },
hints: { hints: {
amount_or_mileage:'Soit dépense ou kilométrage, pas les deux', amount_or_mileage:"Soit dépense ou kilométrage, pas les deux",
comment_required:'un commentaire est requis', comment_required:"un commentaire est requis",
}, },
mileage:'Kilométrage', mileage:"Kilométrage",
open_btn:'Liste des Dépenses', open_btn:"Liste des Dépenses",
title:'Liste des dépenses', title:"Liste des dépenses",
total_amount:'Montant total', total_amount:"Montant total",
total_mileage:'Kilométrage total', total_mileage:"Kilométrage total",
type:'Type', type:"Type",
types: { types: {
PER_DIEM:'Per diem', PER_DIEM:"Per diem",
EXPENSES:'dépense', EXPENSES:"dépense",
MILEAGE:'kilométrage', MILEAGE:"kilométrage",
PRIME_GARDE:'Prime de garde', PRIME_GARDE:"Prime de garde",
}, },
}, },
}, },
timeSheetValidations: {
tableColumnLabelFullname: 'nom complet', timesheet_approvals: {
tableColumnLabelEmail: 'courriel', page_title: "Validation cartes de temps",
tableColumnLabelRegularHours: 'heures régulières', table: {
tableColumnLabelEveningHours: 'soir', full_name: "nom complet",
tableColumnLabelEmergencyHours: 'urgence', email: "courriel",
tableColumnLabelOvertime: 'supplémentaires', expenses: "dépenses",
tableColumnLabelExpenses: 'dépenses', mileage: "kilométrage",
tableColumnLabelMileage: 'kilométrage', verified: "approuvé",
actionTitle: 'Veuillez enregistrer les changements effectués.', unverified: "à vérifier",
actionButton: 'Enregistrer', },
timeSheetStatusVerified: 'validé', chart: {
timeSheetStatusUnverified: 'à valider', hours_worked_title: "heures travaillées",
timeSheetStatusPartial: 'partiellement validé', expenses_title: "dépenses encourues"
timeSheetStatusComplete: 'complet', },
timeSheetStatusEmpty: 'vide', print_report: {
timeSheetStatusBlocked: 'bloqué', company: "compagnie",
showAllCheckbox: 'Afficher tous', type: "types de données",
accumulatedSicknessTotal: 'Accumulées de maladies', shifts: "quarts de travail",
consumedSicknessTotal: 'Consommées de maladies', expenses: "dépenses",
accumulatedVacationTotal: 'Accumulées de vacances', },
consumedVacationTotal: 'Consommées de vacances', tooltip: {
maxVacationPerYear: 'Maximum de vacances par année', button_detailed_view: "vue détaillée",
accumulatedSicknessTotalValidation: 'Cumulatif maladie doit être positif', },
consumedSicknessTotalValidation: 'Maladie utilisé doit être positif',
accumulatedVacationTotalValidation: 'Cumulatif vacances doit être positif',
consumedVacationTotalValidation: 'Vacances utilisées doit être positif',
maxVacationPerYearValidation: 'Maximum vacances annuel doit être positif.',
resteVacationTotal: 'Reste des vacances',
hoursWorkedChartTitle: 'Heures travaillées',
hoursWorkedRegular: 'régulier',
hoursWorkedEvening: 'soir',
hoursWorkedEmergency: 'urgence',
hoursWorkedOvertime: 'supplémentaire',
tooltipTimeline: 'Vue journalière',
tooltipTimesheet: 'Feuille de temps',
reportFilterCategoryCompany: 'Compagnie',
reportFilterCategoryType: 'Types de données',
reportFilterShifts: 'Quarts de travail',
reportFilterExpenses: 'Dépenses',
reportFilterHoliday: 'Jours Fériés',
reportFilterVacation: 'Vacances',
},
usersListPage: {
tableHeader: 'Répertoire du personnel',
searchInput: 'rechercher',
userListFirstName: 'prénom',
userListLastName: 'nom de famille',
userListEmail: 'courriel',
userListPhone: '# téléphone',
userListRole: 'rôle',
userListSupervisor: 'superviseur',
userListCompany: 'Compagnie',
addButton: 'Ajouter employé',
customer: 'Client',
dealer: 'Marchand',
employee: 'Employé',
technician: 'Technicien',
admin: 'Administrateur',
support: 'Support',
}, },
}; };

View File

@ -0,0 +1,100 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useAuthApi } from 'src/modules/auth/composables/use-auth-api';
const auth_api = useAuthApi();
const email = defineModel<string>('email', { default: '', });
const is_remembered = ref<boolean>(false);
const is_employee_email = computed( () => email.value.includes('@targ'));
</script>
<template>
<q-card class="rounded-15">
<q-card-section class="text-center bg-primary q-pa-lg">
<q-img src="/src/assets/logo-targo-white.svg" ratio="4.6" fit="contain" />
</q-card-section>
<div class="q-pt-sm q-px-xl q-pb-lg">
<q-card-section class="text-center text-uppercase">
<div class="text-h6 text-weight-bold">
{{ $t('login.page_header') }}
</div>
</q-card-section>
<q-form @submit="auth_api.login">
<q-input
v-model="email"
dense
outlined
label-color="primary"
:label="$t('login.email')"
/>
<q-card-section class="q-ma-none q-pa-none text-uppercase text-caption text-weight-medium">
<q-toggle
v-model="is_remembered"
color="primary"
:label="$t('login.button.remember_me')"
/>
</q-card-section>
<q-card-actions>
<q-btn
push
rounded
disabled
type="submit"
color="primary"
:label="$t('login.button.connect')"
class="full-width"
/>
</q-card-actions>
<!-- A implémenter plus tard sans doute, pour les clients. A revoir avec Authentik API pour création de users -->
<!-- <q-card-section class="text-center q-pa-none q-mt-none">
<RouterLink disabled class="text-primary" to="/signup">{{ $t('loginPage.signUp') }}</RouterLink>
</q-card-section> -->
</q-form>
<q-card-section class="row q-pt-sm">
<q-separator color="primary" class="col self-center"/>
<span class="col text-primary text-weight-bolder text-center text-uppercase self-center">{{ $t('shared.misc.or') }}</span>
<q-separator color="primary" class="col self-center"/>
</q-card-section>
<q-card-section class="column q-px-sm q-pt-none">
<q-btn
rounded
push
disabled
color="fb-blue"
icon="img:src/assets/Facebook-f_Logo-White-Logo.wine.svg"
:label="$t('login.button.facebook')"
class="full-width row q-mb-sm"
>
<q-tooltip anchor="top middle" class="bg-primary">{{$t('login.tooltip.coming_soon')}}</q-tooltip>
</q-btn>
<q-slide-transition>
<div v-if="is_employee_email">
<transition
slow
enter-active-class="animated zoomIn"
leave-active-class="animated zoomOut"
>
<q-btn
push
rounded
color="primary"
icon="img:src/assets/logo-targo-simple.svg"
:label="$t('login.button.employee')"
class="full-width row"
@click="auth_api.oidcLogin"
/>
</transition>
</div>
</q-slide-transition>
</q-card-section>
</div>
</q-card>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { useAuthApi } from '../composables/use-auth-api';
import { useRouter } from 'vue-router';
const auth_api = useAuthApi();
const router = useRouter();
const setBypassUser = (bypassRole: string) => {
auth_api.setUser(bypassRole);
router.push({ name: 'dashboard' }).catch( err => {
console.error('Router navigation failed: ', err);
});
};
</script>
<template>
<q-card class="absolute-bottom-right q-ma-sm">
<q-card-section class="q-pa-sm text-uppercase text-center"> impersonate </q-card-section>
<q-card-actions vertical>
<q-btn
v-for="role, index in [ 'supervisor', 'accounting', 'human_resources', 'employee' ]"
:key="index"
push
color="primary"
text-color="white"
:label="role"
class="text-uppercase"
@click="setBypassUser(role)"
/>
</q-card-actions>
</q-card>
</template>

View File

@ -1,25 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import LoginConnectionPanel from 'src/modules/auth/components/login-connection-panel.vue';
import { useAuthApi } from '../composables/use-auth-api'; import LoginDevBypass from 'src/modules/auth/components/login-dev-bypass.vue';
import { useRouter } from 'vue-router';
const authApi = useAuthApi();
const email = ref('');
const isShowingEmployeeLoginButton = ref(false);
const isRemembered = ref(false);
const router = useRouter();
const setBypassUser = (bypassRole: string) => {
authApi.setUser(bypassRole);
router.push({ name: 'dashboard' }).catch( err => {
console.error('Router navigation failed: ', err);
});
}
watch(email, (value) => {
isShowingEmployeeLoginButton.value = value.includes('@targ');
});
</script> </script>
<template> <template>
@ -28,71 +9,11 @@
<q-img src="src/assets/village.png" fit="cover" position="50% 100%" class="absolute-full" /> <q-img src="src/assets/village.png" fit="cover" position="50% 100%" class="absolute-full" />
<q-page class="flex flex-center"> <q-page class="flex flex-center">
<transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut"> <transition appear slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
<q-card class="rounded-20"> <LoginConnectionPanel />
<q-card-section class="text-center bg-primary q-pa-lg">
<q-img src="/src/assets/logo-targo-white.svg" ratio="4.6" fit="contain" />
</q-card-section>
<div class="q-pt-sm q-px-xl q-pb-lg">
<q-card-section class="text-center">
<div class="text-h6 text-grey-9 text-weight-bold">
{{ $t('loginPage.title') }}
</div>
</q-card-section>
<q-form class="q-gutter-sm" @submit="authApi.login">
<q-input dense outlined label-color="primary" v-model="email" :label="$t('loginPage.email')" />
<q-card-section class="q-ma-none q-pa-none">
<q-toggle v-model="isRemembered" :label="$t('loginPage.rememberMe')" color="primary" />
</q-card-section>
<q-card-actions>
<q-btn disabled rounded push :label="$t('loginPage.submit')" type="submit" color="primary" class="full-width" />
</q-card-actions>
<!-- A implémenter plus tard sans doute, pour les clients. A revoir avec Authentik API pour création de users -->
<!-- <q-card-section class="text-center q-pa-none q-mt-none">
<RouterLink disabled class="text-primary" to="/signup">{{ $t('loginPage.signUp') }}</RouterLink>
</q-card-section> -->
</q-form>
<q-card-section class="row q-pt-sm">
<q-separator color="primary" class="col self-center"/>
<span class="col text-primary text-weight-bolder text-center vertical-align self-center">{{$t('loginPage.loginOrSeparator')}}</span>
<q-separator color="primary" class="col self-center"/>
</q-card-section>
<q-card-section class="column q-px-sm q-pt-none">
<q-btn disabled rounded push :label="$t('loginPage.facebookLoginButton')" color="fb-blue" class="full-width row q-mb-sm" icon="img:src/assets/Facebook-f_Logo-White-Logo.wine.svg">
<q-tooltip anchor="top middle" class="bg-primary">{{$t('loginPage.tooltipComingSoon')}}</q-tooltip>
</q-btn>
<q-slide-transition>
<div v-if="isShowingEmployeeLoginButton">
<transition slow enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
<q-btn rounded push color="primary" @click="authApi.oidcLogin" :label="$t('loginPage.employeeLoginButton')" class="full-width row" icon="img:src/assets/logo-targo-simple.svg" />
</transition>
</div>
</q-slide-transition>
</q-card-section>
</div>
</q-card>
</transition> </transition>
<!-- DEV TOOLS --> <!-- DEV TOOLS -->
<q-card class="absolute-bottom-right q-ma-sm"> <LoginDevBypass />
<q-card-section class="q-pa-sm text-primary"> BYPASS LOGIN WITH: </q-card-section>
<q-separator color="primary" />
<q-card-section>
<q-btn-group push rounded>
<q-btn push color="primary" text-color="white" label="ACCOUNTING" icon="attach_money" @click="setBypassUser('accounting')"/>
<q-btn push color="primary" text-color="white" label="SUPERVISOR" icon="supervisor_account" @click="setBypassUser('supervisor')"/>
<q-btn push color="primary" text-color="white" label="HR" icon="diversity_3" @click="setBypassUser('human_resources')"/>
<q-btn push color="primary" text-color="white" label="EMPLOYEE" icon="support_agent" @click="setBypassUser('employee')"/>
</q-btn-group>
</q-card-section>
</q-card>
</q-page> </q-page>
</q-page-container> </q-page-container>
</q-layout> </q-layout>

View File

@ -1,13 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable */
import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface'; import type { EmployeeListTableItem } from 'src/modules/employee-list/types/employee-list-table-interface';
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
return first_name.charAt(0) + last_name.charAt(0); // return first_name.charAt(0) + last_name.charAt(0);
}; // };
const props = defineProps<{ const { row } = defineProps<{
row: EmployeeListTableItem row: EmployeeListTableItem
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -18,30 +17,35 @@
<template> <template>
<q-card <q-card
v-ripple v-ripple
class="rounded-15 bg-white col-xs-6 col-sm-4 col-md-3 col-lg-2 column no-wrap cursor-pointer q-ma-sm" class="column col-xs-6 col-sm-4 col-md-3 col-lg-2 no-wrap rounded-15 cursor-pointer q-ma-sm"
style="max-width: 230px;" style="max-width: 230px;"
@click="emit('onProfileClick', props.row.email)" @click="emit('onProfileClick', row.email)"
> >
<q-card-section class="text-center col-5"> <q-card-section class="col-6 text-center">
<q-avatar color="primary" size="8em"> <q-avatar
<img src="src/assets/targo-default-avatar.png" alt="employee avatar" class="q-pa-xs"> color="primary"
size="8em"
class="shadow-3"
>
<img
src="src/assets/targo-default-avatar.png"
alt="employee avatar"
class="q-pa-xs"
>
</q-avatar> </q-avatar>
</q-card-section> </q-card-section>
<q-card-section class="text-center text-h6 text-primary text-weight-medium text-uppercase q-pb-none col-2 content-end" style="line-height: 0.7em;">
<div class="ellipsis"> <q-card-section
{{ props.row.first_name }} {{ props.row.last_name }} class="col-grow text-center text-h6 text-weight-medium text-uppercase q-pb-none"
</div> style="line-height: 0.8em;"
</q-card-section> >
<q-separator color="primary" class="q-mx-sm q-mt-xs" /> <div class="ellipsis text-primary"> {{ row.first_name }} {{ row.last_name }} </div>
<q-card-section class="text-caption text-grey-8 text-body2 text-uppercase q-pt-none text-center col content-start" style="min-height: 5em;"> <q-separator color="primary" class="q-mx-sm q-mt-xs" />
<div class=" ellipsis-2-lines"> <div class=" ellipsis-2-lines text-caption"> {{ row.job_title }} </div>
{{ props.row.job_title }}
</div>
</q-card-section> </q-card-section>
<q-card-section class="bg-primary text-white text-caption text-center q-py-none col-2 content-center "> <q-card-section class="bg-primary text-white text-caption text-center q-py-none col-2 content-center ">
<div> <div> {{ row.email }} </div>
{{ props.row.email }}
</div>
</q-card-section> </q-card-section>
</q-card> </q-card>
</template> </template>

View File

@ -8,28 +8,28 @@
import type { EmployeeListTableItem } from '../../types/employee-list-table-interface'; import type { EmployeeListTableItem } from '../../types/employee-list-table-interface';
import type { QTableColumn } from 'quasar'; import type { QTableColumn } from 'quasar';
const employeeListApi = useEmployeeListApi(); const employee_list_api = useEmployeeListApi();
const employeeStore = useEmployeeStore(); const employee_store = useEmployeeStore();
const isLoadingList = ref<boolean>(true); const is_loading_list = ref<boolean>(true);
const { t } = useI18n(); const { t } = useI18n();
const filter = ref(""); const filter = ref("");
const isGridMode = ref(true); const is_grid_mode = ref(true);
const pagination = ref({ rowsPerPage: 0 }); const pagination = ref({ rowsPerPage: 0 });
const employeeListColumns = computed((): QTableColumn<EmployeeListTableItem>[] => [ const employee_list_columns = computed((): QTableColumn<EmployeeListTableItem>[] => [
{name: 'first_name', label: t('usersListPage.userListFirstName'), 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('usersListPage.userListLastName'), field: 'last_name', align: 'left'}, {name: 'last_name', label: t('employee_list.table.last_name'), field: 'last_name', align: 'left'},
{name: 'email', label: t('usersListPage.userListEmail'), field: 'email', align: 'left'}, {name: 'email', label: t('employee_list.table.email'), field: 'email', align: 'left'},
{name: 'supervisor_full_name', label: t('usersListPage.userListSupervisor'), field: 'supervisor_full_name', align: 'left'}, {name: 'supervisor_full_name', label: t('employee_list.table.supervisor'), field: 'supervisor_full_name', align: 'left'},
{name: 'company_name', label: t('usersListPage.userListCompany'), field: 'company_name', align: 'left'}, {name: 'company_name', label: t('employee_list.table.company'), field: 'company_name', align: 'left'},
{name: 'job_title', label: t('usersListPage.userListRole'), field: 'job_title', align: 'left'}, {name: 'job_title', label: t('employee_list.table.role'), field: 'job_title', align: 'left'},
]); ]);
onMounted( async () => { onMounted( async () => {
isLoadingList.value = true; is_loading_list.value = true;
await employeeListApi.getEmployeeList(); await employee_list_api.getEmployeeList();
isLoadingList.value = false; is_loading_list.value = false;
}) })
</script> </script>
@ -42,23 +42,23 @@
virtual-scroll virtual-scroll
title=" " title=" "
card-style="max-height: 70vh;" card-style="max-height: 70vh;"
:rows="employeeStore.employeeList" :rows="employee_store.employeeList"
:columns="employeeListColumns" :columns="employee_list_columns"
row-key="name" row-key="name"
v-model:pagination="pagination" v-model:pagination="pagination"
: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'"
: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"
card-container-class="justify-center" card-container-class="justify-center"
:grid="isGridMode" :grid="is_grid_mode"
:loading="isLoadingList" :loading="is_loading_list"
:no-data-label="$t('shared.failedToLoad')" :no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.failedToSearch')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.loading')" :loading-label="$t('shared.label.loading')"
table-class="bg-white q-pa-md q-mx-md rounded-10 shadow-12"
table-style=""
@row-click="() => console.log('click!')" @row-click="() => console.log('click!')"
> >
<template v-slot:item="props"> <template v-slot:item="props">
@ -67,27 +67,45 @@
<template v-slot:top> <template v-slot:top>
<div class="row full-width q-mb-sm"> <div class="row full-width q-mb-sm">
<q-btn push icon="person_add" color="primary" :label="$t('usersListPage.addButton')"/> <q-btn
push
color="primary"
icon="person_add"
:label="$t('shared.label.add')"
class="text-uppercase"
/>
<q-space /> <q-space />
<q-btn-toggle push class="q-mr-md" color="white" text-color="primary" toggle-color="primary" v-model="isGridMode" <q-btn-toggle
:options="[ v-model="is_grid_mode"
{icon: 'grid_view', value: true}, push
{icon: 'view_list', value: false}, color="white"
]"/> text-color="primary"
<q-input toggle-color="primary"
outlined class="q-mr-md"
dense :options="[
rounded {icon: 'grid_view', value: true},
v-model="filter" {icon: 'view_list', value: false},
:label="$t('shared.searchBar')" ]"
label-color="primary" bg-color="white" color="primary" />
> <q-input
<template v-slot:append> v-model="filter"
<q-icon name="search" color="primary"/> outlined
</template> dense
</q-input> rounded
color="primary"
bg-color="white"
label-color="primary"
:label="$t('shared.label.search')"
>
<template v-slot:append>
<q-icon
name="search"
color="primary"
/>
</template>
</q-input>
</div> </div>
</template> </template>
@ -97,9 +115,31 @@
<span class="text-h6 q-mt-xl"> <span class="text-h6 q-mt-xl">
{{ message }} {{ message }}
</span> </span>
<q-icon size="4em" :name="filter ? 'filter_alt_off' : 'error_outline'" /> <q-icon
size="4em"
:name="filter ? 'filter_alt_off' : 'error_outline'"
/>
</div> </div>
</template> </template>
</q-table> </q-table>
</div> </div>
</template> </template>
<style lang="sass">
.my-sticky-header-table
thead tr:first-child th
background-color: var(--q-dark)
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>

View File

@ -6,8 +6,8 @@
<template> <template>
<q-page> <q-page>
<EmployeeListAddModifyDialog /> <EmployeeListAddModifyDialog />
<div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder text-grey-8"> <div class="text-h4 row justify-center q-py-sm q-mt-lg text-uppercase text-weight-bolder">
{{ $t('pageTitles.employeeDirectory') }} {{ $t('employee_list.page_header') }}
</div> </div>
<SupervisorCrewTable /> <SupervisorCrewTable />
</q-page> </q-page>

View File

@ -9,4 +9,19 @@ export interface EmployeeProfile {
first_work_day: string; first_work_day: string;
last_work_day: string; last_work_day: string;
residence: string; residence: string;
birth_date: string;
}
export const default_employee_profile: EmployeeProfile = {
first_name: '',
last_name: '',
supervisor_full_name: '',
company_name: -1,
job_title: '',
email: '',
phone_number: '',
first_work_day: '',
last_work_day: '',
residence: '',
birth_date: '',
} }

View File

@ -0,0 +1,109 @@
<script setup lang="ts">
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
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 { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
const { employeeProfile } = defineProps<{
employeeProfile: EmployeeProfile;
}>();
let initial_info: EmployeeProfile = employeeProfile;
let employee_form_data = ref<EmployeeProfile>({ ...employeeProfile });
const is_editing = ref<boolean>(false);
const supervisor_options = [{ label: 'AAA', value: '1' }, { label: 'BBB', value: '2' }, { label: 'CCC', value: '3' }, { label: 'DDD', value: '4' }];
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
return;
}
is_editing.value = false;
initial_info = { ...employee_form_data.value }; // update initial value for future possible resets
if (!deepEqual(employee_form_data, initial_info)) {
// save the new data here
return;
}
};
const onReset = () => {
employee_form_data = ref<EmployeeProfile>(initial_info);
is_editing.value = false;
}
</script>
<template>
<q-form
class="q-pa-md full-height"
@submit="onSubmit"
@reset="onReset"
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="employee_form_data.job_title"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.employee.job_title')"
/>
<ProfileInputField
v-model="employee_form_data.company_name"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.employee.company')"
/>
</div>
<div class="q-mx-xs">
<ProfileSelectField
v-model="employee_form_data.supervisor_full_name"
:options="supervisor_options"
:label-string="$t('profile.employee.supervisor')"
:is-editing="is_editing"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="employee_form_data.email"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.employee.email')"
/>
<ProfileInputField
v-model="employee_form_data.first_work_day"
readonly
class="col"
type="date"
:is-editing="is_editing"
:label-string="$t('profile.employee.hired_date')"
/>
</div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
:label="$t('shared.label.cancel')"
/>
<q-btn
push
size="sm"
color="primary"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
/>
</div>
</q-form>
</template>

View File

@ -0,0 +1,111 @@
<script setup lang="ts">
import { ref } from 'vue';
import { deepEqual } from 'src/utils/deep-equal';
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';
const { employeeProfile } = defineProps<{
employeeProfile: EmployeeProfile;
}>();
const is_editing = ref<boolean>(false);
let initial_info: EmployeeProfile = employeeProfile;
const personal_form_data = ref<EmployeeProfile>({ ...employeeProfile });
const onSubmit = () => {
if (!is_editing.value) {
is_editing.value = true;
return;
}
is_editing.value = false;
initial_info = { ...personal_form_data.value }; // update initial value for future possible resets
if (!deepEqual(personal_form_data.value, initial_info)) {
// save the new data here
return;
}
};
const onReset = () => {
personal_form_data.value= { ...initial_info };
is_editing.value = false;
}
</script>
<template>
<q-form
class="q-pa-md full-height"
@submit="onSubmit"
@reset="onReset"
>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="personal_form_data.first_name"
type="text"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.personal.first_name')"
/>
<ProfileInputField
v-model="personal_form_data.last_name"
class="col"
type="text"
:is-editing="is_editing"
:label-string="$t('profile.personal.last_name')"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="personal_form_data.phone_number"
class="col"
type="text"
:is-editing="is_editing"
:label-string="$t('profile.personal.phone_number')"
/>
<ProfileInputField
v-model="personal_form_data.birth_date"
class="col"
mask="#### / ## / ##"
hint="ex: 1970 / 01 / 01"
:is-editing="is_editing"
:label-string="$t('profile.personal.birthdate')"
/>
</div>
<div :class="$q.screen.lt.md ? 'column' : 'row'">
<ProfileInputField
v-model="personal_form_data.residence"
class="col"
:is-editing="is_editing"
:label-string="$t('profile.personal.address')"
:hint="$t('profile.personal.address_hint')"
/>
</div>
<div class="absolute-bottom" :class="$q.screen.lt.md ? 'column' : 'row'">
<q-space />
<q-btn
v-if="is_editing"
push
size="sm"
color="negative"
type="reset"
icon="cancel"
class="q-ma-sm"
:label="$t('timesheet.cancel_button')"
/>
<q-btn
push
size="sm"
color="primary"
type="submit"
:icon="is_editing ? 'save_alt' : 'create'"
class="q-ma-sm"
:label="is_editing ? $t('shared.label.save') : $t('shared.label.update')"
/>
</div>
</q-form>
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
const { userFirstName = '', userLastName = '' } = defineProps<{
userFirstName: string;
userLastName: string;
}>();
</script>
<template>
<q-img
src="src/assets/profile_header_default.png"
height="15vh"
:width="$q.screen.lt.md ? '80vw' : '40vw'"
class="rounded-5 q-mb-md shadow-2 col-auto"
fit="cover"
>
<div class="absolute-bottom text-h5 text-uppercase text-weight-bolder" style="line-height: 0.8em;">{{ userFirstName }} {{ userLastName }}</div>
</q-img>
</template>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import type { ValidationRule } from 'quasar';
const model = defineModel<string | number>({ required: true });
const { readonly = false, hint = '' } = defineProps<{
labelString: string;
isEditing: boolean;
readonly?: boolean;
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
hint?: string;
mask?: string;
rules?: ValidationRule[];
}>();
</script>
<template>
<q-input
v-model="model"
dense
:stack-label="!isEditing"
autogrow
filled
debounce="500"
label-color="primary"
class="q-ma-xs text-uppercase"
input-class="text-weight-medium text-h6"
:hide-hint="hint === ''"
:hint="isEditing ? hint : ''"
:mask="mask"
:readonly="readonly || !isEditing"
:type="type"
:label="labelString"
:rules="rules"
/>
</template>

View File

@ -0,0 +1,84 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Dark } from 'quasar';
import LanguageSwitch from 'src/modules/shared/components/language-switch.vue';
const initial_dark_mode_value = Dark.isActive;
const is_dark_mode = ref<boolean>(initial_dark_mode_value);
const toggle_dark_mode = (value: boolean) => {
is_dark_mode.value = value;
Dark.set(value);
}
</script>
<template>
<q-form class="q-pa-md column fit">
<div class="col-auto text-uppercase rounded-5" style="line-height: 1em;">{{ $t('profile.preferences.display_options') }}</div>
<q-card
flat
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
style="border: solid #AAA 1px;"
>
<div class="row col">
<q-card flat class="col column q-pa-xs bg-white" style="border: solid 1px var(--q-primary);">
<div
class="col-auto column rounded-4 ellipsis"
style="height: 90px; min-width: 80px; background-color: #DAE0E7;"
>
<div class="bg-primary col-1"></div>
<div class=" row col">
<div class="col-8 q-ma-xs rounded-borders" style="background-color: white;"></div>
<div class="col column q-gutter-xs q-py-xs q-pr-xs">
<div class="col rounded-borders" style="background-color: white;"></div>
<div class="col rounded-borders" style="background-color: white;"></div>
<div class="col rounded-borders" style="background-color: white;"></div>
</div>
</div>
<div class="bg-primary col-1"></div>
</div>
<span class="col-auto text-subtitle2 text-primary text-center text-uppercase">{{$t('profile.preferences.light_mode')}}</span>
</q-card>
<q-toggle
v-model="is_dark_mode"
@update:model-value="value => toggle_dark_mode(value)"
size="xl"
class="col-auto"
checked-icon="dark_mode"
unchecked-icon="light_mode"
/>
<q-card flat class="col column q-pa-xs bg-white" style="border: solid 1px var(--q-primary);">
<div
class="col-auto column rounded-4 ellipsis"
style="height: 90px; min-width: 80px; background-color: #0f1114;"
>
<div class="bg-primary col-1"></div>
<div class=" row col">
<div class="col-8 q-ma-xs rounded-borders" style="background-color: #333;"></div>
<div class="col column q-gutter-xs q-py-xs q-pr-xs">
<div class="col rounded-borders" style="background-color: #333;"></div>
<div class="col rounded-borders" style="background-color: #333;"></div>
<div class="col rounded-borders" style="background-color: #333;"></div>
</div>
</div>
<div class="bg-primary col-1"></div>
</div>
<span class="col-auto text-subtitle2 text-primary text-center text-uppercase">{{$t('profile.preferences.dark_mode')}}</span>
</q-card>
</div>
</q-card>
<div class="col-auto text-uppercase rounded-5" style="line-height: 1em;">{{ $t('profile.preferences.language_options') }}</div>
<q-card
flat
class="col-auto column justify-center items-center content-center q-mb-lg q-pa-md"
style="border: solid #AAA 1px;"
>
<LanguageSwitch class="q-mr-xs col-auto" />
</q-card>
</q-form>
</template>

View File

@ -0,0 +1,31 @@
<script setup lang="ts">
const model = defineModel<string>();
const { readonly = false, localizeOptions = false } = defineProps<{
options: { label: string, value: string }[];
labelString: string;
isEditing: boolean;
readonly?: boolean;
localizeOptions?: boolean;
type?: "number" | "textarea" | "time" | "text" | "tel" | "password" | "email" | "search" | "file" | "url" | "date" | "datetime-local" | undefined;
}>();
</script>
<template>
<q-select
v-model="model"
dense
:stack-label="!isEditing"
filled
label-color="primary"
class="q-ma-xs text-h6 text-uppercase"
popup-content-class="text-weight-medium text-h6"
input-class="text-weight-medium"
:options="options"
:readonly="readonly || !isEditing"
:hide-dropdown-icon="!isEditing"
:label="labelString"
:option-label="opt => localizeOptions ? $t(opt) : opt"
hint=''
/>
</template>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref } from 'vue';
import ProfileHeader from 'src/modules/profile/components/shared/profile-header.vue';
const { firstName, lastName, initialMenu } = defineProps<{
firstName: string;
lastName: string;
initialMenu: string;
}>();
const current_menu = ref<string>(initialMenu);
</script>
<template>
<div
:class="$q.screen.lt.md ? 'column no-wrap' : 'row'"
:style="$q.screen.lt.md ? 'width: 90vw;' : 'width: 40vw;'"
>
<ProfileHeader
:user-first-name="firstName"
:user-last-name="lastName"
/>
<div
class="col-3 no-wrap"
:class="$q.screen.lt.md ? '' : 'column'"
>
<q-card
class="col-auto q-pa-xs"
:class="$q.screen.lt.md ? 'q-mb-sm' : 'q-mr-sm'"
>
<q-tabs
v-model="current_menu"
:vertical="$q.screen.gt.sm"
dense
active-color="primary"
indicator-color="primary"
>
<slot name="tabs"></slot>
</q-tabs>
</q-card>
<div class="col"></div>
</div>
<q-card
class="col"
:class="$q.screen.lt.md ? '' : 'q-ml-sm'"
>
<q-tab-panels
v-model="current_menu"
animated
vertical
transition-prev="jump-up"
transition-next="jump-up"
class="rounded-5"
style="height: 50vh;"
>
<slot name="panels"></slot>
</q-tab-panels>
</q-card>
</div>
</template>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import PanelInfoPersonal from 'src/modules/profile/components/employee/profile-panel-info-personal.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 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';
const PanelNames = {
PERSONAL_INFO: 'personal_info',
EMPLOYEE_INFO: 'employee_info',
PREFERENCES: 'references',
};
const { employeeProfile = default_employee_profile } = defineProps<{
employeeProfile?: EmployeeProfile | undefined;
}>();
</script>
<template>
<q-card flat class="rounded-5 bg-transparent q-pa-none">
<ProfileTabMenuTemplate
:first-name="employeeProfile.first_name"
:last-name="employeeProfile.last_name"
:initial-menu="PanelNames.PERSONAL_INFO"
>
<template #tabs>
<q-tab
:name='PanelNames.PERSONAL_INFO'
icon='person_outline'
:label="$q.screen.lt.md ? '' : $t('profile.personal.tab_title')"
/>
<q-tab
:name="PanelNames.EMPLOYEE_INFO"
icon="work_outline"
:label="$q.screen.lt.md ? '' : $t('profile.employee.tab_title')"
/>
<q-tab
:name="PanelNames.PREFERENCES"
icon="display_settings"
:label="$q.screen.lt.md ? '' : $t('profile.preferences.tab_title')"
/>
</template>
<template #panels>
<q-tab-panel :name="PanelNames.PERSONAL_INFO" class="q-pa-none">
<PanelInfoPersonal :employee-profile="employeeProfile" />
</q-tab-panel>
<q-tab-panel :name="PanelNames.EMPLOYEE_INFO" class="q-pa-none">
<PanelInfoEmployee :employee-profile="employeeProfile" />
</q-tab-panel>
<q-tab-panel :name="PanelNames.PREFERENCES" class="q-pa-none">
<PanelPreferences />
</q-tab-panel>
</template>
</ProfileTabMenuTemplate>
</q-card>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import ProfileEmployee from 'src/modules/profile/pages/employee/profile-employee.vue';
import { useAuthStore } from 'src/stores/auth-store';
import { type EmployeeProfile } from 'src/modules/employee-list/types/employee-profile-interface';
const auth_store = useAuthStore();
const employee_roles = [ 'SUPERVISOR', 'EMPLOYEE', 'ADMIN', 'HR', 'ACCOUNTING' ];
const { employeeProfile } = defineProps<{
employeeProfile?: EmployeeProfile | undefined;
}>();
</script>
<template>
<q-page class="bg-secondary column items-center justify-center">
<ProfileEmployee
v-if="employee_roles.includes( auth_store.user.role.toUpperCase() )"
class="col-auto"
:employee-profile="employeeProfile"
/>
</q-page>
</template>

View File

@ -9,26 +9,28 @@
</script> </script>
<template> <template>
<q-btn-dropdown flat :label="$t('shared.languageLabel')" class="rounded-borders" icon="language"> <div>
<q-list dense class="row">
<q-item v-for="option in localeOptions"
:key="option.value"
tag="label"
v-ripple
>
<q-item-section avatar>
<q-radio v-model="locale" :val="option.value" />
</q-item-section>
<q-item-section>
<q-item-label>{{ option.label }}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<!-- <q-btn-dropdown push color="primary" :label="$t('shared.languageLabel')" icon="language">
<q-list> <q-list>
<q-item clickable v-close-popup v-for="option in localeOptions" :key="option.value" @click="locale = option.value"> <q-item clickable v-close-popup v-for="option in localeOptions" :key="option.value" @click="locale = option.value">
<q-item-section>{{ option.label }}</q-item-section> <q-item-section>{{ option.label }}</q-item-section>
</q-item> </q-item>
</q-list> </q-list>
</q-btn-dropdown> </q-btn-dropdown> -->
<!-- <q-select
v-model="locale"
:options="localeOptions"
dense
borderless
emit-value
map-options
hide-dropdown-icon
class="text-white"
>
<template v-slot:prepend>
<q-icon name="language" color="white"/>
</template>
</q-select> -->
</template> </template>

View File

@ -1,12 +1,7 @@
<script setup lang="ts">
import LanguageSwitch from '../language-switch.vue';
</script>
<template> <template>
<q-footer elevated class="bg-primary text-white"> <q-footer elevated class="bg-primary text-white">
<q-toolbar> <q-toolbar>
<q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title> <q-toolbar-title>© 2025 Targo Communications inc.</q-toolbar-title>
<LanguageSwitch class="q-mr-xs text-white" />
</q-toolbar> </q-toolbar>
</q-footer> </q-footer>
</template> </template>

View File

@ -36,7 +36,7 @@
<q-icon name="home" color="primary" /> <q-icon name="home" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuHome') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.home') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -47,7 +47,7 @@
<q-icon name="event_available" color="primary" /> <q-icon name="event_available" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuShiftValidation') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet_approvals') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -58,7 +58,7 @@
<q-icon name="view_list" color="primary" /> <q-icon name="view_list" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuEmployeeList') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.employee_list') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -69,7 +69,7 @@
<q-icon name="punch_clock" color="primary" /> <q-icon name="punch_clock" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuTimesheetTemp') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.timesheet') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -79,7 +79,7 @@
<q-icon name="account_box" color="primary" /> <q-icon name="account_box" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuProfile') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.profile') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -89,7 +89,7 @@
<q-icon name="contact_support" color="primary" /> <q-icon name="contact_support" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuHelp') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.help') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-list> </q-list>
@ -100,7 +100,7 @@
<q-icon name="exit_to_app" color="primary" /> <q-icon name="exit_to_app" color="primary" />
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-item-label class="text-uppercase text-grey-8 text-weight-bold">{{ $t('navBar.userMenuLogout') }}</q-item-label> <q-item-label class="text-uppercase text-weight-bold">{{ $t('nav_bar.logout') }}</q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
</q-scroll-area> </q-scroll-area>

View File

@ -25,7 +25,7 @@ const emit = defineEmits<{
dense dense
rounded rounded
debounce="300" debounce="300"
:label="$t('shared.searchBar')" :label="$t('shared.label.search')"
label-color="primary" label-color="primary"
bg-color="white" bg-color="white"
color="primary" color="primary"

View File

@ -3,15 +3,17 @@
import { colors } from 'quasar'; import { colors } from 'quasar';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar();
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale); ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale);
ChartJS.defaults.font.family = '"Roboto", sans-serif'; ChartJS.defaults.font.family = '"Roboto", sans-serif';
ChartJS.defaults.maintainAspectRatio = false; ChartJS.defaults.maintainAspectRatio = false;
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
const props = defineProps<{ const props = defineProps<{
rawData: PayPeriodEmployeeDetails | undefined; rawData: PayPeriodEmployeeDetails | undefined;
@ -29,22 +31,22 @@
const datasetConfig = [ const datasetConfig = [
{ {
key: 'regular_hours', key: 'regular_hours',
label: t('timeSheetValidations.hoursWorkedRegular'), label: t('shared.shift_type.regular'),
color: colors.getPaletteColor('green-5'), color: colors.getPaletteColor('green-5'),
}, },
{ {
key: 'evening_hours', key: 'evening_hours',
label: t('timeSheetValidations.hoursWorkedEvening'), label: t('shared.shift_type.evening'),
color: colors.getPaletteColor('green-9'), color: colors.getPaletteColor('green-9'),
}, },
{ {
key: 'emergency_hours', key: 'emergency_hours',
label: t('timeSheetValidations.hoursWorkedEmergency'), label: t('shared.shift_type.emergency'),
color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(), color: getComputedStyle(document.body).getPropertyValue('--q-warning').trim(),
}, },
{ {
key: 'overtime_hours', key: 'overtime_hours',
label: t('timeSheetValidations.hoursWorkedOvertime'), label: t('shared.shift_type.overtime'),
color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(), color: getComputedStyle(document.body).getPropertyValue('--q-negative').trim(),
}, },
] as const; ] as const;
@ -67,33 +69,34 @@
</script> </script>
<template> <template>
<Bar <div>
:data="getHoursWorkedData()" <Bar
:options="({ :data="getHoursWorkedData()"
indexAxis: $q.screen.lt.md? 'y' : 'x', :options="({
plugins: { indexAxis: $q.screen.lt.md? 'y' : 'x',
legend: { plugins: {
labels: { legend: {
boxWidth: 15, labels: {
boxWidth: 15,
},
}, },
title: {
display: true,
text: t('timesheet_approvals.chart.hours_worked_title'),
}
}, },
title: { scales: {
display: true, x: {
text: t('timeSheetValidations.hoursWorkedChartTitle'), stacked: true,
color: '#616161' },
} y: {
}, stacked: true,
scales: { suggestedMin: 0,
x: { suggestedMax: 10,
stacked: true,
},
y: {
stacked: true,
suggestedMin: 0,
suggestedMax: 10,
}
} }
} })"
})" />
/> </div>
</template> </template>

View File

@ -3,15 +3,18 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { colors } from 'quasar'; import { colors } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { Doughnut } from 'vue-chartjs'; import { Doughnut } from 'vue-chartjs';
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js'; import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale, type ChartDataset } from 'chart.js';
import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface'; import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar();
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale); ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale, LinearScale);
ChartJS.defaults.font.family = '"Roboto", sans-serif'; ChartJS.defaults.font.family = '"Roboto", sans-serif';
ChartJS.defaults.maintainAspectRatio = false; ChartJS.defaults.maintainAspectRatio = false;
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
const props = defineProps<{ const props = defineProps<{
rawData: PayPeriodOverviewEmployee | undefined; rawData: PayPeriodOverviewEmployee | undefined;
@ -52,16 +55,18 @@
</script> </script>
<template> <template>
<Doughnut <div>
:data="data" <Doughnut
:options="({ :data="data"
plugins:{ :options="({
legend:{ plugins:{
labels:{ legend:{
boxWidth: 15, labels:{
boxWidth: 15,
}
} }
} }
} })"
})" />
/> </div>
</template> </template>

View File

@ -1,17 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
/*eslint-disable*/
import { ref } from 'vue'; import { ref } from 'vue';
import { Bar } from 'vue-chartjs'; import { Bar } from 'vue-chartjs';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartData, type ChartOptions, type Plugin, type ChartDataset } from 'chart.js'; import { useQuasar } from 'quasar';
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, type ChartData, type ChartDataset } from 'chart.js';
import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
import type { Expense } from 'src/modules/timesheets/types/timesheet-details-interface'; import type { Expense } from 'src/modules/timesheets/types/timesheet-details-interface';
const { t } = useI18n(); const { t } = useI18n();
const $q = useQuasar();
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale); ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale);
ChartJS.defaults.font.family = '"Roboto", sans-serif'; ChartJS.defaults.font.family = '"Roboto", sans-serif';
ChartJS.defaults.maintainAspectRatio = false; ChartJS.defaults.maintainAspectRatio = false;
ChartJS.defaults.color = $q.dark.isActive ? '#F5F5F5' : '#616161';
const props = defineProps<{ const props = defineProps<{
rawData: PayPeriodEmployeeDetails | undefined; rawData: PayPeriodEmployeeDetails | undefined;
@ -32,12 +34,12 @@
expenses_dataset.value = [ expenses_dataset.value = [
{ {
label: t('timesheet.refund'), label: t('timesheet_approvals.table.expenses'),
data: all_costs, data: all_costs,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(), backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-primary').trim(),
}, },
{ {
label: t('timesheet.mileage'), label: t('timesheet_approvals.table.mileage'),
data: all_mileage, data: all_mileage,
backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(), backgroundColor: getComputedStyle(document.body).getPropertyValue('--q-info').trim(),
} }
@ -64,33 +66,34 @@
</script> </script>
<template> <template>
<Bar <div>
:data="getExpensesData()" <Bar
:options="({ :data="getExpensesData()"
indexAxis: $q.screen.lt.md? 'y' : 'x', :options="({
plugins: { indexAxis: $q.screen.lt.md? 'y' : 'x',
title: { plugins: {
display: true,
text: t('timeSheetValidations.reportFilterExpenses'), title: {
color: '#616161' display: true,
text: t('timesheet_approvals.chart.expenses_title'),
},
legend:{
labels:{
boxWidth: 15,
}
}
}, },
legend:{ scales: {
labels:{ x: {
boxWidth: 15, stacked: true
},
y: {
suggestedMin: 0,
suggestedMax: 100,
stacked: true
} }
} }
}, })"
scales: { />
x: { </div>
stacked: true
},
y: {
suggestedMin: 0,
suggestedMax: 100,
stacked: true
}
}
})"
:style="$q.screen.lt.md ? 'min-height: 300px;': '' "
/>
</template> </template>

View File

@ -9,7 +9,7 @@
<!-- punch-in timestamps --> <!-- punch-in timestamps -->
<q-card-section class="col q-pa-none"> <q-card-section class="col q-pa-none">
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;"> <q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
{{ $t('shiftColumns.labelIn') }} {{ $t('shared.misc.in') }}
</q-item-label> </q-item-label>
</q-card-section> </q-card-section>
@ -20,7 +20,7 @@
<!-- punch-out timestamps --> <!-- punch-out timestamps -->
<q-card-section class="col q-pa-none"> <q-card-section class="col q-pa-none">
<q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;"> <q-item-label class="text-weight-bolder text-primary" style="font-size: 0.7em;">
{{ $t('shiftColumns.labelOut') }} {{ $t('shared.misc.out') }}
</q-item-label> </q-item-label>
</q-card-section> </q-card-section>

View File

@ -1,10 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface'; import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
import { ref } from 'vue';
const props = defineProps<{ const props = defineProps<{
shift: Shift; shift: Shift;
}>(); }>();
const is_showing_time_popup = ref<boolean>(false);
const getShiftColor = (type: string): string => { const getShiftColor = (type: string): string => {
switch(type) { switch(type) {
case 'REGULAR': return 'secondary'; case 'REGULAR': return 'secondary';
@ -17,14 +20,6 @@
default : return 'transparent'; default : return 'transparent';
} }
}; };
const getTextColor = (type: string): string => {
switch(type) {
case 'REGULAR': return 'grey-8';
case '': return 'transparent';
default: return 'white';
}
}
</script> </script>
<template> <template>
@ -32,12 +27,13 @@
horizontal horizontal
class="q-pa-none text-uppercase text-center items-center cursor-pointer rounded-10" class="q-pa-none text-uppercase text-center items-center cursor-pointer rounded-10"
style="line-height: 1;" style="line-height: 1;"
@click="is_showing_time_popup = true"
> >
<!-- punch-in timestamps --> <!-- punch-in timestamps -->
<q-card-section class="q-pa-none col"> <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-' + getShiftColor(props.shift.type) + ' text-' + getTextColor(props.shift.type)" :class="'bg-' + getShiftColor(props.shift.type) + (!$q.dark.isActive && props.shift.type === 'REGULAR' ? '' : ' text-white')"
style="font-size: 1.5em; line-height: 80% !important;" style="font-size: 1.5em; line-height: 80% !important;"
> >
{{ props.shift.start_time }} {{ props.shift.start_time }}
@ -68,8 +64,8 @@
<!-- punch-out timestamps --> <!-- punch-out timestamps -->
<q-card-section class="q-pa-none col"> <q-card-section class="q-pa-none col">
<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-' + getShiftColor(props.shift.type) + ' text-' + getTextColor(props.shift.type)" :class="'bg-' + getShiftColor(props.shift.type) + (!$q.dark.isActive && props.shift.type === 'REGULAR' ? '' : ' text-white')"
style="font-size: 1.5em; line-height: 80% !important;" style="font-size: 1.5em; line-height: 80% !important;"
> >
{{ props.shift.end_time }} {{ props.shift.end_time }}
@ -85,7 +81,6 @@
v-if="props.shift.type !== ''" v-if="props.shift.type !== ''"
flat flat
dense dense
color='grey-8'
icon="chat_bubble_outline" icon="chat_bubble_outline"
class="q-pa-none" class="q-pa-none"
/> />
@ -95,7 +90,6 @@
v-if="props.shift.type !== ''" v-if="props.shift.type !== ''"
flat flat
dense dense
color='grey-8'
icon="attach_money" icon="attach_money"
class="q-pa-none q-mx-xs" class="q-pa-none q-mx-xs"
/> />

View File

@ -7,27 +7,16 @@
value: unknown; value: unknown;
}; };
type CardButton = { const { cols, row, initialState } = defineProps<{
icon: string;
label: string;
onClick: () => void;
};
const props = defineProps<{
cols: TableColumn[]; cols: TableColumn[];
row: PayPeriodOverviewEmployee; row: PayPeriodOverviewEmployee;
initialState: boolean; initialState: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
clickDetails: [email: string]; clickDetails: [ email: string ];
updateApproval: [ value: boolean ]; updateApproval: [ value: boolean ];
}>(); }>();
const card_buttons: CardButton[] = [
{ icon: 'work_history', label: 'timeSheetValidations.tooltipTimeline', onClick: () => emit('clickDetails', props.row.email) },
{ icon: 'open_in_new', label: 'timeSheetValidations.tooltipTimesheet', onClick: () => emit('clickDetails', props.row.email) }
];
</script> </script>
<template> <template>
@ -39,7 +28,7 @@
class="q-py-none q-pl-md relative" class="q-py-none q-pl-md relative"
> >
<div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden"> <div class="text-primary text-h5 text-weight-bolder q-pt-xs overflow-hidden">
{{ props.row.employee_name }} {{ row.employee_name }}
</div> </div>
<q-space /> <q-space />
@ -47,30 +36,25 @@
<!-- Buttons to view detailed shifts or view employee timesheet --> <!-- Buttons to view detailed shifts or view employee timesheet -->
<q-btn <q-btn
flat flat
unelevated
square
dense dense
v-for="(button, index) in card_buttons" square
:key="index" unelevated
class="q-py-none q-my-xs" class="q-py-none q-my-xs"
color="primary" color="primary"
:icon="button.icon" icon="work_history"
@click="button.onClick" @click="emit('clickDetails', row.email)"
> >
<q-tooltip <q-tooltip
anchor="top middle" anchor="top middle"
self="center middle" self="center middle"
class="bg-primary text-uppercase text-weight-bold" class="bg-primary text-uppercase text-weight-bold"
> >
{{$t(button.label)}} {{ $t('timesheet_approvals.tooltip.button_detailed_view') }}
</q-tooltip> </q-tooltip>
</q-btn> </q-btn>
</q-card-section> </q-card-section>
<q-separator <q-separator size="2px" />
color="accent"
style="height: 2px;"
/>
<!-- Main body of pay period card --> <!-- Main body of pay period card -->
<q-card-section class="q-pa-none q-mt-xs q-mb-sm"> <q-card-section class="q-pa-none q-mt-xs q-mb-sm">
@ -87,24 +71,21 @@
:class="$q.screen.lt.md ? 'col' : 'col-8'" :class="$q.screen.lt.md ? 'col' : 'col-8'"
> >
<q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption"> <q-item-label class="text-weight-bold text-primary q-pa-none text-uppercase text-caption">
{{ props.cols.find(c => c.name === 'regular_hours')?.label }} {{ cols.find(c => c.name === 'regular_hours')?.label }}
</q-item-label> </q-item-label>
<q-item-label class="text-weight-bolder text-h3 text-grey-8 q-py-none"> <q-item-label class="text-weight-bolder text-h3 q-py-none">
{{ props.cols.find(c => c.name === 'regular_hours')?.value }} {{ cols.find(c => c.name === 'regular_hours')?.value }}
</q-item-label> </q-item-label>
</q-item> </q-item>
<q-separator <q-separator class="q-mx-sm" />
color="accent"
class="q-mx-sm"
/>
<!-- Other hour types segment --> <!-- Other hour types segment -->
<div :class="$q.screen.lt.md ? 'column' : 'row no-wrap'"> <div :class="$q.screen.lt.md ? 'column' : 'row no-wrap'">
<q-item <q-item
dense dense
class="column ellipsis " class="column ellipsis "
v-for="col in props.cols.slice(3, 6)" v-for="col in cols.slice(3, 6)"
:key="col.label" :key="col.label"
> >
<q-item-label <q-item-label
@ -113,7 +94,7 @@
> >
{{ col.label }} {{ col.label }}
</q-item-label> </q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 text-grey-8"> <q-item-label class="text-weight-bolder q-pa-none text-h6 ">
{{ col.value }} {{ col.value }}
</q-item-label> </q-item-label>
</q-item> </q-item>
@ -122,7 +103,6 @@
<q-separator <q-separator
vertical vertical
color="accent"
class="q-mt-xs q-mb-none" class="q-mt-xs q-mb-none"
/> />
@ -131,7 +111,7 @@
<q-item <q-item
dense dense
class="column" class="column"
v-for="col in props.cols.slice(6, )" v-for="col in cols.slice(6, )"
:key="col.label" :key="col.label"
> >
<q-item-label <q-item-label
@ -140,7 +120,7 @@
> >
{{ col.label }} {{ col.label }}
</q-item-label> </q-item-label>
<q-item-label class="text-weight-bolder q-pa-none text-h6 text-grey-8"> <q-item-label class="text-weight-bolder q-pa-none text-h6 ">
{{ col.value }} {{ col.value }}
</q-item-label> </q-item-label>
</q-item> </q-item>
@ -157,8 +137,10 @@
<q-card-section <q-card-section
horizontal horizontal
class="q-pa-sm text-weight-bold" class="q-pa-sm text-weight-bold"
:class="props.initialState ? 'text-white bg-primary' : 'text-primary bg-white'" :class="initialState ? 'text-white bg-primary' : 'bg-dark'"
> >
<q-item-label class="text-uppercase text-h6 q-ml-sm text-weight-bolder"> {{ row.total_hours + ' h' }} </q-item-label>
<q-item-label class="text-uppercase text-weight-bold q-ml-xs"> total </q-item-label>
<q-space /> <q-space />
<q-checkbox <q-checkbox
dense dense
@ -166,10 +148,10 @@
size="lg" size="lg"
checked-icon="lock" checked-icon="lock"
unchecked-icon="lock_open" unchecked-icon="lock_open"
:color="props.initialState ? 'white' : 'primary'" keep-color :color="initialState ? 'white' : 'primary'" keep-color
:model-value="props.initialState" :model-value="initialState"
@update:model-value="val => emit('updateApproval', val)" @update:model-value="val => emit('updateApproval', val)"
:label="props.initialState ? $t('timeSheetValidations.timeSheetStatusVerified') : $t('timeSheetValidations.timeSheetStatusUnverified')" :label="initialState ? $t('timesheet_approvals.table.verified') : $t('timesheet_approvals.table.unverified')"
class="text-uppercase" class="text-uppercase"
/> />
</q-card-section> </q-card-section>

View File

@ -26,65 +26,68 @@
const columns = computed((): QTableColumn<PayPeriodOverviewEmployee>[] => [ const columns = computed((): QTableColumn<PayPeriodOverviewEmployee>[] => [
{ {
name: 'employee_name', name: 'employee_name',
label: t('timeSheetValidations.tableColumnLabelFullname'), label: t('timesheet_approvals.table.full_name'),
field: 'employee_name', field: 'employee_name',
sortable: true sortable: true
}, },
{ {
name: 'email', name: 'email',
label: t('timeSheetValidations.tableColumnLabelEmail'), label: t('timesheet_approvals.table.email'),
field: 'email', field: 'email',
sortable: true, sortable: true,
}, },
{ {
name: 'regular_hours', name: 'regular_hours',
label: t('timeSheetValidations.tableColumnLabelRegularHours'), label: t('shared.shift_type.regular'),
field: 'regular_hours', field: 'regular_hours',
sortable: true sortable: true
}, },
{ {
name: 'evening_hours', name: 'evening_hours',
label: t('timeSheetValidations.tableColumnLabelEveningHours'), label: t('shared.shift_type.evening'),
field: 'evening_hours' field: 'evening_hours'
}, },
{ {
name: 'emergency_hours', name: 'emergency_hours',
label: t('timeSheetValidations.tableColumnLabelEmergencyHours'), label: t('shared.shift_type.emergency'),
field: 'emergency_hours' field: 'emergency_hours'
}, },
{ {
name: 'overtime_hours', name: 'overtime_hours',
label: t('timeSheetValidations.tableColumnLabelOvertime'), label: t('shared.shift_type.overtime'),
field: 'overtime_hours' field: 'overtime_hours'
}, },
{ {
name: 'expenses', name: 'expenses',
label: t('timeSheetValidations.tableColumnLabelExpenses'), label: t('timesheet_approvals.table.expenses'),
field: 'expenses', field: 'expenses',
sortable: true sortable: true
}, },
{ {
name: 'mileage', name: 'mileage',
label: t('timeSheetValidations.tableColumnLabelMileage'), label: t('timesheet_approvals.table.mileage'),
field: 'mileage', field: 'mileage',
sortable: true sortable: true
} }
]); ]);
const has_changes = computed(() => { // const has_changes = computed(() => {
return timesheet_store.pay_period_overview_employees.some(emp => { // return timesheet_store.pay_period_overview_employees.some(emp => {
return emp.is_approved !== original_approvals.value[emp.email]; // return emp.is_approved !== original_approvals.value[emp.email];
}); // });
}); // });
const is_not_enough_filters = computed(() => { const is_not_enough_filters = computed(() => {
return report_filter_company.value.filter(val => val === true).length < 1 || return report_filter_company.value.filter(val => val === true).length < 1 ||
report_filter_type.value.filter(val => val === true).length < 1; report_filter_type.value.filter(val => val === true).length < 1;
}) });
const filter_types_labels = [ const filter_types_labels = [
t('timeSheetValidations.reportFilterShifts'), t('timesheet_approvals.print_report.shifts'),
t('timeSheetValidations.reportFilterExpenses'), t('timesheet_approvals.print_report.expenses'),
t('timeSheetValidations.reportFilterHoliday'), t('shared.shift_type.holiday'),
t('timeSheetValidations.reportFilterVacation'), t('shared.shift_type.vacation'),
] ];
const is_calendar_limit = computed( () => { const is_calendar_limit = computed( () => {
return timesheet_store.current_pay_period.pay_year === 2024 && return timesheet_store.current_pay_period.pay_year === 2024 &&
timesheet_store.current_pay_period.pay_period_no <= 1; timesheet_store.current_pay_period.pay_period_no <= 1;
@ -152,6 +155,7 @@
:update-key="update_key" :update-key="update_key"
/> />
</q-dialog> </q-dialog>
<div class="q-pa-md"> <div class="q-pa-md">
<q-table <q-table
:rows="timesheet_store.pay_period_overview_employees" :rows="timesheet_store.pay_period_overview_employees"
@ -165,12 +169,12 @@
: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"
:no-data-label="$t('shared.failedToLoad')" :no-data-label="$t('shared.error.no_data_found')"
:no-results-label="$t('shared.failedToSearch')" :no-results-label="$t('shared.error.no_search_results')"
:loading-label="$t('shared.loading')" :loading-label="$t('shared.label.loading')"
> >
<!-- Top Bar that contains Search, Title, Filters --> <!-- Top Bar that contains Date Picker, Search, Filters, Print Report, etc -->
<template v-slot:top> <template #top>
<div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'"> <div class="full-width" :class="$q.screen.lt.md ? 'text-center q-gutter-sm' : 'row'">
<!-- Date Picker --> <!-- Date Picker -->
<TimesheetApprovalPeriodPicker <TimesheetApprovalPeriodPicker
@ -202,7 +206,7 @@
<q-list> <q-list>
<q-item> <q-item>
<q-item-section row no-wrap> <q-item-section row no-wrap>
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timeSheetValidations.reportFilterCategoryCompany')}}</p> <p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timesheet_approvals.print_report.company')}}</p>
<q-checkbox <q-checkbox
v-for="label, index in ['Targo', 'Solucom']" v-for="label, index in ['Targo', 'Solucom']"
v-model="report_filter_company[index]" v-model="report_filter_company[index]"
@ -215,7 +219,7 @@
<q-separator color="primary" class="q-mx-md"/> <q-separator color="primary" class="q-mx-md"/>
<q-item> <q-item>
<q-item-section row no-wrap> <q-item-section row no-wrap>
<p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timeSheetValidations.reportFilterCategoryType')}}</p> <p class="text-weight-bolder text-primary q-ma-none q-pa-none text-uppercase">{{$t('timesheet_approvals.print_report.type')}}</p>
<q-checkbox <q-checkbox
v-for="label, index in filter_types_labels" v-for="label, index in filter_types_labels"
v-model="report_filter_type[index]" v-model="report_filter_type[index]"
@ -232,7 +236,7 @@
</template> </template>
<!-- Template for individual employee cards --> <!-- Template for individual employee cards -->
<template v-slot:item="props: { <template #item="props: {
cols: (QTableColumn<PayPeriodOverviewEmployee> & { value: unknown })[], cols: (QTableColumn<PayPeriodOverviewEmployee> & { value: unknown })[],
row: PayPeriodOverviewEmployee, row: PayPeriodOverviewEmployee,
key: string, key: string,
@ -247,7 +251,7 @@
</template> </template>
<!-- Template for custome failed-to-load state --> <!-- Template for custome failed-to-load state -->
<template v-slot:no-data="{ message, filter }"> <template #no-data="{ message, filter }">
<div class="full-width column items-center text-primary q-gutter-sm"> <div class="full-width column items-center text-primary q-gutter-sm">
<span class="text-h6 q-mt-xl"> <span class="text-h6 q-mt-xl">
{{ message }} {{ message }}

View File

@ -5,7 +5,7 @@
import type { QDateDetails } from 'src/modules/shared/types/q-date-details'; import type { QDateDetails } from 'src/modules/shared/types/q-date-details';
const is_showing_calendar_picker = ref(false); const is_showing_calendar_picker = ref(false);
const calendar_date = ref(date.formatDate( Date.now(), 'YYYY/MM/DD' )); const calendar_date = ref(date.formatDate( Date.now(), 'YYYY-MM-DD' ));
const props = defineProps<{ const props = defineProps<{
isDisabled: boolean, isDisabled: boolean,
@ -60,7 +60,7 @@
class="q-mt-xl" class="q-mt-xl"
today-btn today-btn
mask="YYYY-MM-DD" mask="YYYY-MM-DD"
:options="date => date > '2023-12-16'" :options="date => date > '2023/12/16'"
@update:model-value="onDateSelected" @update:model-value="onDateSelected"
/> />
</q-dialog> </q-dialog>

View File

@ -8,8 +8,7 @@
import TimesheetApprovalEmployeeExpensesChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-expenses-chart.vue'; import TimesheetApprovalEmployeeExpensesChart from 'src/modules/timesheet-approval/components/graphs/timesheet-approval-employee-expenses-chart.vue';
import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface'; import type { PayPeriodOverviewEmployee } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-employee-interface';
import type { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface'; import type { PayPeriodEmployeeDetails } from '../types/timesheet-approval-pay-period-employee-details-interface';
import { PayPeriod } from 'src/modules/shared/types/pay-period-interface'; import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
import { colors } from 'quasar';
const props = defineProps<{ const props = defineProps<{
isLoading: boolean; isLoading: boolean;
@ -23,15 +22,6 @@ import { colors } from 'quasar';
const { t } = useI18n(); const { t } = useI18n();
const is_showing_graph = ref<boolean>(true); const is_showing_graph = ref<boolean>(true);
// case 'REGULAR': return 'green-5';
// case 'EVENING': return 'green-9';
// case 'EMERGENCY': return 'warning';
// case 'OVERTIME': return 'negative';
// case 'VACATION': return 'purple-10';
// case 'HOLIDAY': return 'purple-10';
// case 'SICK': return 'grey-9';
// default : return 'transparent';
type shiftColor = { type shiftColor = {
type: string; type: string;
color: string; color: string;
@ -40,32 +30,32 @@ import { colors } from 'quasar';
const shift_type_legend: shiftColor[] = [ const shift_type_legend: shiftColor[] = [
{ {
type: t('timesheet.shift_types.REGULAR'), type: t('shared.shift_type.regular'),
color: 'secondary', color: 'secondary',
text_color: 'grey-8', text_color: '',
}, },
{ {
type: t('timesheet.shift_types.EVENING'), type: t('shared.shift_type.evening'),
color: 'warning', color: 'warning',
}, },
{ {
type: t('timesheet.shift_types.EMERGENCY'), type: t('shared.shift_type.emergency'),
color: 'amber-10', color: 'amber-10',
}, },
{ {
type: t('timeSheetValidations.hoursWorkedOvertime'), type: t('shared.shift_type.overtime'),
color: 'negative', color: 'negative',
}, },
{ {
type: t('timesheet.shift_types.VACATION'), type: t('shared.shift_type.vacation'),
color: 'purple-10', color: 'purple-10',
}, },
{ {
type: t('timesheet.shift_types.HOLIDAY'), type: t('shared.shift_type.holiday'),
color: 'purple-8', color: 'purple-8',
}, },
{ {
type: t('timesheet.shift_types.SICK'), type: t('shared.shift_type.sick'),
color: 'grey-8', color: 'grey-8',
}, },
] ]
@ -73,8 +63,8 @@ import { colors } from 'quasar';
<template> <template>
<q-card <q-card
class="q-pa-sm bg-white shadow-12 rounded-15 column no-wrap relative" class="q-pa-sm shadow-12 rounded-15 column no-wrap relative"
:style="$q.screen.gt.sm ? 'width: 70vw !important; height: 90vh !important;' : '' " :style="$q.screen.lt.md ? '' : 'width: 60vw !important; height: 70vh !important;' "
> >
<!-- loader --> <!-- loader -->
<q-card-section <q-card-section
@ -98,7 +88,7 @@ import { colors } from 'quasar';
> >
{{ props.employeeName }} {{ props.employeeName }}
<q-separator class="q-mb-sm" color="accent" size="2px" /> <q-separator spaced size="2px" />
<q-card-actions align="center" class="q-pa-none"> <q-card-actions align="center" class="q-pa-none">
<q-card flat class="bg-secondary rounded-5 q-pa-xs"> <q-card flat class="bg-secondary rounded-5 q-pa-xs">
<q-btn-toggle <q-btn-toggle
@ -144,33 +134,30 @@ import { colors } from 'quasar';
</q-card-section> </q-card-section>
</q-card-section> </q-card-section>
<!-- employee timesheet details, but look at these graphs --> <!-- employee timesheet details with graphs -->
<q-card-section v-if="!props.isLoading && is_showing_graph" class="q-pa-md col column full-width no-wrap"> <q-card-section v-if="!props.isLoading && is_showing_graph" class="q-pa-md col column full-width no-wrap">
<q-card-section class="q-pa-none col no-wrap" style="min-height: 300px;"> <q-card-section :horizontal="!$q.screen.lt.md" class="q-pa-none col no-wrap" style="min-height: 300px;">
<TimesheetApprovalEmployeeDetailsHoursWorkedChart <TimesheetApprovalEmployeeDetailsHoursWorkedChart
:raw-data="props.employeeDetails" :raw-data="props.employeeDetails"
class="col-7"
/> />
</q-card-section>
<q-separator class="q-ma-sm"/> <q-separator vertical spaced />
<q-card-section <div class="column col justify-center no-wrap q-pa-none">
:horizontal="$q.screen.gt.sm"
class="justify-center no-wrap col full-width q-pa-none"
>
<q-card-section class="q-pa-none q-ma-none col-4">
<TimesheetApprovalEmployeeDetailsShiftTypesChart <TimesheetApprovalEmployeeDetailsShiftTypesChart
:raw-data="props.employeeOverview" :raw-data="props.employeeOverview"
class="col-5"
/> />
</q-card-section>
<q-separator :vertical="$q.screen.gt.sm" class="q-ma-md" /> <q-separator :vertical="$q.screen.lt.md" spaced />
<q-card-section class="q-pa-none q-ma-none col" :class="$q.screen.lt.md ? 'full-width' : ''">
<TimesheetApprovalEmployeeExpensesChart <TimesheetApprovalEmployeeExpensesChart
:raw-data="props.employeeDetails" :raw-data="props.employeeDetails"
class="col"
/> />
</q-card-section>
</div>
</q-card-section> </q-card-section>
</q-card-section> </q-card-section>
</q-card> </q-card>

View File

@ -3,28 +3,20 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useTimesheetStore } from 'src/stores/timesheet-store'; import { useTimesheetStore } from 'src/stores/timesheet-store';
import { date } from 'quasar';
const { locale } = useI18n(); const { d } = useI18n();
const timesheet_store = useTimesheetStore(); const timesheet_store = useTimesheetStore();
const date_options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: "long",
year: 'numeric',
};
const pay_period_label = computed(() => { const pay_period_label = computed(() => {
const dates = timesheet_store.current_pay_period.label.split('.'); const dates = timesheet_store.current_pay_period.label.split('.');
const start_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[0] as string, 'YYYY-MM-DD'));
const end_date = new Intl.DateTimeFormat(locale.value, date_options).format(date.extractDate(dates[1] as string, 'YYYY-MM-DD'));
if ( dates.length === 1 ) { if ( dates.length < 2 ) {
return { return { start_date: '—', end_date: '—' }
start_date: '—',
end_date: '—'
}
} }
const start_date = d(new Date(dates[0] as string), { day: 'numeric', month: 'long', year: 'numeric', });
const end_date = d(new Date(dates[1] as string), { day: 'numeric', month: 'long', year: 'numeric', });
return { start_date, end_date }; return { start_date, end_date };
}); });
</script> </script>
@ -32,29 +24,20 @@
<template> <template>
<q-page <q-page
padding padding
class="q-pa-md bg-secondary" class="q-pa-md bg-secondary "
> >
<div class="text-h4 row justify-center text-center q-mt-lg text-uppercase text-weight-bolder text-grey-8"> <div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
{{ $t('pageTitles.timeSheetValidations') }} {{ $t('timesheet_approvals.page_title') }}
</div> <div class="col row items-center justify-center full-width q-py-none q-my-none">
<div class="row items-center justify-center q-py-none q-my-none"> <div class="text-primary text-weight-bold text-h6">
<div {{ pay_period_label.start_date }}
class="text-primary text-uppercase text-weight-bold" </div>
:class="$q.screen.lt.md ? '' : 'text-h6'" <div class="text-body2 q-mx-md text-weight-medium">
> {{ $t('shared.misc.to') }}
{{ pay_period_label.start_date }} </div>
</div> <div class="text-primary text-weight-bold text-h6">
<div {{ pay_period_label.end_date }}
class="text-grey-8 text-uppercase q-mx-md" </div>
:class="$q.screen.lt.md ? 'text-weight-medium text-caption' : 'text-weight-bold'"
>
{{ $t('timesheet.dateRangesTo') }}
</div>
<div
class="text-primary text-uppercase text-center text-weight-bold"
:class="$q.screen.lt.md ? '' : 'text-h6'"
>
{{ pay_period_label.end_date }}
</div> </div>
</div> </div>

View File

@ -19,13 +19,11 @@ export const timesheetApprovalService = {
getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => { getPayPeriodEmployeeOverviews: async (year: number, period_number: number, supervisor_email: string): Promise<PayPeriodOverview> => {
// TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD // TODO: REMOVE MOCK DATA PEFORE PUSHING TO PROD
const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`); const response = await api.get(`pay-periods/${year}/${period_number}/${supervisor_email}`);
console.log('pay period data: ', response.data);
return response.data; return response.data;
}, },
getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => { getTimesheetsByPayPeriodAndEmail: async (year: number, period_no: number, email: string): Promise<PayPeriodEmployeeDetails> => {
const response = await api.get('timesheets', { params: { year, period_no, email, }}); const response = await api.get('timesheets', { params: { year, period_no, email, }});
console.log('employee details: ', response.data);
return response.data; return response.data;
}, },

View File

@ -5,6 +5,7 @@ export interface PayPeriodOverviewEmployee {
evening_hours: number; evening_hours: number;
emergency_hours: number; emergency_hours: number;
overtime_hours: number; overtime_hours: number;
total_hours: number;
expenses: number; expenses: number;
mileage: number; mileage: number;
is_approved: boolean; is_approved: boolean;
@ -17,6 +18,7 @@ export const default_pay_period_overview_employee: PayPeriodOverviewEmployee = {
evening_hours: -1, evening_hours: -1,
emergency_hours: -1, emergency_hours: -1,
overtime_hours: -1, overtime_hours: -1,
total_hours: -1,
expenses: -1, expenses: -1,
mileage: -1, mileage: -1,
is_approved: false is_approved: false

View File

@ -12,6 +12,7 @@ export interface TimesheetDetailsDailySchedule {
evening_hours: number; evening_hours: number;
emergency_hours: number; emergency_hours: number;
overtime_hours: number; overtime_hours: number;
total_hours: number;
comment: string; comment: string;
short_date: string; // ex. 08/24 short_date: string; // ex. 08/24
break_duration?: number; break_duration?: number;
@ -64,6 +65,7 @@ const emptyDailySchedule = (): TimesheetDetailsDailySchedule => ({
evening_hours: 0, evening_hours: 0,
emergency_hours: 0, emergency_hours: 0,
overtime_hours: 0, overtime_hours: 0,
total_hours: 0,
comment: "", comment: "",
short_date: "", short_date: "",
break_duration: 0, break_duration: 0,

View File

@ -31,7 +31,6 @@ export default defineRouter(function (/* { store, ssrContext } */) {
const authStore = useAuthStore(); const authStore = useAuthStore();
if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) { if (destinationPage.meta.requiresAuth && !authStore.isAuthorizedUser) {
console.log("access denied!")
return { name: 'login' }; return { name: 'login' };
} }
}) })

View File

@ -26,7 +26,12 @@ const routes: RouteRecordRaw[] = [
path: 'timesheet-temp', path: 'timesheet-temp',
name: RouteNames.TIMESHEET_TEMP, name: RouteNames.TIMESHEET_TEMP,
component: () => import('src/modules/timesheets/pages/timesheet-details-overview.vue') component: () => import('src/modules/timesheets/pages/timesheet-details-overview.vue')
} },
{
path: 'user/profile',
name: RouteNames.PROFILE,
component: () => import('src/modules/profile/pages/profile-container.vue'),
},
], ],
}, },

View File

@ -1,11 +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/services-employee-list";
import type { EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface"; import { default_employee_profile, type EmployeeProfile } from "src/modules/employee-list/types/employee-profile-interface";
import type { EmployeeListTableItem } from "src/modules/employee-list/types/employee-list-table-interface"; 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>(); const employee = ref<EmployeeProfile>( default_employee_profile );
const employeeList = ref<EmployeeListTableItem[]>([]); const employeeList = ref<EmployeeListTableItem[]>([]);
const isShowingEmployeeAddModifyWindow = ref<boolean>(false); const isShowingEmployeeAddModifyWindow = ref<boolean>(false);
const isLoadingEmployeeProfile = ref(false); const isLoadingEmployeeProfile = ref(false);
@ -27,7 +27,6 @@ export const useEmployeeStore = defineStore('employee', () => {
isLoadingEmployeeProfile.value = true; isLoadingEmployeeProfile.value = true;
try { try {
const response = await EmployeeListService.getEmployeeDetails(email); const response = await EmployeeListService.getEmployeeDetails(email);
console.log("Employee details: ", response);
employee.value = response; employee.value = response;
} catch (error) { } catch (error) {

View File

@ -3,6 +3,7 @@ import { ref } from 'vue';
export const useUiStore = defineStore('ui', () => { export const useUiStore = defineStore('ui', () => {
const isRightDrawerOpen = ref(true); const isRightDrawerOpen = ref(true);
const toggleRightDrawer = () => { const toggleRightDrawer = () => {
isRightDrawerOpen.value = !isRightDrawerOpen.value; isRightDrawerOpen.value = !isRightDrawerOpen.value;
} }