Merge pull request 'dev/nicolas/timesheet-gui-refactor' (#25) from dev/nicolas/timesheet-gui-refactor into main
Reviewed-on: Targo/targo_frontend#25
This commit is contained in:
commit
1dc001af49
|
|
@ -105,7 +105,7 @@ export default defineConfig((ctx) => {
|
||||||
notify: {
|
notify: {
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
dark: false,
|
dark: 'auto',
|
||||||
},
|
},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 242 KiB |
|
|
@ -15,15 +15,16 @@
|
||||||
$primary : #30303A;
|
$primary : #30303A;
|
||||||
$secondary : #DAE0E7;
|
$secondary : #DAE0E7;
|
||||||
$accent : #0c9a3b;
|
$accent : #0c9a3b;
|
||||||
|
$accent2 : #0a7d32;
|
||||||
|
|
||||||
$dark-shadow-color : #173625;
|
$dark-shadow-color : #173625;
|
||||||
|
|
||||||
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
$elevation-dark-umbra : rgba($dark-shadow-color, 1);
|
||||||
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.5);
|
$elevation-dark-penumbra : rgba($dark-shadow-color, 0.75);
|
||||||
$elevation-dark-ambient : rgba($dark-shadow-color, 0.3);
|
$elevation-dark-ambient : rgba($dark-shadow-color, 0.53);
|
||||||
|
|
||||||
$dark-shadow-2 : 2px 3px $elevation-dark-umbra, 2px 3px 6px $elevation-dark-penumbra, 2px 3px 14px $elevation-dark-ambient;
|
$dark-shadow-2 : 2px 3px $elevation-dark-umbra, 2px 3px 6px $elevation-dark-penumbra, 2px 3px 14px $elevation-dark-ambient;
|
||||||
$layout-shadow-dark : 0 0 10px 5px rgba($dark-shadow-color, 0.5);
|
$layout-shadow-dark : 0 0 5px 5px rgba($dark-shadow-color, 0.5);
|
||||||
|
|
||||||
$input-text-color : #455A64;
|
$input-text-color : #455A64;
|
||||||
$input-autofill-color : #AAD5C4;
|
$input-autofill-color : #AAD5C4;
|
||||||
|
|
|
||||||
|
|
@ -1,234 +1,228 @@
|
||||||
export default {
|
export default {
|
||||||
employee_list: {
|
employee_list: {
|
||||||
page_header: "Employee Directory",
|
page_header: "Employee Directory",
|
||||||
table: {
|
table: {
|
||||||
first_name: "First name",
|
first_name: "First name",
|
||||||
last_name: "Last name",
|
last_name: "Last name",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
phone_number: "Phone number",
|
phone_number: "Phone number",
|
||||||
role: "Role",
|
role: "Role",
|
||||||
supervisor: "Supervisor",
|
supervisor: "Supervisor",
|
||||||
company: "Company",
|
company: "Company",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
login: {
|
login: {
|
||||||
page_header: "account login",
|
page_header: "account login",
|
||||||
email: "e-mail",
|
email: "e-mail",
|
||||||
password: "password",
|
password: "password",
|
||||||
button: {
|
button: {
|
||||||
connect: "connect",
|
connect: "connect",
|
||||||
employee: "employee",
|
employee: "employee",
|
||||||
facebook:"Facebook",
|
facebook: "Facebook",
|
||||||
remember_me: "remember me",
|
remember_me: "remember me",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
coming_soon: "coming soon!",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
login_failed: "Failed to login",
|
||||||
|
popups_blocked: "Popups are blocked on this device",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
|
||||||
coming_soon: "coming soon!",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
login_failed: "Failed to login",
|
|
||||||
popups_blocked: "Popups are blocked on this device",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
nav_bar: {
|
nav_bar: {
|
||||||
home: "homepage",
|
home: "homepage",
|
||||||
employee_list: "employee directory",
|
employee_list: "employee directory",
|
||||||
timesheet_approvals: "timesheet approvals",
|
timesheet_approvals: "timesheet approvals",
|
||||||
timesheet: "timesheet",
|
timesheet: "timesheet",
|
||||||
profile: "profile",
|
profile: "profile",
|
||||||
help: "help",
|
help: "help",
|
||||||
logout: "log out",
|
logout: "log out",
|
||||||
},
|
},
|
||||||
|
|
||||||
profile: {
|
profile: {
|
||||||
personal: {
|
personal: {
|
||||||
tab_title: "personal",
|
tab_title: "personal",
|
||||||
first_name: "first name",
|
first_name: "first name",
|
||||||
last_name: "last name",
|
last_name: "last name",
|
||||||
phone_number: "phone number",
|
phone_number: "phone number",
|
||||||
address: "address",
|
address: "address",
|
||||||
address_hint: "# address, city, region, country",
|
address_hint: "# address, city, region, country",
|
||||||
birthdate: "birthdate",
|
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",
|
||||||
|
},
|
||||||
|
schedule_presets: {
|
||||||
|
tab_title: "Schedule",
|
||||||
|
selected_schedule: "Selected Schedule Preset",
|
||||||
|
new_preset: "Build a new preset",
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
must_enter_birthdate: "You must enter a valid 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",
|
|
||||||
},
|
|
||||||
schedule_presets: {
|
|
||||||
tab_title: "Schedule",
|
|
||||||
selected_schedule: "Selected Schedule Preset",
|
|
||||||
new_preset: "Build a new preset",
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
must_enter_birthdate: "You must enter a valid birthdate",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
shared:{
|
shared: {
|
||||||
error: {
|
error: {
|
||||||
no_data_found: "no data found",
|
no_data_found: "no data found",
|
||||||
no_search_results: "no results matching search",
|
no_search_results: "no results matching search",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
search: "search",
|
||||||
|
filter: "filters",
|
||||||
|
loading: "loading...",
|
||||||
|
language: "Language",
|
||||||
|
add: "ajouter",
|
||||||
|
save: "save",
|
||||||
|
remove: "remove",
|
||||||
|
cancel: "cancel",
|
||||||
|
update: "update",
|
||||||
|
modify: "modify",
|
||||||
|
close: "close",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
label: {
|
|
||||||
search: "search",
|
|
||||||
filter: "filters",
|
|
||||||
loading: "loading...",
|
|
||||||
language: "Language",
|
|
||||||
add: "ajouter",
|
|
||||||
save: "save",
|
|
||||||
remove: "remove",
|
|
||||||
cancel: "cancel",
|
|
||||||
update: "update",
|
|
||||||
modify: "modify",
|
|
||||||
close: "close",
|
|
||||||
},
|
|
||||||
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: {
|
timesheet: {
|
||||||
page_header:"Timesheet",
|
page_header: "Timesheet",
|
||||||
nav_button: {
|
nav_button: {
|
||||||
calendar_date_picker:"Calendar",
|
calendar_date_picker: "Calendar",
|
||||||
current_week:"This week",
|
current_week: "This week",
|
||||||
next_week:"Next week",
|
next_week: "Next week",
|
||||||
previous_week:"Previous week",
|
previous_week: "Previous week",
|
||||||
|
},
|
||||||
|
save_button: "Save",
|
||||||
|
cancel_button: "Cancel",
|
||||||
|
remote_button: "Remote work",
|
||||||
|
delete_button: "Delete",
|
||||||
|
shift: {
|
||||||
|
actions: {
|
||||||
|
add: "Add Shift",
|
||||||
|
edit: "Edit shift",
|
||||||
|
delete: "Delete shift",
|
||||||
|
delete_confirmation_msg: "Do you want to delete this shift completly?",
|
||||||
|
},
|
||||||
|
types: {
|
||||||
|
label: "Shift`s Type",
|
||||||
|
EMERGENCY: "Emergency",
|
||||||
|
EVENING: "Evening",
|
||||||
|
HOLIDAY: "Holiday",
|
||||||
|
OVERTIME: "Overtime",
|
||||||
|
REGULAR: "Regular",
|
||||||
|
SICK: "Sick Leave",
|
||||||
|
VACATION: "Vacation",
|
||||||
|
REMOTE: "Remote work",
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
start: "Start (HH:mm)",
|
||||||
|
end: "End (HH:mm)",
|
||||||
|
header_comment: "Shift`s comment",
|
||||||
|
textarea_comment: "Leave a comment here",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expense: {
|
||||||
|
add_expense: 'Add Expense',
|
||||||
|
amount: 'Amount',
|
||||||
|
date: 'Date',
|
||||||
|
empty_list: 'No registered expenses',
|
||||||
|
employee_comment: 'Comment',
|
||||||
|
supervisor_comment: 'Supervisor note',
|
||||||
|
hints: {
|
||||||
|
amount_or_mileage: "Either amount or mileage, not both",
|
||||||
|
comment_required: "A comment required",
|
||||||
|
attach_file: "Attach File"
|
||||||
|
},
|
||||||
|
mileage: "mileage",
|
||||||
|
open_btn: "list of expenses",
|
||||||
|
title: "List of all expenses",
|
||||||
|
total_amount: "Total amount",
|
||||||
|
total_mileage: "Total mileage",
|
||||||
|
type: "Type",
|
||||||
|
types: {
|
||||||
|
PER_DIEM: "Per Diem",
|
||||||
|
EXPENSES: "expense",
|
||||||
|
MILEAGE: "mileage",
|
||||||
|
ON_CALL: "on-call allowance",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
INVALID_SHIFT_TIME: "In and Out shift times are reversed",
|
||||||
|
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
||||||
|
INVALID_SHIFT: "A shift contains missing or corrupted data",
|
||||||
|
SHIFT_NOT_FOUND: "Shift missing or deleted",
|
||||||
|
PAY_PERIOD_NOT_FOUND: "No pay period matching given dates",
|
||||||
|
EMPLOYEE_NOT_FOUND: "No employee matching current login details",
|
||||||
|
INVALID_TIMESHEET: "Timesheet data is missing or corrupted",
|
||||||
|
TIMESHEET_NOT_FOUND: "No timesheet found with provided data",
|
||||||
|
INVALID_EXPENSE: "An expense contains missing or corrupted data",
|
||||||
|
EXPENSE_NOT_FOUND: "No expense found with provided data",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
save_button:"Save",
|
|
||||||
cancel_button:"Cancel",
|
|
||||||
remote_button: "Remote work",
|
|
||||||
delete_button: "Delete",
|
|
||||||
shift: {
|
|
||||||
actions: {
|
|
||||||
add:"Add Shift",
|
|
||||||
edit: "Edit shift",
|
|
||||||
delete: "Delete shift",
|
|
||||||
delete_confirmation_msg: "Do you want to delete this shift completly?",
|
|
||||||
},
|
|
||||||
types: {
|
|
||||||
label: "Shift`s Type",
|
|
||||||
EMERGENCY: "Emergency",
|
|
||||||
EVENING: "Evening",
|
|
||||||
HOLIDAY: "Holiday",
|
|
||||||
OVERTIME: "Overtime",
|
|
||||||
REGULAR: "Regular",
|
|
||||||
SICK: "Sick Leave",
|
|
||||||
VACATION: "Vacation",
|
|
||||||
REMOTE: "Remote work",
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
not_found: "Shift not found",
|
|
||||||
SHIFT_OVERLAP: "An overlaps occured between 2 or more shifts",
|
|
||||||
invalid: "Invalid shift`s entry",
|
|
||||||
unknown: "Unknown error",
|
|
||||||
comment_required: "A comment is required",
|
|
||||||
comment_too_long: "Your comment is too long",
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
start:"Start (HH:mm)",
|
|
||||||
end:"End (HH:mm)",
|
|
||||||
header_comment:"Shift`s comment",
|
|
||||||
textarea_comment: "Leave a comment here",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expense: {
|
|
||||||
add_expense:'Add Expense',
|
|
||||||
amount:'Amount',
|
|
||||||
date:'Date',
|
|
||||||
empty_list:'No registered expenses',
|
|
||||||
employee_comment:'Comment',
|
|
||||||
supervisor_comment:'Supervisor note',
|
|
||||||
errors: {
|
|
||||||
date_required_or_invalid:"the date is missing or invalid",
|
|
||||||
comment_required:"A comment required",
|
|
||||||
comment_too_long:"Your comment is too long",
|
|
||||||
amount_must_be_positive:"the amount 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",
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
hints: {
|
|
||||||
amount_or_mileage:"Either amount or mileage, not both",
|
|
||||||
comment_required:"A comment required",
|
|
||||||
attach_file:"Attach File"
|
|
||||||
},
|
|
||||||
mileage:"mileage",
|
|
||||||
open_btn:"list of expenses",
|
|
||||||
title:"List of all expenses",
|
|
||||||
total_amount:"Total amount",
|
|
||||||
total_mileage:"Total mileage",
|
|
||||||
type:"Type",
|
|
||||||
types: {
|
|
||||||
PER_DIEM:"Per Diem",
|
|
||||||
EXPENSES:"expense",
|
|
||||||
MILEAGE:"mileage",
|
|
||||||
ON_CALL:"on-call allowance",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
timesheet_approvals: {
|
timesheet_approvals: {
|
||||||
page_title: "Validation cartes de temps",
|
page_title: "Validation cartes de temps",
|
||||||
table: {
|
table: {
|
||||||
full_name: "full name",
|
full_name: "full name",
|
||||||
email: "email address",
|
email: "email address",
|
||||||
expenses: "expenses",
|
expenses: "expenses",
|
||||||
mileage: "mileage",
|
mileage: "mileage",
|
||||||
verified: "approved",
|
verified: "approved",
|
||||||
unverified: "pending",
|
unverified: "pending",
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
hours_worked_title: "hours worked",
|
||||||
|
expenses_title: "expenses accrued",
|
||||||
|
},
|
||||||
|
print_report: {
|
||||||
|
company: "company",
|
||||||
|
type: "type",
|
||||||
|
shifts: "shifts",
|
||||||
|
expenses: "expenses",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
button_detailed_view: "detailed view",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
chart: {
|
|
||||||
hours_worked_title: "hours worked",
|
|
||||||
expenses_title: "expenses accrued",
|
|
||||||
},
|
|
||||||
print_report: {
|
|
||||||
company: "company",
|
|
||||||
type: "type",
|
|
||||||
shifts: "shifts",
|
|
||||||
expenses: "expenses",
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
button_detailed_view: "detailed view",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
@ -1,235 +1,229 @@
|
||||||
export default {
|
export default {
|
||||||
employee_list: {
|
employee_list: {
|
||||||
page_header: "Répertoire du personnel",
|
page_header: "Répertoire du personnel",
|
||||||
table: {
|
table: {
|
||||||
first_name: "prénom",
|
first_name: "prénom",
|
||||||
last_name: "nom de famille",
|
last_name: "nom de famille",
|
||||||
email: "courriel",
|
email: "courriel",
|
||||||
phone_number: "# téléphone",
|
phone_number: "# téléphone",
|
||||||
role: "rôle",
|
role: "rôle",
|
||||||
supervisor: "superviseur",
|
supervisor: "superviseur",
|
||||||
company: "Compagnie",
|
company: "Compagnie",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
login: {
|
login: {
|
||||||
page_header: "connexion au compte",
|
page_header: "connexion au compte",
|
||||||
email: "courriel",
|
email: "courriel",
|
||||||
password: "mot de passe",
|
password: "mot de passe",
|
||||||
button: {
|
button: {
|
||||||
connect: "connecter",
|
connect: "connecter",
|
||||||
employee: "employé",
|
employee: "employé",
|
||||||
facebook:"Facebook",
|
facebook: "Facebook",
|
||||||
remember_me: "rester connecté",
|
remember_me: "rester connecté",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
coming_soon: "à venir!",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
login_failed: "Échec à la connexion",
|
||||||
|
popups_blocked: "Les fenêtres contextuelles sont bloqués sur cet appareil",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
|
||||||
coming_soon: "à venir!",
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
login_failed: "Échec à la connexion",
|
|
||||||
popups_blocked: "Les fenêtres contextuelles sont bloqués sur cet appareil",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
nav_bar: {
|
nav_bar: {
|
||||||
home: "accueil",
|
home: "accueil",
|
||||||
employee_list: "répertoire employés",
|
employee_list: "répertoire employés",
|
||||||
timesheet_approvals: "valider les heures",
|
timesheet_approvals: "valider les heures",
|
||||||
timesheet: "carte de temps",
|
timesheet: "carte de temps",
|
||||||
profile: "profil",
|
profile: "profil",
|
||||||
help: "aide",
|
help: "aide",
|
||||||
logout: "déconnecter",
|
logout: "déconnecter",
|
||||||
},
|
},
|
||||||
|
|
||||||
profile: {
|
profile: {
|
||||||
personal: {
|
personal: {
|
||||||
tab_title: "personnelle",
|
tab_title: "personnelle",
|
||||||
first_name: "prénom",
|
first_name: "prénom",
|
||||||
last_name: "nom de famille",
|
last_name: "nom de famille",
|
||||||
phone_number: "numéro de téléphone",
|
phone_number: "numéro de téléphone",
|
||||||
address: "adresse",
|
address: "adresse",
|
||||||
address_hint: "# addresse, ville, région, pays",
|
address_hint: "# addresse, ville, région, pays",
|
||||||
birthdate: "date de naissance",
|
birthdate: "date de naissance",
|
||||||
},
|
},
|
||||||
employee: {
|
employee: {
|
||||||
tab_title: "carrière",
|
tab_title: "carrière",
|
||||||
email: "courriel",
|
email: "courriel",
|
||||||
job_title: "poste",
|
job_title: "poste",
|
||||||
company: "compagnie",
|
company: "compagnie",
|
||||||
supervisor: "nom du superviseur",
|
supervisor: "nom du superviseur",
|
||||||
hired_date: "date d'embauche",
|
hired_date: "date d'embauche",
|
||||||
},
|
},
|
||||||
preferences: {
|
preferences: {
|
||||||
tab_title: "préférences",
|
tab_title: "préférences",
|
||||||
display_options: "Options d'affichage",
|
display_options: "Options d'affichage",
|
||||||
language_options: "Options de langue",
|
language_options: "Options de langue",
|
||||||
dark_mode: "sombre",
|
dark_mode: "sombre",
|
||||||
light_mode: "clair",
|
light_mode: "clair",
|
||||||
},
|
},
|
||||||
schedule_presets: {
|
schedule_presets: {
|
||||||
tab_title: "horaire",
|
tab_title: "horaire",
|
||||||
selected_schedule: "Horaire Sélectionné",
|
selected_schedule: "Horaire Sélectionné",
|
||||||
new_preset: "Construire un nouvel horaire",
|
new_preset: "Construire un nouvel horaire",
|
||||||
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
must_enter_birthdate: "Vous devez entrer une date de naissance valide",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
shared: {
|
},
|
||||||
error: {
|
errors: {
|
||||||
no_data_found: 'aucune donnée à afficher',
|
must_enter_birthdate: "Vous devez entrer une date de naissance valide",
|
||||||
no_search_results: 'aucun résultat ne correspond à la recherche',
|
}
|
||||||
},
|
},
|
||||||
label: {
|
|
||||||
search: 'recherche',
|
|
||||||
filter: "filtres",
|
|
||||||
loading: 'chargement en cours...',
|
|
||||||
language: 'langue',
|
|
||||||
add: "ajouter",
|
|
||||||
save: "sauvegarder",
|
|
||||||
remove: "supprimer",
|
|
||||||
cancel: "annuler",
|
|
||||||
update: "mettre à jour",
|
|
||||||
modify: "modifier",
|
|
||||||
close: "fermer",
|
|
||||||
},
|
|
||||||
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: {
|
shared: {
|
||||||
page_header:"Carte de temps",
|
error: {
|
||||||
nav_button: {
|
no_data_found: 'aucune donnée à afficher',
|
||||||
calendar_date_picker:"Calendrier",
|
no_search_results: 'aucun résultat ne correspond à la recherche',
|
||||||
current_week:"Semaine actuelle",
|
},
|
||||||
next_week:"Prochaine semaine",
|
label: {
|
||||||
previous_week:"Semaine précédente",
|
search: 'recherche',
|
||||||
|
filter: "filtres",
|
||||||
|
loading: 'chargement en cours...',
|
||||||
|
language: 'langue',
|
||||||
|
add: "ajouter",
|
||||||
|
save: "sauvegarder",
|
||||||
|
remove: "supprimer",
|
||||||
|
cancel: "annuler",
|
||||||
|
update: "mettre à jour",
|
||||||
|
modify: "modifier",
|
||||||
|
close: "fermer",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
save_button:"Enregistrer",
|
|
||||||
cancel_button:"Annuler",
|
|
||||||
remote_button: "Télétravail",
|
|
||||||
delete_button: "Supprimer",
|
|
||||||
shift: {
|
|
||||||
actions: {
|
|
||||||
add:"Ajouter un Quart",
|
|
||||||
edit: "Modifier un Quart",
|
|
||||||
delete: "Supprimer un Quart",
|
|
||||||
delete_confirmation_msg: "Voulez-vous complètement supprimer ce quart?",
|
|
||||||
},
|
|
||||||
types: {
|
|
||||||
label: "Type de Quart",
|
|
||||||
EMERGENCY: "Urgence",
|
|
||||||
EVENING: "Soir",
|
|
||||||
HOLIDAY: "Férié",
|
|
||||||
OVERTIME: "Supplémentaire",
|
|
||||||
REGULAR: "Régulier",
|
|
||||||
SICK: "Maladie",
|
|
||||||
VACATION: "Vacance",
|
|
||||||
REMOTE: "Télétravail",
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
not_found: "Aucun quart trouvé",
|
|
||||||
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
|
||||||
invalid: "Entrée du quart invalide",
|
|
||||||
unknown: "Erreur inconnue",
|
|
||||||
comment_required: "un commentaire est requis",
|
|
||||||
comment_too_long: "votre commentaire est trop long",
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
start:"Début (HH:mm)",
|
|
||||||
end:"Fin (HH:mm)",
|
|
||||||
header_comment:"Commentaire du Quart",
|
|
||||||
textarea_comment: "Laissez votre commentaire ici",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expense: {
|
|
||||||
add_expense:'Ajouter une dépense',
|
|
||||||
amount:'Montant',
|
|
||||||
date:'Date',
|
|
||||||
empty_list:'Aucun dépense enregistrée',
|
|
||||||
employee_comment:'Commentaire',
|
|
||||||
supervisor_comment:'Note du Superviseur',
|
|
||||||
errors: {
|
|
||||||
date_required_or_invalid:"La date est manquante ou invalide",
|
|
||||||
comment_required:"un commentaire est requis",
|
|
||||||
comment_too_long:"votre commentaire est trop long",
|
|
||||||
amount_must_be_positive:"le montant 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",
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
hints: {
|
|
||||||
amount_or_mileage:"Soit dépense ou kilométrage, pas les deux",
|
|
||||||
comment_required:"un commentaire est requis",
|
|
||||||
attach_file:"Pièce jointe"
|
|
||||||
},
|
|
||||||
mileage:"Kilométrage",
|
|
||||||
open_btn:"Liste des Dépenses",
|
|
||||||
title:"Liste des dépenses",
|
|
||||||
total_amount:"Montant total",
|
|
||||||
total_mileage:"Kilométrage total",
|
|
||||||
type:"Type",
|
|
||||||
types: {
|
|
||||||
PER_DIEM:"Per diem",
|
|
||||||
EXPENSES:"dépense",
|
|
||||||
MILEAGE:"kilométrage",
|
|
||||||
ON_CALL:"Prime de garde",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
timesheet_approvals: {
|
timesheet: {
|
||||||
page_title: "Validation cartes de temps",
|
page_header: "Carte de temps",
|
||||||
table: {
|
nav_button: {
|
||||||
full_name: "nom complet",
|
calendar_date_picker: "Calendrier",
|
||||||
email: "courriel",
|
current_week: "Semaine actuelle",
|
||||||
expenses: "dépenses",
|
next_week: "Prochaine semaine",
|
||||||
mileage: "kilométrage",
|
previous_week: "Semaine précédente",
|
||||||
verified: "approuvé",
|
},
|
||||||
unverified: "à vérifier",
|
save_button: "Enregistrer",
|
||||||
|
cancel_button: "Annuler",
|
||||||
|
remote_button: "Télétravail",
|
||||||
|
delete_button: "Supprimer",
|
||||||
|
shift: {
|
||||||
|
actions: {
|
||||||
|
add: "Ajouter un Quart",
|
||||||
|
edit: "Modifier un Quart",
|
||||||
|
delete: "Supprimer un Quart",
|
||||||
|
delete_confirmation_msg: "Voulez-vous complètement supprimer ce quart?",
|
||||||
|
},
|
||||||
|
types: {
|
||||||
|
label: "Type de Quart",
|
||||||
|
EMERGENCY: "Urgence",
|
||||||
|
EVENING: "Soir",
|
||||||
|
HOLIDAY: "Férié",
|
||||||
|
OVERTIME: "Supplémentaire",
|
||||||
|
REGULAR: "Régulier",
|
||||||
|
SICK: "Maladie",
|
||||||
|
VACATION: "Vacance",
|
||||||
|
REMOTE: "Télétravail",
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
start: "Début (HH:mm)",
|
||||||
|
end: "Fin (HH:mm)",
|
||||||
|
header_comment: "Commentaire du Quart",
|
||||||
|
textarea_comment: "Laissez votre commentaire ici",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expense: {
|
||||||
|
add_expense: 'Ajouter une dépense',
|
||||||
|
amount: 'Montant',
|
||||||
|
date: 'Date',
|
||||||
|
empty_list: 'Aucun dépense enregistrée',
|
||||||
|
employee_comment: 'Commentaire',
|
||||||
|
supervisor_comment: 'Note du Superviseur',
|
||||||
|
hints: {
|
||||||
|
amount_or_mileage: "Soit dépense ou kilométrage, pas les deux",
|
||||||
|
comment_required: "un commentaire est requis",
|
||||||
|
attach_file: "Pièce jointe"
|
||||||
|
},
|
||||||
|
mileage: "Kilométrage",
|
||||||
|
open_btn: "Liste des Dépenses",
|
||||||
|
title: "Liste des dépenses",
|
||||||
|
total_amount: "Montant total",
|
||||||
|
total_mileage: "Kilométrage total",
|
||||||
|
type: "Type",
|
||||||
|
types: {
|
||||||
|
PER_DIEM: "Per diem",
|
||||||
|
EXPENSES: "dépense",
|
||||||
|
MILEAGE: "kilométrage",
|
||||||
|
ON_CALL: "Prime de garde",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
INVALID_SHIFT_TIME: "Les heures d'entrée et de sortie sont inversées",
|
||||||
|
SHIFT_OVERLAP: "Il y a un chevauchement entre deux ou plusieurs quarts",
|
||||||
|
INVALID_SHIFT: "Un quart de travail contient des données manquantes ou corrompues",
|
||||||
|
SHIFT_NOT_FOUND: "Quart de travail manquant ou supprimé",
|
||||||
|
PAY_PERIOD_NOT_FOUND: "Aucune période de paie ne correspond aux dates fournies",
|
||||||
|
EMPLOYEE_NOT_FOUND: "Aucun employé ne correspond aux détails de votre connexion",
|
||||||
|
INVALID_TIMESHEET: "Une feuille de temps contient des données manquantes ou corrompues",
|
||||||
|
TIMESHEET_NOT_FOUND: "Aucune feuille de temps ne correspond au détails fournis",
|
||||||
|
INVALID_EXPENSE: "Une dépense contient des données manquantes ou corrompues",
|
||||||
|
EXPENSE_NOT_FOUND: "Aucune dépense ne correspond aux détails fournis",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
chart: {
|
|
||||||
hours_worked_title: "heures travaillées",
|
timesheet_approvals: {
|
||||||
expenses_title: "dépenses encourues"
|
page_title: "Validation cartes de temps",
|
||||||
|
table: {
|
||||||
|
full_name: "nom complet",
|
||||||
|
email: "courriel",
|
||||||
|
expenses: "dépenses",
|
||||||
|
mileage: "kilométrage",
|
||||||
|
verified: "approuvé",
|
||||||
|
unverified: "à vérifier",
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
hours_worked_title: "heures travaillées",
|
||||||
|
expenses_title: "dépenses encourues"
|
||||||
|
},
|
||||||
|
print_report: {
|
||||||
|
company: "compagnie",
|
||||||
|
type: "types de données",
|
||||||
|
shifts: "quarts de travail",
|
||||||
|
expenses: "dépenses",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
button_detailed_view: "vue détaillée",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
print_report: {
|
|
||||||
company: "compagnie",
|
|
||||||
type: "types de données",
|
|
||||||
shifts: "quarts de travail",
|
|
||||||
expenses: "dépenses",
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
button_detailed_view: "vue détaillée",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
@ -34,10 +34,11 @@
|
||||||
v-model="email"
|
v-model="email"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
|
color="accent"
|
||||||
label-color="accent"
|
label-color="accent"
|
||||||
class="rounded-5 inset-shadow bg-blue-grey-1"
|
class="rounded-5 inset-shadow bg-white"
|
||||||
label-slot
|
label-slot
|
||||||
input-class="text-weight-medium text-h6"
|
input-class="text-h6 text-dark"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
<span class="text-weight-bolder text-uppercase text-overline"> {{ $t('login.email') }} </span>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
dense
|
dense
|
||||||
:stack-label="!isEditing"
|
:stack-label="!isEditing"
|
||||||
autogrow
|
autogrow
|
||||||
filled
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
debounce="500"
|
debounce="500"
|
||||||
label-color="accent"
|
label-color="accent"
|
||||||
class="q-ma-xs text-uppercase"
|
class="q-ma-xs text-uppercase"
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
v-model="model"
|
v-model="model"
|
||||||
dense
|
dense
|
||||||
:stack-label="!isEditing"
|
:stack-label="!isEditing"
|
||||||
filled
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
label-color="accent"
|
label-color="accent"
|
||||||
class="q-ma-xs text-h6 text-uppercase"
|
class="q-ma-xs text-h6 text-uppercase"
|
||||||
popup-content-class="text-weight-medium text-h6"
|
popup-content-class="text-weight-medium text-h6"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
|
|
||||||
const { title, startDate = "", endDate = "" } = defineProps<{
|
const { title, startDate = "", endDate = "" } = defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -11,25 +11,33 @@
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const date_format_options = { day: 'numeric', month: 'long', year: 'numeric', };
|
const date_format_options = { day: 'numeric', month: 'long', year: 'numeric', };
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
<div class="column q-mt-lg text-uppercase text-center text-weight-bolder text-h4">
|
||||||
<span class="col">{{ $t(title) }}</span>
|
<span class="col">{{ $t(title) }}</span>
|
||||||
|
|
||||||
<div
|
<transition
|
||||||
v-if="startDate.length > 0"
|
enter-active-class="animated fadeInDown"
|
||||||
class="col row flex-center full-width q-py-none q-my-none"
|
leave-active-class="animated fadeOutDown"
|
||||||
|
mode="out-in"
|
||||||
>
|
>
|
||||||
<div class="text-accent text-weight-bold text-h6">
|
<div
|
||||||
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
:key="startDate"
|
||||||
|
v-if="startDate.length > 0"
|
||||||
|
class="col row flex-center full-width q-py-none q-my-none"
|
||||||
|
>
|
||||||
|
<div class="text-accent text-weight-bold text-h6">
|
||||||
|
{{ $d(date.extractDate(startDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-body2 q-mx-md text-weight-medium">
|
||||||
|
{{ $t('shared.misc.to') }}
|
||||||
|
</div>
|
||||||
|
<div class="text-accent text-weight-bold text-h6">
|
||||||
|
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body2 q-mx-md text-weight-medium">
|
</transition>
|
||||||
{{ $t('shared.misc.to') }}
|
|
||||||
</div>
|
|
||||||
<div class="text-accent text-weight-bold text-h6">
|
|
||||||
{{ $d(date.extractDate(endDate, 'YYYY-MM-DD'), date_format_options) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -7,9 +7,10 @@
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
push
|
push
|
||||||
rounded
|
rounded
|
||||||
class="q-mr-md bg-white text-primary"
|
|
||||||
:label="$t('shared.label.filter')"
|
|
||||||
icon="filter_alt"
|
icon="filter_alt"
|
||||||
|
color="accent"
|
||||||
|
:label="$t('shared.label.filter')"
|
||||||
|
class="q-mr-md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Search bar -->
|
<!-- Search bar -->
|
||||||
|
|
@ -21,14 +22,14 @@
|
||||||
debounce="300"
|
debounce="300"
|
||||||
class="right-rounded"
|
class="right-rounded"
|
||||||
:label="$t('shared.label.search')"
|
:label="$t('shared.label.search')"
|
||||||
label-color="primary"
|
label-color="accent"
|
||||||
bg-color="white"
|
bg-color="white"
|
||||||
color="primary"
|
color="accent"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<q-icon
|
<q-icon
|
||||||
name="search"
|
name="search"
|
||||||
color="primary"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { provide, ref } from 'vue';
|
import { provide, ref } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import DetailedDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-hours-worked.vue';
|
import DetailsDialogChartHoursWorked from 'src/modules/timesheet-approval/components/details-dialog-chart-hours-worked.vue';
|
||||||
import DetailedDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-shift-types.vue';
|
import DetailsDialogChartShiftTypes from 'src/modules/timesheet-approval/components/details-dialog-chart-shift-types.vue';
|
||||||
import DetailedDialogChartExpenses from 'src/modules/timesheet-approval/components/details-crud-dialog-chart-expenses.vue';
|
import DetailsDialogChartExpenses from 'src/modules/timesheet-approval/components/details-dialog-chart-expenses.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
import ExpenseCrudDialogList from 'src/modules/timesheets/components/expense-crud-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
|
|
||||||
const { employeeEmail } = defineProps<{
|
const { employeeEmail } = defineProps<{
|
||||||
employeeEmail: string;
|
employeeEmail: string;
|
||||||
|
|
@ -48,17 +48,17 @@
|
||||||
:horizontal="!$q.screen.lt.md"
|
:horizontal="!$q.screen.lt.md"
|
||||||
class=" col-auto q-px-sm no-wrap"
|
class=" col-auto q-px-sm no-wrap"
|
||||||
>
|
>
|
||||||
<DetailedDialogChartHoursWorked
|
<DetailsDialogChartHoursWorked
|
||||||
:key="render_key"
|
:key="render_key"
|
||||||
class="col"
|
class="col"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DetailedDialogChartShiftTypes
|
<DetailsDialogChartShiftTypes
|
||||||
:key="render_key + 1"
|
:key="render_key + 1"
|
||||||
class="col-2 q-ma-lg"
|
class="col-2 q-ma-lg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DetailedDialogChartExpenses
|
<DetailsDialogChartExpenses
|
||||||
:key="render_key + 2"
|
:key="render_key + 2"
|
||||||
class="col"
|
class="col"
|
||||||
/>
|
/>
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
|
|
||||||
<q-card-section class="col-auto">
|
<q-card-section class="col-auto">
|
||||||
<q-separator />
|
<q-separator />
|
||||||
<ExpenseCrudDialogList
|
<ExpenseDialogList
|
||||||
horizontal
|
horizontal
|
||||||
:employee-email="employeeEmail"
|
:employee-email="employeeEmail"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -42,18 +42,6 @@
|
||||||
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
|
await timesheet_store.getTimesheetsByEmployeeEmail(employee_email);
|
||||||
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
|
// await expenses_store.getPayPeriodExpensesByTimesheetId(employee_email);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getListModeTextColor = (type: string): string => {
|
|
||||||
console.log('type: ', type);
|
|
||||||
if (IS_ABNORMAL_SHIFT.includes(type)) {
|
|
||||||
return ' text-negative text-weight-bolder';
|
|
||||||
}
|
|
||||||
else if (IS_PTO.includes(type)) {
|
|
||||||
return ' text-warning text-weight-bold';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -93,8 +81,8 @@
|
||||||
v-model="is_grid_mode"
|
v-model="is_grid_mode"
|
||||||
push
|
push
|
||||||
color="white"
|
color="white"
|
||||||
text-color="primary"
|
text-color="accent"
|
||||||
toggle-color="primary"
|
toggle-color="accent"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
:options="[
|
:options="[
|
||||||
{ icon: 'grid_view', value: true },
|
{ icon: 'grid_view', value: true },
|
||||||
|
|
@ -133,7 +121,6 @@
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'"
|
v-if="(props.value > 0 && typeof props.value !== 'boolean') || typeof props.value === 'string'"
|
||||||
:class="getListModeTextColor(props.col.name)"
|
|
||||||
>{{ props.value }}</span>
|
>{{ props.value }}</span>
|
||||||
<q-icon
|
<q-icon
|
||||||
v-if="typeof props.value === 'boolean'"
|
v-if="typeof props.value === 'boolean'"
|
||||||
|
|
@ -155,7 +142,7 @@
|
||||||
|
|
||||||
<!-- Template for custome failed-to-load state -->
|
<!-- Template for custome failed-to-load state -->
|
||||||
<template #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-accent q-gutter-sm">
|
||||||
<q-icon
|
<q-icon
|
||||||
size="4em"
|
size="4em"
|
||||||
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
:name="filter ? 'filter_alt_off' : 'error_outline'"
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,75 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
|
||||||
import { computed, inject, ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
import { useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { Expense, EXPENSE_TYPE, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
|
interface ExpenseOption {
|
||||||
|
label: string;
|
||||||
|
value: ExpenseType;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
const files = defineModel<File[] | null>('files');
|
const files = defineModel<File[] | null>('files');
|
||||||
|
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
|
const is_initial_expense = ref(true);
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
const employee_email = inject<string>('employeeEmail');
|
|
||||||
const rules = useExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||||
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||||
|
|
||||||
|
const expense_options: ExpenseOption[] = [
|
||||||
|
{label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM')},
|
||||||
|
{label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES')},
|
||||||
|
{label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE')},
|
||||||
|
{label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL')},
|
||||||
|
]
|
||||||
|
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'onClickUpdateCancel': [void];
|
||||||
|
'onClickSaveUpdates': [void];
|
||||||
|
}>();
|
||||||
|
|
||||||
const openDatePicker = () => {
|
const openDatePicker = () => {
|
||||||
is_navigator_open.value = true;
|
is_navigator_open.value = true;
|
||||||
if (timesheet_store.pay_period !== undefined) {
|
if (expenses_store.current_expense.date === undefined) {
|
||||||
expenses_store.current_expense.date = timesheet_store.pay_period.period_start;
|
expenses_store.current_expense.date = timesheet_store.pay_period?.period_start ?? '';
|
||||||
}
|
}
|
||||||
console.log('current pay period start date: ', period_start_date.value);
|
|
||||||
console.log('current pay period end date: ', period_end_date.value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelUpdateMode = () => {
|
const closeDatePicker = (date: string) => {
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
is_navigator_open.value = false;
|
||||||
expenses_store.initial_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
expenses_store.current_expense.date = date;
|
||||||
expenses_store.mode = 'create';
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
if (expenses_store.mode === 'create') await expenses_api.createExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||||
else await expenses_api.updateExpenseByEmployeeEmail(employee_email ?? '', expenses_store.current_expense?.date ?? '');
|
|
||||||
|
if (expenses_store.current_expense.id) {
|
||||||
|
emit('onClickSaveUpdates');
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
watch(expenses_store.current_expense, () => {
|
||||||
|
is_initial_expense.value = deepEqual(expenses_store.current_expense, expenses_store.initial_expense);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -55,25 +79,30 @@
|
||||||
:key="expenses_store.current_expense.id"
|
:key="expenses_store.current_expense.id"
|
||||||
flat
|
flat
|
||||||
@submit.prevent="requestExpenseCreationOrUpdate"
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
|
class="full-width"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="text-uppercase text-weight-medium q-pt-sm q-px-lg q-ma-sm"
|
class="text-uppercase text-weight-medium q-pt-sm q-ma-sm"
|
||||||
:class="expenses_store.mode === 'create' ? '' : 'invisible'"
|
:class="expenses_store.mode === 'create' ? 'q-px-lg' : 'invisible'"
|
||||||
>
|
>
|
||||||
{{ $t('timesheet.expense.add_expense') }}
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-between items-start rounded-5 q-px-lg q-pb-sm">
|
<div
|
||||||
|
class="row justify-between items-start rounded-5 q-pb-sm"
|
||||||
|
:class="expenses_store.mode === 'create' ? 'q-px-lg' : ''"
|
||||||
|
>
|
||||||
<!-- date selection input -->
|
<!-- date selection input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
dense
|
dense
|
||||||
outlined
|
type="date"
|
||||||
|
borderless
|
||||||
readonly
|
readonly
|
||||||
stack-label
|
stack-label
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col q-px-xs"
|
class="col-auto q-px-xs"
|
||||||
input-class="text-weight-medium"
|
input-class="text-weight-medium"
|
||||||
input-style="font-size: 1.2em;"
|
input-style="font-size: 1em;"
|
||||||
:label="$t('timesheet.expense.date')"
|
:label="$t('timesheet.expense.date')"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
|
|
@ -96,7 +125,7 @@
|
||||||
mask="YYYY-MM-DD"
|
mask="YYYY-MM-DD"
|
||||||
event-color="accent"
|
event-color="accent"
|
||||||
:options="date => date >= period_start_date && date <= period_end_date"
|
:options="date => date >= period_start_date && date <= period_end_date"
|
||||||
@update:model-value="is_navigator_open = false"
|
@update:model-value="closeDatePicker"
|
||||||
/>
|
/>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -110,12 +139,12 @@
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
<!-- expenses type selection -->
|
||||||
<q-select
|
<q-select
|
||||||
v-model="expenses_store.current_expense.type"
|
v-model="expense_selected"
|
||||||
:options="EXPENSE_TYPE"
|
|
||||||
standout="bg-blue-grey-9"
|
standout="bg-blue-grey-9"
|
||||||
dense
|
dense
|
||||||
emit-value
|
:options="expense_options"
|
||||||
hide-dropdown-icon
|
hide-dropdown-icon
|
||||||
|
stack-label
|
||||||
label-slot
|
label-slot
|
||||||
class="col q-px-xs"
|
class="col q-px-xs"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -126,7 +155,7 @@
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
:option-label="label => $t(`timesheet.expense.types.${label}`)"
|
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
|
@ -136,13 +165,18 @@
|
||||||
|
|
||||||
<template #selected-item="scope">
|
<template #selected-item="scope">
|
||||||
<div
|
<div
|
||||||
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
class="row items-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
||||||
:tabindex="scope.tabindex"
|
:tabindex="scope.tabindex"
|
||||||
>
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
size="xs"
|
||||||
|
class="col-auto q-mx-xs"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
style="line-height: 0.9em;"
|
style="line-height: 1em;"
|
||||||
class="col-auto ellipsis"
|
class="col-auto ellipsis text-uppercase"
|
||||||
>{{ scope.opt.label }}</span>
|
>{{ scope.opt.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -153,16 +187,18 @@
|
||||||
<q-input
|
<q-input
|
||||||
key="amount"
|
key="amount"
|
||||||
v-model.number="expenses_store.current_expense.amount"
|
v-model.number="expenses_store.current_expense.amount"
|
||||||
filled
|
standout="bg-blue-grey-9"
|
||||||
input-class="text-right"
|
|
||||||
dense
|
dense
|
||||||
stack-label
|
|
||||||
color="primary"
|
|
||||||
class="col q-px-xs"
|
|
||||||
label-slot
|
label-slot
|
||||||
|
stack-label
|
||||||
suffix="$"
|
suffix="$"
|
||||||
|
color="primary"
|
||||||
|
class="col-auto q-px-xs"
|
||||||
|
input-class="text-right text-weight-bold"
|
||||||
|
:input-style="'font-size: 1.2em;'"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.amountRequired]"
|
:rules="[rules.amountRequired]"
|
||||||
|
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expenses_store.current_expense.amount)"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
|
@ -177,7 +213,7 @@
|
||||||
<q-input
|
<q-input
|
||||||
key="mileage"
|
key="mileage"
|
||||||
v-model.number="expenses_store.current_expense.mileage"
|
v-model.number="expenses_store.current_expense.mileage"
|
||||||
filled
|
standout="bg-blue-grey-9"
|
||||||
input-class="text-right"
|
input-class="text-right"
|
||||||
dense
|
dense
|
||||||
stack-label
|
stack-label
|
||||||
|
|
@ -200,14 +236,13 @@
|
||||||
<!-- employee comment input -->
|
<!-- employee comment input -->
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.comment"
|
v-model="expenses_store.current_expense.comment"
|
||||||
filled
|
standout="bg-blue-grey-9"
|
||||||
dense
|
dense
|
||||||
stack-label
|
stack-label
|
||||||
label-slot
|
label-slot
|
||||||
color="primary"
|
color="primary"
|
||||||
type="text"
|
type="text"
|
||||||
class="col q-px-sm"
|
class="col q-px-sm"
|
||||||
:counter="true"
|
|
||||||
:maxlength="COMMENT_MAX_LENGTH"
|
:maxlength="COMMENT_MAX_LENGTH"
|
||||||
lazy-rules="ondemand"
|
lazy-rules="ondemand"
|
||||||
:rules="[rules.commentRequired]"
|
:rules="[rules.commentRequired]"
|
||||||
|
|
@ -222,8 +257,8 @@
|
||||||
<!-- import attach file section -->
|
<!-- import attach file section -->
|
||||||
<q-file
|
<q-file
|
||||||
v-model="files"
|
v-model="files"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
dense
|
dense
|
||||||
filled
|
|
||||||
use-chips
|
use-chips
|
||||||
multiple
|
multiple
|
||||||
stack-label
|
stack-label
|
||||||
|
|
@ -235,7 +270,7 @@
|
||||||
<q-icon
|
<q-icon
|
||||||
name="attach_file"
|
name="attach_file"
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="accent"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -257,15 +292,17 @@
|
||||||
icon="clear"
|
icon="clear"
|
||||||
color="negative"
|
color="negative"
|
||||||
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
|
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
|
||||||
@click="cancelUpdateMode"
|
@click="$emit('onClickUpdateCancel')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
color="accent"
|
:disable="is_initial_expense"
|
||||||
|
:color="is_initial_expense ? 'grey-5' : 'accent'"
|
||||||
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
||||||
:label="$q.screen.gt.sm ? (expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')) : ''"
|
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||||
class="q-px-sm q-mb-sm q-mx-lg"
|
class="q-px-sm "
|
||||||
|
:class="expenses_store.mode === 'create' ? 'q-mr-lg q-mb-md' : 'q-mb-sm q-ml-lg'"
|
||||||
type="submit"
|
type="submit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
icon="clear"
|
icon="clear"
|
||||||
color="negative"
|
color="negative"
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
style="border-radius: 0 0 0 5px;"
|
style="border-radius: 0 5px 0 10px;"
|
||||||
@click="expense_store.close"
|
@click="expense_store.close"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,20 +48,20 @@
|
||||||
{{ $t('timesheet.expense.total_amount') }} :
|
{{ $t('timesheet.expense.total_amount') }} :
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
name="payments"
|
|
||||||
size="sm"
|
|
||||||
color="accent"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="col-auto text-weight-light"
|
class="col-auto text-weight-light"
|
||||||
style="font-size: 2.5em; line-height: 1em;"
|
style="font-size: 2.5em; line-height: 1em;"
|
||||||
>
|
>
|
||||||
{{ weekly_totals.expenses.toFixed(2) }}
|
{{ weekly_totals.expenses.toFixed(2) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
name="attach_money"
|
||||||
|
size="md"
|
||||||
|
color="accent"
|
||||||
|
class="col q-ml-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto row items-center q-px-sm">
|
<div class="col-auto row items-center q-px-sm">
|
||||||
|
|
@ -72,20 +72,20 @@
|
||||||
{{ $t('timesheet.expense.total_mileage') }} :
|
{{ $t('timesheet.expense.total_mileage') }} :
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<q-icon
|
|
||||||
v-else
|
|
||||||
name="drive_eta"
|
|
||||||
size="sm"
|
|
||||||
color="accent"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="col-auto text-weight-light"
|
class="col-auto text-weight-light"
|
||||||
style="font-size: 2.5em; line-height: 1em;"
|
style="font-size: 2.5em; line-height: 1em;"
|
||||||
>
|
>
|
||||||
{{ weekly_totals.mileage.toFixed(1) }}
|
{{ weekly_totals.mileage.toFixed(1) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<q-icon
|
||||||
|
v-if="$q.screen.lt.md"
|
||||||
|
name="commute"
|
||||||
|
size="md"
|
||||||
|
color="accent"
|
||||||
|
class="col q-ml-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, toRaw } from 'vue';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
import { deepEqual } from 'src/utils/deep-equal';
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
|
|
@ -27,10 +27,10 @@
|
||||||
|
|
||||||
const refresh_key = ref(1);
|
const refresh_key = ref(1);
|
||||||
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
|
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
|
||||||
const background_style = computed(() => deepEqual(expense, expenses_store.current_expense) ? 'border: 3px solid var(--q-accent);' : '');
|
|
||||||
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
||||||
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
|
const is_authorized_to_approve = computed(() => CAN_APPROVE_PAY_PERIODS.includes(auth_store.user?.role ?? 'GUEST'))
|
||||||
const is_showing_update_form = ref(false);
|
const is_showing_update_form = ref(false);
|
||||||
|
const is_current_expense = computed(() => expense.id === expenses_store.current_expense.id);
|
||||||
|
|
||||||
const requestExpenseDeletion = async () => {
|
const requestExpenseDeletion = async () => {
|
||||||
await expenses_api.deleteExpenseById(expense.id);
|
await expenses_api.deleteExpenseById(expense.id);
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
const onUpdateClicked = () => {
|
const onUpdateClicked = () => {
|
||||||
if (deepEqual(expense, expenses_store.current_expense)) {
|
if (deepEqual(expense, expenses_store.current_expense)) {
|
||||||
expenses_store.mode = 'create';
|
expenses_store.mode = 'create';
|
||||||
|
Object.assign(expense, toRaw(expenses_store.initial_expense))
|
||||||
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
is_showing_update_form.value = false;
|
is_showing_update_form.value = false;
|
||||||
return;
|
return;
|
||||||
|
|
@ -56,6 +57,12 @@
|
||||||
expenses_store.initial_expense = unwrapAndClone(expense);
|
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||||
is_showing_update_form.value = true;
|
is_showing_update_form.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSaveUpdatesClicked = () => {
|
||||||
|
is_showing_update_form.value = false;
|
||||||
|
expenses_store.mode = 'create';
|
||||||
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -64,43 +71,25 @@
|
||||||
:clickable="horizontal"
|
:clickable="horizontal"
|
||||||
class="column col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
|
class="column col-4 items-center q-my-sm q-py-none shadow-3 rounded-5 bg-dark"
|
||||||
:class="background_class + approved_class"
|
:class="background_class + approved_class"
|
||||||
:style="background_style"
|
|
||||||
@click="onExpenseClicked"
|
@click="onExpenseClicked"
|
||||||
>
|
>
|
||||||
<div class="row full-width items-center">
|
<div class="col row fit items-center">
|
||||||
<!-- avatar type icon section -->
|
<!-- avatar type icon section -->
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon
|
<q-icon
|
||||||
:name="getExpenseIcon(expense.type)"
|
:name="getExpenseIcon(expense.type)"
|
||||||
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'blue-grey-2' : 'primary')"
|
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
/>
|
||||||
<q-badge
|
|
||||||
v-if="expense.type === 'ON_CALL'"
|
|
||||||
floating
|
|
||||||
class="q-pa-none rounded-50 bg-white z-top"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="shield"
|
|
||||||
size="xs"
|
|
||||||
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'blue-grey-2' : 'primary')"
|
|
||||||
/>
|
|
||||||
</q-badge>
|
|
||||||
</q-icon>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<!-- amount or mileage section -->
|
<!-- amount or mileage section -->
|
||||||
<q-item-section class="col col-md-2 text-weight-bold">
|
<q-item-section class="col col-md-2 text-weight-bold">
|
||||||
<q-item-label v-if="String(expense.type).trim().toUpperCase() === 'MILEAGE'">
|
<q-item-label v-if="expense.type === 'MILEAGE'">
|
||||||
<template v-if="typeof expense.mileage === 'number'">
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
{{ expense.mileage?.toFixed(1) }} km
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
${{ expense.amount.toFixed(2) }}
|
|
||||||
</template>
|
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label v-else>
|
<q-item-label v-else>
|
||||||
${{ expense.amount.toFixed(2) }}
|
$ {{ expense.amount.toFixed(2) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
||||||
<!-- date label -->
|
<!-- date label -->
|
||||||
|
|
@ -110,7 +99,10 @@
|
||||||
class="text-uppercase text-weight-light"
|
class="text-uppercase text-weight-light"
|
||||||
:class="approved_class"
|
:class="approved_class"
|
||||||
>
|
>
|
||||||
{{ $d(new Date(expense.date), { month: 'short', day: 'numeric', weekday: 'long' }) }}
|
{{ $d(date.extractDate(expense.date, 'YYYY-MM-DD'), {
|
||||||
|
month: 'short', day: 'numeric', weekday:
|
||||||
|
'long'
|
||||||
|
}) }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
|
|
@ -171,37 +163,58 @@
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section :side="$q.screen.gt.sm">
|
|
||||||
|
<q-item-section
|
||||||
|
:key="refresh_key"
|
||||||
|
side
|
||||||
|
:class="is_current_expense ? 'invisible' : ''"
|
||||||
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
|
dense
|
||||||
size="lg"
|
size="lg"
|
||||||
icon="edit"
|
icon="edit"
|
||||||
color="accent"
|
color="accent"
|
||||||
:disable="expense.is_approved"
|
:disable="expense.is_approved"
|
||||||
class="q-pa-none z-top"
|
class="q-py-none z-top"
|
||||||
:class="expense.is_approved ? 'invisible no-pointer' : ''"
|
:class="expense.is_approved ? 'invisible no-pointer' : ''"
|
||||||
@click.stop="onUpdateClicked"
|
@click.stop="onUpdateClicked"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section :side="$q.screen.gt.sm">
|
<q-item-section
|
||||||
<q-btn
|
side
|
||||||
flat
|
:class="is_current_expense ? 'invisible' : ''"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
v-if="expense.is_approved"
|
||||||
|
name="verified"
|
||||||
|
color="white"
|
||||||
size="lg"
|
size="lg"
|
||||||
:icon="expense.is_approved ? 'verified' : 'close'"
|
/>
|
||||||
:color="expense.is_approved ? 'white' : 'negative'"
|
|
||||||
class="q-pa-none z-top"
|
<q-btn
|
||||||
:class="expense.is_approved ? 'no-pointer' : ''"
|
v-else
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="lg"
|
||||||
|
icon="close"
|
||||||
|
color="negative"
|
||||||
|
class="q-py-none z-top q-my-xs"
|
||||||
@click.stop="requestExpenseDeletion"
|
@click.stop="requestExpenseDeletion"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-slide-transition
|
<q-slide-transition
|
||||||
@hide="expenses_store.is_hiding_create_form = false"
|
@hide="expenses_store.mode === 'update' ? null : expenses_store.is_hiding_create_form = false"
|
||||||
:duration="200"
|
:duration="200"
|
||||||
>
|
>
|
||||||
<ExpenseDialogForm v-if="is_showing_update_form && expenses_store.is_hiding_create_form" />
|
<ExpenseDialogForm
|
||||||
|
v-if="is_current_expense && expenses_store.is_hiding_create_form"
|
||||||
|
@on-click-update-cancel="onUpdateClicked"
|
||||||
|
@on-click-save-updates="onSaveUpdatesClicked"
|
||||||
|
/>
|
||||||
</q-slide-transition>
|
</q-slide-transition>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
|
import ExpenseDialogListItem from 'src/modules/timesheets/components/expense-dialog-list-item.vue';
|
||||||
|
import ExpenseDialogListItemMobile from 'src/modules/timesheets/components/mobile/expense-dialog-list-item-mobile.vue';
|
||||||
|
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
|
|
@ -38,14 +39,25 @@
|
||||||
<q-separator spaced />
|
<q-separator spaced />
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
|
||||||
<ExpenseDialogListItem
|
<div
|
||||||
v-for="(expense, index) in expenses_list"
|
v-for="(expense, index) in expenses_list"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-model="expense.is_approved"
|
>
|
||||||
:index="index"
|
<ExpenseDialogListItemMobile
|
||||||
:expense="expense"
|
v-if="$q.screen.lt.md"
|
||||||
:horizontal="horizontal"
|
v-model="expense.is_approved"
|
||||||
/>
|
:index="index"
|
||||||
</q-list>
|
:expense="expense"
|
||||||
|
:horizontal="horizontal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ExpenseDialogListItem
|
||||||
|
v-else
|
||||||
|
v-model="expense.is_approved"
|
||||||
|
:index="index"
|
||||||
|
:expense="expense"
|
||||||
|
:horizontal="horizontal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
import { useExpensesStore } from 'src/stores/expense-store';
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
import ExpenseDialogList from 'src/modules/timesheets/components/expense-dialog-list.vue';
|
||||||
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
|
||||||
|
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
||||||
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
|
||||||
|
|
||||||
const expense_store = useExpensesStore();
|
const expense_store = useExpensesStore();
|
||||||
|
|
@ -18,8 +19,10 @@
|
||||||
transition-hide="jump-down"
|
transition-hide="jump-down"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
class="q-pa-none rounded-10 shadow-10 bg-secondary"
|
class="q-pa-none rounded-10 shadow-10"
|
||||||
|
:class="$q.screen.lt.md ? ' bg-primary' : 'bg-secondary'"
|
||||||
style=" min-width: 70vw;"
|
style=" min-width: 70vw;"
|
||||||
|
:style="$q.dark.isActive ? 'border: solid 2px var(--q-accent);' : ''"
|
||||||
>
|
>
|
||||||
<q-inner-loading :showing="expense_store.is_loading">
|
<q-inner-loading :showing="expense_store.is_loading">
|
||||||
<q-spinner size="32px" />
|
<q-spinner size="32px" />
|
||||||
|
|
@ -38,8 +41,13 @@
|
||||||
|
|
||||||
<ExpenseDialogList />
|
<ExpenseDialogList />
|
||||||
|
|
||||||
|
<q-separator v-if="$q.screen.lt.md" spaced color="accent" size="2px" class="q-mx-md" />
|
||||||
|
|
||||||
<q-slide-transition @hide="expense_store.is_hiding_create_form = true" :duration="200">
|
<q-slide-transition @hide="expense_store.is_hiding_create_form = true" :duration="200">
|
||||||
<ExpenseDialogForm v-if="!expense_store.current_expense.is_approved && expense_store.mode !== 'update' && expense_store.is_hiding_create_form === false" />
|
<div v-if="!expense_store.current_expense.is_approved && expense_store.mode !== 'update' && expense_store.is_hiding_create_form === false">
|
||||||
|
<ExpenseDialogFormMobile v-if="$q.screen.lt.md" />
|
||||||
|
<ExpenseDialogForm v-else/>
|
||||||
|
</div>
|
||||||
</q-slide-transition>
|
</q-slide-transition>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
|
import { convertToMonetaryAmount, getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
|
interface ExpenseOption {
|
||||||
|
label: string;
|
||||||
|
value: ExpenseType;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const ui_store = useUiStore();
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
|
const files = defineModel<File[] | null>('files');
|
||||||
|
const is_navigator_open = ref(false);
|
||||||
|
const is_showing_comment_dialog_mobile = ref(false);
|
||||||
|
|
||||||
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
|
const period_start_date = computed(() => timesheet_store.pay_period?.period_start.replaceAll('-', '/') ?? '');
|
||||||
|
const period_end_date = computed(() => timesheet_store.pay_period?.period_end.replaceAll('-', '/') ?? '');
|
||||||
|
|
||||||
|
const expense_options: ExpenseOption[] = [
|
||||||
|
{ label: t('timesheet.expense.types.PER_DIEM'), value: 'PER_DIEM', icon: getExpenseIcon('PER_DIEM') },
|
||||||
|
{ label: t('timesheet.expense.types.EXPENSES'), value: 'EXPENSES', icon: getExpenseIcon('EXPENSES') },
|
||||||
|
{ label: t('timesheet.expense.types.MILEAGE'), value: 'MILEAGE', icon: getExpenseIcon('MILEAGE') },
|
||||||
|
{ label: t('timesheet.expense.types.ON_CALL'), value: 'ON_CALL', icon: getExpenseIcon('ON_CALL') },
|
||||||
|
]
|
||||||
|
const expense_selected = ref(expense_options.find(expense => expense.value == expenses_store.current_expense.type));
|
||||||
|
|
||||||
|
const openDatePicker = () => {
|
||||||
|
is_navigator_open.value = true;
|
||||||
|
if (timesheet_store.pay_period !== undefined) {
|
||||||
|
expenses_store.current_expense.date = timesheet_store.pay_period.period_start;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
|
await expenses_api.upsertExpense(expenses_store.current_expense);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
'onClickUpdateCancel': [void];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-form
|
||||||
|
v-if="!timesheet_store.timesheets?.every(timesheet => timesheet.is_approved)"
|
||||||
|
:key="expenses_store.current_expense.id"
|
||||||
|
flat
|
||||||
|
@submit.prevent="requestExpenseCreationOrUpdate"
|
||||||
|
class="column full-width"
|
||||||
|
>
|
||||||
|
<!-- header -->
|
||||||
|
<div
|
||||||
|
class="col text-uppercase text-weight-medium text-h6 q-ma-xs"
|
||||||
|
:class="expenses_store.mode === 'create' ? 'q-px-md' : 'invisible'"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.add_expense') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col column items-start rounded-5 q-pb-sm"
|
||||||
|
:class="expenses_store.mode === 'create' ? 'q-px-md' : ''"
|
||||||
|
>
|
||||||
|
<!-- date and type row -->
|
||||||
|
<div class="col row q-my-xs full-width">
|
||||||
|
<!-- date selection input -->
|
||||||
|
<q-input
|
||||||
|
v-model="expenses_store.current_expense.date"
|
||||||
|
dense
|
||||||
|
type="date"
|
||||||
|
outlined
|
||||||
|
readonly
|
||||||
|
stack-label
|
||||||
|
hide-bottom-space
|
||||||
|
color="primary"
|
||||||
|
class="col-auto q-mr-sm"
|
||||||
|
input-class="text-weight-medium"
|
||||||
|
input-style="font-size: 1em;"
|
||||||
|
:label="$t('timesheet.expense.date')"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
dense
|
||||||
|
icon="event"
|
||||||
|
color="accent"
|
||||||
|
class="q-mr-sm"
|
||||||
|
@click="openDatePicker"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-dialog
|
||||||
|
v-model="is_navigator_open"
|
||||||
|
transition-show="jump-right"
|
||||||
|
transition-hide="jump-right"
|
||||||
|
>
|
||||||
|
<q-date
|
||||||
|
v-model="expenses_store.current_expense.date"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
event-color="accent"
|
||||||
|
:options="date => date >= period_start_date && date <= period_end_date"
|
||||||
|
@update:model-value="is_navigator_open = false"
|
||||||
|
/>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.date') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- expenses type selection -->
|
||||||
|
<q-select
|
||||||
|
v-model="expense_selected"
|
||||||
|
standout="bg-blue-grey-9 text-white"
|
||||||
|
dense
|
||||||
|
:options="expense_options"
|
||||||
|
hide-dropdown-icon
|
||||||
|
stack-label
|
||||||
|
label-slot
|
||||||
|
hide-bottom-space
|
||||||
|
class="col"
|
||||||
|
color="primary"
|
||||||
|
:label="$t('timesheet.expense.type')"
|
||||||
|
:menu-offset="[0, 10]"
|
||||||
|
menu-anchor="bottom middle"
|
||||||
|
menu-self="top middle"
|
||||||
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
|
:rules="[rules.typeRequired]"
|
||||||
|
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.type') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #selected-item="scope">
|
||||||
|
<div
|
||||||
|
class="row items-center text-weight-bold q-ma-none q-pa-none full-width"
|
||||||
|
:class="ui_store.is_mobile_mode ? 'full-height' : ''"
|
||||||
|
:tabindex="scope.tabindex"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="scope.opt.icon"
|
||||||
|
size="sm"
|
||||||
|
class="col-auto q-mx-xs"
|
||||||
|
/>
|
||||||
|
<span class="col text-uppercase ellipsis">{{ scope.opt.label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- amount and comment row -->
|
||||||
|
<div class="col row q-my-xs full-width">
|
||||||
|
<!-- amount input -->
|
||||||
|
<div class="col q-mr-sm">
|
||||||
|
<q-input
|
||||||
|
v-if="TYPES_WITH_AMOUNT_ONLY.includes(expenses_store.current_expense?.type ?? 'EXPENSES')"
|
||||||
|
v-model.number="expenses_store.current_expense.amount"
|
||||||
|
key="amount"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
dense
|
||||||
|
label-slot
|
||||||
|
stack-label
|
||||||
|
hide-bottom-space
|
||||||
|
suffix="$"
|
||||||
|
color="primary"
|
||||||
|
input-class="text-right text-weight-bold"
|
||||||
|
:input-style="'font-size: 1.2em;'"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[rules.amountRequired]"
|
||||||
|
@blur="expenses_store.current_expense.amount = convertToMonetaryAmount(expenses_store.current_expense.amount)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.amount') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<!-- mileage input -->
|
||||||
|
<q-input
|
||||||
|
v-else
|
||||||
|
v-model.number="expenses_store.current_expense.mileage"
|
||||||
|
key="mileage"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
input-class="text-right"
|
||||||
|
dense
|
||||||
|
stack-label
|
||||||
|
clearable
|
||||||
|
hide-bottom-space
|
||||||
|
color="primary"
|
||||||
|
label-slot
|
||||||
|
suffix="km"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[rules.mileageRequired]"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.mileage') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- employee comment input -->
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
color="accent"
|
||||||
|
:icon="expenses_store.current_expense.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
|
@click="is_showing_comment_dialog_mobile = true"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-dialog v-model="is_showing_comment_dialog_mobile">
|
||||||
|
<q-card class="full-width bg-secondary rounded-10">
|
||||||
|
<q-card-section class="q-pa-none">
|
||||||
|
<span
|
||||||
|
class="text-weight-bold text-accent text-uppercase text-caption"
|
||||||
|
style="font-size: 1.5em;"
|
||||||
|
>
|
||||||
|
{{ $t('timesheet.expense.employee_comment') }}
|
||||||
|
</span>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="q-pa-none bg-primary rounded-10">
|
||||||
|
<q-input
|
||||||
|
v-model="expenses_store.current_expense.comment"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
dense
|
||||||
|
hide-bottom-space
|
||||||
|
color="primary"
|
||||||
|
type="textarea"
|
||||||
|
:maxlength="COMMENT_MAX_LENGTH"
|
||||||
|
lazy-rules="ondemand"
|
||||||
|
:rules="[rules.commentRequired]"
|
||||||
|
>
|
||||||
|
</q-input>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- import attach file section -->
|
||||||
|
<q-file
|
||||||
|
v-model="files"
|
||||||
|
standout="bg-blue-grey-9"
|
||||||
|
dense
|
||||||
|
use-chips
|
||||||
|
multiple
|
||||||
|
stack-label
|
||||||
|
:label="$t('timesheet.expense.hints.attach_file')"
|
||||||
|
class="col full-width q-my-xs"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon
|
||||||
|
name="attach_file"
|
||||||
|
size="sm"
|
||||||
|
color="accent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #label>
|
||||||
|
<span class="text-weight-bold text-accent text-uppercase text-caption">
|
||||||
|
{{ $t('timesheet.expense.hints.attach_file') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
|
</div>
|
||||||
|
<div class="col row full-width items-center">
|
||||||
|
<q-space />
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="expenses_store.mode === 'update'"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
class="col-auto"
|
||||||
|
icon="clear"
|
||||||
|
color="negative"
|
||||||
|
:label="$q.screen.gt.sm ? $t('shared.label.cancel') : ''"
|
||||||
|
@click="$emit('onClickUpdateCancel')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
color="accent"
|
||||||
|
:icon="expenses_store.mode === 'update' ? 'save' : 'upload'"
|
||||||
|
:label="expenses_store.mode === 'update' ? $t('shared.label.update') : $t('shared.label.add')"
|
||||||
|
class="q-px-sm"
|
||||||
|
:class="expenses_store.mode === 'create' ? 'q-mr-md q-mb-md' : 'q-mb-sm q-ml-lg'"
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
<script
|
||||||
|
setup
|
||||||
|
lang="ts"
|
||||||
|
>
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
||||||
|
import { deepEqual } from 'src/utils/deep-equal';
|
||||||
|
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||||
|
import { useExpensesStore } from 'src/stores/expense-store';
|
||||||
|
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
|
import { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
|
||||||
|
|
||||||
|
const { expense, horizontal = false } = defineProps<{
|
||||||
|
expense: Expense;
|
||||||
|
index: number;
|
||||||
|
horizontal?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
|
|
||||||
|
const refresh_key = ref(1);
|
||||||
|
const background_class = computed(() => deepEqual(expense, expenses_store.current_expense) ? '' : '');
|
||||||
|
const approved_class = computed(() => expense.is_approved ? ' bg-accent text-white' : '')
|
||||||
|
const is_showing_update_form = ref(false);
|
||||||
|
|
||||||
|
const requestExpenseDeletion = async () => {
|
||||||
|
await expenses_api.deleteExpenseById(expense.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdateClicked = () => {
|
||||||
|
if (expense.is_approved) return;
|
||||||
|
|
||||||
|
if (deepEqual(expense, expenses_store.current_expense)) {
|
||||||
|
expenses_store.mode = 'create';
|
||||||
|
expenses_store.current_expense = new Expense(date.formatDate(new Date(), 'YYYY-MM-DD'));
|
||||||
|
is_showing_update_form.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
expenses_store.mode = 'update';
|
||||||
|
expenses_store.current_expense = expense;
|
||||||
|
expenses_store.initial_expense = unwrapAndClone(expense);
|
||||||
|
is_showing_update_form.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="column bg-dark rounded-5 q-my-sm full-width">
|
||||||
|
<q-slide-item
|
||||||
|
right-color="negative"
|
||||||
|
class="rounded-5 bg-dark full-width"
|
||||||
|
@right="requestExpenseDeletion"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
#right
|
||||||
|
v-if="$q.screen.lt.md && !expenses_store.is_hiding_create_form && !expense.is_approved"
|
||||||
|
>
|
||||||
|
<q-icon name="delete" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
:key="refresh_key"
|
||||||
|
clickable
|
||||||
|
class="row q-py-none q-pa-xs rounded-5 full-width"
|
||||||
|
:class="background_class + approved_class"
|
||||||
|
@click="onUpdateClicked"
|
||||||
|
>
|
||||||
|
<div class="column col">
|
||||||
|
<!-- date label -->
|
||||||
|
<div class="col-auto row items-center q-pl-xs">
|
||||||
|
<q-icon
|
||||||
|
name="calendar_month"
|
||||||
|
size="sm"
|
||||||
|
class="col-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="col text-uppercase text-weight-light full-width q-pl-sm text-h6"
|
||||||
|
:class="approved_class"
|
||||||
|
>
|
||||||
|
{{ $d(
|
||||||
|
date.extractDate(expense.date, 'YYYY-MM-DD'),
|
||||||
|
{ month: 'long', day: 'numeric' }
|
||||||
|
) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col row full-width items-center">
|
||||||
|
<!-- avatar type icon section -->
|
||||||
|
<q-icon
|
||||||
|
:name="getExpenseIcon(expense.type)"
|
||||||
|
:color="expense.is_approved ? 'white' : ($q.dark.isActive ? 'white' : 'primary')"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- amount or mileage section -->
|
||||||
|
<q-item-section class="col text-weight-bold text-h6">
|
||||||
|
<q-item-label v-if="expense.type === 'MILEAGE'">
|
||||||
|
{{ expense.mileage?.toFixed(1) }} km
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label v-else>
|
||||||
|
$ {{ expense.amount.toFixed(2) }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-space v-if="horizontal" />
|
||||||
|
|
||||||
|
<!-- attachment file icon -->
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-btn
|
||||||
|
push
|
||||||
|
:color="expense.is_approved ? 'white' : 'accent'"
|
||||||
|
:text-color="expense.is_approved ? 'accent' : 'white'"
|
||||||
|
class="col-auto q-mx-sm q-px-sm q-pb-sm"
|
||||||
|
icon="attach_file"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-auto q-px-sm"
|
||||||
|
:class="expense.is_approved ? '' : 'invisible'"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
v-if="expense.is_approved"
|
||||||
|
name="verified"
|
||||||
|
color="white"
|
||||||
|
size="lg"
|
||||||
|
class="full-height"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-item>
|
||||||
|
</q-slide-item>
|
||||||
|
|
||||||
|
<q-slide-transition
|
||||||
|
@hide="expenses_store.is_hiding_create_form = false"
|
||||||
|
:duration="200"
|
||||||
|
>
|
||||||
|
<ExpenseDialogFormMobile
|
||||||
|
v-if="is_showing_update_form && expenses_store.is_hiding_create_form"
|
||||||
|
class="q-mt-sm q-pa-sm"
|
||||||
|
@on-click-update-cancel="onUpdateClicked"
|
||||||
|
/>
|
||||||
|
</q-slide-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
/* eslint-disable*/
|
/* eslint-disable*/
|
||||||
import { onBeforeUnmount, onMounted, ref, toRaw, useTemplateRef } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import { unwrapAndClone } from 'src/utils/unwrap-and-clone';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { QSelect } from 'quasar';
|
import { QSelect } from 'quasar';
|
||||||
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
import { Shift, ShiftType } from 'src/modules/timesheets/models/shift.models';
|
||||||
|
|
@ -22,27 +21,28 @@
|
||||||
|
|
||||||
const COMMENT_LENGTH_MAX = 280;
|
const COMMENT_LENGTH_MAX = 280;
|
||||||
const SHIFT_OPTIONS: ShiftOption[] = [
|
const SHIFT_OPTIONS: ShiftOption[] = [
|
||||||
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: '' },
|
{ label: t('timesheet.shift.types.REGULAR'), value: 'REGULAR', icon: 'wb_sunny', icon_color: 'blue-grey-3' },
|
||||||
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
{ label: t('timesheet.shift.types.EVENING'), value: 'EVENING', icon: 'bedtime', icon_color: 'indigo-8' },
|
||||||
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
{ label: t('timesheet.shift.types.EMERGENCY'), value: 'EMERGENCY', icon: 'ring_volume', icon_color: 'red-8' },
|
||||||
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
{ label: t('timesheet.shift.types.VACATION'), value: 'VACATION', icon: 'beach_access', icon_color: 'yellow-8' },
|
||||||
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
{ label: t('timesheet.shift.types.HOLIDAY'), value: 'HOLIDAY', icon: 'forest', icon_color: 'green-8' },
|
||||||
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'cyan-8' },
|
{ label: t('timesheet.shift.types.SICK'), value: 'SICK', icon: 'medication_liquid', icon_color: 'light-blue-6' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const shift = defineModel<Shift>('shift', { required: true });
|
const shift = defineModel<Shift>('shift', { required: true });
|
||||||
const { dense = false, outlined = false } = defineProps<{
|
const { dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
outlined?: boolean;
|
hasShiftAfter?: boolean;
|
||||||
|
isTimesheetApproved?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'saveComment': [comment: string, shift_id: number];
|
'saveComment': [comment: string, shift_id: number];
|
||||||
'requestDelete': [void];
|
'requestDelete': [void];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const is_showing_time_picker = ref(false);
|
|
||||||
const select_ref = useTemplateRef<QSelect>('select');
|
const select_ref = useTemplateRef<QSelect>('select');
|
||||||
const initial_shift = ref<Shift>(unwrapAndClone(toRaw(shift.value)))
|
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
|
||||||
|
|
@ -85,8 +85,8 @@
|
||||||
<template>
|
<template>
|
||||||
<q-slide-item
|
<q-slide-item
|
||||||
right-color="negative"
|
right-color="negative"
|
||||||
class="rounded-5 bg-transparent"
|
class="rounded-5 transparent"
|
||||||
:class="ui_store.is_mobile_mode ? 'q-my-md' : ''"
|
:class="ui_store.is_mobile_mode ? 'q-my-md' : 'q-mr-xs'"
|
||||||
@right="details => slideDeleteShift(details.reset)"
|
@right="details => slideDeleteShift(details.reset)"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
|
|
@ -95,226 +95,246 @@
|
||||||
>
|
>
|
||||||
<q-icon name="delete" />
|
<q-icon name="delete" />
|
||||||
</template>
|
</template>
|
||||||
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
|
||||||
<div class="row items-center text-uppercase rounded-5 bg-transparent q-mb-xs" :class="ui_store.is_mobile_mode ? 'col' : 'col-4'">
|
<div :class="ui_store.is_mobile_mode ? 'column' : 'row'">
|
||||||
<!-- mobile comment button -->
|
<div
|
||||||
<q-btn
|
class="row items-center text-uppercase rounded-5"
|
||||||
v-if="ui_store.is_mobile_mode && !dense"
|
:class="ui_store.is_mobile_mode ? 'col q-mb-xs' : 'col-4'"
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
>
|
||||||
:text-color="shift.comment ? 'accent' : 'grey-5'"
|
<!-- mobile comment button -->
|
||||||
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
<q-btn
|
||||||
>
|
v-if="ui_store.is_mobile_mode && !dense"
|
||||||
<q-popup-edit
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
v-model="shift.comment"
|
:text-color="shift.comment ? ((shift.is_approved || isTimesheetApproved) ? 'white' : 'accent') : 'grey-5'"
|
||||||
:title="$t('timesheet.shift.fields.header_comment')"
|
class="col-auto full-height q-mx-xs rounded-5 shadow-1"
|
||||||
auto-save
|
>
|
||||||
v-slot="scope"
|
<q-popup-edit
|
||||||
class="bg-dark"
|
v-model="shift.comment"
|
||||||
>
|
:title="$t('timesheet.shift.fields.header_comment')"
|
||||||
<q-input
|
auto-save
|
||||||
color="white"
|
v-slot="scope"
|
||||||
v-model="scope.value"
|
class="bg-dark"
|
||||||
dense
|
>
|
||||||
:readonly="shift.is_approved"
|
<q-input
|
||||||
autofocus
|
color="white"
|
||||||
counter
|
v-model="scope.value"
|
||||||
bottom-slots
|
dense
|
||||||
:maxlength="COMMENT_LENGTH_MAX"
|
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||||
class="q-pb-lg"
|
autofocus
|
||||||
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
|
counter
|
||||||
@keyup.enter="scope.set"
|
bottom-slots
|
||||||
>
|
:maxlength="COMMENT_LENGTH_MAX"
|
||||||
<template #append>
|
class="q-pb-lg"
|
||||||
<q-icon name="edit" />
|
:class="(shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed' : ''"
|
||||||
</template>
|
@keyup.enter="scope.set"
|
||||||
|
>
|
||||||
<template #counter>
|
<template #append>
|
||||||
<div class="row flex-center">
|
<q-icon name="edit" />
|
||||||
<q-space />
|
</template>
|
||||||
<q-knob
|
|
||||||
:model-value="scope.value?.length"
|
<template #counter>
|
||||||
readonly
|
<div class="row flex-center">
|
||||||
:max="COMMENT_LENGTH_MAX"
|
<q-space />
|
||||||
size="1.6em"
|
<q-knob
|
||||||
:thickness="0.4"
|
:model-value="scope.value?.length"
|
||||||
:color="getCommentCounterColor(scope.value?.length ?? 0)"
|
readonly
|
||||||
track-color="grey-4"
|
:max="COMMENT_LENGTH_MAX"
|
||||||
class="col-auto q-mr-xs"
|
size="1.6em"
|
||||||
/>
|
:thickness="0.4"
|
||||||
<span
|
:color="getCommentCounterColor(scope.value?.length ?? 0)"
|
||||||
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
|
track-color="grey-4"
|
||||||
>{{ 280 - (scope.value?.length ?? 0) }}</span>
|
class="col-auto q-mr-xs"
|
||||||
</div>
|
/>
|
||||||
</template>
|
<span
|
||||||
</q-input>
|
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
|
||||||
</q-popup-edit>
|
>{{ 280 - (scope.value?.length ?? 0) }}</span>
|
||||||
</q-btn>
|
</div>
|
||||||
|
</template>
|
||||||
<!-- shift type -->
|
</q-input>
|
||||||
<q-select
|
</q-popup-edit>
|
||||||
ref="select"
|
</q-btn>
|
||||||
v-model="shift_type_selected"
|
|
||||||
standout="bg-blue-grey-9"
|
<!-- shift type -->
|
||||||
dense
|
<q-select
|
||||||
:readonly="shift.is_approved"
|
ref="select"
|
||||||
:options-dense="!ui_store.is_mobile_mode"
|
v-model="shift_type_selected"
|
||||||
hide-dropdown-icon
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
:menu-offset="[0, 10]"
|
dense
|
||||||
menu-anchor="bottom middle"
|
:borderless="(shift.is_approved || isTimesheetApproved)"
|
||||||
menu-self="top middle"
|
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||||
:options="SHIFT_OPTIONS"
|
:options-dense="!ui_store.is_mobile_mode"
|
||||||
class="col rounded-5 q-mx-xs bg-dark"
|
hide-dropdown-icon
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
:menu-offset="[0, 10]"
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
menu-anchor="bottom middle"
|
||||||
@blur="onBlurShiftTypeSelect"
|
menu-self="top middle"
|
||||||
@update:model-value="option => shift.type = option.value"
|
:options="SHIFT_OPTIONS"
|
||||||
>
|
class="col rounded-5 q-mx-xs bg-dark"
|
||||||
<template #selected-item="scope">
|
:class="(shift.is_approved || isTimesheetApproved) ? 'inset-shadow' : ''"
|
||||||
<div
|
:style="(shift.is_approved || isTimesheetApproved) ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
||||||
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
|
popup-content-style="border: 2px solid var(--q-accent)"
|
||||||
:tabindex="scope.tabindex"
|
@blur="onBlurShiftTypeSelect"
|
||||||
>
|
@update:model-value="option => shift.type = option.value"
|
||||||
<q-icon
|
>
|
||||||
:name="scope.opt.icon"
|
<template #selected-item="scope">
|
||||||
:color="scope.opt.icon_color"
|
<div
|
||||||
size="sm"
|
class="row flex-center text-weight-bold q-ma-none q-pa-none no-wrap ellipsis full-width"
|
||||||
class="col-auto q-mx-xs"
|
:class="ui_store.is_mobile_mode ? 'items-center full-height' : 'flex-center'"
|
||||||
/>
|
:tabindex="scope.tabindex"
|
||||||
<span
|
>
|
||||||
style="line-height: 0.9em;"
|
<q-icon
|
||||||
class="col-auto ellipsis"
|
:name="scope.opt.icon"
|
||||||
>{{ scope.opt.label }}</span>
|
:color="scope.opt.icon_color"
|
||||||
</div>
|
size="sm"
|
||||||
</template>
|
class="col-auto q-mx-xs"
|
||||||
</q-select>
|
/>
|
||||||
</div>
|
<span
|
||||||
|
style="line-height: 0.9em;"
|
||||||
<div class="col row flex-center text-uppercase rounded-5 bg-transparent q-pa-xs">
|
class="col-auto ellipsis"
|
||||||
<!-- punch in field -->
|
:class="(shift.is_approved || isTimesheetApproved) ? 'text-white' : ''"
|
||||||
<q-input
|
>{{ scope.opt.label }}</span>
|
||||||
v-model="shift.start_time"
|
</div>
|
||||||
dense
|
</template>
|
||||||
:readonly="shift.is_approved"
|
</q-select>
|
||||||
type="time"
|
</div>
|
||||||
:standout="$q.dark.isActive ? 'bg-blue-grey-9' : 'bg-blue-grey-1 text-white'"
|
|
||||||
label-slot
|
<div class="col row flex-center text-uppercase rounded-5 bg-transparent q-pa-xs">
|
||||||
label-color="accent"
|
<!-- punch in field -->
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
<q-input
|
||||||
input-style="font-size: 1.2em;"
|
v-model="shift.start_time"
|
||||||
class="col rounded-5 bg-dark"
|
dense
|
||||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
:borderless="(shift.is_approved || isTimesheetApproved)"
|
||||||
>
|
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||||
<template #label>
|
type="time"
|
||||||
<span
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
class="text-weight-bolder"
|
label-slot
|
||||||
style="font-size: 0.95em;"
|
:label-color="(shift.is_approved || isTimesheetApproved) ? 'white' : 'accent'"
|
||||||
>{{ $t('shared.misc.in') }}</span>
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed text-white' : '')"
|
||||||
</template>
|
input-style="font-size: 1.2em;"
|
||||||
</q-input>
|
class="col rounded-5 bg-dark"
|
||||||
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-mr-xs ' : 'q-mx-xs ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
|
||||||
<!-- punch out field -->
|
:style="(shift.is_approved || isTimesheetApproved) ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
<q-input
|
>
|
||||||
v-model="shift.end_time"
|
<template #label>
|
||||||
dense
|
<span
|
||||||
:readonly="shift.is_approved"
|
class="text-weight-bolder"
|
||||||
type="time"
|
style="font-size: 0.95em;"
|
||||||
standout="bg-blue-grey-9"
|
>{{ $t('shared.misc.in') }}</span>
|
||||||
label-slot
|
</template>
|
||||||
label-color="accent"
|
</q-input>
|
||||||
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
|
||||||
input-style="font-size: 1.2em;"
|
<!-- punch out field -->
|
||||||
class="col rounded-5 bg-dark"
|
<q-input
|
||||||
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + (shift.is_approved ? 'cursor-not-allowed' : '')"
|
v-model="shift.end_time"
|
||||||
>
|
:standout="$q.dark.isActive ? 'bg-blue-grey-3' : 'bg-blue-grey-9'"
|
||||||
<template #label>
|
dense
|
||||||
<span
|
:borderless="(shift.is_approved || isTimesheetApproved)"
|
||||||
class="text-weight-bolder"
|
:readonly="(shift.is_approved || isTimesheetApproved)"
|
||||||
style="font-size: 0.95em;"
|
type="time"
|
||||||
>{{ $t('shared.misc.out') }}</span>
|
label-slot
|
||||||
</template>
|
:label-color="(shift.is_approved || isTimesheetApproved) ? 'white' : 'accent'"
|
||||||
</q-input>
|
:input-class="'text-weight-medium ' + (shift.id === -2 ? 'text-white ' : ' ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed text-white' : '')"
|
||||||
|
input-style="font-size: 1.2em;"
|
||||||
<!-- comment and delete buttons -->
|
class="col rounded-5 bg-dark"
|
||||||
<div :class="ui_store.is_mobile_mode ? 'col-12 row' : 'col-auto'">
|
:class="(shift.id === -2 ? 'bg-negative ' : ' ') + (ui_store.is_mobile_mode ? 'q-ml-xs ' : 'q-mx-xs ') + ((shift.is_approved || isTimesheetApproved) ? 'cursor-not-allowed q-px-xs transparent inset-shadow' : '')"
|
||||||
<q-icon
|
:style="shift.is_approved ? 'background-color: #0a7d32 !important;' : ''"
|
||||||
v-if="shift.type && dense"
|
>
|
||||||
:name="shift.comment ? 'comment' : ''"
|
<template #label>
|
||||||
color="primary"
|
<span
|
||||||
:size="dense ? 'xs' : 'sm'"
|
class="text-weight-bolder"
|
||||||
class="col"
|
style="font-size: 0.95em;"
|
||||||
/>
|
>{{ $t('shared.misc.out') }}</span>
|
||||||
|
</template>
|
||||||
<!-- desktop comment button -->
|
</q-input>
|
||||||
<q-btn
|
|
||||||
v-else-if="!ui_store.is_mobile_mode"
|
<!-- comment and delete buttons -->
|
||||||
flat
|
<div :class="ui_store.is_mobile_mode ? 'col-12 row' : 'col-auto'">
|
||||||
dense
|
<q-icon
|
||||||
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
v-if="shift.type && dense"
|
||||||
:text-color="shift.comment ? 'accent' : 'grey-5'"
|
:name="shift.comment ? 'comment' : ''"
|
||||||
class="col"
|
color="primary"
|
||||||
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
:size="dense ? 'xs' : 'sm'"
|
||||||
>
|
class="col"
|
||||||
<q-popup-edit
|
/>
|
||||||
v-model="shift.comment"
|
|
||||||
:title="$t('timesheet.shift.fields.header_comment')"
|
<!-- desktop comment button -->
|
||||||
auto-save
|
<q-btn
|
||||||
v-slot="scope"
|
v-else-if="!ui_store.is_mobile_mode"
|
||||||
class="bg-dark"
|
push
|
||||||
>
|
dense
|
||||||
<q-input
|
:color="shift.is_approved ? 'accent' : 'dark'"
|
||||||
color="white"
|
:icon="shift.comment ? 'chat' : 'chat_bubble_outline'"
|
||||||
v-model="scope.value"
|
:text-color="shift.is_approved ? '' : (shift.comment ? 'accent' : 'grey-5')"
|
||||||
dense
|
class="col"
|
||||||
:readonly="shift.is_approved"
|
:class="ui_store.is_mobile_mode ? 'q-mt-xs bg-dark' : ''"
|
||||||
autofocus
|
>
|
||||||
counter
|
<q-popup-edit
|
||||||
bottom-slots
|
v-model="shift.comment"
|
||||||
:maxlength="COMMENT_LENGTH_MAX"
|
:title="$t('timesheet.shift.fields.header_comment')"
|
||||||
class="q-pb-lg"
|
auto-save
|
||||||
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
|
v-slot="scope"
|
||||||
@keyup.enter="scope.set"
|
class="bg-dark"
|
||||||
>
|
>
|
||||||
<template #append>
|
<q-input
|
||||||
<q-icon name="edit" />
|
color="white"
|
||||||
</template>
|
v-model="scope.value"
|
||||||
|
dense
|
||||||
<template #counter>
|
:readonly="shift.is_approved"
|
||||||
<div class="row flex-center">
|
autofocus
|
||||||
<q-space />
|
counter
|
||||||
<q-knob
|
bottom-slots
|
||||||
:model-value="scope.value?.length"
|
:maxlength="COMMENT_LENGTH_MAX"
|
||||||
readonly
|
class="q-pb-lg"
|
||||||
:max="COMMENT_LENGTH_MAX"
|
:class="shift.is_approved ? 'cursor-not-allowed' : ''"
|
||||||
size="1.6em"
|
@keyup.enter="scope.set"
|
||||||
:thickness="0.4"
|
>
|
||||||
:color="getCommentCounterColor(scope.value?.length ?? 0)"
|
<template #append>
|
||||||
track-color="grey-4"
|
<q-icon name="edit" />
|
||||||
class="col-auto q-mr-xs"
|
</template>
|
||||||
/>
|
|
||||||
<span
|
<template #counter>
|
||||||
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
|
<div class="row flex-center">
|
||||||
>{{ 280 - (scope.value?.length ?? 0) }}</span>
|
<q-space />
|
||||||
</div>
|
<q-knob
|
||||||
</template>
|
:model-value="scope.value?.length"
|
||||||
</q-input>
|
readonly
|
||||||
</q-popup-edit>
|
:max="COMMENT_LENGTH_MAX"
|
||||||
</q-btn>
|
size="1.6em"
|
||||||
|
:thickness="0.4"
|
||||||
<q-btn
|
:color="getCommentCounterColor(scope.value?.length ?? 0)"
|
||||||
v-if="!ui_store.is_mobile_mode"
|
track-color="grey-4"
|
||||||
flat
|
class="col-auto q-mr-xs"
|
||||||
dense
|
/>
|
||||||
:disable="shift.is_approved"
|
<span
|
||||||
tabindex="-1"
|
:class="'col-auto text-weight-bolder text-' + getCommentCounterColor(scope.value?.length ?? 0)"
|
||||||
icon="cancel"
|
>{{ 280 - (scope.value?.length ?? 0) }}</span>
|
||||||
text-color="negative"
|
</div>
|
||||||
class="col"
|
</template>
|
||||||
:class="shift.is_approved ? 'invisible' : ''"
|
</q-input>
|
||||||
@click="$emit('requestDelete')"
|
</q-popup-edit>
|
||||||
/>
|
</q-btn>
|
||||||
</div>
|
|
||||||
</div>
|
<q-btn
|
||||||
</div>
|
v-if="!ui_store.is_mobile_mode"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:disable="shift.is_approved"
|
||||||
|
tabindex="-1"
|
||||||
|
icon="cancel"
|
||||||
|
text-color="negative"
|
||||||
|
class="col"
|
||||||
|
:class="shift.is_approved ? 'invisible' : ''"
|
||||||
|
@click="$emit('requestDelete')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</q-slide-item>
|
</q-slide-item>
|
||||||
|
|
||||||
|
<q-separator
|
||||||
|
v-if="hasShiftAfter && ui_store.is_mobile_mode"
|
||||||
|
spaced
|
||||||
|
color="accent"
|
||||||
|
class="q-mx-md"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -9,10 +9,9 @@
|
||||||
|
|
||||||
const shift_api = useShiftApi();
|
const shift_api = useShiftApi();
|
||||||
|
|
||||||
const { day, dense = false, outlined = false, approved = false } = defineProps<{
|
const { day, dense = false, approved = false } = defineProps<{
|
||||||
day: TimesheetDay;
|
day: TimesheetDay;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
outlined?: boolean;
|
|
||||||
approved?: boolean;
|
approved?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
@ -32,13 +31,14 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="column justify-center q-py-xs" :class="approved ? '' : ''">
|
<div class="column justify-center q-py-xs" :class="approved ? '' : ''">
|
||||||
<ShiftListDayRow
|
<ShiftListDayRow
|
||||||
v-for="shift, shift_index in day.shifts"
|
v-for="shift, shift_index in day.shifts"
|
||||||
:key="shift_index"
|
:key="shift_index"
|
||||||
v-model:shift="day.shifts[shift_index]!"
|
v-model:shift="day.shifts[shift_index]!"
|
||||||
:outlined="outlined"
|
:is-timesheet-approved="approved"
|
||||||
:dense="dense"
|
:dense="dense"
|
||||||
@request-delete="deleteCurrentShift(shift)"
|
:has-shift-after="shift_index < day.shifts.length - 1"
|
||||||
/>
|
@request-delete="deleteCurrentShift(shift)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import { computed } from 'vue';
|
||||||
import { date } from 'quasar';
|
import { date } from 'quasar';
|
||||||
import { useUiStore } from 'src/stores/ui-store';
|
import { useUiStore } from 'src/stores/ui-store';
|
||||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
@ -15,6 +16,8 @@
|
||||||
const ui_store = useUiStore();
|
const ui_store = useUiStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
|
const animation_style = computed(() => ui_store.is_mobile_mode ? 'fadeInLeft' : 'fadeInDown' );
|
||||||
|
|
||||||
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
const addNewShift = (day_shifts: Shift[], date: string, timesheet_id: number) => {
|
||||||
ui_store.focus_next_component = true;
|
ui_store.focus_next_component = true;
|
||||||
const new_shift = new Shift;
|
const new_shift = new Shift;
|
||||||
|
|
@ -48,118 +51,130 @@
|
||||||
:key="timesheet.timesheet_id"
|
:key="timesheet.timesheet_id"
|
||||||
class="col column"
|
class="col column"
|
||||||
>
|
>
|
||||||
<div
|
<transition-group
|
||||||
v-for="day, day_index in timesheet.days"
|
appear
|
||||||
:key="day.date"
|
:enter-active-class="`animated ${animation_style}`"
|
||||||
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="ui_store.is_mobile_mode"
|
v-for="day, day_index in timesheet.days"
|
||||||
class="col column full-width"
|
:key="day.date"
|
||||||
>
|
class="col-auto row rounded-10 q-ma-sm shadow-10"
|
||||||
<q-card
|
:style="`animation-delay: ${day_index / 15}s;`"
|
||||||
class="rounded-10 bg-dark"
|
|
||||||
:style="ui_store.is_mobile_mode ? (getDayApproval(day) ? 'border: 3px solid var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
|
||||||
>
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
|
||||||
:class="getDayApproval(day) ? 'bg-dark text-accent' : 'bg-primary text-white'"
|
|
||||||
style="line-height: 1em;"
|
|
||||||
>
|
|
||||||
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
|
||||||
weekday: 'long', day: 'numeric', month:
|
|
||||||
'long'
|
|
||||||
}) }}</span>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section
|
|
||||||
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
|
||||||
class="q-pa-none transparent"
|
|
||||||
>
|
|
||||||
<ShiftListDay
|
|
||||||
outlined
|
|
||||||
:approved="getDayApproval(day)"
|
|
||||||
:day="day"
|
|
||||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-actions class="q-pa-none">
|
|
||||||
<q-btn
|
|
||||||
v-if="!getDayApproval(day)"
|
|
||||||
square
|
|
||||||
color="accent"
|
|
||||||
icon="more_time"
|
|
||||||
class="full-width"
|
|
||||||
style="border-radius: 0 0 5px 5px;"
|
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
|
||||||
/>
|
|
||||||
</q-card-actions>
|
|
||||||
|
|
||||||
<q-badge
|
|
||||||
v-if="getDayApproval(day)"
|
|
||||||
floating
|
|
||||||
class="transparent q-pa-none rounded-50"
|
|
||||||
style="transform: translate(15px, -5px);"
|
|
||||||
>
|
|
||||||
<q-icon
|
|
||||||
name="verified"
|
|
||||||
size="5em"
|
|
||||||
color="accent"
|
|
||||||
/>
|
|
||||||
</q-badge>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="col row full-width"
|
|
||||||
:class="getDayApproval(day) ? 'rounded-10 bg-accent' : ''"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="col row bg-dark"
|
v-if="ui_store.is_mobile_mode"
|
||||||
:class="getDayApproval(day) ? 'bg-transparent' : ''"
|
class="col column full-width"
|
||||||
style="border-radius: 10px 0 0 10px;"
|
|
||||||
>
|
>
|
||||||
<!-- Date block -->
|
<q-card
|
||||||
<ShiftListDateWidget
|
class="rounded-10"
|
||||||
:display-date="day.date"
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-accent' : 'bg-dark'"
|
||||||
:approved="getDayApproval(day)"
|
:style="ui_store.is_mobile_mode ? ((getDayApproval(day) || timesheet.is_approved) ? 'border: 6px inset var(--q-accent)' : 'border: 1px solid var(--q-accent);') : ''"
|
||||||
class="col-auto"
|
>
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- List of shifts -->
|
<q-card-section
|
||||||
<ShiftListDay
|
class="text-weight-bolder text-uppercase text-h6 q-py-xs"
|
||||||
:day="day"
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-dark text-white' : 'bg-primary text-white'"
|
||||||
class="col"
|
style="line-height: 1em;"
|
||||||
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
>
|
||||||
/>
|
<span> {{ $d(extractDate(day.date, 'YYYY-MM-DD'), {
|
||||||
|
weekday: 'long', day: 'numeric', month:
|
||||||
|
'long'
|
||||||
|
}) }}</span>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section
|
||||||
|
v-if="day.shifts.filter(shift => shift.id !== 0).length > 0"
|
||||||
|
class="q-pa-none transparent"
|
||||||
|
>
|
||||||
|
<ShiftListDay
|
||||||
|
outlined
|
||||||
|
:animation-delay-multiplier="day_index"
|
||||||
|
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
:day="day"
|
||||||
|
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions class="q-pa-none">
|
||||||
|
<q-btn
|
||||||
|
v-if="!(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
square
|
||||||
|
color="accent"
|
||||||
|
icon="more_time"
|
||||||
|
class="full-width"
|
||||||
|
style="border-radius: 0 0 5px 5px;"
|
||||||
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
<q-badge
|
||||||
|
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
floating
|
||||||
|
class="transparent q-pa-none rounded-50"
|
||||||
|
style="transform: translate(15px, -5px);"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="verified"
|
||||||
|
size="5em"
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
</q-badge>
|
||||||
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto self-stretch">
|
<div
|
||||||
<q-icon
|
v-else
|
||||||
v-if="getDayApproval(day)"
|
class="col row full-width"
|
||||||
name="verified"
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'rounded-10 bg-accent' : ''"
|
||||||
color="white"
|
>
|
||||||
size="xl"
|
<!-- List of shifts -->
|
||||||
class="full-height"
|
|
||||||
/>
|
<div
|
||||||
<q-btn
|
class="col row bg-dark"
|
||||||
v-else
|
:class="(getDayApproval(day) || timesheet.is_approved) ? 'bg-transparent' : ''"
|
||||||
:dense="!ui_store.is_mobile_mode"
|
style="border-radius: 10px 0 0 10px;"
|
||||||
icon="more_time"
|
>
|
||||||
size="lg"
|
<!-- Date block -->
|
||||||
color="accent"
|
<ShiftListDateWidget
|
||||||
text-color="white"
|
:display-date="day.date"
|
||||||
class="full-height"
|
:approved="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
class="col-auto"
|
||||||
style="border-radius: 0 10px 10px 0;"
|
/>
|
||||||
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
|
||||||
/>
|
|
||||||
|
<ShiftListDay
|
||||||
|
:day="day"
|
||||||
|
:approved="getDayApproval(day) || timesheet.is_approved"
|
||||||
|
class="col"
|
||||||
|
@delete-unsaved-shift="deleteUnsavedShift(timesheet_index, day_index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-auto self-stretch">
|
||||||
|
<q-icon
|
||||||
|
v-if="(getDayApproval(day) || timesheet.is_approved)"
|
||||||
|
name="verified"
|
||||||
|
color="white"
|
||||||
|
size="xl"
|
||||||
|
class="full-height"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
:dense="!ui_store.is_mobile_mode"
|
||||||
|
icon="more_time"
|
||||||
|
size="lg"
|
||||||
|
color="accent"
|
||||||
|
text-color="white"
|
||||||
|
class="full-height"
|
||||||
|
:class="$q.screen.lt.md ? 'q-px-xs ' : ' '"
|
||||||
|
style="border-radius: 0 10px 10px 0;"
|
||||||
|
@click="addNewShift(day.shifts, day.date, timesheet.timesheet_id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
>
|
>
|
||||||
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
|
<q-card class="q-pa-xl rounded-200 bg-white frosted-glass">
|
||||||
<q-spinner-radio
|
<q-spinner-radio
|
||||||
color="primary"
|
color="accent"
|
||||||
size="20vh"
|
size="20vh"
|
||||||
/>
|
/>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
import { normalizeObject } from "src/utils/normalize-object";
|
|
||||||
import { useExpensesStore } from "src/stores/expense-store";
|
import { useExpensesStore } from "src/stores/expense-store";
|
||||||
import { expense_validation_schema } from "src/modules/timesheets/models/expense-validation.models";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const useExpensesApi = () => {
|
export const useExpensesApi = () => {
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
const createExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
|
||||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateExpenseByEmployeeEmail = async (employee_email: string, date: string): Promise<void> => {
|
const upsertExpense = async (expense: Expense): Promise<void> => {
|
||||||
// await expenses_store.upsertOrDeleteExpensesByEmployeeEmail(employee_email, date, upsert_expense);
|
const success = await expenses_store.upsertExpense(expense);
|
||||||
|
if (success) {
|
||||||
|
timesheet_store.getTimesheetsByEmployeeEmail();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteExpenseById = async (expense_id: number): Promise<void> => {
|
const deleteExpenseById = async (expense_id: number): Promise<void> => {
|
||||||
await expenses_store.deleteExpenseById(expense_id);
|
const success = await expenses_store.deleteExpenseById(expense_id);
|
||||||
|
if (success) {
|
||||||
|
timesheet_store.getTimesheetsByEmployeeEmail();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createExpenseByEmployeeEmail,
|
upsertExpense,
|
||||||
updateExpenseByEmployeeEmail,
|
|
||||||
deleteExpenseById,
|
deleteExpenseById,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1,78 +1,78 @@
|
||||||
import { type Expense, EXPENSE_TYPE, type ExpenseType } from "src/modules/timesheets/models/expense.models";
|
// import { type Expense, EXPENSE_TYPE, type ExpenseType } from "src/modules/timesheets/models/expense.models";
|
||||||
import type { Normalizer } from "src/utils/normalize-object";
|
// import type { Normalizer } from "src/utils/normalize-object";
|
||||||
|
|
||||||
export interface ApiErrorPayload {
|
// export interface ApiErrorPayload {
|
||||||
status_code: number;
|
// status_code: number;
|
||||||
error_code?: string;
|
// error_code?: string;
|
||||||
message?: string;
|
// message?: string;
|
||||||
context?: Record<string, unknown>;
|
// context?: Record<string, unknown>;
|
||||||
};
|
// };
|
||||||
|
|
||||||
export abstract class ApiError extends Error {
|
// export abstract class ApiError extends Error {
|
||||||
status_code: number;
|
// status_code: number;
|
||||||
error_code?: string;
|
// error_code?: string;
|
||||||
context?: Record<string, unknown>;
|
// context?: Record<string, unknown>;
|
||||||
|
|
||||||
constructor(payload: ApiErrorPayload, defaultMessage: string) {
|
// constructor(payload: ApiErrorPayload, defaultMessage: string) {
|
||||||
super(payload.message || defaultMessage);
|
// super(payload.message || defaultMessage);
|
||||||
this.status_code = payload.status_code;
|
// this.status_code = payload.status_code;
|
||||||
this.error_code = payload.error_code ?? "unknown";
|
// this.error_code = payload.error_code ?? "unknown";
|
||||||
this.context = payload.context ?? {'unknown': 'unknown error has occured', };
|
// this.context = payload.context ?? {'unknown': 'unknown error has occured', };
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
export class GenericApiError extends ApiError {
|
// export class GenericApiError extends ApiError {
|
||||||
constructor(payload: ApiErrorPayload) {
|
// constructor(payload: ApiErrorPayload) {
|
||||||
super(payload, 'Encountered an error processing request');
|
// super(payload, 'Encountered an error processing request');
|
||||||
this.name = 'GenericApiError';
|
// this.name = 'GenericApiError';
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
export class ExpensesValidationError extends ApiError {
|
// export class ExpensesValidationError extends ApiError {
|
||||||
constructor(payload: ApiErrorPayload) {
|
// constructor(payload: ApiErrorPayload) {
|
||||||
super(payload, 'Invalid expense payload');
|
// super(payload, 'Invalid expense payload');
|
||||||
this.name = 'ExpensesValidationError';
|
// this.name = 'ExpensesValidationError';
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
export class ExpensesApiError extends ApiError {
|
// export class ExpensesApiError extends ApiError {
|
||||||
constructor(payload: ApiErrorPayload) {
|
// constructor(payload: ApiErrorPayload) {
|
||||||
super(payload, 'Request failed');
|
// super(payload, 'Request failed');
|
||||||
this.name = 'ExpensesApiError';
|
// this.name = 'ExpensesApiError';
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const expense_validation_schema: Normalizer<Expense> = {
|
// export const expense_validation_schema: Normalizer<Expense> = {
|
||||||
id: v => typeof v === 'number' ? v : -1,
|
// id: v => typeof v === 'number' ? v : -1,
|
||||||
date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
// date: v => typeof v === 'string' ? v.trim() : '1970-01-01',
|
||||||
type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
// type: v => EXPENSE_TYPE.includes(v as ExpenseType) ? v as ExpenseType : "EXPENSES",
|
||||||
amount: v => typeof v === "number" ? v : -1,
|
// amount: v => typeof v === "number" ? v : -1,
|
||||||
mileage: v => typeof v === "number" ? v : undefined,
|
// mileage: v => typeof v === "number" ? v : undefined,
|
||||||
comment: v => typeof v === 'string' ? v.trim() : '',
|
// comment: v => typeof v === 'string' ? v.trim() : '',
|
||||||
supervisor_comment: v => typeof v === 'string' ? v.trim() : '',
|
// supervisor_comment: v => typeof v === 'string' ? v.trim() : '',
|
||||||
is_approved: v => !!v,
|
// is_approved: v => !!v,
|
||||||
};
|
// };
|
||||||
|
|
||||||
export function toExpensesError(err: unknown): ExpensesValidationError | ExpensesApiError {
|
// export function toExpensesError(err: unknown): ExpensesValidationError | ExpensesApiError {
|
||||||
if (err instanceof ExpensesValidationError || err instanceof ExpensesApiError) {
|
// if (err instanceof ExpensesValidationError || err instanceof ExpensesApiError) {
|
||||||
return err;
|
// return err;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (typeof err === 'object' && err !== null && 'status_code' in err) {
|
// if (typeof err === 'object' && err !== null && 'status_code' in err) {
|
||||||
const payload = err as ApiErrorPayload;
|
// const payload = err as ApiErrorPayload;
|
||||||
|
|
||||||
// Don't know how to differentiate both types of errors, can be updated here
|
// // Don't know how to differentiate both types of errors, can be updated here
|
||||||
if (payload.error_code?.startsWith('API_')) {
|
// if (payload.error_code?.startsWith('API_')) {
|
||||||
return new ExpensesApiError(payload);
|
// return new ExpensesApiError(payload);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return new ExpensesValidationError(payload);
|
// return new ExpensesValidationError(payload);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Fallback with ValidationError as default
|
// // Fallback with ValidationError as default
|
||||||
return new ExpensesValidationError({
|
// return new ExpensesValidationError({
|
||||||
status_code: 500,
|
// status_code: 500,
|
||||||
message: err instanceof Error ? err.message : 'Unknown error',
|
// message: err instanceof Error ? err.message : 'Unknown error',
|
||||||
context: { original: err }
|
// context: { original: err }
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const TYPES_WITH_AMOUNT_ONLY: Readonly<ExpenseType[]> = ['PER_DIEM', 'EXP
|
||||||
|
|
||||||
export class Expense {
|
export class Expense {
|
||||||
id: number;
|
id: number;
|
||||||
|
timesheet_id: number;
|
||||||
date: string; //YYYY-MM-DD
|
date: string; //YYYY-MM-DD
|
||||||
type: ExpenseType;
|
type: ExpenseType;
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|
@ -16,6 +17,7 @@ export class Expense {
|
||||||
|
|
||||||
constructor(date: string) {
|
constructor(date: string) {
|
||||||
this.id = -1;
|
this.id = -1;
|
||||||
|
this.timesheet_id = -1;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.type = 'EXPENSES';
|
this.type = 'EXPENSES';
|
||||||
this.amount = 0;
|
this.amount = 0;
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,17 @@ import { api } from "src/boot/axios";
|
||||||
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
import type { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
|
|
||||||
export const ExpenseService = {
|
export const ExpenseService = {
|
||||||
createExpense: async (expense: Expense) => {
|
createExpense: async (expense: Expense): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
||||||
const response = await api.post('expense/create', expense);
|
const response = await api.post('expense/create', expense);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateExpenseById: async (expense: Expense) => {
|
updateExpense: async (expense: Expense): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
||||||
const response = await api.patch(`expense/update`, expense);
|
const response = await api.patch(`expense/update`, expense);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteExpenseById: async (expense_id: number): Promise<{ok: boolean, id: number, error?: unknown}> => {
|
deleteExpenseById: async (expense_id: number): Promise<{success: boolean, data: number, error?: unknown}> => {
|
||||||
const response = await api.delete(`expense/delete/${expense_id}`);
|
const response = await api.delete(`expense/delete/${expense_id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,21 @@ export const useExpenseRules = (t: (_key: string) => string) => {
|
||||||
mileageRequired,
|
mileageRequired,
|
||||||
commentRequired,
|
commentRequired,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToMonetaryAmount = (amount: number | string): number => {
|
||||||
|
if (typeof amount === 'number') return Number(amount.toFixed(2));
|
||||||
|
|
||||||
|
if (typeof amount === 'string') {
|
||||||
|
try {
|
||||||
|
const single_decimal_amount = amount.replace(/\.(?=.*\.)/g, '');
|
||||||
|
const numbers_only_decimal = single_decimal_amount.replace(/[^0-9.]/g, '');
|
||||||
|
|
||||||
|
return Number(numbers_only_decimal);
|
||||||
|
} catch(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-layout view="hHh lpR fFf">
|
<q-layout view="hHh lpR fFf">
|
||||||
<q-page-container class="bg-dark">
|
<q-page-container class="bg-secondary">
|
||||||
<q-img src="src/assets/village.png" fit="cover" position="50% 100%" class="absolute-full" />
|
<q-page class="column">
|
||||||
<q-page class="flex flex-center">
|
<q-img src="src/assets/village.png" fit="contain" class="col absolute-bottom-right" style="opacity: 50%;" />
|
||||||
<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" class="col absolute-center">
|
||||||
<LoginConnectionPanel />
|
<LoginConnectionPanel />
|
||||||
</transition>
|
</transition>
|
||||||
</q-page>
|
</q-page>
|
||||||
|
|
|
||||||
|
|
@ -25,24 +25,28 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
is_open.value = false;
|
is_open.value = false;
|
||||||
|
is_hiding_create_form.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const upsertExpensesById = async (expense_id: number, expense: Expense): Promise<void> => {
|
const upsertExpense = async (expense: Expense): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
if (expense_id < 0) {
|
if (expense.id < 0) {
|
||||||
const data = await ExpenseService.createExpense(expense);
|
const data = await ExpenseService.createExpense(expense);
|
||||||
return data;
|
return data.success;
|
||||||
}
|
}
|
||||||
// TODO: Save response data into proper ref
|
const data = await ExpenseService.updateExpense(expense);
|
||||||
|
return data.success;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// setErrorFrom(err);
|
// setErrorFrom(err);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteExpenseById = async (expense_id: number): Promise<boolean> => {
|
const deleteExpenseById = async (expense_id: number): Promise<boolean> => {
|
||||||
const data = await ExpenseService.deleteExpenseById(expense_id);
|
const data = await ExpenseService.deleteExpenseById(expense_id);
|
||||||
return data.ok;
|
console.log('data received from expense deletion: ', data);
|
||||||
|
return data.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -53,7 +57,7 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
current_expense,
|
current_expense,
|
||||||
initial_expense,
|
initial_expense,
|
||||||
open,
|
open,
|
||||||
upsertExpensesById,
|
upsertExpense,
|
||||||
deleteExpenseById,
|
deleteExpenseById,
|
||||||
close,
|
close,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user