diff --git a/src/modules/timesheets/components/expense-dialog-attachment-viewer.vue b/src/modules/timesheets/components/expense-dialog-attachment-viewer.vue
new file mode 100644
index 0000000..db7d2f1
--- /dev/null
+++ b/src/modules/timesheets/components/expense-dialog-attachment-viewer.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/timesheets/components/expense-dialog-list-item.vue b/src/modules/timesheets/components/expense-dialog-list-item.vue
index 0d74415..ba87962 100644
--- a/src/modules/timesheets/components/expense-dialog-list-item.vue
+++ b/src/modules/timesheets/components/expense-dialog-list-item.vue
@@ -67,6 +67,12 @@
});
}
}
+
+ const onClickAttachment = async () => {
+ expenses_store.isShowingAttachmentDialog = true;
+ await expenses_store.getAttachmentURL(expense.value.attachment_key);
+ console.log('image url: ', expenses_store.attachmentURL);
+ }
@@ -131,6 +137,7 @@
:text-color="expense.is_approved ? 'accent' : 'white'"
class="col-auto q-px-sm q-mr-sm"
icon="attach_file"
+ @click.stop="onClickAttachment"
/>
diff --git a/src/modules/timesheets/components/expense-dialog.vue b/src/modules/timesheets/components/expense-dialog.vue
index 2171f8b..c2fa470 100644
--- a/src/modules/timesheets/components/expense-dialog.vue
+++ b/src/modules/timesheets/components/expense-dialog.vue
@@ -6,6 +6,7 @@
import ExpenseDialogForm from 'src/modules/timesheets/components/expense-dialog-form.vue';
import ExpenseDialogHeader from 'src/modules/timesheets/components/expense-dialog-header.vue';
import ExpenseDialogFormMobile from 'src/modules/timesheets/components/mobile/expense-dialog-form-mobile.vue';
+ import ExpenseDialogAttachmentViewer from 'src/modules/timesheets/components/expense-dialog-attachment-viewer.vue';
import { date } from 'quasar';
import { useExpensesStore } from 'src/stores/expense-store';
@@ -32,6 +33,8 @@
transition-show="jump-down"
transition-hide="jump-down"
>
+
+
{
diff --git a/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue b/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue
index c2e3084..2939255 100644
--- a/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue
+++ b/src/modules/timesheets/components/mobile/shift-list-day-row-mobile.vue
@@ -7,19 +7,14 @@
import { useUiStore } from 'src/stores/ui-store';
import { SHIFT_OPTIONS } from 'src/modules/timesheets/utils/shift.util';
import type { Shift } from 'src/modules/timesheets/models/shift.models';
-
- const ui_store = useUiStore();
+
+ // ========== state ========================================
const COMMENT_LENGTH_MAX = 280;
const shift = defineModel('shift', { required: true });
- const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
- const select_ref = ref(null);
- const is_showing_comment_popup = ref(false);
- const comment_length = computed(() => shift.value.comment?.length ?? 0);
- const error_message = ref('');
-
- const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
+
+ const { errorMessage = undefined, dense = false, hasShiftAfter = false, isTimesheetApproved = false } = defineProps<{
dense?: boolean;
hasShiftAfter?: boolean;
isTimesheetApproved?: boolean;
@@ -30,6 +25,18 @@
'requestDelete': [void];
'onTimeFieldBlur': [void];
}>();
+
+ const ui_store = useUiStore();
+ const shift_type_selected = ref(SHIFT_OPTIONS.find(option => option.value == shift.value.type));
+ const select_ref = ref(null);
+ const is_showing_comment_popup = ref(false);
+ const error_message = ref('');
+
+ // ========== computed ========================================
+
+ const comment_length = computed(() => shift.value.comment?.length ?? 0);
+
+ // ========== methods =========================================
const onBlurShiftTypeSelect = () => {
if (shift_type_selected.value === undefined) {
diff --git a/src/modules/timesheets/composables/use-expense-api.ts b/src/modules/timesheets/composables/use-expense-api.ts
index 18a204e..7190bea 100644
--- a/src/modules/timesheets/composables/use-expense-api.ts
+++ b/src/modules/timesheets/composables/use-expense-api.ts
@@ -10,9 +10,14 @@ export const useExpensesApi = () => {
const upsertExpense = async (expense: Expense, employee_email: string, file?: File): Promise => {
if (file) {
- const presignedURL = expenses_store.uploadAttachment(file);
+ const attachmentKey = await expenses_store.uploadAttachment(file);
- if (!presignedURL) return 'PRESIGN_FAILED';
+ if (!attachmentKey)
+ console.error('failed to upload attachment');
+ else {
+ expense.attachment_key = attachmentKey;
+ expense.attachment_name = file.name;
+ }
}
const success = await expenses_store.upsertExpense(expense, employee_email);
diff --git a/src/modules/timesheets/models/expense.models.ts b/src/modules/timesheets/models/expense.models.ts
index 93701ee..67a7b4b 100644
--- a/src/modules/timesheets/models/expense.models.ts
+++ b/src/modules/timesheets/models/expense.models.ts
@@ -32,4 +32,9 @@ export interface ExpenseOption {
label: string;
value: ExpenseType;
icon: string;
+}
+
+export interface AttachmentPresignedURLResponse {
+ url: string;
+ key: string;
}
\ No newline at end of file
diff --git a/src/modules/timesheets/services/expense-service.ts b/src/modules/timesheets/services/expense-service.ts
index 0b0c0f0..2473bb2 100644
--- a/src/modules/timesheets/services/expense-service.ts
+++ b/src/modules/timesheets/services/expense-service.ts
@@ -1,6 +1,6 @@
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 { AttachmentPresignedURLResponse, Expense } from "src/modules/timesheets/models/expense.models";
export const ExpenseService = {
createExpense: async (expense: Expense): Promise<{ success: boolean, data: Expense, error?: unknown }> => {
@@ -18,14 +18,19 @@ export const ExpenseService = {
return response.data;
},
- getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise> => {
+ getPresignedUploadURL: async (file: File, checksum_crc32: string): Promise> => {
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);
+ uploadAttachmentWithPresignedUrl: async (file: File, url: string): Promise => {
+ const response = await api.put(url, file, { headers: { 'Content-Type': `image/${file.type}`, }, withCredentials: false });
+ return response.status;
+ },
+
+ getPresignedDownloadURL: async (key: string): Promise> => {
+ const response = await api.get>(`attachments/s3/download?attachmentKey=${key}`);
+ return response.data;
}
};
\ No newline at end of file
diff --git a/src/stores/expense-store.ts b/src/stores/expense-store.ts
index 8e7ee27..9bf61e5 100644
--- a/src/stores/expense-store.ts
+++ b/src/stores/expense-store.ts
@@ -11,9 +11,11 @@ export const useExpensesStore = defineStore('expenses', () => {
const is_open = ref(false);
const is_loading = ref(false);
const is_showing_create_form = ref(false);
+ const attachmentURL = ref('');
const mode = ref<'create' | 'update' | 'delete'>('create');
const current_expense = ref(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
const initial_expense = ref(new Expense(date.formatDate(new Date(), 'YYYY-MM-DD')));
+ const isShowingAttachmentDialog = ref(false);
const is_save_disabled = computed(() => JSON.stringify(current_expense.value) === JSON.stringify(initial_expense.value))
const open = (): void => {
@@ -51,22 +53,48 @@ export const useExpensesStore = defineStore('expenses', () => {
return data.success;
}
- const uploadAttachment = async (file: File) => {
+ /**
+ * Attemps to upload the provided image file to the S3 storage bucket for attachments.
+ *
+ * @param file image file to be uploaded to the S3 storage
+ * @returns Key `string` associated with the uploaded image file if successful,
+ * `undefined` if it fails.
+ */
+ const uploadAttachment = async (file: File): Promise => {
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);
+ if (!presignedUrlResponse.success || !presignedUrlResponse.data) {
+ console.error('failed to get presigned URL from server');
+ return;
}
+
+ const { url, key } = presignedUrlResponse.data;
+
+ const responseStatus = await ExpenseService.uploadAttachmentWithPresignedUrl(file, url);
+
+ if (responseStatus >= 400) {
+ console.error('an error occured during upload: error ', responseStatus);
+ return;
+ }
+
+ return key;
} catch (error) {
console.error(error);
}
}
+ const getAttachmentURL = async (key?: string) => {
+ if (!key)
+ return;
+
+ const presignedAttachmentURL = await ExpenseService.getPresignedDownloadURL(key);
+
+ if (presignedAttachmentURL.success && presignedAttachmentURL.data)
+ attachmentURL.value = presignedAttachmentURL.data;
+ }
+
return {
is_open,
is_loading,
@@ -74,11 +102,14 @@ export const useExpensesStore = defineStore('expenses', () => {
mode,
current_expense,
initial_expense,
+ isShowingAttachmentDialog,
is_save_disabled,
+ attachmentURL,
open,
upsertExpense,
deleteExpenseById,
close,
uploadAttachment,
+ getAttachmentURL,
};
});
\ No newline at end of file