Merge pull request 'release/nicolas/v1.1' (#73) from release/nicolas/v1.1 into main
Reviewed-on: Targo/targo_frontend#73
This commit is contained in:
commit
5889482df5
105
package-lock.json
generated
105
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"@quasar/extras": "^1.17.0",
|
"@quasar/extras": "^1.17.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"crc": "^4.3.2",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
|
|
@ -3091,7 +3092,7 @@
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -3152,6 +3153,30 @@
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bl/node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bl/node_modules/readable-stream": {
|
"node_modules/bl/node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
|
@ -3318,10 +3343,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -3336,10 +3361,9 @@
|
||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.1",
|
"base64-js": "^1.3.1",
|
||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-builder": {
|
"node_modules/buffer-builder": {
|
||||||
|
|
@ -4018,6 +4042,22 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crc": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"buffer": ">=6.0.3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"buffer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crc-32": {
|
"node_modules/crc-32": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
|
@ -4139,6 +4179,30 @@
|
||||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cypress/node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cypress/node_modules/proxy-from-env": {
|
"node_modules/cypress/node_modules/proxy-from-env": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
|
||||||
|
|
@ -5870,7 +5934,7 @@
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -8173,31 +8237,6 @@
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readable-stream/node_modules/buffer": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.3.1",
|
|
||||||
"ieee754": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/readdir-glob": {
|
"node_modules/readdir-glob": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"@quasar/extras": "^1.17.0",
|
"@quasar/extras": "^1.17.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
|
"crc": "^4.3.2",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,22 @@
|
||||||
import SchedulePresetsDialog from 'src/modules/employee-list/components/schedule-presets-dialog.vue';
|
import SchedulePresetsDialog from 'src/modules/employee-list/components/schedule-presets-dialog.vue';
|
||||||
import AddModifyDialogSchedulePreview from './add-modify-dialog-schedule-preview.vue';
|
import AddModifyDialogSchedulePreview from './add-modify-dialog-schedule-preview.vue';
|
||||||
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
import { useSchedulePresetsStore } from 'src/stores/schedule-presets.store';
|
||||||
import { useEmployeeStore } from 'src/stores/employee-store';
|
import { useEmployeeStore } from 'src/stores/employee-store';
|
||||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||||
import type { PresetManagerMode } from 'src/modules/employee-list/models/schedule-presets.models';
|
import type { PresetManagerMode } from 'src/modules/employee-list/models/schedule-presets.models';
|
||||||
|
|
||||||
|
// ================= state ======================
|
||||||
|
|
||||||
const schedule_preset_store = useSchedulePresetsStore();
|
const schedule_preset_store = useSchedulePresetsStore();
|
||||||
const employee_store = useEmployeeStore();
|
const employee_store = useEmployeeStore();
|
||||||
const employee_list_api = useEmployeeListApi();
|
const employee_list_api = useEmployeeListApi();
|
||||||
|
|
||||||
const preset_options = ref<{ label: string, value: number }[]>([]);
|
const preset_options = ref<{ label: string, value: number }[]>([]);
|
||||||
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 });
|
const current_preset = ref<{ label: string | undefined, value: number }>({ label: undefined, value: -1 });
|
||||||
const manager_watcher = ref(schedule_preset_store.is_manager_open);
|
|
||||||
|
// ====================== methods ========================
|
||||||
|
|
||||||
const getPresetOptions = (): { label: string, value: number }[] => {
|
const getPresetOptions = (): { label: string, value: number }[] => {
|
||||||
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
const options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
||||||
|
|
@ -41,8 +44,6 @@
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadSelectedPresetOption();
|
loadSelectedPresetOption();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(manager_watcher, loadSelectedPresetOption)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -50,7 +51,7 @@
|
||||||
:key="schedule_preset_store.is_manager_open === false ? '0' : '1'"
|
:key="schedule_preset_store.is_manager_open === false ? '0' : '1'"
|
||||||
class="column full-width flex-center items-start"
|
class="column full-width flex-center items-start"
|
||||||
>
|
>
|
||||||
<SchedulePresetsDialog />
|
<SchedulePresetsDialog @before-hide="loadSelectedPresetOption"/>
|
||||||
|
|
||||||
<div class="col row justify-center full-width no-wrap">
|
<div class="col row justify-center full-width no-wrap">
|
||||||
<q-select
|
<q-select
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,22 @@
|
||||||
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 { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { Expense, type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { Expense, type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
|
||||||
// ================= state ======================
|
// ================= state ======================
|
||||||
|
|
||||||
interface ExpenseOption {
|
|
||||||
label: string;
|
|
||||||
value: ExpenseType;
|
|
||||||
icon: string;
|
|
||||||
}
|
|
||||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
|
|
||||||
|
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||||
|
const file = defineModel<File>('file');
|
||||||
|
|
||||||
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 auth_store = useAuthStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
const files = defineModel<File[] | null>('files');
|
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
const rules = useExpenseRules(t);
|
const rules = useExpenseRules(t);
|
||||||
|
|
||||||
|
|
@ -62,8 +59,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
await expenses_api.upsertExpense(expenses_store.current_expense, employeeEmail);
|
if (file.value)
|
||||||
|
await expenses_api.upsertExpense(expenses_store.current_expense, file.value, employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL');
|
||||||
|
|
||||||
expenses_store.is_showing_create_form = true;
|
expenses_store.is_showing_create_form = true;
|
||||||
expenses_store.mode = 'create';
|
expenses_store.mode = 'create';
|
||||||
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'));
|
||||||
|
|
@ -255,11 +253,9 @@
|
||||||
<!-- import attach file section -->
|
<!-- import attach file section -->
|
||||||
<div class="col q-px-xs">
|
<div class="col q-px-xs">
|
||||||
<q-file
|
<q-file
|
||||||
v-model="files"
|
v-model="file"
|
||||||
standout
|
standout
|
||||||
dense
|
dense
|
||||||
use-chips
|
|
||||||
multiple
|
|
||||||
stack-label
|
stack-label
|
||||||
label-slot
|
label-slot
|
||||||
>
|
>
|
||||||
|
|
@ -298,7 +294,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
||||||
border: transparent;
|
border: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -14,19 +14,21 @@
|
||||||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import type { Expense } from 'src/modules/timesheets/models/expense.models';
|
import type { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
|
||||||
const { t } = useI18n();
|
// ================== state =====================
|
||||||
|
|
||||||
const expense = defineModel<Expense>({ required: true });
|
const expense = defineModel<Expense>({ required: true });
|
||||||
|
|
||||||
const expenses_api = useExpensesApi();
|
|
||||||
const expenses_store = useExpensesStore();
|
|
||||||
const timesheet_store = useTimesheetStore();
|
|
||||||
|
|
||||||
const is_showing_update_form = ref(false);
|
|
||||||
|
|
||||||
const { mode = 'normal' } = defineProps<{
|
const { mode = 'normal' } = defineProps<{
|
||||||
mode?: 'approval' | 'normal';
|
mode?: 'approval' | 'normal';
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const expenses_api = useExpensesApi();
|
||||||
|
const expenses_store = useExpensesStore();
|
||||||
|
const timesheet_store = useTimesheetStore();
|
||||||
|
const is_showing_update_form = ref(false);
|
||||||
|
|
||||||
|
// ===================== methods =========================
|
||||||
|
|
||||||
const requestExpenseDeletion = async () => {
|
const requestExpenseDeletion = async () => {
|
||||||
await expenses_api.deleteExpenseById(expense.value.id);
|
await expenses_api.deleteExpenseById(expense.value.id);
|
||||||
|
|
|
||||||
|
|
@ -2,32 +2,34 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
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 { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
import { getExpenseIcon, useExpenseRules } from 'src/modules/timesheets/utils/expense.util';
|
||||||
import { type ExpenseType, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
import { Expense, type ExpenseOption, TYPES_WITH_AMOUNT_ONLY } from 'src/modules/timesheets/models/expense.models';
|
||||||
|
import { useAuthStore } from 'src/stores/auth-store';
|
||||||
|
|
||||||
interface ExpenseOption {
|
const COMMENT_MAX_LENGTH = 280;
|
||||||
label: string;
|
|
||||||
value: ExpenseType;
|
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||||
icon: string;
|
const file = defineModel<File | undefined>('file');
|
||||||
}
|
|
||||||
|
const { employeeEmail } = defineProps<{
|
||||||
|
employeeEmail?: 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 auth_store = useAuthStore();
|
||||||
const expenses_api = useExpensesApi();
|
const expenses_api = useExpensesApi();
|
||||||
const files = defineModel<File[] | null>('files');
|
|
||||||
const is_navigator_open = ref(false);
|
const is_navigator_open = ref(false);
|
||||||
const is_showing_comment_dialog_mobile = ref(false);
|
const is_showing_comment_dialog_mobile = ref(false);
|
||||||
|
|
||||||
const COMMENT_MAX_LENGTH = 280;
|
|
||||||
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('-', '/') ?? '');
|
||||||
|
|
@ -49,8 +51,16 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestExpenseCreationOrUpdate = async () => {
|
const requestExpenseCreationOrUpdate = async () => {
|
||||||
await expenses_api.upsertExpense(expenses_store.current_expense);
|
if (file.value)
|
||||||
|
await expenses_api.upsertExpense(expenses_store.current_expense, file.value, employeeEmail ?? auth_store.user?.email ?? 'MISSING_EMAIL');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (expense.value)
|
||||||
|
expense_selected.value = expense_options.find(expense_option => expense_option.value === expense.value.type);
|
||||||
|
else
|
||||||
|
expense_selected.value = expense_options[1];
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -64,9 +74,8 @@
|
||||||
class="col column items-start rounded-5 q-pb-sm"
|
class="col column items-start rounded-5 q-pb-sm"
|
||||||
:class="expenses_store.is_showing_create_form ? 'q-px-md' : 'q-px-sm'"
|
:class="expenses_store.is_showing_create_form ? 'q-px-md' : 'q-px-sm'"
|
||||||
>
|
>
|
||||||
<!-- date and type row -->
|
<!-- date selection input -->
|
||||||
<div class="col row q-my-xs full-width">
|
<div class="col-auto row q-my-xs full-width">
|
||||||
<!-- date selection input -->
|
|
||||||
<q-input
|
<q-input
|
||||||
v-model="expenses_store.current_expense.date"
|
v-model="expenses_store.current_expense.date"
|
||||||
dense
|
dense
|
||||||
|
|
@ -75,7 +84,7 @@
|
||||||
stack-label
|
stack-label
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
color="primary"
|
color="primary"
|
||||||
class="col-auto q-mr-sm"
|
class="col-auto full-width"
|
||||||
input-class="text-weight-medium"
|
input-class="text-weight-medium"
|
||||||
input-style="font-size: 1em;"
|
input-style="font-size: 1em;"
|
||||||
:label="$t('timesheet.expense.date')"
|
:label="$t('timesheet.expense.date')"
|
||||||
|
|
@ -112,8 +121,10 @@
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- expenses type selection -->
|
<!-- expenses type selection -->
|
||||||
|
<div class="col-auto row q-my-xs full-width">
|
||||||
<q-select
|
<q-select
|
||||||
v-model="expense_selected"
|
v-model="expense_selected"
|
||||||
standout="bg-blue-grey-9 text-white"
|
standout="bg-blue-grey-9 text-white"
|
||||||
|
|
@ -125,12 +136,12 @@
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
class="col"
|
class="col"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
behavior="menu"
|
||||||
|
:menu-offset="[0, 5]"
|
||||||
:label="$t('timesheet.expense.type')"
|
:label="$t('timesheet.expense.type')"
|
||||||
:menu-offset="[0, 10]"
|
popup-content-class="text-uppercase text-weight-bold text-center rounded-5 z-top"
|
||||||
menu-anchor="bottom middle"
|
popup-content-style="border: 3px solid var(--q-accent)"
|
||||||
menu-self="top middle"
|
options-selected-class="text-weight-bolder text-white bg-accent"
|
||||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5"
|
|
||||||
popup-content-style="border: 2px solid var(--q-accent)"
|
|
||||||
:rules="[rules.typeRequired]"
|
:rules="[rules.typeRequired]"
|
||||||
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
||||||
>
|
>
|
||||||
|
|
@ -218,7 +229,10 @@
|
||||||
class="col-auto"
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-dialog v-model="is_showing_comment_dialog_mobile" class="z-top">
|
<q-dialog
|
||||||
|
v-model="is_showing_comment_dialog_mobile"
|
||||||
|
class="z-top"
|
||||||
|
>
|
||||||
<q-card class="full-width bg-primary rounded-10">
|
<q-card class="full-width bg-primary rounded-10">
|
||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<span
|
<span
|
||||||
|
|
@ -248,7 +262,7 @@
|
||||||
|
|
||||||
<!-- import attach file section -->
|
<!-- import attach file section -->
|
||||||
<q-file
|
<q-file
|
||||||
v-model="files"
|
v-model="file"
|
||||||
standout="bg-blue-grey-9"
|
standout="bg-blue-grey-9"
|
||||||
dense
|
dense
|
||||||
use-chips
|
use-chips
|
||||||
|
|
@ -273,7 +287,10 @@
|
||||||
</q-file>
|
</q-file>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col row full-width items-center" :class="expenses_store.mode === 'create' ? 'q-px-md q-py-xs' : ''">
|
<div
|
||||||
|
class="col row full-width items-center"
|
||||||
|
:class="expenses_store.mode === 'create' ? 'q-px-md q-py-xs' : ''"
|
||||||
|
>
|
||||||
<q-btn
|
<q-btn
|
||||||
push
|
push
|
||||||
color="accent"
|
color="accent"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import ShiftListScrollable from 'src/modules/timesheets/components/shift-list-scrollable.vue';
|
import ShiftListScrollable from 'src/modules/timesheets/components/shift-list-scrollable.vue';
|
||||||
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.vue';
|
||||||
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
|
import ExpenseDialog from 'src/modules/timesheets/components/expense-dialog.vue';
|
||||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
|
||||||
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
import PayPeriodNavigator from 'src/modules/shared/components/pay-period-navigator.vue';
|
||||||
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
import TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||||
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
|
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
|
||||||
|
|
@ -104,15 +103,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageHeaderTemplate
|
<q-space />
|
||||||
v-if="mode === 'normal'"
|
|
||||||
:title="'timesheet.page_header'"
|
|
||||||
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
|
||||||
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
|
||||||
class="col"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-space v-if="!$q.platform.is.mobile && mode === 'approval'" />
|
|
||||||
|
|
||||||
<!-- employee weekly overview -->
|
<!-- employee weekly overview -->
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,19 @@ export const useExpensesApi = () => {
|
||||||
const expenses_store = useExpensesStore();
|
const expenses_store = useExpensesStore();
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const upsertExpense = async (expense: Expense, employee_email?: string): Promise<void> => {
|
const upsertExpense = async (expense: Expense, file: File, employee_email: string): Promise<string> => {
|
||||||
|
const presignedURL = expenses_store.uploadAttachment(file);
|
||||||
|
if (!presignedURL) return 'PRESIGN_FAILED';
|
||||||
|
|
||||||
const success = await expenses_store.upsertExpense(expense, employee_email);
|
const success = await expenses_store.upsertExpense(expense, employee_email);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
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'));
|
||||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||||
|
return 'SUCCESS';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 'INVALID_EXPENSE';
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
|
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,10 @@ export class Expense {
|
||||||
this.comment = '';
|
this.comment = '';
|
||||||
this.is_approved = false;
|
this.is_approved = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ExpenseOption {
|
||||||
|
label: string;
|
||||||
|
value: ExpenseType;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,31 @@
|
||||||
import { api } from "src/boot/axios";
|
import { api } from "src/boot/axios";
|
||||||
|
import type { BackendResponse } from "src/modules/shared/models/backend-response.models";
|
||||||
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): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateExpense: async (expense: Expense, email?: string): Promise<{success: boolean, data: Expense, error?: unknown}> => {
|
updateExpense: async (expense: Expense, email?: string): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
|
||||||
const response = await api.patch(`expense/update${email ? '?employee_email=' + email : ''}`, expense);
|
const response = await api.patch(`expense/update${email ? '?employee_email=' + email : ''}`, expense);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteExpenseById: async (expense_id: number): Promise<{success: boolean, data: 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;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise<BackendResponse<string>> => {
|
||||||
|
const [file_name, file_type] = file.name.split('.');
|
||||||
|
const response = await api.post(`attachments/s3/upload?file-name=${file_name}&file-type=${file_type}&checksumCRC32=${checksum_crc32}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadAttachmentWithPresignedUrl: async (file: File, url: string) => {
|
||||||
|
const response = await api.put(url, file, { headers: { 'Content-Type': file.type, }, withCredentials: false });
|
||||||
|
console.log('response to upload: ', response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
setup
|
setup
|
||||||
lang="ts"
|
lang="ts"
|
||||||
>
|
>
|
||||||
|
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||||
|
|
||||||
|
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||||
|
|
||||||
import { useAuthStore } from 'src/stores/auth-store';
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
||||||
const auth_store = useAuthStore();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -16,10 +16,14 @@
|
||||||
class="col column fit"
|
class="col column fit"
|
||||||
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"
|
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"
|
||||||
>
|
>
|
||||||
<TimesheetWrapper
|
<PageHeaderTemplate
|
||||||
:employee-email="auth_store.user?.email ?? ''"
|
:title="'timesheet.page_header'"
|
||||||
class="col"
|
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||||
|
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||||
|
class="col-auto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TimesheetWrapper class="col" />
|
||||||
</div>
|
</div>
|
||||||
</q-page>
|
</q-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -4,6 +4,7 @@ import { defineStore } from "pinia";
|
||||||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||||
import { Expense } from "src/modules/timesheets/models/expense.models";
|
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||||
|
import { computeCRC32Base64 } from "src/utils/crc-encoder";
|
||||||
|
|
||||||
export const useExpensesStore = defineStore('expenses', () => {
|
export const useExpensesStore = defineStore('expenses', () => {
|
||||||
const timesheet_store = useTimesheetStore();
|
const timesheet_store = useTimesheetStore();
|
||||||
|
|
@ -50,6 +51,22 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
return data.success;
|
return data.success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadAttachment = async (file: File) => {
|
||||||
|
try {
|
||||||
|
const checksum = await computeCRC32Base64(file);
|
||||||
|
const presignedUrlResponse = await ExpenseService.getPresignedUploadURL(file, checksum);
|
||||||
|
|
||||||
|
if (presignedUrlResponse.success && presignedUrlResponse.data) {
|
||||||
|
const { url, key } = JSON.parse(presignedUrlResponse.data);
|
||||||
|
console.log('key: ', key);
|
||||||
|
|
||||||
|
await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
is_open,
|
is_open,
|
||||||
is_loading,
|
is_loading,
|
||||||
|
|
@ -62,5 +79,6 @@ export const useExpensesStore = defineStore('expenses', () => {
|
||||||
upsertExpense,
|
upsertExpense,
|
||||||
deleteExpenseById,
|
deleteExpenseById,
|
||||||
close,
|
close,
|
||||||
|
uploadAttachment,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
17
src/utils/crc-encoder.ts
Normal file
17
src/utils/crc-encoder.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { crc32 } from 'crc';
|
||||||
|
|
||||||
|
export async function computeCRC32Base64(file: File): Promise<string> {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
// Pass arrayBuffer directly to crc32
|
||||||
|
let crc = crc32(arrayBuffer);
|
||||||
|
crc >>>= 0;
|
||||||
|
|
||||||
|
// Convert to 4-byte big-endian
|
||||||
|
const buffer = new ArrayBuffer(4);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint32(0, crc, false);
|
||||||
|
|
||||||
|
// Base64 encode
|
||||||
|
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user