Merge branch 'main' into dev/lion/chatbot
This commit is contained in:
commit
901ed27b57
|
|
@ -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: "auto",
|
||||||
},
|
},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
|
|
||||||
BIN
src/assets/profile_header_default.png
Normal file
BIN
src/assets/profile_header_default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -12,20 +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;
|
|
||||||
$white: white;
|
|
||||||
|
|
||||||
|
$dark : #333;
|
||||||
|
$dark-page : #343434;
|
||||||
|
|
||||||
|
$positive : #21ba45;
|
||||||
|
$negative : #e6364b;
|
||||||
|
$info : #6bb9e7;
|
||||||
|
$warning : #e4a944;
|
||||||
|
$white : white;
|
||||||
|
|
|
||||||
|
|
@ -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 we’ll 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: 'Don’t 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: {
|
||||||
|
|
@ -301,141 +164,60 @@ export default {
|
||||||
amount:'Amount',
|
amount:'Amount',
|
||||||
date:'Date',
|
date:'Date',
|
||||||
empty_list:'No registered expenses',
|
empty_list:'No registered expenses',
|
||||||
|
employee_comment:'Comment',
|
||||||
|
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",
|
||||||
|
attach_file:"Attach File"
|
||||||
},
|
},
|
||||||
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: 'Supervisor’s report',
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
recordsTitle: 'Records per page:',
|
|
||||||
noResultsLabel: 'The filter didn’t uncover any results',
|
|
||||||
noDataLabel: 'I didn’t 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',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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 d’emploi',
|
|
||||||
company: 'Entreprise',
|
|
||||||
supervisor: 'Superviseur',
|
|
||||||
isSupervisor: 'Est un superviseur',
|
|
||||||
hours_bank_max: 'Maximum de banque d’heures ',
|
|
||||||
onboarding: 'Date d’embauche',
|
|
||||||
offboarding: 'date de départ',
|
|
||||||
employee_number: 'Numéro d’employé (Matricule employeur D)',
|
|
||||||
regular_hours_day: 'nombre régulier d’heures 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 d’emploi doit être rempli.',
|
|
||||||
companyValidation: 'Entreprise doit être rempli.',
|
|
||||||
supervisorValidation: 'Superviseur doit être rempli.',
|
|
||||||
hours_bank_maxValidation: 'Maximum de banque d’heures doit être rempli.',
|
|
||||||
onboardingValidation: 'Date d’embauche doit être rempli.',
|
|
||||||
employee_numberValidation: 'Numéro d’employé doit être rempli.',
|
|
||||||
regular_hours_dayValidation:
|
|
||||||
'nombre régulier d’heures 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 n’interagissez 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 d’emploi',
|
|
||||||
company: 'Entreprise',
|
|
||||||
supervisor: 'Superviseur',
|
|
||||||
isSupervisor: 'Est un superviseur',
|
|
||||||
hours_bank_max: 'Maximum de banque d’heures ',
|
|
||||||
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 d’emploi doit être rempli.',
|
|
||||||
companyValidation: 'Entreprise doit être rempli.',
|
|
||||||
supervisorValidation: 'Superviseur doit être rempli.',
|
|
||||||
hours_bank_maxValidation: 'Maximum de banque d’heures 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? S’il vous plaît utiliser cette zone pour fournir un message détaillé, Merci!*',
|
|
||||||
//form validation
|
|
||||||
fullNameValidation: 'Le nom complet doit être rempli.',
|
|
||||||
emailValidation: 'L’e-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 n’avez 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 d’accueil',
|
|
||||||
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 l’utilisateur',
|
|
||||||
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 n’a 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 d’au 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:
|
|
||||||
'L’heure de début ne peut pas être après l’heure de fin',
|
|
||||||
endTimeBeforeStartTimeValidation:
|
|
||||||
'L’heure de fin ne peut pas être précédente à l’heure 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: 'S’inscrire',
|
|
||||||
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 d’au 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 n’a révélé aucun résultat',
|
|
||||||
noDataLabel: 'Je n’ai 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: {
|
||||||
|
|
@ -351,93 +164,60 @@ export default {
|
||||||
amount:'Montant',
|
amount:'Montant',
|
||||||
date:'Date',
|
date:'Date',
|
||||||
empty_list:'Aucun dépense enregistrée',
|
empty_list:'Aucun dépense enregistrée',
|
||||||
|
employee_comment:'Commentaire',
|
||||||
|
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",
|
||||||
|
attach_file:"Pièce jointe"
|
||||||
},
|
},
|
||||||
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',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
100
src/modules/auth/components/login-connection-panel.vue
Normal file
100
src/modules/auth/components/login-connection-panel.vue
Normal 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>
|
||||||
33
src/modules/auth/components/login-dev-bypass.vue
Normal file
33
src/modules/auth/components/login-dev-bypass.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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: '',
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
18
src/modules/profile/components/shared/profile-header.vue
Normal file
18
src/modules/profile/components/shared/profile-header.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
60
src/modules/profile/pages/employee/profile-employee.vue
Normal file
60
src/modules/profile/pages/employee/profile-employee.vue
Normal 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>
|
||||||
22
src/modules/profile/pages/profile-container.vue
Normal file
22
src/modules/profile/pages/profile-container.vue
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import LanguageSwitch from "../language-switch.vue";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-footer
|
<q-footer
|
||||||
elevated
|
elevated
|
||||||
|
|
@ -9,7 +5,6 @@ import LanguageSwitch from "../language-switch.vue";
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
17
src/modules/shared/composables/use-toggle.ts
Normal file
17
src/modules/shared/composables/use-toggle.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
//date picker state
|
||||||
|
export const useToggle = (initial = false) => {
|
||||||
|
const state = ref<boolean>(initial);
|
||||||
|
|
||||||
|
const setTrue = () => { state.value = true; };
|
||||||
|
const setFalse = () => { state.value = false; };
|
||||||
|
const toggle = () => { state.value = !state.value; };
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
setTrue,
|
||||||
|
setFalse,
|
||||||
|
toggle
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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/expense.interfaces';
|
||||||
|
|
||||||
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>
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
import { ref } from 'vue';
|
||||||
|
import type { Shift } from 'src/modules/timesheets/types/shift.interfaces';
|
||||||
|
|
||||||
|
|
||||||
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 +21,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 +28,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 }}
|
||||||
|
|
@ -56,7 +53,7 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
name="double_arrow"
|
name="double_arrow"
|
||||||
:color="icon_data.color"
|
:color="icon_data.color"
|
||||||
size="24px"
|
size="24px"
|
||||||
|
|
@ -68,8 +65,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 }}
|
||||||
|
|
@ -82,20 +79,18 @@
|
||||||
>
|
>
|
||||||
<!-- chat_bubble_outline or announcement -->
|
<!-- chat_bubble_outline or announcement -->
|
||||||
<q-btn
|
<q-btn
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- insert_drive_file or request_quote -->
|
<!-- insert_drive_file or request_quote -->
|
||||||
<q-btn
|
<q-btn
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue';
|
import TimesheetApprovalEmployeeDetailsShiftsRow from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row.vue';
|
||||||
import TimesheetApprovalEmployeeDetailsShiftsRowHeader from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row-header.vue';
|
import TimesheetApprovalEmployeeDetailsShiftsRowHeader from 'src/modules/timesheet-approval/components/timesheet-approval-employee-details-shifts-row-header.vue';
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
import { default_shift, type Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
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 { Shift } from 'src/modules/timesheets/types/shift.interfaces';
|
||||||
|
import { default_shift } from 'src/modules/timesheets/types/shift.defaults';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawData: PayPeriodEmployeeDetails;
|
rawData: PayPeriodEmployeeDetails;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { date, type QTableColumn } from 'quasar';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
import { useTimesheetApprovalApi } from '../composables/use-timesheet-approval-api';
|
||||||
import TimesheetApprovalPeriodPicker from '../components/timesheet-approval-period-picker.vue';
|
import PayPeriodCalendarPicker from 'src/modules/shared/components/utils/pay-period-calendar-picker.vue';
|
||||||
import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
|
import TimesheetApprovalEmployeeOverviewListItem from './timesheet-approval-employee-overview-list-item.vue';
|
||||||
import TimesheetApprovalEmployeeDetails from 'src/modules/timesheet-approval/pages/timesheet-approval-employee-details.vue';
|
import TimesheetApprovalEmployeeDetails from 'src/modules/timesheet-approval/pages/timesheet-approval-employee-details.vue';
|
||||||
import { date, type QTableColumn } from 'quasar';
|
import { type PayPeriodOverviewEmployee } from '../types/timesheet-approval-pay-period-overview-employee-interface';
|
||||||
import type { PayPeriodOverviewEmployee } from '../types/timesheet-approval-pay-period-overview-employee-interface';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -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,15 +169,15 @@
|
||||||
: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
|
<PayPeriodCalendarPicker
|
||||||
:is-disabled="timesheet_store.is_loading"
|
:is-disabled="timesheet_store.is_loading"
|
||||||
:is-previous-limit="is_calendar_limit"
|
:is-previous-limit="is_calendar_limit"
|
||||||
@date-selected="onDateSelected"
|
@date-selected="onDateSelected"
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { default_timesheet_details_week, type TimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet-details-interface";
|
import { defaultTimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet.defaults";
|
||||||
|
import type { TimesheetDetailsWeek } from "src/modules/timesheets/types/timesheet.interfaces";
|
||||||
|
|
||||||
|
|
||||||
export interface PayPeriodEmployeeDetails {
|
export interface PayPeriodEmployeeDetails {
|
||||||
week1: TimesheetDetailsWeek;
|
week1: TimesheetDetailsWeek;
|
||||||
|
|
@ -6,6 +8,6 @@ export interface PayPeriodEmployeeDetails {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const default_pay_period_employee_details = {
|
export const default_pay_period_employee_details = {
|
||||||
week1: default_timesheet_details_week(),
|
week1: defaultTimesheetDetailsWeek(),
|
||||||
week2: default_timesheet_details_week(),
|
week2: defaultTimesheetDetailsWeek(),
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
199
src/modules/timesheets/components/expenses/expense-form.vue
Normal file
199
src/modules/timesheets/components/expenses/expense-form.vue
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { QForm } from 'quasar';
|
||||||
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
import type { ExpenseType } from '../../types/expense.types';
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
//---------------- v-models ------------------
|
||||||
|
const draft = defineModel<Partial<TimesheetExpense>>('draft');
|
||||||
|
const files = defineModel<File[] | null>('files');
|
||||||
|
const datePickerOpen = defineModel<boolean | null>('datePickerOpen', {default: false });
|
||||||
|
|
||||||
|
//------------------ Props ------------------
|
||||||
|
const {setType} = defineProps<{
|
||||||
|
type_options: { label: string; value: ExpenseType }[];
|
||||||
|
show_amount: boolean;
|
||||||
|
is_readonly: boolean;
|
||||||
|
rules: {
|
||||||
|
typeRequired: (val: unknown) => true | string;
|
||||||
|
amountRequired: (val: unknown) => true | string;
|
||||||
|
mileageRequired: (val: unknown) => true | string;
|
||||||
|
commentRequired: (val: unknown) => true | string;
|
||||||
|
commentTooLong: (val: unknown) => true | string;
|
||||||
|
};
|
||||||
|
comment_max_length: number;
|
||||||
|
setType: (val: ExpenseType) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
//------------------ Emits ------------------
|
||||||
|
defineEmits<{
|
||||||
|
'submit': [void];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
//------------------ Exposes ------------------
|
||||||
|
const inner_form = ref<QForm | null>(null);
|
||||||
|
defineExpose({
|
||||||
|
validate: async ( force = true ) => (await inner_form.value?.validate(force)) === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
//------------------ Handlers ------------------
|
||||||
|
const onTypeChange = (val: ExpenseType) => {
|
||||||
|
setType(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-form
|
||||||
|
ref="inner_form"
|
||||||
|
flat
|
||||||
|
v-if="!is_readonly"
|
||||||
|
@submit.prevent="$emit('submit')"
|
||||||
|
>
|
||||||
|
<div class="text-subtitle2 q-py-sm">
|
||||||
|
{{ $t('timesheet.expense.add_expense')}}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-between">
|
||||||
|
|
||||||
|
<!-- date selection input -->
|
||||||
|
<q-input
|
||||||
|
v-model="draft!.date"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
readonly
|
||||||
|
stack-label
|
||||||
|
class="col q-px-xs"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.date')"
|
||||||
|
>
|
||||||
|
<template #before>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
icon="event"
|
||||||
|
color="primary"
|
||||||
|
@click="datePickerOpen = true"
|
||||||
|
/>
|
||||||
|
<q-dialog v-model="datePickerOpen">
|
||||||
|
<q-date
|
||||||
|
v-model="draft!.date"
|
||||||
|
@update:model-value="datePickerOpen = false"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- expenses type selection -->
|
||||||
|
<q-select
|
||||||
|
v-model="draft!.type"
|
||||||
|
:options="type_options"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
class="col q-px-xs"
|
||||||
|
color="primary"
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
:label="$t('timesheet.expense.type')"
|
||||||
|
:rules="[ rules.typeRequired ]"
|
||||||
|
@update:model-value="val => setType(val as ExpenseType)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- amount input -->
|
||||||
|
<template v-if="show_amount">
|
||||||
|
<q-input
|
||||||
|
key="amount"
|
||||||
|
v-model.number="draft!.amount"
|
||||||
|
filled
|
||||||
|
input-class="text-right"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
color="primary"
|
||||||
|
class="col q-px-xs"
|
||||||
|
:label="$t('timesheet.expense.amount')"
|
||||||
|
suffix="$"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[ rules.amountRequired ]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- mileage input -->
|
||||||
|
<template v-else>
|
||||||
|
<q-input
|
||||||
|
key="mileage"
|
||||||
|
v-model.number="draft!.mileage"
|
||||||
|
filled
|
||||||
|
input-class="text-right"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
color="primary"
|
||||||
|
class="col q-px-xs"
|
||||||
|
:label="$t('timesheet.expense.mileage')"
|
||||||
|
suffix="km"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[ rules.mileageRequired ]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- employee comment input -->
|
||||||
|
<q-input
|
||||||
|
v-model="draft!.comment"
|
||||||
|
filled
|
||||||
|
color="primary"
|
||||||
|
type="text"
|
||||||
|
class="col q-px-sm"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
:counter="true"
|
||||||
|
:maxlength="comment_max_length"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[ rules.commentRequired, rules.commentTooLong ]"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold ">
|
||||||
|
{{ $t('timesheet.expense.comment') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- import attach file section -->
|
||||||
|
<q-file
|
||||||
|
v-model="files"
|
||||||
|
:label="$t('timesheet.expense.hints.attach_file')"
|
||||||
|
filled
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
stack-label
|
||||||
|
class="col"
|
||||||
|
style="max-width: 300px;"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon
|
||||||
|
name="attach_file"
|
||||||
|
size="sm"
|
||||||
|
color="primary"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
|
||||||
|
<!-- add btn section -->
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
color="primary"
|
||||||
|
icon="add"
|
||||||
|
size="sm"
|
||||||
|
class="q-mt-sm q-ml-sm"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
117
src/modules/timesheets/components/expenses/expense-list.vue
Normal file
117
src/modules/timesheets/components/expenses/expense-list.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
import { expenseTypeIcon } from '../../utils/expense.util';
|
||||||
|
/* eslint-disable */
|
||||||
|
defineProps<{
|
||||||
|
items: TimesheetExpense[];
|
||||||
|
is_readonly: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
(e: 'remove', index: number): void;
|
||||||
|
(e: 'edit' , index: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- liste des dépenses pré existantes -->
|
||||||
|
<q-list
|
||||||
|
padding
|
||||||
|
class="rounded-borders"
|
||||||
|
|
||||||
|
>
|
||||||
|
<q-item-label v-if="items.length === 0" class="text-italic q-px-sm">
|
||||||
|
{{ $t('timesheet.expense.empty_list') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item
|
||||||
|
style="border: solid 1px lightgrey; border-radius: 7px;"
|
||||||
|
v-for="(expense, index) in items" :key="index"
|
||||||
|
class="q-my-xs shadow-1"
|
||||||
|
>
|
||||||
|
<!-- avatar type icon section -->
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon :name="expenseTypeIcon(expense.type)" color="primary"/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- amount or mileage section -->
|
||||||
|
<q-item-section top>
|
||||||
|
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
||||||
|
<template v-if="typeof expense.mileage === 'number'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ expense.amount?.toFixed(2) }} $
|
||||||
|
</template>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-else>
|
||||||
|
{{ expense.amount?.toFixed(2) }} $
|
||||||
|
</q-item-label>
|
||||||
|
|
||||||
|
<!-- date label -->
|
||||||
|
<q-item-label caption lines="2">
|
||||||
|
{{ $d(new Date(expense.date + 'T00:00:00'), { year:'numeric', month:'short', day: 'numeric', weekday: 'short'}) }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- attachment file icon -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
color="primary"
|
||||||
|
class="q-mx-lg"
|
||||||
|
icon="attach_file"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- comment section -->
|
||||||
|
<q-item-section top>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption lines="2">
|
||||||
|
{{ expense.comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- supervisor comment section -->
|
||||||
|
<q-item-section top>
|
||||||
|
<q-item-label lines="1">
|
||||||
|
{{ $t('timesheet.expense.supervisor_comment') }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-if="expense.supervisor_comment" caption lines="2">
|
||||||
|
{{ expense.supervisor_comment }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- delete btn -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
v-if="!is_readonly"
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="primary"
|
||||||
|
icon="edit"
|
||||||
|
@click="$emit('edit', index)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<!-- delete btn -->
|
||||||
|
<q-item-section side>
|
||||||
|
<q-btn
|
||||||
|
v-if="!is_readonly"
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
color="negative"
|
||||||
|
icon="close"
|
||||||
|
@click="$emit('remove', index)"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
@ -1,16 +1,37 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { EXPENSE_TYPE, type ExpenseType, type TimesheetExpense } from '../../types/timesheet-expenses-interface';
|
import { computed, ref } from 'vue';
|
||||||
import { compute_expense_totals, ExpensesValidationError, normalize_expense, validate_expense_UI } from '../../utils/timesheet-expenses-validators';
|
import { useExpenseForm } from '../../composables/use-expense-form';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useExpenseDraft } from '../../composables/use-expense-draft';
|
||||||
import { COMMENT_MAX_LENGTH } from '../../composables/use-shift-api';
|
import { useExpenseItems } from '../../composables/use-expense-items';
|
||||||
|
import { useToggle } from 'src/modules/shared/composables/use-toggle';
|
||||||
|
import ExpenseList from './expense-list.vue';
|
||||||
|
import ExpenseForm from './expense-form.vue';
|
||||||
|
import {
|
||||||
|
buildExpenseTypeOptions,
|
||||||
|
computeExpenseTotals,
|
||||||
|
makeExpenseRules,
|
||||||
|
buildExpenseSavePayload
|
||||||
|
} from '../../utils/expense.util';
|
||||||
|
import { COMMENT_MAX_LENGTH } from '../../constants/expense.constants';
|
||||||
|
import { ExpensesValidationError } from '../../types/expense-validation.interface';
|
||||||
|
import { EXPENSE_TYPE } from '../../types/expense.types';
|
||||||
|
import type { ExpenseType } from '../../types/expense.types';
|
||||||
|
import type { ExpenseDay, TimesheetExpense } from '../../types/expense.interfaces';
|
||||||
|
import {
|
||||||
|
createExpenseByDate,
|
||||||
|
deleteExpenseByDate,
|
||||||
|
getPayPeriodExpenses,
|
||||||
|
updateExpenseByDate
|
||||||
|
} from '../../composables/api/use-expense-api';
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
//props
|
const { t, locale } = useI18n();
|
||||||
const props = defineProps<{
|
const rules = makeExpenseRules(t, COMMENT_MAX_LENGTH);
|
||||||
|
|
||||||
|
//------------------ props ------------------
|
||||||
|
const {email, pay_period_no, pay_year, is_approved, initial_expenses} = defineProps<{
|
||||||
pay_period_no: number;
|
pay_period_no: number;
|
||||||
pay_year: number;
|
pay_year: number;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -18,98 +39,99 @@ const props = defineProps<{
|
||||||
initial_expenses?: TimesheetExpense[];
|
initial_expenses?: TimesheetExpense[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
//emits
|
//------------------ emits ------------------
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e:'close'): void;
|
(e: 'close'): void;
|
||||||
(e: 'save', payload: {
|
(e: 'save', payload: {
|
||||||
pay_period_no: number;
|
pay_period_no: number;
|
||||||
pay_year: number;
|
pay_year: number;
|
||||||
email: string;
|
email: string;
|
||||||
expenses: TimesheetExpense[];
|
expenses: TimesheetExpense[];
|
||||||
}): void;
|
}): void;
|
||||||
(e: 'error', err: ExpensesValidationError): void;
|
(e: 'error', err: ExpensesValidationError): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
//q-select mapper
|
//------------------ q-select mapper ------------------
|
||||||
const type_options = computed(()=> EXPENSE_TYPE.map((val)=> ({
|
const type_options = computed(() => {
|
||||||
label: t(`timesheet.expense.types.${val}`, val),
|
void locale.value;
|
||||||
value: val,
|
return buildExpenseTypeOptions(EXPENSE_TYPE, t);
|
||||||
})));
|
})
|
||||||
|
|
||||||
//refs & states
|
//------------------ refs and computed ------------------
|
||||||
const items = ref<TimesheetExpense[]>(Array.isArray(props.initial_expenses) ? props.initial_expenses.map(normalize_expense): []);
|
const files = ref<File[] | null>(null);
|
||||||
const draft = ref<Partial<TimesheetExpense>>({
|
const is_readonly = computed(() => !!is_approved);
|
||||||
date:'',
|
const editing_old = ref<ExpenseDay | null>(null);
|
||||||
type: 'EXPENSES',
|
|
||||||
comment:'',
|
const { state: is_open_date_picker } = useToggle();
|
||||||
|
const { draft, setType, reset, showAmount } = useExpenseDraft();
|
||||||
|
const { formRef, validateAnd } = useExpenseForm();
|
||||||
|
const { items, validateAll, payload } = useExpenseItems({
|
||||||
|
initial_expenses: initial_expenses,
|
||||||
|
is_approved: is_readonly,
|
||||||
|
draft,
|
||||||
});
|
});
|
||||||
|
const totals = computed(() => computeExpenseTotals(items.value));
|
||||||
|
|
||||||
// computeds
|
//------------------ actions ------------------
|
||||||
const totals = computed(()=> compute_expense_totals(items.value));
|
const onSave = () => {
|
||||||
const remaining_comment_chars = computed(()=> {
|
|
||||||
const comment = String(draft.value.comment ?? '');
|
|
||||||
return COMMENT_MAX_LENGTH - comment.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
//actions
|
|
||||||
const reset_draft = () => {
|
|
||||||
draft.value.date = '';
|
|
||||||
draft.value.type = 'EXPENSES';
|
|
||||||
delete draft.value.amount;
|
|
||||||
delete draft.value.mileage;
|
|
||||||
draft.value.comment = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const add_draft_as_item = () => {
|
|
||||||
const candidate: TimesheetExpense = normalize_expense({
|
|
||||||
date: String(draft.value.date ?? '').trim(),
|
|
||||||
type: String(draft.value.type ?? '').trim(),
|
|
||||||
...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
|
|
||||||
...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
|
|
||||||
comment: String(draft.value.comment ?? '').trim(),
|
|
||||||
} as TimesheetExpense);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
validate_expense_UI(candidate, 'expense_draft');
|
validateAll();
|
||||||
items.value = [ ...items.value, candidate];
|
reset();
|
||||||
reset_draft();
|
emit('save', buildExpenseSavePayload({
|
||||||
|
pay_period_no: pay_period_no,
|
||||||
|
pay_year: pay_year,
|
||||||
|
email: email,
|
||||||
|
expenses: payload(),
|
||||||
|
}));
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const e = err instanceof ExpensesValidationError
|
const e = err instanceof ExpensesValidationError
|
||||||
? err : new ExpensesValidationError({
|
? err
|
||||||
status_code: 400,
|
: new ExpensesValidationError({
|
||||||
message: String(err?.message || err)
|
status_code: 400,
|
||||||
});
|
message: String(err?.message || err)
|
||||||
|
});
|
||||||
emit('error', e);
|
emit('error', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove_item_at = (index: number) => {
|
const refreshFromServer = async () => {
|
||||||
if(props.is_approved) return;
|
const fresh = await getPayPeriodExpenses(email, pay_year, pay_period_no);
|
||||||
if(index < 0 || index >= items.value.length) return;
|
items.value = Array.isArray(fresh.expenses) ? fresh.expenses : [];
|
||||||
items.value = items.value.filter((_,i) => i !== index);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate_all = () => {
|
const onFormSubmit = async () => {
|
||||||
for(const expense of items.value) {
|
|
||||||
validate_expense_UI(expense, 'expense_item');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_save = () => {
|
|
||||||
try {
|
try {
|
||||||
validate_all();
|
await validateAnd(async () => {
|
||||||
const payload = items.value.map(normalize_expense);
|
if (is_readonly.value) throw new Error(t('common.read_only') || 'Read-only');
|
||||||
|
|
||||||
emit('save', {
|
const day = draft.value;
|
||||||
pay_period_no: props.pay_period_no,
|
if (!day?.date || !day?.type || !day?.comment) {
|
||||||
pay_year: props.pay_year,
|
throw new ExpensesValidationError({ status_code: 400, message: 'Missing required fields' });
|
||||||
email: props.email,
|
}
|
||||||
expenses: payload,
|
|
||||||
|
const is_mileage = String(day.type).toUpperCase() === 'MILEAGE';
|
||||||
|
const new_payload = {
|
||||||
|
date: day.date,
|
||||||
|
type: day.type as ExpenseType,
|
||||||
|
comment: day.comment,
|
||||||
|
...(is_mileage && typeof day.mileage === 'number'
|
||||||
|
? { mileage: day.mileage }
|
||||||
|
: !is_mileage && typeof day.amount === 'number'
|
||||||
|
? { amount: day.amount }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
if(editing_old.value) {
|
||||||
|
await updateExpenseByDate(email, editing_old.value, new_payload as any);
|
||||||
|
editing_old.value = null;
|
||||||
|
} else {
|
||||||
|
await createExpenseByDate(email, new_payload as any);
|
||||||
|
}
|
||||||
|
await refreshFromServer();
|
||||||
|
reset();
|
||||||
});
|
});
|
||||||
|
} catch (err: any) {
|
||||||
} catch(err: any) {
|
const e = err instanceof ExpensesValidationError ? err : new ExpensesValidationError({
|
||||||
const e = err instanceof ExpensesValidationError
|
|
||||||
? err: new ExpensesValidationError({
|
|
||||||
status_code: 400,
|
status_code: 400,
|
||||||
message: String(err?.message || err)
|
message: String(err?.message || err)
|
||||||
});
|
});
|
||||||
|
|
@ -117,229 +139,129 @@ const on_save = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const on_close = () => emit('close');
|
const onRemove = async (index: number) => {
|
||||||
|
try {
|
||||||
|
if (is_readonly.value) throw new Error(t('common.read_only') || 'Read-only');
|
||||||
|
|
||||||
|
const item = items.value[index];
|
||||||
|
if (!item) return;
|
||||||
|
const is_mileage = String(item.type).toUpperCase() === 'MILEAGE';
|
||||||
|
|
||||||
//read-only guard for supervisor comment and approved expenses
|
const old_payload: any = {
|
||||||
const is_readonly = computed(()=> !!props.is_approved);
|
date: item.date,
|
||||||
|
type: item.type as ExpenseType,
|
||||||
|
comment: item.comment ?? '',
|
||||||
|
...(is_mileage && typeof item.mileage === 'number'
|
||||||
|
? { mileage: item.mileage }
|
||||||
|
: !is_mileage && typeof item.amount === 'number'
|
||||||
|
? { amount: item.amount }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await deleteExpenseByDate(email, old_payload as any);
|
||||||
const set_draft_type = (value: ExpenseType) => (draft.value.type = value);
|
await refreshFromServer();
|
||||||
const set_draft_amount = (value: number | null) => {
|
} catch (err: any) {
|
||||||
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
const e = err instanceof ExpensesValidationError ? err : new ExpensesValidationError({
|
||||||
delete draft.value.amount;
|
status_code: 400, message: String(err?.message || err)
|
||||||
} else {
|
});
|
||||||
draft.value.amount = Number(value);
|
emit('error', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const set_draft_mileage = (value: number | null) => {
|
|
||||||
if(value === null || value === undefined || Number.isNaN(Number(value))) {
|
const onEdit = async (index: number) => {
|
||||||
delete draft.value.mileage;
|
if(is_readonly) return;
|
||||||
} else {
|
const item = items.value[index];
|
||||||
draft.value.mileage = Number(value);
|
if(!item) return;
|
||||||
}
|
const old_amount = Number(item.amount) || 0;
|
||||||
|
editing_old.value = {
|
||||||
|
date: item.date,
|
||||||
|
type: item.type as ExpenseType,
|
||||||
|
amount: old_amount,
|
||||||
|
comment: item.comment ?? '',
|
||||||
|
is_approved: !!item.is_approved,
|
||||||
|
};
|
||||||
|
|
||||||
|
const is_mileage = String(item.type).toUpperCase() === 'MILEAGE';
|
||||||
|
const next: Partial<TimesheetExpense> = {
|
||||||
|
date: item.date,
|
||||||
|
type: item.type,
|
||||||
|
comment: item.comment ?? '',
|
||||||
|
...(is_mileage
|
||||||
|
? (typeof item.mileage === 'number' ? { mileage: item.mileage } : {})
|
||||||
|
: (typeof item.amount === 'number' ? { amnount: item.amount } : {})),
|
||||||
|
};
|
||||||
|
(draft as any).value = next;
|
||||||
|
setType(item.type as ExpenseType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClose = () => emit('close');
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card class="q-pa-md q-gutter-md" flat bordered>
|
<div>
|
||||||
<!-- header (title with totals)-->
|
<!-- header (title with totals)-->
|
||||||
<div class="row items-center justify-between">
|
<q-item class="row justify-between">
|
||||||
<div class="text-h6"> {{ $t('timesheet.expense.title') }}</div>
|
<q-item-label
|
||||||
<div class="row items-center q-gutter-sm">
|
header
|
||||||
<q-badge :label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"/>
|
class="text-h6 col-auto"
|
||||||
<q-badge :label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(2)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator/>
|
|
||||||
|
|
||||||
<!-- liste des dépenses pré existantes -->
|
|
||||||
<div class="column q-gutter-sm">
|
|
||||||
<div
|
|
||||||
v-if="items.length === 0"
|
|
||||||
class="text-italic text-secondary"
|
|
||||||
>{{ $t('timesheet.expense.empty_list') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-card
|
|
||||||
v-for="(expense, index) in items"
|
|
||||||
:key="index"
|
|
||||||
flat
|
|
||||||
bordered
|
|
||||||
class="q-pa-sm"
|
|
||||||
>
|
>
|
||||||
<!-- date section -->
|
{{ $t('timesheet.expense.title') }}
|
||||||
<div class="row items-start q-col-gutter-md">
|
</q-item-label>
|
||||||
<div class="col-12 col-md-2">
|
<q-item-section class="items-center col-auto">
|
||||||
<div class="text-caption text-secondary">{{ $t('timesheet.expense.date') }} </div>
|
<q-badge
|
||||||
<div class="text-body2"> {{ expense.date }}</div>
|
lines="1"
|
||||||
</div>
|
class="q-pa-sm q-px-md"
|
||||||
</div>
|
:label="$t('timesheet.expense.total_amount') + ': ' + totals.amount.toFixed(2)"
|
||||||
<!-- expense type section -->
|
/>
|
||||||
<div class="row items-start q-col-gutter-md">
|
<q-separator spaced />
|
||||||
<div class="col-12 col-md-2">
|
<q-badge
|
||||||
<div class="text-body2"> {{ $t('timesheet.expense.types') + expense.type, expense.type }} </div>
|
lines="2"
|
||||||
<div class="text-body2">
|
class="q-pa-sm q-px-md"
|
||||||
{{ $t('timesheet.expense.types') + expense.type, expense.type }}
|
:label="$t('timesheet.expense.total_mileage') + ': ' + totals.mileage.toFixed(1)"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</q-item-section>
|
||||||
<div class="col-12 col-sm-3">
|
</q-item>
|
||||||
<q-select
|
<ExpenseList
|
||||||
v-model="draft.type"
|
:items="items"
|
||||||
:options="type_options"
|
:is_readonly="is_readonly"
|
||||||
filled
|
@remove="onRemove"
|
||||||
color="primary"
|
@edit="onEdit"
|
||||||
emit-value
|
/>
|
||||||
map-options
|
<ExpenseForm
|
||||||
:label="$t('timesheet.expense.type')"
|
ref="formRef"
|
||||||
:rules="[ val => !! val || $t('timesheet.expense.errors.type_required')]"
|
v-model:draft="draft"
|
||||||
@update:model-value="val => set_draft_type(val as ExpenseType)"
|
v-model:files="files"
|
||||||
/>
|
v-model:date-picker-open="is_open_date_picker"
|
||||||
</div>
|
:type_options="type_options"
|
||||||
</div>
|
:show_amount="showAmount"
|
||||||
|
:is_readonly="is_readonly"
|
||||||
|
:rules="rules"
|
||||||
|
:comment_max_length="COMMENT_MAX_LENGTH"
|
||||||
|
:set-type="setType"
|
||||||
|
@submit="onFormSubmit"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- amount section -->
|
<q-separator spaced />
|
||||||
<div class="col-6 col-md-2">
|
|
||||||
<div class="text-caption text-secondary"> {{ $t('timesheet.expense.amount') }}</div>
|
|
||||||
<div class="text-body2">
|
|
||||||
<span v-if="typeof expense.amount === 'number'">{{ expense.amount.toFixed(2) }}</span>
|
|
||||||
<span v-else class="text-grey-6">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- comment section -->
|
|
||||||
<div class="col-12 col-md">
|
|
||||||
<div class="text-caption text-secondary">{{ $t('timesheet.expense.employee_comment') }}</div>
|
|
||||||
<div class="text-body2">{{ expense.comment }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- supervisor comment section -->
|
<div class="row col-auto justify-end">
|
||||||
<div class="col-12 col-md">
|
|
||||||
<div class="text-caption text-secondary">{{ $t('timesheet.expense.supervisor_comment') }}</div>
|
|
||||||
<div class="text-body2">
|
|
||||||
<span v-if="expense.supervisor_comment">{{ expense.supervisor_comment }}</span>
|
|
||||||
<span v-else class="text-grey-6">-</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- delete btn -->
|
|
||||||
<div class="col-auto q-ml-auto">
|
|
||||||
<q-btn
|
|
||||||
v-if="!is_readonly"
|
|
||||||
flat
|
|
||||||
round
|
|
||||||
size="sm"
|
|
||||||
color="negative"
|
|
||||||
icon="delete"
|
|
||||||
:aria-label="$t('timesheet.delete_button')"
|
|
||||||
@click="remove_item_at(index)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator spaced/>
|
|
||||||
|
|
||||||
<div v-if="!is_readonly" class="column q-gutter-sm">
|
|
||||||
<div class="text-subtitle2">{{ $t('timesheet.expense.add_expense')}}</div>
|
|
||||||
<!-- add a new expense btn -->
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-12 col-sm-3">
|
|
||||||
<!-- date selection input -->
|
|
||||||
<q-input
|
|
||||||
v-model="draft.date"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
type="date"
|
|
||||||
:label="$t('timesheet.expense.date')"
|
|
||||||
:rules="[ value =>!!value || $t('timesheet.expense.errors.date_required_or_invalid')]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-sm-3">
|
|
||||||
<!-- amount input -->
|
|
||||||
<q-input
|
|
||||||
:model-value="draft.amount"
|
|
||||||
@update:model-value="val => set_draft_amount(val as any)"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
:disable="(draft.type as string) === 'MILEAGE'"
|
|
||||||
:label="$t('timesheet.expense.amount')"
|
|
||||||
:hint="$t('timesheet.expense.hints.amount_or_mileage')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 col-sm-3">
|
|
||||||
<!-- mileage input -->
|
|
||||||
<q-input
|
|
||||||
:model-value="draft.mileage"
|
|
||||||
@update:model-value="val => set_draft_mileage(val as any)"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
type="number"
|
|
||||||
step="0.1"
|
|
||||||
min="0"
|
|
||||||
:disable="(draft.type as string) !== 'MILEAGE'"
|
|
||||||
:label="$t('timesheet.expense.mileage')"
|
|
||||||
:hint="$t('timesheet.expense.hints.amount_or_mileage')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<!-- employee comment input -->
|
|
||||||
<q-input
|
|
||||||
v-model="draft.comment"
|
|
||||||
filled
|
|
||||||
color="primary"
|
|
||||||
type="textarea"
|
|
||||||
:label="$t('timesheet.expense.employee_comment')"
|
|
||||||
:counter="true"
|
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
|
||||||
:rules="[
|
|
||||||
value => (value && String(value).trim().length) || $t('timesheet.expense.errors.comment_required'),
|
|
||||||
value => String(value || '').length <= COMMENT_MAX_LENGTH || $t('timesheet.expense.errors.comment_too_long')
|
|
||||||
]"
|
|
||||||
:hint="$t('timesheet.expense.hints.comment_required')"
|
|
||||||
/>
|
|
||||||
<div class="text-right text-caption text-secondary q-mt-xs">
|
|
||||||
{{ remaining_comment_chars }} {{ $t('general.chars_left') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12">
|
|
||||||
<!-- add expense btn -->
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
unelevated
|
|
||||||
:label="$t('timesheet.expense.add_expense')"
|
|
||||||
icon="add"
|
|
||||||
@click="add_draft_as_item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-separator spaced/>
|
|
||||||
|
|
||||||
<div class="row justify-end q-gutter-sm">
|
|
||||||
<!-- close btn -->
|
<!-- close btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
|
class="q-mr-sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
:label="$t('timesheet.cancel_button')"
|
:label="$t('timesheet.cancel_button')"
|
||||||
@click="on_close"
|
@click="onClose"
|
||||||
/>
|
/>
|
||||||
<!-- save btn -->
|
<!-- save btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
unelevated
|
unelevated
|
||||||
|
push
|
||||||
:disable="is_readonly || items.length === 0"
|
:disable="is_readonly || items.length === 0"
|
||||||
:label="$t('timesheet.save_button')"
|
:label="$t('timesheet.save_button')"
|
||||||
@click="on_save"
|
@click="onSave"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</q-card>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import type { Shift } from '../../types/shift.interfaces';
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -52,7 +52,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
<q-card-section
|
<q-card-section
|
||||||
horizontal
|
horizontal
|
||||||
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
class="q-pa-none text-uppercase text-center items-center rounded-10"
|
||||||
:class="props.shift.type === '' ? '': 'cursor-pointer'"
|
:class="props.shift.type"
|
||||||
style="line-height: 1;"
|
style="line-height: 1;"
|
||||||
@click.stop="on_click_edit(props.shift.type)"
|
@click.stop="on_click_edit(props.shift.type)"
|
||||||
>
|
>
|
||||||
|
|
@ -79,7 +79,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
name="double_arrow"
|
name="double_arrow"
|
||||||
:color="icon_data.color"
|
:color="icon_data.color"
|
||||||
size="24px"
|
size="24px"
|
||||||
|
|
@ -105,7 +105,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
>
|
>
|
||||||
<!-- comment btn -->
|
<!-- comment btn -->
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
:name="comment_icon"
|
:name="comment_icon"
|
||||||
:color="comment_color"
|
:color="comment_color"
|
||||||
class="q-pa-none q-mx-xs"
|
class="q-pa-none q-mx-xs"
|
||||||
|
|
@ -113,7 +113,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
/>
|
/>
|
||||||
<!-- expenses btn -->
|
<!-- expenses btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
color='grey-8'
|
color='grey-8'
|
||||||
|
|
@ -122,7 +122,7 @@ const on_click_delete = () => emit('request-delete', { shift: props.shift });
|
||||||
/>
|
/>
|
||||||
<!-- delete btn -->
|
<!-- delete btn -->
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.shift.type !== ''"
|
v-if="props.shift.type"
|
||||||
push
|
push
|
||||||
dense
|
dense
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { default_shift, type Shift } from 'src/modules/timesheets/types/timesheet-shift-interface';
|
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
import type { TimesheetPayPeriodDetailsOverview } from '../../types/timesheet-pay-period-details-overview-interface';
|
import detailedShiftListHeader from './detailed-shift-list-header.vue';
|
||||||
import TimesheetDetailsShiftsRowHeader from './timesheet-details-shifts-row-header.vue';
|
import detailedShiftListRow from './detailed-shift-list-row.vue';
|
||||||
import TimesheetDetailsShiftsRow from './timesheet-details-shifts-row.vue';
|
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
|
import type { TimesheetPayPeriodDetailsOverview } from '../../types/timesheet.interfaces';
|
||||||
|
import type { Shift } from '../../types/shift.interfaces';
|
||||||
|
import { default_shift } from '../../types/shift.defaults';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawData: TimesheetPayPeriodDetailsOverview;
|
rawData: TimesheetPayPeriodDetailsOverview;
|
||||||
|
|
@ -72,13 +73,13 @@ import { date } from 'quasar';
|
||||||
|
|
||||||
<!-- List of shifts column -->
|
<!-- List of shifts column -->
|
||||||
<q-card-section class="col q-pa-none">
|
<q-card-section class="col q-pa-none">
|
||||||
<TimesheetDetailsShiftsRowHeader />
|
<detailedShiftListHeader />
|
||||||
<TimesheetDetailsShiftsRow
|
<detailedShiftListRow
|
||||||
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
v-for="shift, shift_index in shifts_or_placeholder(day.shifts)"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
:shift="shift"
|
:shift="shift"
|
||||||
@request-edit=" ({ shift }) => on_request_edit(to_iso_date(day.short_date), shift )"
|
@request-edit=" ({ shift }) => on_request_edit(to_iso_date(day.short_date), shift )"
|
||||||
@request-delete="({ shift }) => on_request_delete(to_iso_date(day.short_date), shift )"
|
@request-delete="({ shift }) => on_request_delete(to_iso_date(day.short_date), shift )"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- add shift btn column -->
|
<!-- add shift btn column -->
|
||||||
|
|
@ -1,76 +1,80 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { upsert_shifts_by_date, type UpsertShiftsBody, type ShiftPayload } from '../../composables/use-shift-api';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
/* eslint-disable */
|
import { SHIFT_KEY, type ShiftKey, type ShiftPayload, type ShiftSelectOption } from '../../types/shift.types';
|
||||||
type Option = { value: string; label: string };
|
import type { UpsertShiftsBody } from '../../types/shift.interfaces';
|
||||||
|
import { upsertShiftsByDate } from '../../composables/api/use-shift-api';
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
mode: 'create' | 'edit' | 'delete';
|
mode: 'create' | 'edit' | 'delete';
|
||||||
dateIso: string;
|
dateIso: string;
|
||||||
initialShift?: ShiftPayload | null;
|
initialShift?: ShiftPayload | null;
|
||||||
shiftOptions: Option[];
|
shiftOptions: ShiftSelectOption[];
|
||||||
email: string;
|
email: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close' ): void;
|
'close': []
|
||||||
(e: 'saved'): void;
|
'saved': []
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
const errorBanner = ref<string | null>(null);
|
const errorBanner = ref<string | null>(null);
|
||||||
const conflicts = ref<Array<{start_time: string; end_time: string; type: string}>>([]);
|
const conflicts = ref<Array<{ start_time: string; end_time: string; type: string }>>([]);
|
||||||
|
|
||||||
const opened = defineModel<boolean>( { default: false });
|
const opened = defineModel<boolean>({ default: false });
|
||||||
const startTime = defineModel<string> ('startTime', { default: '' });
|
const startTime = defineModel<string>('startTime', { default: '' });
|
||||||
const endTime = defineModel<string> ('endTime' , { default: '' });
|
const endTime = defineModel<string>('endTime', { default: '' });
|
||||||
const type = defineModel<string> ('type' , { default: '' });
|
const type = defineModel<ShiftKey | ''>('type', { default: '' });
|
||||||
const isRemote = defineModel<boolean>('isRemote' , { default: false });
|
const isRemote = defineModel<boolean>('isRemote', { default: false });
|
||||||
const comment = defineModel<string> ('comment' , { default: '' });
|
const comment = defineModel<string>('comment', { default: '' });
|
||||||
|
|
||||||
|
const isShiftKey = (val: unknown): val is ShiftKey => SHIFT_KEY.includes(val as ShiftKey);
|
||||||
|
|
||||||
const buildNewShiftPayload = (): ShiftPayload => {
|
const buildNewShiftPayload = (): ShiftPayload => {
|
||||||
const trimmed = (comment.value ?? '').trim();
|
if (!isShiftKey(type.value)) throw new Error('Invalid shift type');
|
||||||
|
const trimmed = (comment.value ?? '').trim();
|
||||||
return {
|
return {
|
||||||
start_time: startTime.value,
|
start_time: startTime.value,
|
||||||
end_time: endTime.value,
|
end_time: endTime.value,
|
||||||
type: type.value,
|
type: type.value,
|
||||||
is_remote: isRemote.value,
|
is_remote: isRemote.value,
|
||||||
...(trimmed ? { comment: trimmed } : {}),
|
...(trimmed ? { comment: trimmed } : {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
errorBanner.value = null;
|
errorBanner.value = null;
|
||||||
conflicts.value = [];
|
conflicts.value = [];
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
|
|
||||||
try{
|
try {
|
||||||
let body: UpsertShiftsBody;
|
let body: UpsertShiftsBody;
|
||||||
if(props.mode === 'create') {
|
if (props.mode === 'create') {
|
||||||
body = { new_shift: buildNewShiftPayload() };
|
body = { new_shift: buildNewShiftPayload() };
|
||||||
} else if (props.mode === 'edit') {
|
} else if (props.mode === 'edit') {
|
||||||
if(!props.initialShift) throw new Error('Missing initial Shift for edit');
|
if (!props.initialShift) throw new Error('Missing initial Shift for edit');
|
||||||
body = { old_shift: props.initialShift, new_shift: buildNewShiftPayload() };
|
body = { old_shift: props.initialShift, new_shift: buildNewShiftPayload() };
|
||||||
} else {
|
} else {
|
||||||
if (!props.initialShift) throw new Error('Missing initial Shift for delete.');
|
if (!props.initialShift) throw new Error('Missing initial Shift for delete.');
|
||||||
body = { old_shift: props.initialShift };
|
body = { old_shift: props.initialShift };
|
||||||
}
|
}
|
||||||
await upsert_shifts_by_date(props.email, props.dateIso, body);
|
await upsertShiftsByDate(props.email, props.dateIso, body);
|
||||||
opened.value = false;
|
opened.value = false;
|
||||||
emit('saved');
|
emit('saved');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const status = error?.status_code ?? error.response?.status ?? 500;
|
const status = error?.status_code ?? error.response?.status ?? 500;
|
||||||
|
|
||||||
const apiConflicts = error?.response?.data?.conflicts ?? error?.data?.conflicts;
|
const apiConflicts = error?.response?.data?.conflicts ?? error?.data?.conflicts;
|
||||||
if(Array.isArray(apiConflicts)){
|
if (Array.isArray(apiConflicts)) {
|
||||||
conflicts.value = apiConflicts.map((c:any)=> ({
|
conflicts.value = apiConflicts.map((c: any) => ({
|
||||||
start_time: String(c.start_time ?? ''),
|
start_time: String(c.start_time ?? ''),
|
||||||
end_time: String(c.end_time ?? ''),
|
end_time: String(c.end_time ?? ''),
|
||||||
type: String(c.type ?? ''),
|
type: String(c.type ?? ''),
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
conflicts.value = [];
|
conflicts.value = [];
|
||||||
|
|
@ -80,78 +84,92 @@ const onSubmit = async () => {
|
||||||
else if (status === 409) errorBanner.value = t('timesheet.shift.errors.overlap')
|
else if (status === 409) errorBanner.value = t('timesheet.shift.errors.overlap')
|
||||||
else if (status === 422) errorBanner.value = t('timesheet.shift.errors.invalid')
|
else if (status === 422) errorBanner.value = t('timesheet.shift.errors.invalid')
|
||||||
else errorBanner.value = t('timesheet.shift.errors.unknown')
|
else errorBanner.value = t('timesheet.shift.errors.unknown')
|
||||||
//add conflicts.value error management
|
//add conflicts.value error management
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateFromProps = () => {
|
const hydrateFromProps = () => {
|
||||||
if(props.mode === 'edit' || props.mode === 'delete') {
|
if (props.mode === 'edit' || props.mode === 'delete') {
|
||||||
const shift = props.initialShift;
|
const shift = props.initialShift;
|
||||||
startTime.value = shift?.start_time ?? '';
|
startTime.value = shift?.start_time ?? '';
|
||||||
endTime.value = shift?.end_time ?? '';
|
endTime.value = shift?.end_time ?? '';
|
||||||
type.value = shift?.type ?? '';
|
type.value = shift?.type ?? '';
|
||||||
isRemote.value = !!shift?.is_remote;
|
isRemote.value = !!shift?.is_remote;
|
||||||
comment.value = (shift as any)?.comment ?? '';
|
comment.value = (shift as any)?.comment ?? '';
|
||||||
} else {
|
} else {
|
||||||
startTime.value = '';
|
startTime.value = '';
|
||||||
endTime.value = '';
|
endTime.value = '';
|
||||||
type.value = '';
|
type.value = '';
|
||||||
isRemote.value = false;
|
isRemote.value = false;
|
||||||
comment.value = '';
|
comment.value = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const canSubmit = computed(() =>
|
const canSubmit = computed(() =>
|
||||||
props.mode === 'delete' ||
|
props.mode === 'delete' ||
|
||||||
(startTime.value.trim().length === 5 &&
|
(startTime.value.trim().length === 5 &&
|
||||||
endTime.value.trim().length === 5 &&
|
endTime.value.trim().length === 5 &&
|
||||||
type.value.trim().length > 0)
|
isShiftKey(type.value))
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
()=> [opened.value, props.mode, props.initialShift, props.dateIso],
|
() => [opened.value, props.mode, props.initialShift, props.dateIso],
|
||||||
()=> { if (opened.value) hydrateFromProps();},
|
() => { if (opened.value) hydrateFromProps(); },
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- create/edit/delete shifts dialog -->
|
<!-- create/edit/delete shifts dialog -->
|
||||||
<template>
|
<template>
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="opened"
|
v-model="opened"
|
||||||
persistent
|
persistent
|
||||||
transition-show="fade"
|
transition-show="fade"
|
||||||
transition-hide="fade"
|
transition-hide="fade"
|
||||||
>
|
>
|
||||||
|
|
||||||
<q-card class="q-pa-md">
|
<q-card class="q-pa-md">
|
||||||
<div class="row items-center q-mb-sm">
|
<div class="row items-center q-mb-sm">
|
||||||
<q-icon name="schedule" size="24px" class="q-mr-sm"/>
|
<q-icon
|
||||||
|
name="schedule"
|
||||||
|
size="24px"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
{{
|
{{
|
||||||
props.mode === 'create'
|
props.mode === 'create'
|
||||||
? $t('timesheet.shift.actions.add')
|
? $t('timesheet.shift.actions.add')
|
||||||
: props.mode === 'edit'
|
: props.mode === 'edit'
|
||||||
? $t('timesheet.shift.actions.edit')
|
? $t('timesheet.shift.actions.edit')
|
||||||
: $t('timesheet.shift.actions.delete')
|
: $t('timesheet.shift.actions.delete')
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<q-space/>
|
<q-space />
|
||||||
<q-badge outline color="primary">{{ props.dateIso }}</q-badge>
|
<q-badge
|
||||||
|
outline
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ props.dateIso }}
|
||||||
|
</q-badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-separator spaced/>
|
<q-separator spaced />
|
||||||
|
|
||||||
<div v-if="props.mode !== 'delete'" class="column q-gutter-md">
|
<div
|
||||||
|
v-if="props.mode !== 'delete'"
|
||||||
|
class="column q-gutter-md"
|
||||||
|
>
|
||||||
<div class="row ">
|
<div class="row ">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-input
|
<q-input
|
||||||
v-model="startTime"
|
v-model="startTime"
|
||||||
:label="$t('timesheet.shift.fields.start')"
|
:label="$t('timesheet.shift.fields.start')"
|
||||||
filled dense
|
filled
|
||||||
|
dense
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
mask="##:##"
|
mask="##:##"
|
||||||
/>
|
/>
|
||||||
|
|
@ -160,7 +178,8 @@ watch(
|
||||||
<q-input
|
<q-input
|
||||||
v-model="endTime"
|
v-model="endTime"
|
||||||
:label="$t('timesheet.shift.fields.end')"
|
:label="$t('timesheet.shift.fields.end')"
|
||||||
filled dense
|
filled
|
||||||
|
dense
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
mask="##:##"
|
mask="##:##"
|
||||||
/>
|
/>
|
||||||
|
|
@ -174,7 +193,8 @@ watch(
|
||||||
:label="$t('timesheet.shift.types.label')"
|
:label="$t('timesheet.shift.types.label')"
|
||||||
class="col"
|
class="col"
|
||||||
color="primary"
|
color="primary"
|
||||||
filled dense
|
filled
|
||||||
|
dense
|
||||||
hide-dropdown-icon
|
hide-dropdown-icon
|
||||||
emit-value
|
emit-value
|
||||||
map-options
|
map-options
|
||||||
|
|
@ -182,27 +202,46 @@ watch(
|
||||||
<q-toggle
|
<q-toggle
|
||||||
v-model="isRemote"
|
v-model="isRemote"
|
||||||
:label="$t('timesheet.shift.types.REMOTE')"
|
:label="$t('timesheet.shift.types.REMOTE')"
|
||||||
class="col-auto" />
|
class="col-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
v-model="comment"
|
v-model="comment"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
autogrow filled dense
|
autogrow
|
||||||
|
filled
|
||||||
|
dense
|
||||||
:label="$t('timesheet.shift.fields.header_comment')"
|
:label="$t('timesheet.shift.fields.header_comment')"
|
||||||
:counter="true" :maxlength="512"
|
:counter="true"
|
||||||
|
:maxlength="512"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="q-pa-md">
|
<div
|
||||||
|
v-else
|
||||||
|
class="q-pa-md"
|
||||||
|
>
|
||||||
{{ $t('timesheet.shift.actions.delete_confirmation_msg') }}
|
{{ $t('timesheet.shift.actions.delete_confirmation_msg') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="errorBanner" class="q-mt-md">
|
<div
|
||||||
<q-banner dense class="bg-red-2 text-negative">{{ errorBanner }}</q-banner>
|
v-if="errorBanner"
|
||||||
<div v-if="conflicts.length" class="q-mt-xs">
|
class="q-mt-md"
|
||||||
|
>
|
||||||
|
<q-banner
|
||||||
|
dense
|
||||||
|
class="bg-red-2 text-negative"
|
||||||
|
>{{ errorBanner }}</q-banner>
|
||||||
|
<div
|
||||||
|
v-if="conflicts.length"
|
||||||
|
class="q-mt-xs"
|
||||||
|
>
|
||||||
<div class="text-caption">Conflits :</div>
|
<div class="text-caption">Conflits :</div>
|
||||||
<ul class="q-pl-md q-mt-xs">
|
<ul class="q-pl-md q-mt-xs">
|
||||||
<li v-for="(c, i) in conflicts" :key="i">
|
<li
|
||||||
|
v-for="(c, i) in conflicts"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
{{ c.start_time }}–{{ c.end_time }} ({{ c.type }})
|
{{ c.start_time }}–{{ c.end_time }} ({{ c.type }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -216,11 +255,12 @@ watch(
|
||||||
flat
|
flat
|
||||||
color="grey-8"
|
color="grey-8"
|
||||||
:label="$t('timesheet.cancel_button')"
|
:label="$t('timesheet.cancel_button')"
|
||||||
@click="() => { opened = false; emit('close');}"
|
@click="() => { opened = false; emit('close'); }"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<q-btn
|
||||||
v-if="props.mode === 'delete'"
|
v-if="props.mode === 'delete'"
|
||||||
outline color="negative"
|
outline
|
||||||
|
color="negative"
|
||||||
icon="cancel"
|
icon="cancel"
|
||||||
:label="$t('timesheet.delete_button')"
|
:label="$t('timesheet.delete_button')"
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import type { ShiftLegendItem } from '../../types/shift.types';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const props = defineProps<{ isLoading: boolean; }>();
|
const props = defineProps<{ isLoading: boolean; }>();
|
||||||
|
|
||||||
type ShiftLegendItem = {
|
|
||||||
type: 'REGULAR'|'EVENING'|'EMERGENCY'|'OVERTIME'|'VACATION'|'HOLIDAY'|'SICK';
|
|
||||||
color: string;
|
|
||||||
label_key: string;
|
|
||||||
text_color?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const legend: ShiftLegendItem[] = [
|
const legend: ShiftLegendItem[] = [
|
||||||
{type:'REGULAR' , color: 'secondary', label_key: 'timesheet.shift.types.REGULAR', text_color: 'grey-8'},
|
{type:'REGULAR' , color: 'secondary', label_key: 'timesheet.shift.types.REGULAR', text_color: 'grey-8'},
|
||||||
{type:'EVENING' , color: 'warning' , label_key: 'timesheet.shift.types.EVENING'},
|
{type:'EVENING' , color: 'warning' , label_key: 'timesheet.shift.types.EVENING'},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable */
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { CreateShiftPayload } from '../../types/timesheet-shifts-payload-interface';
|
import type { CreateShiftPayload, Shift } from '../../types/shift.interfaces';
|
||||||
import type { Shift } from '../../types/timesheet-shift-interface';
|
|
||||||
|
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rows: Shift[];
|
rows: Shift[];
|
||||||
week_dates: string[];
|
week_dates: string[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,28 @@
|
||||||
import { isProxy, toRaw } from "vue";
|
|
||||||
import { type ExpenseType, type TimesheetExpense } from "../types/timesheet-expenses-interface";
|
|
||||||
import { type PayPeriodExpenses } from "../types/timesheet-expenses-list-interface";
|
|
||||||
import { normalize_expense, validate_expense_UI } from "../utils/timesheet-expenses-validators";
|
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
|
import { isProxy, toRaw } from "vue";
|
||||||
|
import { normalizeExpense, validateExpenseUI } from "../../utils/expenses-validators";
|
||||||
|
import type { ExpenseType } from "../../types/expense.types";
|
||||||
|
import { ExpensesApiError } from "../../types/expense-validation.interface";
|
||||||
|
import type {
|
||||||
|
ExpensePayload,
|
||||||
|
PayPeriodExpenses,
|
||||||
|
TimesheetExpense,
|
||||||
|
UpsertExpenseResult,
|
||||||
|
UpsertExpensesBody,
|
||||||
|
UpsertExpensesResponse
|
||||||
|
} from "../../types/expense.interfaces";
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export interface ExpensePayload{
|
const toPlain = <T extends object>(obj:T): T => {
|
||||||
date: string;
|
|
||||||
type: ExpenseType;
|
|
||||||
amount?: number;
|
|
||||||
mileage?: number;
|
|
||||||
comment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertExpensesBody {
|
|
||||||
expenses: ExpensePayload[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertExpensesResponse {
|
|
||||||
data: PayPeriodExpenses;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiErrorPayload {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string;
|
|
||||||
message?: string;
|
|
||||||
context?: Record<string,unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExpensesApiError extends Error {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string;
|
|
||||||
context?: Record<string, unknown>;
|
|
||||||
constructor(payload: ApiErrorPayload) {
|
|
||||||
super(payload.message || 'Request failed');
|
|
||||||
this.name = 'ExpensesApiError';
|
|
||||||
this.status_code = payload.status_code;
|
|
||||||
|
|
||||||
if(payload.error_code !== undefined) this.error_code = payload.error_code;
|
|
||||||
if(payload.context !== undefined) this.context = payload.context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const to_plain = <T extends object>(obj:T): T => {
|
|
||||||
const raw = isProxy(obj) ? toRaw(obj) : obj;
|
const raw = isProxy(obj) ? toRaw(obj) : obj;
|
||||||
if(typeof (globalThis as any).structuredClone === 'function') {
|
if( typeof (globalThis as any).structuredClone === 'function') {
|
||||||
return (globalThis as any).structuredClone(raw);
|
return (globalThis as any).structuredClone(raw);
|
||||||
}
|
}
|
||||||
return JSON.parse(JSON.stringify(raw));
|
return JSON.parse(JSON.stringify(raw));
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalize_payload = (expense: ExpensePayload): ExpensePayload => {
|
const normalizePayload = (expense: ExpensePayload): ExpensePayload => {
|
||||||
const exp = normalize_expense(expense as unknown as TimesheetExpense);
|
const exp = normalizeExpense(expense as unknown as TimesheetExpense);
|
||||||
const out: ExpensePayload = {
|
const out: ExpensePayload = {
|
||||||
date: exp.date,
|
date: exp.date,
|
||||||
type: exp.type as ExpenseType,
|
type: exp.type as ExpenseType,
|
||||||
|
|
@ -62,7 +34,7 @@ const normalize_payload = (expense: ExpensePayload): ExpensePayload => {
|
||||||
}
|
}
|
||||||
|
|
||||||
//GET by email, year and period no
|
//GET by email, year and period no
|
||||||
export const get_pay_period_expenses = async (
|
export const getPayPeriodExpenses = async (
|
||||||
email: string,
|
email: string,
|
||||||
pay_year: number,
|
pay_year: number,
|
||||||
pay_period_no: number
|
pay_period_no: number
|
||||||
|
|
@ -74,7 +46,7 @@ export const get_pay_period_expenses = async (
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get<PayPeriodExpenses>(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
|
const { data } = await api.get<PayPeriodExpenses>(`/expenses/${encoded_email}/${encoded_year}/${encoded_pay_period_no}`);
|
||||||
|
|
||||||
const items = Array.isArray(data.expenses) ? data.expenses.map(normalize_expense) : [];
|
const items = Array.isArray(data.expenses) ? data.expenses.map(normalizeExpense) : [];
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
expenses: items,
|
expenses: items,
|
||||||
|
|
@ -92,7 +64,7 @@ export const get_pay_period_expenses = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
//PUT by email, year and period no
|
//PUT by email, year and period no
|
||||||
export const put_pay_period_expenses = async (
|
export const putPayPeriodExpenses = async (
|
||||||
email: string,
|
email: string,
|
||||||
pay_year: number,
|
pay_year: number,
|
||||||
pay_period_no: number,
|
pay_period_no: number,
|
||||||
|
|
@ -102,12 +74,12 @@ export const put_pay_period_expenses = async (
|
||||||
const encoded_year = encodeURIComponent(String(pay_year));
|
const encoded_year = encodeURIComponent(String(pay_year));
|
||||||
const encoded_pay_period_no = encodeURIComponent(String(pay_period_no));
|
const encoded_pay_period_no = encodeURIComponent(String(pay_period_no));
|
||||||
|
|
||||||
const plain = Array.isArray(expenses) ? expenses.map(to_plain): [];
|
const plain = Array.isArray(expenses) ? expenses.map(toPlain): [];
|
||||||
|
|
||||||
const normalized: ExpensePayload[] = plain.map((exp) => {
|
const normalized: ExpensePayload[] = plain.map((exp) => {
|
||||||
const norm = normalize_expense(exp as TimesheetExpense);
|
const norm = normalizeExpense(exp as TimesheetExpense);
|
||||||
validate_expense_UI(norm, 'expense_item');
|
validateExpenseUI(norm, 'expense_item');
|
||||||
return normalize_payload(norm as unknown as ExpensePayload);
|
return normalizePayload(norm as unknown as ExpensePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const body: UpsertExpensesBody = {expenses: normalized};
|
const body: UpsertExpensesBody = {expenses: normalized};
|
||||||
|
|
@ -120,7 +92,7 @@ export const put_pay_period_expenses = async (
|
||||||
);
|
);
|
||||||
|
|
||||||
const items = Array.isArray(data?.data?.expenses)
|
const items = Array.isArray(data?.data?.expenses)
|
||||||
? data.data.expenses.map(normalize_expense)
|
? data.data.expenses.map(normalizeExpense)
|
||||||
: [];
|
: [];
|
||||||
return {
|
return {
|
||||||
...(data?.data ?? {
|
...(data?.data ?? {
|
||||||
|
|
@ -145,7 +117,7 @@ export const put_pay_period_expenses = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const post_pay_period_expenses = async (
|
export const postPayPeriodExpenses = async (
|
||||||
email: string,
|
email: string,
|
||||||
pay_year: number,
|
pay_year: number,
|
||||||
pay_period_no: number,
|
pay_period_no: number,
|
||||||
|
|
@ -155,11 +127,11 @@ export const post_pay_period_expenses = async (
|
||||||
const encoded_year = encodeURIComponent(String(pay_year));
|
const encoded_year = encodeURIComponent(String(pay_year));
|
||||||
const encoded_pp = encodeURIComponent(String(pay_period_no));
|
const encoded_pp = encodeURIComponent(String(pay_period_no));
|
||||||
|
|
||||||
const plain = Array.isArray(new_expenses) ? new_expenses.map(to_plain) : [];
|
const plain = Array.isArray(new_expenses) ? new_expenses.map(toPlain) : [];
|
||||||
const normalized: ExpensePayload[] = plain.map((exp) => {
|
const normalized: ExpensePayload[] = plain.map((exp) => {
|
||||||
const norm = normalize_expense(exp as TimesheetExpense);
|
const norm = normalizeExpense(exp as TimesheetExpense);
|
||||||
validate_expense_UI(norm, 'expense_item');
|
validateExpenseUI(norm, 'expense_item');
|
||||||
return normalize_payload(norm as unknown as ExpensePayload);
|
return normalizePayload(norm as unknown as ExpensePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const body: UpsertExpensesBody = { expenses: normalized };
|
const body: UpsertExpensesBody = { expenses: normalized };
|
||||||
|
|
@ -171,7 +143,7 @@ export const post_pay_period_expenses = async (
|
||||||
{ headers: { 'content-type': 'application/json' } }
|
{ headers: { 'content-type': 'application/json' } }
|
||||||
);
|
);
|
||||||
const items = Array.isArray(data?.data?.expenses)
|
const items = Array.isArray(data?.data?.expenses)
|
||||||
? data.data.expenses.map(normalize_expense)
|
? data.data.expenses.map(normalizeExpense)
|
||||||
: [];
|
: [];
|
||||||
return {
|
return {
|
||||||
...(data?.data ?? {
|
...(data?.data ?? {
|
||||||
|
|
@ -196,3 +168,58 @@ export const post_pay_period_expenses = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveDateISO = (a?: ExpensePayload, b?: ExpensePayload): string => {
|
||||||
|
const d = a?.date || b?.date;
|
||||||
|
if(!d) throw new Error('date is required in payload');
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeBody = (obj: {
|
||||||
|
old_expense?: ExpensePayload;
|
||||||
|
new_expense?: ExpensePayload;
|
||||||
|
}) => obj;
|
||||||
|
|
||||||
|
const postUpsert = async (email: string, date: string, body: {
|
||||||
|
old_expense?: ExpensePayload;
|
||||||
|
new_expense?: ExpensePayload;
|
||||||
|
}): Promise<UpsertExpenseResult> => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `/expenses/upsert/${encodeURIComponent(email)}/${date}`;
|
||||||
|
const { data } = await api.post<UpsertExpenseResult>(url, body, {
|
||||||
|
headers: { 'Content-Type': 'application/json'},
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
} catch(err:any) {
|
||||||
|
const status_code: number = err?.response?.status ?? 500;
|
||||||
|
const data = err?.response?.data ?? {};
|
||||||
|
throw new ExpensesApiError({
|
||||||
|
status_code,
|
||||||
|
error_code: data.error_code,
|
||||||
|
message: data.message || data.error || err.message,
|
||||||
|
context: data.context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//create a new expense
|
||||||
|
export const createExpenseByDate = async ( email: string, payload: ExpensePayload): Promise<UpsertExpenseResult> => {
|
||||||
|
const new_expense = normalizePayload(payload);
|
||||||
|
const date = resolveDateISO(new_expense);
|
||||||
|
return postUpsert(email, date, makeBody({ new_expense: new_expense }));
|
||||||
|
};
|
||||||
|
|
||||||
|
//update an expense
|
||||||
|
export const updateExpenseByDate = async ( email: string, old_expense: ExpensePayload, new_expense: ExpensePayload): Promise<UpsertExpenseResult> => {
|
||||||
|
const old_exp = normalizePayload(old_expense);
|
||||||
|
const new_exp = normalizePayload(new_expense);
|
||||||
|
const date = resolveDateISO(new_exp, old_exp);
|
||||||
|
return postUpsert(email, date, makeBody({ old_expense: old_exp,new_expense: new_exp }));
|
||||||
|
};
|
||||||
|
|
||||||
|
//delete an expense
|
||||||
|
export const deleteExpenseByDate = async (email: string, old_expense: ExpensePayload): Promise<UpsertExpenseResult> => {
|
||||||
|
const old = normalizePayload(old_expense);
|
||||||
|
const date = resolveDateISO(undefined, old);
|
||||||
|
return postUpsert(email, date, makeBody({ old_expense: old }));
|
||||||
|
};
|
||||||
|
|
@ -1,37 +1,9 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import { isProxy, toRaw } from "vue";
|
import { isProxy, toRaw } from "vue";
|
||||||
|
import { DATE_FORMAT_PATTERN, TIME_FORMAT_PATTERN } from "../../constants/shift.constants";
|
||||||
|
import type { ShiftPayload } from "../../types/shift.types";
|
||||||
|
import type { UpsertShiftsBody, UpsertShiftsResponse } from "../../types/shift.interfaces";
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export interface ShiftPayload {
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
type: string;
|
|
||||||
is_remote: boolean;
|
|
||||||
comment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertShiftsBody {
|
|
||||||
old_shift?: ShiftPayload;
|
|
||||||
new_shift?: ShiftPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
|
||||||
|
|
||||||
export interface DayShift {
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
type: string;
|
|
||||||
is_remote: boolean;
|
|
||||||
comment?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpsertShiftsResponse {
|
|
||||||
action: UpsertAction;
|
|
||||||
day: DayShift[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
||||||
export const COMMENT_MAX_LENGTH = 512 as const;
|
|
||||||
|
|
||||||
//normalize payload to match backend data
|
//normalize payload to match backend data
|
||||||
export const normalize_comment = (input?: string): string | undefined => {
|
export const normalize_comment = (input?: string): string | undefined => {
|
||||||
|
|
@ -40,14 +12,12 @@ export const normalize_comment = (input?: string): string | undefined => {
|
||||||
return trimmed.length ? trimmed : undefined;
|
return trimmed.length ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const normalize_type = (input: string): string => (input ?? '').trim().toUpperCase();
|
|
||||||
|
|
||||||
export const normalize_payload = (payload: ShiftPayload): ShiftPayload => {
|
export const normalize_payload = (payload: ShiftPayload): ShiftPayload => {
|
||||||
const comment = normalize_comment(payload.comment);
|
const comment = normalize_comment(payload.comment);
|
||||||
return {
|
return {
|
||||||
start_time: payload.start_time,
|
start_time: payload.start_time,
|
||||||
end_time: payload.end_time,
|
end_time: payload.end_time,
|
||||||
type: normalize_type(payload.type),
|
type: payload.type,
|
||||||
is_remote: Boolean(payload.is_remote),
|
is_remote: Boolean(payload.is_remote),
|
||||||
...(comment !== undefined ? { comment } : {}),
|
...(comment !== undefined ? { comment } : {}),
|
||||||
};
|
};
|
||||||
|
|
@ -120,7 +90,7 @@ const validateShift = (payload: ShiftPayload, label: 'old_shift'|'new_shift') =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const upsert_shifts_by_date = async (
|
export const upsertShiftsByDate = async (
|
||||||
email: string,
|
email: string,
|
||||||
date: string,
|
date: string,
|
||||||
body: UpsertShiftsBody,
|
body: UpsertShiftsBody,
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useAuthStore } from "src/stores/auth-store";
|
import { useAuthStore } from "src/stores/auth-store";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store"
|
import { useTimesheetStore } from "src/stores/timesheet-store"
|
||||||
|
/* eslint-disable */
|
||||||
export const useTimesheetApi = () => {
|
export const useTimesheetApi = () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const auth_store = useAuthStore();
|
const auth_store = useAuthStore();
|
||||||
|
|
@ -35,15 +35,24 @@ export const useTimesheetApi = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentPayPeriod = async () => fetchPayPeriod(0);
|
|
||||||
const getNextPayPeriod = async () => fetchPayPeriod(1);
|
const getNextPayPeriod = async () => fetchPayPeriod(1);
|
||||||
const getPreviousPayPeriod = async () => fetchPayPeriod(-1);
|
const getPreviousPayPeriod = async () => fetchPayPeriod(-1);
|
||||||
|
|
||||||
|
const getPreviousPeriodForUser = async (_employee_email: string) => {
|
||||||
|
await getPreviousPayPeriod();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextPeriodForUser = async (_employee_email: string) => {
|
||||||
|
await getNextPayPeriod();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getTimesheetsByDate,
|
getTimesheetsByDate,
|
||||||
fetchPayPeriod,
|
fetchPayPeriod,
|
||||||
getCurrentPayPeriod,
|
// getCurrentPayPeriod,
|
||||||
getNextPayPeriod,
|
getNextPayPeriod,
|
||||||
getPreviousPayPeriod,
|
getPreviousPayPeriod,
|
||||||
|
getPreviousPeriodForUser,
|
||||||
|
getNextPeriodForUser,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
36
src/modules/timesheets/composables/use-expense-draft.ts
Normal file
36
src/modules/timesheets/composables/use-expense-draft.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
|
import type { ExpenseType } from "../types/expense.types";
|
||||||
|
|
||||||
|
export const useExpenseDraft = (initial?: Partial<TimesheetExpense>) => {
|
||||||
|
const DEFAULT_TYPE: ExpenseType = 'EXPENSES';
|
||||||
|
|
||||||
|
const draft = ref<Partial<TimesheetExpense>>({
|
||||||
|
date: '',
|
||||||
|
type: DEFAULT_TYPE,
|
||||||
|
comment: '',
|
||||||
|
...(initial ?? {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
draft.value = {
|
||||||
|
date: '',
|
||||||
|
type: DEFAULT_TYPE,
|
||||||
|
comment: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setType = (value: ExpenseType) => {
|
||||||
|
draft.value.type = value;
|
||||||
|
if(value === 'MILEAGE') {
|
||||||
|
delete draft.value.amount;
|
||||||
|
} else {
|
||||||
|
delete draft.value.mileage;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMileage = computed(()=> (draft.value.type as string) === 'MILEAGE');
|
||||||
|
const showAmount = computed(()=> !showMileage.value);
|
||||||
|
|
||||||
|
return { draft, setType, reset, showMileage, showAmount };
|
||||||
|
}
|
||||||
21
src/modules/timesheets/composables/use-expense-form.ts
Normal file
21
src/modules/timesheets/composables/use-expense-form.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
import type { QForm } from "quasar"
|
||||||
|
|
||||||
|
|
||||||
|
export const useExpenseForm = () => {
|
||||||
|
const formRef = ref<QForm | null>(null);
|
||||||
|
const triedSubmit = ref(false);
|
||||||
|
|
||||||
|
const validateAnd = async (fn: ()=> void | Promise<void>) => {
|
||||||
|
triedSubmit.value = true;
|
||||||
|
const ok = await formRef.value?.validate(true);
|
||||||
|
if(!ok) return false;
|
||||||
|
await fn();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
formRef,
|
||||||
|
validateAnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
55
src/modules/timesheets/composables/use-expense-items.ts
Normal file
55
src/modules/timesheets/composables/use-expense-items.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { ref, type Ref } from "vue";
|
||||||
|
import { normalizeExpense, validateExpenseUI } from "../utils/expenses-validators";
|
||||||
|
import { normExpenseType } from "../utils/expense.util";
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
|
|
||||||
|
type UseExpenseItemsParams = {
|
||||||
|
initial_expenses?: TimesheetExpense[] | null | undefined;
|
||||||
|
draft: Ref<Partial<TimesheetExpense>>;
|
||||||
|
is_approved: Ref<boolean> | boolean;
|
||||||
|
};
|
||||||
|
export const useExpenseItems = ({
|
||||||
|
initial_expenses,
|
||||||
|
draft,
|
||||||
|
is_approved
|
||||||
|
}: UseExpenseItemsParams) => {
|
||||||
|
const items = ref<TimesheetExpense[]>(
|
||||||
|
Array.isArray(initial_expenses) ? initial_expenses.map(normalizeExpense) : []
|
||||||
|
);
|
||||||
|
|
||||||
|
const addFromDraft = () => {
|
||||||
|
const candidate: TimesheetExpense = normalizeExpense({
|
||||||
|
date: draft.value.date,
|
||||||
|
type: normExpenseType(draft.value.type),
|
||||||
|
...(typeof draft.value.amount === 'number' ? { amount: draft.value.amount }: {}),
|
||||||
|
...(typeof draft.value.mileage === 'number' ? { mileage: draft.value.mileage }: {}),
|
||||||
|
comment: String(draft.value.comment ?? '').trim(),
|
||||||
|
} as TimesheetExpense);
|
||||||
|
|
||||||
|
validateExpenseUI(candidate, 'expense_draft');
|
||||||
|
items.value = [ ...items.value, candidate];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAt = (index: number) => {
|
||||||
|
const locked = typeof is_approved === 'boolean' ? is_approved : is_approved.value;
|
||||||
|
if(locked) return;
|
||||||
|
if(index < 0 || index >= items.value.length) return;
|
||||||
|
items.value = items.value.filter((_,i)=> i !== index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAll = () => {
|
||||||
|
for (const expense of items.value) {
|
||||||
|
validateExpenseUI(expense, 'expense_item');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = () => items.value.map(normalizeExpense);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
addFromDraft,
|
||||||
|
removeAt,
|
||||||
|
validateAll,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
};
|
||||||
3
src/modules/timesheets/constants/expense.constants.ts
Normal file
3
src/modules/timesheets/constants/expense.constants.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const COMMENT_MAX_LENGTH = 280;
|
||||||
|
|
||||||
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
2
src/modules/timesheets/constants/shift.constants.ts
Normal file
2
src/modules/timesheets/constants/shift.constants.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const TIME_FORMAT_PATTERN = /^\d{2}:\d{2}$/;
|
||||||
|
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
@ -1,193 +1,76 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
|
||||||
import { useTimesheetApi } from '../composables/use-timesheet-api';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { computed, onMounted, ref } from 'vue';
|
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import TimesheetNavigation from '../components/timesheet/timesheet-navigation.vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import ShiftsLegend from '../components/shift/shifts-legend.vue';
|
import { useI18n } from 'vue-i18n';
|
||||||
import TimesheetDetailsShifts from '../components/shift/timesheet-details-shifts.vue';
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
import { type ShiftPayload } from '../composables/use-shift-api';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { ExpensesApiError, get_pay_period_expenses, put_pay_period_expenses } from '../composables/use-expense-api';
|
import { useShiftStore } from 'src/stores/shift-store';
|
||||||
import type { PayPeriodExpenses } from '../types/timesheet-expenses-list-interface';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import type { TimesheetExpense } from '../types/timesheet-expenses-interface';
|
import { useTimesheetApi } from '../composables/api/use-timesheet-api';
|
||||||
import TimesheetDetailsExpenses from '../components/expenses/timesheet-details-expenses.vue';
|
import { buildShiftOptions } from '../utils/shift.util';
|
||||||
import ShiftCrudDialog from '../components/shift/shift-crud-dialog.vue';
|
import { formatPayPeriodLabel } from '../utils/timesheet-format.util';
|
||||||
|
import TimesheetNavigation from '../components/timesheet/timesheet-navigation.vue';
|
||||||
|
import ShiftsLegend from '../components/shift/shifts-legend.vue';
|
||||||
|
import ShiftCrudDialog from '../components/shift/shift-crud-dialog.vue';
|
||||||
|
import TimesheetDetailsExpenses from '../components/expenses/timesheet-details-expenses.vue';
|
||||||
|
import { SHIFT_KEY } from '../types/shift.types';
|
||||||
|
import type { TimesheetExpense } from '../types/expense.interfaces';
|
||||||
|
import DetailedShiftList from '../components/shift/detailed-shift-list.vue';
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
/* eslint-disable */
|
//------------------- stores -------------------
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
const auth_store = useAuthStore();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const shift_store = useShiftStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const auth_store = useAuthStore();
|
const timesheet_api = useTimesheetApi();
|
||||||
const timesheet_api = useTimesheetApi();
|
|
||||||
|
|
||||||
//expenses refs
|
//------------------- expenses -------------------
|
||||||
const show_expenses_dialog = ref(false);
|
const openExpensesDialog = () => expenses_store.openDialog({
|
||||||
const is_loading_expenses = ref(false);
|
email: auth_store.user.email,
|
||||||
const expenses_data = ref<PayPeriodExpenses | null>(null);
|
pay_year: timesheet_store.current_pay_period.pay_year,
|
||||||
|
pay_period_no: timesheet_store.current_pay_period.pay_period_no,
|
||||||
const notify_error = (err: number) => {
|
t,
|
||||||
const e = err as any;
|
|
||||||
expenses_error.value = (e instanceof ExpensesApiError && t(e.message)) || e?.message || 'Unknown error';
|
|
||||||
};
|
|
||||||
|
|
||||||
const open_expenses_dialog = async () => {
|
|
||||||
show_expenses_dialog.value = true;
|
|
||||||
is_loading_expenses.value = true;
|
|
||||||
expenses_error.value = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await get_pay_period_expenses(
|
|
||||||
auth_store.user.email,
|
|
||||||
timesheet_store.current_pay_period.pay_year,
|
|
||||||
timesheet_store.current_pay_period.pay_period_no,
|
|
||||||
);
|
|
||||||
} catch(err) {
|
|
||||||
notify_error(err as any);
|
|
||||||
expenses_data.value = {
|
|
||||||
pay_period_no: timesheet_store.current_pay_period.pay_period_no,
|
|
||||||
pay_year: timesheet_store.current_pay_period.pay_year,
|
|
||||||
employee_email: auth_store.user.email,
|
|
||||||
is_approved: false,
|
|
||||||
expenses: [],
|
|
||||||
totals: {amount:0, mileage:0},
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
is_loading_expenses.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_save_expenses = async (payload: {
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
email: string;
|
|
||||||
expenses: TimesheetExpense[];
|
|
||||||
}) => {
|
|
||||||
is_loading_expenses.value = true;
|
|
||||||
expenses_error.value = null;
|
|
||||||
|
|
||||||
try{
|
|
||||||
const updated = await put_pay_period_expenses(
|
|
||||||
payload.email,
|
|
||||||
payload.pay_year,
|
|
||||||
payload.pay_period_no,
|
|
||||||
payload.expenses
|
|
||||||
);
|
|
||||||
expenses_data.value = updated;
|
|
||||||
|
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
|
||||||
|
|
||||||
show_expenses_dialog.value = false;
|
|
||||||
} catch(err) {
|
|
||||||
notify_error(err as any);
|
|
||||||
} finally {
|
|
||||||
is_loading_expenses.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_close_expenses = () => {
|
|
||||||
show_expenses_dialog.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const date_options: Intl.DateTimeFormatOptions = {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
};
|
|
||||||
|
|
||||||
const pay_period_label = computed(() => {
|
|
||||||
const label = timesheet_store.current_pay_period?.label ?? '';
|
|
||||||
const dates = label.split('.');
|
|
||||||
if ( dates.length < 2 ) {
|
|
||||||
return { start_date: '—', end_date:'—' }
|
|
||||||
}
|
|
||||||
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'));
|
|
||||||
return { start_date, end_date };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const is_calendar_limit = computed( () => {
|
const onSaveExpenses = async ( payload: { email: string; pay_year: number; pay_period_no: number; expenses: TimesheetExpense[] }) => {
|
||||||
return timesheet_store.current_pay_period.pay_year === 2024 &&
|
await expenses_store.saveExpenses({...payload, t});
|
||||||
timesheet_store.current_pay_period.pay_period_no <= 1;
|
await timesheet_store.refreshCurrentPeriodForUser(auth_store.user.email);
|
||||||
});
|
};
|
||||||
|
|
||||||
const SHIFT_KEY = ['REGULAR', 'EVENING', 'EMERGENCY', 'HOLIDAY', 'VACATION', 'SICK'] as const;
|
const onCloseExpenses = () => expenses_store.closeDialog();
|
||||||
const shift_options = computed(()=> {
|
|
||||||
void locale.value;
|
|
||||||
return SHIFT_KEY.map(key => ({ value: key, label: t(`timesheet.shift_types.${key}`)}))
|
|
||||||
});
|
|
||||||
|
|
||||||
|
//------------------- pay-period format label -------------------
|
||||||
|
const date_options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'long', year: 'numeric' };
|
||||||
|
|
||||||
|
const pay_period_label = computed(() => formatPayPeriodLabel(
|
||||||
|
timesheet_store.current_pay_period?.label,
|
||||||
|
locale.value,
|
||||||
|
date.extractDate,
|
||||||
|
date_options
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
//------------------- q-select Shift options -------------------
|
||||||
|
const shift_options = computed(() => buildShiftOptions(SHIFT_KEY, t));
|
||||||
|
|
||||||
|
//------------------- navigation by date -------------------
|
||||||
const onDateSelected = async (date_string: string) => {
|
const onDateSelected = async (date_string: string) => {
|
||||||
await timesheet_api.getTimesheetsByDate(date_string);
|
await timesheet_store.loadByIsoDate(date_string, auth_store.user.email);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadByDate = async (isoDate: string) => {
|
onMounted(async () => {
|
||||||
await timesheet_store.getPayPeriodByDate(isoDate);
|
await timesheet_store.loadToday(auth_store.user.email);
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted( async () => {
|
|
||||||
await loadByDate(date.formatDate(new Date(), 'YYYY-MM-DD' ));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ------------------- shifts -------------------
|
||||||
|
const onRequestAdd = ({ date }: { date: string }) => shift_store.openCreate(date);
|
||||||
|
const onRequestEdit = ({ date, shift }: { date: string; shift: any }) => shift_store.openEdit(date, shift);
|
||||||
|
const onRequestDelete = async ({ date, shift }: { date: string; shift: any }) => shift_store.openDelete(date, shift);
|
||||||
const onShiftSaved = async () => {
|
const onShiftSaved = async () => {
|
||||||
await timesheet_store.getTimesheetsByPayPeriodAndEmail(auth_store.user.email);
|
await timesheet_store.refreshCurrentPeriodForUser(auth_store.user.email);
|
||||||
}
|
|
||||||
|
|
||||||
type FormMode = 'create' | 'edit' | 'delete';
|
|
||||||
|
|
||||||
const is_dialog_open = ref<boolean>(false);
|
|
||||||
const form_mode = ref<FormMode>('create');
|
|
||||||
const selected_date = ref<string>('');
|
|
||||||
const old_shift_ref = ref<ShiftPayload | undefined>(undefined);
|
|
||||||
|
|
||||||
const open_create_dialog = (iso_date: string) => {
|
|
||||||
form_mode.value = 'create';
|
|
||||||
selected_date.value = iso_date;
|
|
||||||
old_shift_ref.value = undefined;
|
|
||||||
is_dialog_open.value = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const open_edit_dialog = (iso_date: string, shift: any) => {
|
|
||||||
form_mode.value = 'edit';
|
|
||||||
selected_date.value = iso_date;
|
|
||||||
is_dialog_open.value = true;
|
|
||||||
old_shift_ref.value = {
|
|
||||||
start_time: shift.start_time,
|
|
||||||
end_time: shift.end_time,
|
|
||||||
type: shift.type,
|
|
||||||
is_remote: !!shift.is_remote,
|
|
||||||
...(shift.comment ? { comment: String(shift.comment)} : {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const open_delete_dialog = (iso_date: string, shift: any) => {
|
|
||||||
form_mode.value = 'delete';
|
|
||||||
selected_date.value = iso_date;
|
|
||||||
old_shift_ref.value = {
|
|
||||||
start_time: shift.start_time,
|
|
||||||
end_time: shift.end_time,
|
|
||||||
type: shift.type,
|
|
||||||
is_remote: !!shift.is_remote,
|
|
||||||
...(shift.comment ? { comment: String(shift.comment)} : {}),
|
|
||||||
};
|
|
||||||
is_dialog_open.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const expenses_error = ref<string|null>(null);
|
|
||||||
|
|
||||||
const close_dialog = () => {
|
|
||||||
expenses_error.value = null;
|
|
||||||
is_dialog_open.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const on_request_add = ({ date }: { date: string }) => open_create_dialog(date);
|
|
||||||
const on_request_edit = ({ date, shift }: { date: string; shift: any }) => open_edit_dialog(date, shift);
|
|
||||||
const on_request_delete = async ({ date, shift }: { date: string; shift: any }) => open_delete_dialog(date, shift);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -226,16 +109,16 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
unelevated
|
unelevated
|
||||||
icon="receipt_long"
|
icon="receipt_long"
|
||||||
:label="$t('timesheet.expense.open_btn')"
|
:label="$t('timesheet.expense.open_btn')"
|
||||||
@click="open_expenses_dialog"
|
@click="openExpensesDialog"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-section class="row items-center justify-between q-px-md q-pb-none">
|
<q-card-section class="row items-center justify-between q-px-md q-pb-none">
|
||||||
<TimesheetNavigation
|
<TimesheetNavigation
|
||||||
:is-disabled="timesheet_store.is_loading"
|
:is-disabled="timesheet_store.is_loading"
|
||||||
:is-previous-limit="is_calendar_limit"
|
:is-previous-limit="timesheet_store.is_calendar_limit"
|
||||||
@date-selected="value => onDateSelected(value)"
|
@date-selected="onDateSelected"
|
||||||
@pressed-previous-button="timesheet_api.getPreviousPayPeriod()"
|
@pressed-previous-button="timesheet_api.getPreviousPeriodForUser(auth_store.user.email)"
|
||||||
@pressed-next-button="timesheet_api.getNextPayPeriod()"
|
@pressed-next-button="timesheet_api.getNextPeriodForUser(auth_store.user.email)"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- shift's colored legend -->
|
<!-- shift's colored legend -->
|
||||||
|
|
@ -244,12 +127,12 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
/>
|
/>
|
||||||
<q-card-section horizontal>
|
<q-card-section horizontal>
|
||||||
<!-- display of shifts for 2 timesheets -->
|
<!-- display of shifts for 2 timesheets -->
|
||||||
<TimesheetDetailsShifts
|
<DetailedShiftList
|
||||||
:raw-data="timesheet_store.pay_period_employee_details"
|
:raw-data="timesheet_store.pay_period_employee_details"
|
||||||
:current-pay-period="timesheet_store.current_pay_period"
|
:current-pay-period="timesheet_store.current_pay_period"
|
||||||
@request-add="on_request_add"
|
@request-add="onRequestAdd"
|
||||||
@request-edit="on_request_edit"
|
@request-edit="onRequestEdit"
|
||||||
@request-delete="on_request_delete"
|
@request-delete="onRequestDelete"
|
||||||
/>
|
/>
|
||||||
<q-inner-loading :showing="timesheet_store.is_loading" color="primary"/>
|
<q-inner-loading :showing="timesheet_store.is_loading" color="primary"/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
@ -257,48 +140,49 @@ const on_request_delete = async ({ date, shift }: { date: string; shift: any })
|
||||||
</div>
|
</div>
|
||||||
<!-- read/edit/create/delete expense dialog -->
|
<!-- read/edit/create/delete expense dialog -->
|
||||||
<q-dialog
|
<q-dialog
|
||||||
v-model="show_expenses_dialog"
|
v-model="expenses_store.is_dialog_open"
|
||||||
persistent
|
persistent
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="q-pa=md"
|
class="q-pa-md column"
|
||||||
style="min-width:900px; max-width: 95vw;"
|
style=" min-width: 70vw;"
|
||||||
>
|
>
|
||||||
<q-inner-loading :showing="is_loading_expenses">
|
<q-inner-loading :showing="expenses_store.is_loading">
|
||||||
<q-spinner size="32px"/>
|
<q-spinner size="32px"/>
|
||||||
</q-inner-loading>
|
</q-inner-loading>
|
||||||
<q-banner
|
|
||||||
|
<!-- <q-banner
|
||||||
v-if="expenses_error"
|
v-if="expenses_error"
|
||||||
dense
|
dense
|
||||||
class="bg-red-2 text-negative q-mt-sm"
|
class="bg-red-2 col-auto text-negative q-mt-sm"
|
||||||
>
|
>
|
||||||
{{ expenses_error }}
|
{{ expenses_error }}
|
||||||
</q-banner>
|
</q-banner> -->
|
||||||
|
|
||||||
<TimesheetDetailsExpenses
|
<TimesheetDetailsExpenses
|
||||||
v-if="expenses_data"
|
v-if="expenses_store.data"
|
||||||
:pay_period_no="expenses_data.pay_period_no"
|
:pay_period_no="expenses_store.data.pay_period_no"
|
||||||
:pay_year="expenses_data.pay_year"
|
:pay_year="expenses_store.data.pay_year"
|
||||||
:email="expenses_data.employee_email"
|
:email="expenses_store.data.employee_email"
|
||||||
:is_approved="expenses_data.is_approved"
|
:is_approved="expenses_store.data.is_approved"
|
||||||
:initial_expenses="expenses_data.expenses"
|
:initial_expenses="expenses_store.data.expenses"
|
||||||
@save="on_save_expenses"
|
@save="onSaveExpenses"
|
||||||
@close="on_close_expenses"
|
@close="onCloseExpenses"
|
||||||
@error=" "
|
@error=" "
|
||||||
/>
|
/>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
<!-- shift crud dialog -->
|
<!-- shift crud dialog -->
|
||||||
<ShiftCrudDialog
|
<ShiftCrudDialog
|
||||||
v-model="is_dialog_open"
|
v-model="shift_store.is_open"
|
||||||
:mode="form_mode"
|
:mode="shift_store.mode"
|
||||||
:date-iso="selected_date"
|
:date-iso="shift_store.date_iso"
|
||||||
:email="auth_store.user.email"
|
:email="auth_store.user.email"
|
||||||
:initial-shift="old_shift_ref || null"
|
:initial-shift="shift_store.initial_shift"
|
||||||
:shift-options="shift_options"
|
:shift-options="shift_options"
|
||||||
@close="close_dialog"
|
@close="shift_store.close"
|
||||||
@saved="onShiftSaved"
|
@saved="onShiftSaved"
|
||||||
/>
|
/>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
import type {Timesheet} from "src/modules/timesheets/types/timesheet-interface";
|
|
||||||
import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/timesheet-shifts-payload-interface";
|
|
||||||
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
|
import type { PayPeriod } from "src/modules/shared/types/pay-period-interface";
|
||||||
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 { PayPeriodOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-interface";
|
import type { PayPeriodOverview } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-overview-interface";
|
||||||
import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface";
|
import type { PayPeriodReportFilters } from "src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface";
|
||||||
|
import type { Timesheet } from "../types/timesheet.interfaces";
|
||||||
|
import type { CreateShiftPayload, CreateWeekShiftPayload } from "../types/shift.interfaces";
|
||||||
|
|
||||||
export const timesheetTempService = {
|
export const timesheetTempService = {
|
||||||
//GET
|
//GET
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
//mock data
|
|
||||||
34
src/modules/timesheets/types/expense-validation.interface.ts
Normal file
34
src/modules/timesheets/types/expense-validation.interface.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
export interface ApiErrorPayload {
|
||||||
|
status_code: number;
|
||||||
|
error_code?: string;
|
||||||
|
message?: string;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpensesValidationError extends Error {
|
||||||
|
status_code: number;
|
||||||
|
error_code?: string | undefined;
|
||||||
|
context?: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
constructor(payload: ApiErrorPayload) {
|
||||||
|
super(payload.message || 'Invalid expense payload');
|
||||||
|
this.name = 'ExpensesValidationError';
|
||||||
|
this.status_code = payload.status_code;
|
||||||
|
this.error_code = payload.error_code;
|
||||||
|
this.context = payload.context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpensesApiError extends Error {
|
||||||
|
status_code: number;
|
||||||
|
error_code?: string;
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
constructor(payload: ApiErrorPayload) {
|
||||||
|
super(payload.message || 'Request failed');
|
||||||
|
this.name = 'ExpensesApiError';
|
||||||
|
this.status_code = payload.status_code;
|
||||||
|
|
||||||
|
if(payload.error_code !== undefined) this.error_code = payload.error_code;
|
||||||
|
if(payload.context !== undefined) this.context = payload.context;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/modules/timesheets/types/expense.interfaces.ts
Normal file
63
src/modules/timesheets/types/expense.interfaces.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import type { ExpenseType } from "./expense.types";
|
||||||
|
|
||||||
|
export interface Expense {
|
||||||
|
is_approved: boolean;
|
||||||
|
comment: string;
|
||||||
|
amount: number;
|
||||||
|
supervisor_comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetExpense {
|
||||||
|
date: string;
|
||||||
|
type: string;
|
||||||
|
amount?: number;
|
||||||
|
mileage?: number;
|
||||||
|
comment?: string;
|
||||||
|
supervisor_comment?: string;
|
||||||
|
is_approved?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayPeriodExpenses {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
employee_email: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
totals: {
|
||||||
|
amount: number;
|
||||||
|
mileage: number;
|
||||||
|
reimbursable_total?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//used by expenses form, either amount or mileage, not both will be sent to the backend
|
||||||
|
export interface ExpensePayload{
|
||||||
|
date: string;
|
||||||
|
type: ExpenseType;
|
||||||
|
amount?: number;
|
||||||
|
mileage?: number;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
//amount is required since mileage is returned in $ ( km * modifier )
|
||||||
|
export interface ExpenseDay{
|
||||||
|
date: string;
|
||||||
|
type: ExpenseType;
|
||||||
|
amount: number;
|
||||||
|
mileage?: number;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertExpensesBody {
|
||||||
|
expenses: ExpensePayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertExpensesResponse {
|
||||||
|
data: PayPeriodExpenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertExpenseResult {
|
||||||
|
action: 'created' | 'updated' | 'deleted';
|
||||||
|
day: ExpenseDay[];
|
||||||
|
}
|
||||||
29
src/modules/timesheets/types/expense.types.ts
Normal file
29
src/modules/timesheets/types/expense.types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import type { TimesheetExpense } from "./expense.interfaces";
|
||||||
|
|
||||||
|
export const EXPENSE_TYPE = [
|
||||||
|
'PER_DIEM',
|
||||||
|
'MILEAGE',
|
||||||
|
'EXPENSES',
|
||||||
|
'PRIME_GARDE',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ExpenseType = (typeof EXPENSE_TYPE)[number];
|
||||||
|
|
||||||
|
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
||||||
|
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = [
|
||||||
|
'PER_DIEM',
|
||||||
|
'EXPENSES',
|
||||||
|
'PRIME_GARDE',
|
||||||
|
];
|
||||||
|
|
||||||
|
export type ExpenseTotals = {
|
||||||
|
amount: number;
|
||||||
|
mileage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExpenseSavePayload = {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
email: string;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
};
|
||||||
11
src/modules/timesheets/types/shift.defaults.ts
Normal file
11
src/modules/timesheets/types/shift.defaults.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Shift } from "./shift.interfaces";
|
||||||
|
|
||||||
|
export const default_shift: Readonly<Shift> = {
|
||||||
|
date: '',
|
||||||
|
start_time: '--:--',
|
||||||
|
end_time: '--:--',
|
||||||
|
type:'REGULAR',
|
||||||
|
comment: '',
|
||||||
|
is_approved: false,
|
||||||
|
is_remote: false,
|
||||||
|
};
|
||||||
45
src/modules/timesheets/types/shift.interfaces.ts
Normal file
45
src/modules/timesheets/types/shift.interfaces.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import type { ShiftKey, ShiftPayload } from "./shift.types";
|
||||||
|
import type { UpsertAction } from "./ui.types";
|
||||||
|
|
||||||
|
export interface Shift {
|
||||||
|
date: string;
|
||||||
|
type: ShiftKey;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateShiftPayload {
|
||||||
|
date: string;
|
||||||
|
type: ShiftKey;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
comment?: string;
|
||||||
|
is_remote?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateWeekShiftPayload {
|
||||||
|
shifts: CreateShiftPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertShiftsBody {
|
||||||
|
old_shift?: ShiftPayload;
|
||||||
|
new_shift?: ShiftPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayShift {
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
type: string;
|
||||||
|
is_remote: boolean;
|
||||||
|
comment?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertShiftsResponse {
|
||||||
|
action: UpsertAction;
|
||||||
|
day: DayShift[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
27
src/modules/timesheets/types/shift.types.ts
Normal file
27
src/modules/timesheets/types/shift.types.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
export const SHIFT_KEY = [
|
||||||
|
'REGULAR',
|
||||||
|
'EVENING',
|
||||||
|
'EMERGENCY',
|
||||||
|
'HOLIDAY',
|
||||||
|
'VACATION',
|
||||||
|
'SICK'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ShiftKey = typeof SHIFT_KEY[number];
|
||||||
|
|
||||||
|
export type ShiftSelectOption = { value: ShiftKey; label: string };
|
||||||
|
|
||||||
|
export type ShiftPayload = {
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
type: ShiftKey;
|
||||||
|
is_remote: boolean;
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShiftLegendItem = {
|
||||||
|
type: 'REGULAR'|'EVENING'|'EMERGENCY'|'OVERTIME'|'VACATION'|'HOLIDAY'|'SICK';
|
||||||
|
color: string;
|
||||||
|
label_key: string;
|
||||||
|
text_color?: string;
|
||||||
|
};
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import type { Shift } from "./timesheet-shift-interface";
|
|
||||||
|
|
||||||
export interface TimesheetDetailsWeek {
|
|
||||||
is_approved: boolean;
|
|
||||||
shifts: WeekDay<TimesheetDetailsDailySchedule>;
|
|
||||||
expenses: WeekDay<TimesheetDetailsDailyExpenses>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimesheetDetailsDailySchedule {
|
|
||||||
shifts: Shift[];
|
|
||||||
regular_hours: number;
|
|
||||||
evening_hours: number;
|
|
||||||
emergency_hours: number;
|
|
||||||
overtime_hours: number;
|
|
||||||
comment: string;
|
|
||||||
short_date: string; // ex. 08/24
|
|
||||||
break_duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Expense {
|
|
||||||
is_approved: boolean;
|
|
||||||
comment: string;
|
|
||||||
supervisor_comment: string;
|
|
||||||
amount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WeekDay<T> = {
|
|
||||||
sun: T;
|
|
||||||
mon: T;
|
|
||||||
tue: T;
|
|
||||||
wed: T;
|
|
||||||
thu: T;
|
|
||||||
fri: T;
|
|
||||||
sat: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface TimesheetDetailsDailyExpenses {
|
|
||||||
cash: Expense[];
|
|
||||||
km: Expense[];
|
|
||||||
[otherType: string]: Expense[]; //for possible future types of expenses
|
|
||||||
}
|
|
||||||
|
|
||||||
//employee timesheet template
|
|
||||||
export interface EmployeeTimesheetDetailsWeek {
|
|
||||||
is_approved: boolean;
|
|
||||||
shifts: WeekDay<TimesheetDetailsDailySchedule>;
|
|
||||||
expenses: WeekDay<TimesheetDetailsDailyExpenses>;
|
|
||||||
}
|
|
||||||
// empty default builder
|
|
||||||
const makeWeek = <T>(factory: () => T): WeekDay<T> => ({
|
|
||||||
sun: factory(),
|
|
||||||
mon: factory(),
|
|
||||||
tue: factory(),
|
|
||||||
wed: factory(),
|
|
||||||
thu: factory(),
|
|
||||||
fri: factory(),
|
|
||||||
sat: factory(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDailySchedule = (): TimesheetDetailsDailySchedule => ({
|
|
||||||
shifts: [],
|
|
||||||
regular_hours: 0,
|
|
||||||
evening_hours: 0,
|
|
||||||
emergency_hours: 0,
|
|
||||||
overtime_hours: 0,
|
|
||||||
comment: "",
|
|
||||||
short_date: "",
|
|
||||||
break_duration: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emptyDailyExpenses = (): TimesheetDetailsDailyExpenses => ({
|
|
||||||
cash: [],
|
|
||||||
km: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const default_timesheet_details_week = (): TimesheetDetailsWeek => ({
|
|
||||||
is_approved: false,
|
|
||||||
shifts: makeWeek(emptyDailySchedule),
|
|
||||||
expenses: makeWeek(emptyDailyExpenses),
|
|
||||||
});
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
export interface TimesheetExpense {
|
|
||||||
date: string;
|
|
||||||
amount?: number;
|
|
||||||
mileage?: number;
|
|
||||||
comment?: string;
|
|
||||||
supervisor_comment?: string;
|
|
||||||
is_approved?: boolean;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EXPENSE_TYPE = [
|
|
||||||
'PER_DIEM',
|
|
||||||
'MILEAGE',
|
|
||||||
'EXPENSES',
|
|
||||||
'PRIME_DISPO',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type ExpenseType = typeof EXPENSE_TYPE[number];
|
|
||||||
|
|
||||||
export const TYPES_WITH_MILEAGE_ONLY: Readonly<ExpenseType[]> = ['MILEAGE'];
|
|
||||||
export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXPENSES', 'PRIME_DISPO']
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import type { TimesheetExpense } from "./timesheet-expenses-interface";
|
|
||||||
|
|
||||||
export interface PayPeriodExpenses {
|
|
||||||
pay_period_no: number;
|
|
||||||
pay_year: number;
|
|
||||||
employee_email: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
expenses: TimesheetExpense[];
|
|
||||||
totals: {
|
|
||||||
amount: number;
|
|
||||||
mileage: number;
|
|
||||||
reimbursable_total?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
export interface Timesheet {
|
|
||||||
is_approved: boolean;
|
|
||||||
start_day: string;
|
|
||||||
end_day: string;
|
|
||||||
label: string;
|
|
||||||
shifts: Shifts[];
|
|
||||||
expenses: Expenses[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type Shifts = {
|
|
||||||
bank_type: string;
|
|
||||||
date: string;
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
comment: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
is_remote: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Expenses = {
|
|
||||||
bank_type: string;
|
|
||||||
date: string;
|
|
||||||
amount: number;
|
|
||||||
km: number;
|
|
||||||
comment: string;
|
|
||||||
supervisor_comment: string;
|
|
||||||
is_approved: boolean;
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { default_timesheet_details_week, type TimesheetDetailsWeek } from "./timesheet-details-interface";
|
|
||||||
|
|
||||||
export interface TimesheetPayPeriodDetailsOverview {
|
|
||||||
week1: TimesheetDetailsWeek;
|
|
||||||
week2: TimesheetDetailsWeek;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const default_pay_period_employee_details = {
|
|
||||||
week1: default_timesheet_details_week(),
|
|
||||||
week2: default_timesheet_details_week(),
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
export interface Shift {
|
|
||||||
date : string;
|
|
||||||
type : string;
|
|
||||||
start_time : string;
|
|
||||||
end_time : string;
|
|
||||||
comment : string;
|
|
||||||
is_approved: boolean;
|
|
||||||
is_remote : boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const default_shift: Shift = {
|
|
||||||
date: '',
|
|
||||||
start_time: '--:--',
|
|
||||||
end_time: '--:--',
|
|
||||||
type: '',
|
|
||||||
comment: '',
|
|
||||||
is_approved: false,
|
|
||||||
is_remote: false,
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export interface CreateShiftPayload {
|
|
||||||
date: string;
|
|
||||||
type: string;
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
comment?: string;
|
|
||||||
is_remote?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CreateWeekShiftPayload {
|
|
||||||
shifts: CreateShiftPayload[];
|
|
||||||
}
|
|
||||||
39
src/modules/timesheets/types/timesheet.defaults.ts
Normal file
39
src/modules/timesheets/types/timesheet.defaults.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { WeekDay } from "./timesheet.types";
|
||||||
|
import type {
|
||||||
|
TimesheetDetailsDailyExpenses,
|
||||||
|
TimesheetDetailsDailySchedule,
|
||||||
|
TimesheetDetailsWeek
|
||||||
|
} from "./timesheet.interfaces";
|
||||||
|
|
||||||
|
const makeWeek = <T>(factory: ()=> T): WeekDay<T> => ({
|
||||||
|
sun: factory(),
|
||||||
|
mon: factory(),
|
||||||
|
tue: factory(),
|
||||||
|
wed: factory(),
|
||||||
|
thu: factory(),
|
||||||
|
fri: factory(),
|
||||||
|
sat: factory(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyDailySchedule = (): TimesheetDetailsDailySchedule => ({
|
||||||
|
shifts: [],
|
||||||
|
regular_hours: 0,
|
||||||
|
evening_hours: 0,
|
||||||
|
emergency_hours: 0,
|
||||||
|
overtime_hours: 0,
|
||||||
|
total_hours: 0,
|
||||||
|
comment: "",
|
||||||
|
short_date: "",
|
||||||
|
break_duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyDailyExpenses = (): TimesheetDetailsDailyExpenses => ({
|
||||||
|
cash: [],
|
||||||
|
km: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultTimesheetDetailsWeek = (): TimesheetDetailsWeek => ({
|
||||||
|
is_approved: false,
|
||||||
|
shifts: makeWeek(emptyDailySchedule),
|
||||||
|
expenses: makeWeek(emptyDailyExpenses),
|
||||||
|
});
|
||||||
54
src/modules/timesheets/types/timesheet.interfaces.ts
Normal file
54
src/modules/timesheets/types/timesheet.interfaces.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import type { Shift } from "./shift.interfaces";
|
||||||
|
import type {
|
||||||
|
TimesheetExpenseEntry,
|
||||||
|
TimesheetShiftEntry,
|
||||||
|
WeekDay
|
||||||
|
} from "./timesheet.types";
|
||||||
|
|
||||||
|
|
||||||
|
export interface Timesheet {
|
||||||
|
is_approved: boolean;
|
||||||
|
start_day: string;
|
||||||
|
end_day: string;
|
||||||
|
label: string;
|
||||||
|
shifts: TimesheetShiftEntry[];
|
||||||
|
expenses: TimesheetExpenseEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDetailsWeek {
|
||||||
|
is_approved: boolean;
|
||||||
|
shifts: WeekDay<TimesheetDetailsDailySchedule>
|
||||||
|
expenses: WeekDay<TimesheetDetailsDailyExpenses>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDetailsDailySchedule {
|
||||||
|
shifts: Shift[];
|
||||||
|
regular_hours: number;
|
||||||
|
evening_hours: number;
|
||||||
|
emergency_hours: number;
|
||||||
|
overtime_hours: number;
|
||||||
|
total_hours: number;
|
||||||
|
comment: string;
|
||||||
|
short_date: string;
|
||||||
|
break_duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailyExpense {
|
||||||
|
is_approved: boolean;
|
||||||
|
comment: string;
|
||||||
|
amount: number;
|
||||||
|
supervisor_comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimesheetDetailsDailyExpenses {
|
||||||
|
cash: DailyExpense[];
|
||||||
|
km: DailyExpense[];
|
||||||
|
[otherType: string]: DailyExpense[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface TimesheetPayPeriodDetailsOverview {
|
||||||
|
week1: TimesheetDetailsWeek;
|
||||||
|
week2: TimesheetDetailsWeek;
|
||||||
|
}
|
||||||
|
|
||||||
29
src/modules/timesheets/types/timesheet.types.ts
Normal file
29
src/modules/timesheets/types/timesheet.types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export type TimesheetShiftEntry = {
|
||||||
|
bank_type: string;
|
||||||
|
date: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimesheetExpenseEntry = {
|
||||||
|
bank_type: string;
|
||||||
|
date: string;
|
||||||
|
amount: number;
|
||||||
|
km: number;
|
||||||
|
comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
supervisor_comment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WeekDay<T> = {
|
||||||
|
sun: T;
|
||||||
|
mon: T;
|
||||||
|
tue: T;
|
||||||
|
wed: T;
|
||||||
|
thu: T;
|
||||||
|
fri: T;
|
||||||
|
sat: T;
|
||||||
|
};
|
||||||
8
src/modules/timesheets/types/ui.types.ts
Normal file
8
src/modules/timesheets/types/ui.types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export type FormMode = 'create' | 'edit' | 'delete';
|
||||||
|
|
||||||
|
export type PayPeriodLabel = {
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertAction = 'created' | 'updated' | 'deleted';
|
||||||
76
src/modules/timesheets/utils/expense.util.ts
Normal file
76
src/modules/timesheets/utils/expense.util.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
|
import type { ExpenseSavePayload, ExpenseTotals, ExpenseType } from "../types/expense.types";
|
||||||
|
/* eslint-disable */
|
||||||
|
//------------------ normalization / icons ------------------
|
||||||
|
export const normExpenseType = (type: unknown): string =>
|
||||||
|
String(type ?? '').trim().toUpperCase();
|
||||||
|
|
||||||
|
const icon_map: Record<string,string> = {
|
||||||
|
MILEAGE: 'time_to_leave',
|
||||||
|
EXPENSES: 'receipt_long',
|
||||||
|
PER_DIEM: 'hotel',
|
||||||
|
PRIME_GARDE: 'admin_panel_settings',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expenseTypeIcon = (type: unknown): string => {
|
||||||
|
const t = normExpenseType(type);
|
||||||
|
return (
|
||||||
|
icon_map[t.toLowerCase()] ??
|
||||||
|
icon_map[t.replace('-','_').toLowerCase()] ??
|
||||||
|
'help_outline'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//------------------ q-select options ------------------
|
||||||
|
export const buildExpenseTypeOptions = ( types: readonly ExpenseType[], t: (key:string) => string):
|
||||||
|
{ label: string; value: ExpenseType } [] =>
|
||||||
|
types.map((val)=> ({
|
||||||
|
label: t(`timesheet.expense.types.${val}`),
|
||||||
|
value: val,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//------------------ totals ------------------
|
||||||
|
export const computeExpenseTotals = (items: readonly TimesheetExpense[]): ExpenseTotals =>
|
||||||
|
items.reduce<ExpenseTotals>(
|
||||||
|
(acc, e) => ({
|
||||||
|
amount: acc.amount + (Number(e.amount) || 0),
|
||||||
|
mileage: acc.mileage + (Number(e.mileage) || 0),
|
||||||
|
}),
|
||||||
|
{ amount: 0, mileage: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
//------------------ Quasar :rules=[] ------------------
|
||||||
|
export const makeExpenseRules = (t: (key: string) => string, max_comment_char: number) => {
|
||||||
|
const isPresent = (val: unknown) => val !== undefined && val !== null && val !== '';
|
||||||
|
|
||||||
|
const typeRequired = (val: unknown) => (!!val) || t('timesheet.expense.errors.type_required');
|
||||||
|
|
||||||
|
const amountRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.amount_required_for_type');
|
||||||
|
|
||||||
|
const mileageRequired = (val: unknown) => (isPresent(val)) || t('timesheet.expense.errors.mileage_required_for_type');
|
||||||
|
|
||||||
|
const commentRequired = (val: unknown) => (String(val ?? '').trim().length > 0) || t('timesheet.expense.errors.comment_required');
|
||||||
|
|
||||||
|
const commentTooLong = (val: unknown) => (String(val ?? '').trim().length <= max_comment_char) || t('timesheet.expense.errors.comment_too_long');
|
||||||
|
|
||||||
|
return {
|
||||||
|
typeRequired,
|
||||||
|
amountRequired,
|
||||||
|
mileageRequired,
|
||||||
|
commentRequired,
|
||||||
|
commentTooLong,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
//------------------ saving payload ------------------
|
||||||
|
export const buildExpenseSavePayload = (args: {
|
||||||
|
pay_period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
email: string;
|
||||||
|
expenses: TimesheetExpense[];
|
||||||
|
}): ExpenseSavePayload => ({
|
||||||
|
pay_period_no: args.pay_period_no,
|
||||||
|
pay_year: args.pay_year,
|
||||||
|
email: args.email,
|
||||||
|
expenses: args.expenses,
|
||||||
|
});
|
||||||
|
|
@ -1,31 +1,11 @@
|
||||||
import { type ExpenseType, type TimesheetExpense, TYPES_WITH_AMOUNT_ONLY, TYPES_WITH_MILEAGE_ONLY } from "../types/timesheet-expenses-interface";
|
import { COMMENT_MAX_LENGTH, DATE_FORMAT_PATTERN } from "../constants/expense.constants";
|
||||||
|
import { ExpensesValidationError } from "../types/expense-validation.interface";
|
||||||
|
import type { TimesheetExpense } from "../types/expense.interfaces";
|
||||||
export const DATE_FORMAT_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
import {
|
||||||
export const COMMENT_MAX_LENGTH = 512 as const;
|
type ExpenseType,
|
||||||
|
TYPES_WITH_AMOUNT_ONLY,
|
||||||
|
TYPES_WITH_MILEAGE_ONLY
|
||||||
//errors handling
|
} from "../types/expense.types";
|
||||||
export interface ApiErrorPayload {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string;
|
|
||||||
message?: string;
|
|
||||||
context?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExpensesValidationError extends Error {
|
|
||||||
status_code: number;
|
|
||||||
error_code?: string | undefined;
|
|
||||||
context?: Record<string, unknown> | undefined;
|
|
||||||
|
|
||||||
constructor(payload: ApiErrorPayload) {
|
|
||||||
super(payload.message || 'Invalid expense payload');
|
|
||||||
this.name = 'ExpensesValidationError';
|
|
||||||
this.status_code = payload.status_code;
|
|
||||||
this.error_code = payload.error_code;
|
|
||||||
this.context = payload.context;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//normalization helpers
|
//normalization helpers
|
||||||
export const toNumOrUndefined = (value: unknown): number | undefined => {
|
export const toNumOrUndefined = (value: unknown): number | undefined => {
|
||||||
|
|
@ -34,21 +14,21 @@ export const toNumOrUndefined = (value: unknown): number | undefined => {
|
||||||
return Number.isFinite(num) ? num : undefined;
|
return Number.isFinite(num) ? num : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalize_comment = (input?: string): string | undefined => {
|
export const normalizeComment = (input?: string): string | undefined => {
|
||||||
if(typeof input === 'undefined' || input === null) return undefined;
|
if(typeof input === 'undefined' || input === null) return undefined;
|
||||||
const trimmed = String(input).trim();
|
const trimmed = String(input).trim();
|
||||||
return trimmed.length ? trimmed : undefined;
|
return trimmed.length ? trimmed : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const normalize_type = (input: string): string => (input ?? '').trim().toUpperCase();
|
export const normalizeType = (input: string): string => (input ?? '').trim().toUpperCase();
|
||||||
|
|
||||||
export const normalize_expense = (expense: TimesheetExpense): TimesheetExpense => {
|
export const normalizeExpense = (expense: TimesheetExpense): TimesheetExpense => {
|
||||||
const comment = normalize_comment(expense.comment);
|
const comment = normalizeComment(expense.comment);
|
||||||
const amount = toNumOrUndefined(expense.amount);
|
const amount = toNumOrUndefined(expense.amount);
|
||||||
const mileage = toNumOrUndefined(expense.mileage);
|
const mileage = toNumOrUndefined(expense.mileage);
|
||||||
return {
|
return {
|
||||||
date: (expense.date ?? '').trim(),
|
date: (expense.date ?? '').trim(),
|
||||||
type: normalize_type(expense.type),
|
type: normalizeType(expense.type),
|
||||||
...(amount !== undefined ? { amount } : {}),
|
...(amount !== undefined ? { amount } : {}),
|
||||||
...(mileage !== undefined ? { mileage } : {}),
|
...(mileage !== undefined ? { mileage } : {}),
|
||||||
...(comment !== undefined ? { comment } : {}),
|
...(comment !== undefined ? { comment } : {}),
|
||||||
|
|
@ -60,8 +40,8 @@ export const normalize_expense = (expense: TimesheetExpense): TimesheetExpense =
|
||||||
};
|
};
|
||||||
|
|
||||||
//UI validation error messages
|
//UI validation error messages
|
||||||
export const validate_expense_UI = (raw: TimesheetExpense, label: string = 'expense'): void => {
|
export const validateExpenseUI = (raw: TimesheetExpense, label: string = 'expense'): void => {
|
||||||
const expense = normalize_expense(raw);
|
const expense = normalizeExpense(raw);
|
||||||
|
|
||||||
//Date input validation
|
//Date input validation
|
||||||
if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
if(!DATE_FORMAT_PATTERN.test(expense.date)) {
|
||||||
|
|
@ -135,14 +115,3 @@ export const validate_expense_UI = (raw: TimesheetExpense, label: string = 'expe
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//totals per pay-period
|
|
||||||
export const compute_expense_totals = (items: TimesheetExpense[]) => items.reduce(
|
|
||||||
(acc, raw) => {
|
|
||||||
const expense = normalize_expense(raw);
|
|
||||||
if(typeof expense.amount === 'number' && expense.amount > 0) acc.amount += expense.amount;
|
|
||||||
if(typeof expense.mileage === 'number' && expense.mileage > 0) acc.mileage += expense.mileage;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ amount: 0, mileage: 0 }
|
|
||||||
);
|
|
||||||
19
src/modules/timesheets/utils/shift.util.ts
Normal file
19
src/modules/timesheets/utils/shift.util.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type { ShiftKey, ShiftPayload, ShiftSelectOption } from "../types/shift.types";
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
export const toShiftPayload = (shift: any): ShiftPayload => ({
|
||||||
|
start_time: String(shift.start_time),
|
||||||
|
end_time: String(shift.end_time),
|
||||||
|
type: String(shift.type).toUpperCase() as ShiftKey,
|
||||||
|
is_remote: !!shift.is_remote,
|
||||||
|
...(shift.comment ? { comment: String(shift.comment) } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const buildShiftOptions = (
|
||||||
|
keys: readonly string[],
|
||||||
|
t:(k: string) => string
|
||||||
|
): ShiftSelectOption[] =>
|
||||||
|
keys.map((key) => ({
|
||||||
|
value: key as any,
|
||||||
|
label: t(`timesheet.shift.types.${key}`),
|
||||||
|
}));
|
||||||
17
src/modules/timesheets/utils/timesheet-format.util.ts
Normal file
17
src/modules/timesheets/utils/timesheet-format.util.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import type { PayPeriodLabel } from "../types/ui.types";
|
||||||
|
|
||||||
|
export const formatPayPeriodLabel = (
|
||||||
|
raw_label: string | undefined,
|
||||||
|
locale: string,
|
||||||
|
extractDate: (_input: string, _mask: string) => Date,
|
||||||
|
opts: Intl.DateTimeFormatOptions
|
||||||
|
): PayPeriodLabel => {
|
||||||
|
const label = raw_label ?? '';
|
||||||
|
const dates = label.split('.');
|
||||||
|
if(dates.length < 2) return { start_date: '—', end_date:'—' };
|
||||||
|
|
||||||
|
const fmt = new Intl.DateTimeFormat(locale, opts);
|
||||||
|
const start = fmt.format(extractDate(dates[0]!, 'YYYY-MM-DD'));
|
||||||
|
const end = fmt.format(extractDate(dates[1]!, 'YYYY-MM-DD'));
|
||||||
|
return { start_date: start, end_date: end };
|
||||||
|
}
|
||||||
|
|
@ -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' };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
88
src/stores/expense-store.ts
Normal file
88
src/stores/expense-store.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { type PayPeriodExpenses } from "src/modules/timesheets/types/expense.interfaces";
|
||||||
|
import { ExpensesApiError } from "src/modules/timesheets/types/expense-validation.interface";
|
||||||
|
import { getPayPeriodExpenses, putPayPeriodExpenses } from "src/modules/timesheets/composables/api/use-expense-api";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
|
const is_dialog_open = ref(false);
|
||||||
|
const is_loading = ref(false);
|
||||||
|
const data = ref<PayPeriodExpenses | null>(null);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const setErrorFrom = (err: unknown, t?: (_key: string) => string) => {
|
||||||
|
const e = err as any;
|
||||||
|
error.value = (err instanceof ExpensesApiError && t
|
||||||
|
? t(e.message): undefined)
|
||||||
|
|| e?.message
|
||||||
|
|| 'Unknown error';
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDialog = async (
|
||||||
|
params: { email: string; pay_year: number; pay_period_no: number; t?: (_key: string)=> string}) => {
|
||||||
|
is_dialog_open.value = true;
|
||||||
|
is_loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
try {
|
||||||
|
const response = await getPayPeriodExpenses(
|
||||||
|
params.email,
|
||||||
|
params.pay_year,
|
||||||
|
params.pay_period_no,
|
||||||
|
);
|
||||||
|
data.value = response;
|
||||||
|
} catch (err) {
|
||||||
|
setErrorFrom(err, params.t);
|
||||||
|
data.value = {
|
||||||
|
pay_period_no: params.pay_period_no,
|
||||||
|
pay_year: params.pay_year,
|
||||||
|
employee_email: params.email,
|
||||||
|
is_approved: false,
|
||||||
|
expenses: [],
|
||||||
|
totals: { amount: 0, mileage: 0},
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveExpenses = async (payload: {
|
||||||
|
email: string;
|
||||||
|
pay_year: number;
|
||||||
|
pay_period_no: number;
|
||||||
|
expenses: any[]; t?: (_key: string) => string
|
||||||
|
}) => {
|
||||||
|
is_loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await putPayPeriodExpenses(
|
||||||
|
payload.email,
|
||||||
|
payload.pay_year,
|
||||||
|
payload.pay_period_no,
|
||||||
|
payload.expenses
|
||||||
|
);
|
||||||
|
data.value = updated;
|
||||||
|
is_dialog_open.value = false;
|
||||||
|
} catch (err) {
|
||||||
|
setErrorFrom(err, payload.t);
|
||||||
|
} finally {
|
||||||
|
is_loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
error.value = null;
|
||||||
|
is_dialog_open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_dialog_open,
|
||||||
|
is_loading,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
openDialog,
|
||||||
|
saveExpenses,
|
||||||
|
closeDialog,
|
||||||
|
};
|
||||||
|
});
|
||||||
50
src/stores/shift-store.ts
Normal file
50
src/stores/shift-store.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { toShiftPayload } from "src/modules/timesheets/utils/shift.util";
|
||||||
|
import type { FormMode } from "src/modules/timesheets/types/ui.types";
|
||||||
|
import type { ShiftPayload } from "src/modules/timesheets/types/shift.types";
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
export const useShiftStore = defineStore('shift', () => {
|
||||||
|
const is_open = ref(false);
|
||||||
|
const mode = ref<FormMode>('create');
|
||||||
|
const date_iso = ref<string>('');
|
||||||
|
const initial_shift = ref<ShiftPayload | null>(null);
|
||||||
|
|
||||||
|
const open = (nextMode: FormMode, date: string, payload: ShiftPayload | null) => {
|
||||||
|
mode.value = nextMode;
|
||||||
|
date_iso.value = date;
|
||||||
|
initial_shift.value = payload;
|
||||||
|
is_open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreate = (date: string) => {
|
||||||
|
open('create', date, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = (date: string, shift: any) => {
|
||||||
|
open('edit', date, toShiftPayload(shift as any));
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDelete = (date: string, shift: any) => {
|
||||||
|
open('delete', date, toShiftPayload(shift as any));
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
is_open.value = false;
|
||||||
|
mode.value = 'create';
|
||||||
|
date_iso.value = '';
|
||||||
|
initial_shift.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_open,
|
||||||
|
mode,
|
||||||
|
date_iso,
|
||||||
|
initial_shift,
|
||||||
|
openCreate,
|
||||||
|
openEdit,
|
||||||
|
openDelete,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
|
import { date } from 'quasar';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
|
import { timesheetApprovalService } from 'src/modules/timesheet-approval/services/services-timesheet-approval';
|
||||||
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
|
import { timesheetTempService } from 'src/modules/timesheets/services/timesheet-services';
|
||||||
import { default_pay_period_employee_details, type PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
import { default_pay_period_employee_details, type PayPeriodEmployeeDetails } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-employee-details-interface';
|
||||||
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
import type { PayPeriod } from 'src/modules/shared/types/pay-period-interface';
|
||||||
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 { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface';
|
import type { PayPeriodReportFilters } from 'src/modules/timesheet-approval/types/timesheet-approval-pay-period-report-interface';
|
||||||
import type { Timesheet } from 'src/modules/timesheets/types/timesheet-interface';
|
import type { Timesheet } from 'src/modules/timesheets/types/timesheet.interfaces';
|
||||||
import type { CreateShiftPayload } from 'src/modules/timesheets/types/timesheet-shifts-payload-interface';
|
import type { CreateShiftPayload } from 'src/modules/timesheets/types/shift.interfaces';
|
||||||
|
|
||||||
const default_pay_period: PayPeriod = {
|
const default_pay_period: PayPeriod = {
|
||||||
pay_period_no: -1,
|
pay_period_no: -1,
|
||||||
|
|
@ -127,6 +128,33 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const loadByIsoDate = async (iso_date: string, employee_email: string) => {
|
||||||
|
const ok = await getPayPeriodByDate(iso_date);
|
||||||
|
if(ok) {
|
||||||
|
await getTimesheetsByPayPeriodAndEmail(employee_email);
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
};
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const is_calendar_limit = computed(()=>
|
||||||
|
current_pay_period.value.pay_year === 2024 &&
|
||||||
|
current_pay_period.value.pay_period_no <= 1
|
||||||
|
);
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const refreshCurrentPeriodForUser = async (employee_email: string) => {
|
||||||
|
await getTimesheetsByPayPeriodAndEmail(employee_email);
|
||||||
|
};
|
||||||
|
|
||||||
|
//employee timesheet
|
||||||
|
const loadToday = async (employee_email: string) => {
|
||||||
|
const today = date.formatDate(new Date(), 'YYYY-MM-DD');
|
||||||
|
return loadByIsoDate(today, employee_email);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
|
const getTimesheetsByPayPeriodAndEmail = async (employee_email: string) => {
|
||||||
is_loading.value = true;
|
is_loading.value = true;
|
||||||
|
|
||||||
|
|
@ -170,6 +198,10 @@ export const useTimesheetStore = defineStore('timesheet', () => {
|
||||||
pay_period_employee_details,
|
pay_period_employee_details,
|
||||||
current_timesheet,
|
current_timesheet,
|
||||||
is_loading,
|
is_loading,
|
||||||
|
is_calendar_limit,
|
||||||
|
loadByIsoDate,
|
||||||
|
refreshCurrentPeriodForUser,
|
||||||
|
loadToday,
|
||||||
getPayPeriodByDate,
|
getPayPeriodByDate,
|
||||||
getTimesheetByEmail,
|
getTimesheetByEmail,
|
||||||
createTimesheetShifts,
|
createTimesheetShifts,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
src/utils/with-loading.util.ts
Normal file
10
src/utils/with-loading.util.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
|
||||||
|
export const withLoading = async <T>(loading_ref: Ref<boolean>, fn: ()=> Promise<T>): Promise<T> => {
|
||||||
|
loading_ref.value = true;
|
||||||
|
try{
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
loading_ref.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user