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",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"crc": "^4.3.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
|
|
@ -3091,7 +3092,7 @@
|
|||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -3152,6 +3153,30 @@
|
|||
"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": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
|
|
@ -3318,10 +3343,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -3336,10 +3361,9 @@
|
|||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-builder": {
|
||||
|
|
@ -4018,6 +4042,22 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"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_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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
|
||||
|
|
@ -5870,7 +5934,7 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -8173,31 +8237,6 @@
|
|||
"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": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"@quasar/extras": "^1.17.0",
|
||||
"axios": "^1.11.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"crc": "^4.3.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
|
|
|
|||
|
|
@ -6,19 +6,22 @@
|
|||
import SchedulePresetsDialog from 'src/modules/employee-list/components/schedule-presets-dialog.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 { useEmployeeStore } from 'src/stores/employee-store';
|
||||
import { useEmployeeListApi } from '../composables/use-employee-api';
|
||||
import type { PresetManagerMode } from 'src/modules/employee-list/models/schedule-presets.models';
|
||||
|
||||
// ================= state ======================
|
||||
|
||||
const schedule_preset_store = useSchedulePresetsStore();
|
||||
const employee_store = useEmployeeStore();
|
||||
const employee_list_api = useEmployeeListApi();
|
||||
|
||||
const preset_options = ref<{ label: string, value: number }[]>([]);
|
||||
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 options = schedule_preset_store.schedule_presets.map(preset => { return { label: preset.name, value: preset.id } });
|
||||
|
|
@ -41,8 +44,6 @@
|
|||
onMounted(() => {
|
||||
loadSelectedPresetOption();
|
||||
});
|
||||
|
||||
watch(manager_watcher, loadSelectedPresetOption)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
:key="schedule_preset_store.is_manager_open === false ? '0' : '1'"
|
||||
class="column full-width flex-center items-start"
|
||||
>
|
||||
<SchedulePresetsDialog />
|
||||
<SchedulePresetsDialog @before-hide="loadSelectedPresetOption"/>
|
||||
|
||||
<div class="col row justify-center full-width no-wrap">
|
||||
<q-select
|
||||
|
|
|
|||
|
|
@ -10,25 +10,22 @@
|
|||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
import { useExpensesApi } from 'src/modules/timesheets/composables/use-expense-api';
|
||||
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 ======================
|
||||
|
||||
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 expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||
const file = defineModel<File>('file');
|
||||
|
||||
const { t } = useI18n();
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const auth_store = useAuthStore();
|
||||
const expenses_api = useExpensesApi();
|
||||
const files = defineModel<File[] | null>('files');
|
||||
const is_navigator_open = ref(false);
|
||||
const rules = useExpenseRules(t);
|
||||
|
||||
|
|
@ -62,7 +59,8 @@
|
|||
}
|
||||
|
||||
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.mode = 'create';
|
||||
|
|
@ -255,11 +253,9 @@
|
|||
<!-- import attach file section -->
|
||||
<div class="col q-px-xs">
|
||||
<q-file
|
||||
v-model="files"
|
||||
v-model="file"
|
||||
standout
|
||||
dense
|
||||
use-chips
|
||||
multiple
|
||||
stack-label
|
||||
label-slot
|
||||
>
|
||||
|
|
@ -298,7 +294,7 @@
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
||||
border: transparent;
|
||||
}
|
||||
:deep(.q-field--standout.q-field--readonly .q-field__control::before) {
|
||||
border: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -14,20 +14,22 @@
|
|||
import { getExpenseIcon } from 'src/modules/timesheets/utils/expense.util';
|
||||
import type { Expense } from 'src/modules/timesheets/models/expense.models';
|
||||
|
||||
const { t } = useI18n();
|
||||
// ================== state =====================
|
||||
|
||||
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<{
|
||||
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 () => {
|
||||
await expenses_api.deleteExpenseById(expense.value.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,32 +2,34 @@
|
|||
setup
|
||||
lang="ts"
|
||||
>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, onMounted } 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 { 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 {
|
||||
label: string;
|
||||
value: ExpenseType;
|
||||
icon: string;
|
||||
}
|
||||
const COMMENT_MAX_LENGTH = 280;
|
||||
|
||||
const expense = defineModel<Expense>({ default: new Expense(new Date().toISOString().slice(0, 10)) })
|
||||
const file = defineModel<File | undefined>('file');
|
||||
|
||||
const { employeeEmail } = defineProps<{
|
||||
employeeEmail?: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const ui_store = useUiStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
const expenses_store = useExpensesStore();
|
||||
const auth_store = useAuthStore();
|
||||
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('-', '/') ?? '');
|
||||
|
|
@ -49,8 +51,16 @@
|
|||
};
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
|
@ -64,9 +74,8 @@
|
|||
class="col column items-start rounded-5 q-pb-sm"
|
||||
:class="expenses_store.is_showing_create_form ? 'q-px-md' : 'q-px-sm'"
|
||||
>
|
||||
<!-- date and type row -->
|
||||
<div class="col row q-my-xs full-width">
|
||||
<!-- date selection input -->
|
||||
<!-- date selection input -->
|
||||
<div class="col-auto row q-my-xs full-width">
|
||||
<q-input
|
||||
v-model="expenses_store.current_expense.date"
|
||||
dense
|
||||
|
|
@ -75,7 +84,7 @@
|
|||
stack-label
|
||||
hide-bottom-space
|
||||
color="primary"
|
||||
class="col-auto q-mr-sm"
|
||||
class="col-auto full-width"
|
||||
input-class="text-weight-medium"
|
||||
input-style="font-size: 1em;"
|
||||
:label="$t('timesheet.expense.date')"
|
||||
|
|
@ -112,8 +121,10 @@
|
|||
</span>
|
||||
</template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
<!-- expenses type selection -->
|
||||
<!-- expenses type selection -->
|
||||
<div class="col-auto row q-my-xs full-width">
|
||||
<q-select
|
||||
v-model="expense_selected"
|
||||
standout="bg-blue-grey-9 text-white"
|
||||
|
|
@ -125,12 +136,12 @@
|
|||
hide-bottom-space
|
||||
class="col"
|
||||
color="primary"
|
||||
behavior="menu"
|
||||
:menu-offset="[0, 5]"
|
||||
: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)"
|
||||
popup-content-class="text-uppercase text-weight-bold text-center rounded-5 z-top"
|
||||
popup-content-style="border: 3px solid var(--q-accent)"
|
||||
options-selected-class="text-weight-bolder text-white bg-accent"
|
||||
:rules="[rules.typeRequired]"
|
||||
@update:model-value="option => expenses_store.current_expense.type = option.value"
|
||||
>
|
||||
|
|
@ -218,7 +229,10 @@
|
|||
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-section class="q-pa-none">
|
||||
<span
|
||||
|
|
@ -248,7 +262,7 @@
|
|||
|
||||
<!-- import attach file section -->
|
||||
<q-file
|
||||
v-model="files"
|
||||
v-model="file"
|
||||
standout="bg-blue-grey-9"
|
||||
dense
|
||||
use-chips
|
||||
|
|
@ -273,7 +287,10 @@
|
|||
</q-file>
|
||||
</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
|
||||
push
|
||||
color="accent"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
import ShiftListScrollable from 'src/modules/timesheets/components/shift-list-scrollable.vue';
|
||||
import LoadingOverlay from 'src/modules/shared/components/loading-overlay.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 TimesheetErrorWidget from 'src/modules/timesheets/components/timesheet-error-widget.vue';
|
||||
import ShiftListWeeklyOverview from 'src/modules/timesheets/components/shift-list-weekly-overview.vue';
|
||||
|
|
@ -104,15 +103,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<PageHeaderTemplate
|
||||
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'" />
|
||||
<q-space />
|
||||
|
||||
<!-- employee weekly overview -->
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -8,13 +8,19 @@ export const useExpensesApi = () => {
|
|||
const expenses_store = useExpensesStore();
|
||||
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);
|
||||
|
||||
if (success) {
|
||||
expenses_store.current_expense = new Expense(date.formatDate( new Date(), 'YYYY-MM-DD'));
|
||||
timesheet_store.getTimesheetsByOptionalEmployeeEmail(employee_email);
|
||||
return 'SUCCESS';
|
||||
}
|
||||
|
||||
return 'INVALID_EXPENSE';
|
||||
};
|
||||
|
||||
const deleteExpenseById = async (expense_id: number, employee_email?: string): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -25,3 +25,9 @@ export class Expense {
|
|||
this.is_approved = false;
|
||||
};
|
||||
};
|
||||
|
||||
export interface ExpenseOption {
|
||||
label: string;
|
||||
value: ExpenseType;
|
||||
icon: string;
|
||||
}
|
||||
|
|
@ -1,19 +1,31 @@
|
|||
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";
|
||||
|
||||
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);
|
||||
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);
|
||||
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}`);
|
||||
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
|
||||
lang="ts"
|
||||
>
|
||||
|
||||
import PageHeaderTemplate from 'src/modules/shared/components/page-header-template.vue';
|
||||
import TimesheetWrapper from 'src/modules/timesheets/components/timesheet-wrapper.vue';
|
||||
|
||||
import { useAuthStore } from 'src/stores/auth-store';
|
||||
import { useTimesheetStore } from 'src/stores/timesheet-store';
|
||||
|
||||
const auth_store = useAuthStore();
|
||||
const timesheet_store = useTimesheetStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -16,10 +16,14 @@
|
|||
class="col column fit"
|
||||
:style="$q.platform.is.mobile && ($q.screen.width < $q.screen.height) ? '' : 'width: 90vw'"
|
||||
>
|
||||
<TimesheetWrapper
|
||||
:employee-email="auth_store.user?.email ?? ''"
|
||||
class="col"
|
||||
<PageHeaderTemplate
|
||||
:title="'timesheet.page_header'"
|
||||
:start-date="timesheet_store.pay_period?.period_start ?? ''"
|
||||
:end-date="timesheet_store.pay_period?.period_end ?? ''"
|
||||
class="col-auto"
|
||||
/>
|
||||
|
||||
<TimesheetWrapper class="col" />
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
|
@ -4,6 +4,7 @@ import { defineStore } from "pinia";
|
|||
import { useTimesheetStore } from "src/stores/timesheet-store";
|
||||
import { Expense } from "src/modules/timesheets/models/expense.models";
|
||||
import { ExpenseService } from "src/modules/timesheets/services/expense-service";
|
||||
import { computeCRC32Base64 } from "src/utils/crc-encoder";
|
||||
|
||||
export const useExpensesStore = defineStore('expenses', () => {
|
||||
const timesheet_store = useTimesheetStore();
|
||||
|
|
@ -50,6 +51,22 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
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 {
|
||||
is_open,
|
||||
is_loading,
|
||||
|
|
@ -62,5 +79,6 @@ export const useExpensesStore = defineStore('expenses', () => {
|
|||
upsertExpense,
|
||||
deleteExpenseById,
|
||||
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