Files
pleno-vue/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileAttachment.vue
T

411 lines
15 KiB
Vue

<script setup lang="ts">
import { onBeforeUnmount, watch } from 'vue';
import WhiteBoxCard from "@/components/displays/boxes/WhiteBoxCard.vue";
import SessionUser from "@/components/session/token/SessionUser.vue";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
type attachment = {
id: number;
object_type: string;
object_id: number;
content: {
image: string | null;
document: string | null;
relation: string | null;
other: unknown;
src: string | null; // For document preview (e.g., PDF URL) // THIS IS NEVER STORED, JUST FOR PREVIEW PURPOSES
};
created_at: string;
updated_at: string | null;
deleted_at: string | null;
}
const SELF_SERVE_WASH_ATTACHMENT_TYPE = 'SELF_SERVE_WASH';
const getAttachmentContent = (attachmentEntry: attachment) => {
if (!attachmentEntry.content) {
return {
image: null,
document: null,
relation: null,
other: null,
src: null,
};
}
return {
image: attachmentEntry.content.image ?? null,
document: attachmentEntry.content.document ?? null,
relation: attachmentEntry.content.relation ?? null,
other: attachmentEntry.content.other ?? null,
src: attachmentEntry.content.src ?? null,
};
};
const getAttachmentOtherText = (attachmentEntry: attachment) => {
const other = getAttachmentContent(attachmentEntry).other;
if (typeof other === 'string') {
return other;
}
if (isSelfServeWashAttachment(attachmentEntry)) {
const customerNumber = getSelfServeWashPayload(attachmentEntry)?.customer_number;
return customerNumber
? `${t('admin.pos.settings_wheel.self_serve_wash_attachment')} #${customerNumber}`
: t('admin.pos.settings_wheel.self_serve_wash_attachment');
}
return other && typeof other === 'object' ? JSON.stringify(other) : '';
};
const props = defineProps({
attachments: {
type: Array as () => attachment[],
required: true
},
getPreviewLink: {
type: Function as () => (id: number) => Promise<string>,
required: true
},
showAttachWashCertificateAction: {
type: Boolean,
default: false
}
})
const emits = defineEmits<{
(e: 'delete', id: number): void;
(e: 'upload', payload: { filename: string; base64String: string | ArrayBuffer | null }): void;
(e: 'download', id: number): void;
(e: 'attach-wash-certificate'): void;
}>();
const onClickDelete = (id: number) => {
emits('delete', id);
}
const determineAttachmentType = (attachment: attachment): 'image' | 'document' | 'relation' | 'other' | 'unknown' => {
const content = getAttachmentContent(attachment);
if (content.image) return 'image';
if (content.document) return 'document';
if (content.relation) return 'relation';
if (content.other) return 'other';
return 'unknown';
};
const getSelfServeWashPayload = (attachmentEntry: attachment): Record<string, any> | null => {
const other = getAttachmentContent(attachmentEntry).other;
return other && typeof other === 'object' && (other as Record<string, any>).type === SELF_SERVE_WASH_ATTACHMENT_TYPE
? other as Record<string, any>
: null;
};
const isSelfServeWashAttachment = (attachmentEntry: attachment): boolean => {
return getSelfServeWashPayload(attachmentEntry) !== null;
};
const formatSelfServeDriver = (payload: Record<string, any>): string => {
return payload.subuser?.name || payload.subuser?.username || (payload.subuser_id ? `#${payload.subuser_id}` : '-');
};
const formatElapsedMinutes = (seconds: unknown): string => {
const parsed = Number(seconds);
return Number.isFinite(parsed) && parsed > 0 ? `${Math.ceil(parsed / 60)} min` : '-';
};
const getAttachmentTypeIcon = (attachment: attachment): string => {
const type = determineAttachmentType(attachment);
switch (type) {
case 'image':
return 'fas fa-image';
case 'document':
return 'fas fa-file-alt';
case 'relation':
return 'fas fa-link';
case 'other':
return 'fas fa-ellipsis-h';
default:
return 'fas fa-question';
}
};
const isAttachmentTypeIn = (types: ('image' | 'document' | 'relation' | 'other' | 'unknown')[]): boolean => {
return props.attachments.some(att => types.includes(determineAttachmentType(att)));
};
const isOfficeDocument = (attachmentEntry: attachment): boolean => {
const otherText = getAttachmentOtherText(attachmentEntry).toLowerCase();
return ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'].some(extension => otherText.endsWith(extension));
};
const previewRequestsInFlight = new Set<number>();
const generatedObjectUrls = new Set<string>();
const isObjectUrl = (url: string | null): boolean => Boolean(url && url.startsWith('blob:'));
const releaseObjectUrl = (url: string | null): void => {
if (!isObjectUrl(url)) {
return;
}
URL.revokeObjectURL(url as string);
generatedObjectUrls.delete(url as string);
};
const shouldLoadDocumentPreview = (attachmentEntry: attachment): boolean => {
const content = getAttachmentContent(attachmentEntry);
return determineAttachmentType(attachmentEntry) === 'document'
&& Boolean(content.document)
&& !isOfficeDocument(attachmentEntry)
&& !content.src;
};
const createEmbeddablePreviewUrl = async (downloadLink: string | null): Promise<string | null> => {
if (!downloadLink) {
return null;
}
try {
const response = await fetch(downloadLink, { method: 'GET' });
if (!response.ok) {
return null;
}
const fileBlob = await response.blob();
if (!fileBlob || fileBlob.size === 0) {
return null;
}
const objectUrl = URL.createObjectURL(fileBlob);
generatedObjectUrls.add(objectUrl);
return objectUrl;
} catch (error) {
console.warn('Unable to create embeddable preview blob', error);
return null;
}
};
const loadPreviewLink = async (id: number): Promise<void> => {
if (previewRequestsInFlight.has(id)) {
return;
}
previewRequestsInFlight.add(id);
try {
const downloadLink = await props.getPreviewLink(id);
const src = await createEmbeddablePreviewUrl(downloadLink);
const attachment = props.attachments.find(att => att.id === id);
if (!attachment) {
return;
}
const content = getAttachmentContent(attachment);
if (content.src && content.src !== src) {
releaseObjectUrl(content.src);
}
content.src = src || null;
attachment.content = content;
} catch (error) {
console.warn('Error loading preview link for attachment', id, error);
} finally {
previewRequestsInFlight.delete(id);
}
};
watch(
() => props.attachments.map((attachment) => `${attachment.id}:${getAttachmentContent(attachment).src ?? ''}:${getAttachmentOtherText(attachment)}`),
() => {
props.attachments.forEach((attachment) => {
if (shouldLoadDocumentPreview(attachment)) {
void loadPreviewLink(attachment.id);
}
});
},
{ immediate: true }
);
onBeforeUnmount(() => {
generatedObjectUrls.forEach((url) => URL.revokeObjectURL(url));
generatedObjectUrls.clear();
});
const uploadAttachment = (file: File) => {
// Implement the upload logic here
console.warn("Uploading file:", file);
// Base64 encode the file to easily display it
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target?.result;
const filename = file.name;
emits('upload', { filename, base64String });
}
reader.readAsDataURL(file);
};
const onClickUpload = () => {
// Trigger file input click
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*,application/pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx'; // Accept images and common document types
fileInput.onchange = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files[0]) {
uploadAttachment(target.files[0]);
}
};
fileInput.click();
};
const onClickAttachWashCertificate = () => {
emits('attach-wash-certificate');
};
</script>
<template>
<div class="is-fullwidth">
<div class="columns is-mobile is-multiline is-variable is-1">
<!-- Add Attachment Button -->
<div class="column is-12">
<WhiteBoxCard :toggleable="true" data-testid="pos-order-attachments-add-card">
<template #header>
<div class="card-header-icon">
<span class="icon">
<i class="fas fa-plus" aria-hidden="true"></i>
</span>
</div>
<div class="card-header-title">
{{ t('admin.pos.attachments_add') }}
</div>
<div class="card-header-icon">
<span class="icon">
<i class="fas fa-chevron-down" aria-hidden="true"></i>
</span>
</div>
</template>
<template #content>
<!-- Content for adding a new attachment can go here -->
<p>{{ t('admin.pos.attachments_upload_description') }}</p>
</template>
<template #footer>
<a
class="card-footer-item"
data-testid="pos-order-attachments-upload-action"
@click="onClickUpload"
>
<!-- Footer actions can go here -->
{{ t('admin.pos.attachments_upload_action') }}
</a>
<a
v-if="props.showAttachWashCertificateAction"
class="card-footer-item"
data-testid="pos-order-attachments-attach-wash-certificate"
@click="onClickAttachWashCertificate"
>
{{ t('admin.pos.settings_wheel.attach_wash_certificate') }}
</a>
</template>
</WhiteBoxCard>
</div>
<!-- Existing Attachments -->
<template v-for="attachment in props.attachments" :key="attachment.id">
<div class="column is-12">
<WhiteBoxCard :toggleable="true" :data-testid="`pos-order-attachment-card-${attachment.id}`">
<template #header>
<div class="card-header-icon">
<span class="icon">
<i class="fas fa-paperclip" aria-hidden="true"></i>
</span>
<span class="icon">
<i :class="getAttachmentTypeIcon(attachment)" aria-hidden="true"></i>
</span>
</div>
<div class="card-header-title">
{{ t('admin.pos.attachment') }} #{{ attachment.id }}
</div>
<div class="card-header-icon">
<span class="icon">
<i class="fas fa-chevron-down" aria-hidden="true"></i>
</span>
</div>
</template>
<template #content>
<!--{{ attachment }}-->
<!-- DOCUMENT PREVIEW -->
<template v-if="determineAttachmentType(attachment) === 'document' && getAttachmentContent(attachment).document">
<!-- If the file extension is office document, you can use an online viewer -->
<template v-if="isOfficeDocument(attachment)">
<span>{{ t('admin.pos.attachments_office_preview_unavailable') }} <a @click="$emit('download', attachment.id)">{{ t('admin.pos.attachments_download_action') }}</a></span>
</template>
<iframe
v-else-if="getAttachmentContent(attachment).src"
:src="getAttachmentContent(attachment).src || ''"
style="width: 100%; height: 400px; border: none;"
></iframe>
<span v-else>{{ t('admin.pos.attachments_no_preview') }}</span>
</template>
<!-- OTHER PREVIEW -->
<template v-else-if="determineAttachmentType(attachment) === 'other' && getAttachmentContent(attachment).other">
<template v-if="isSelfServeWashAttachment(attachment)">
<div class="content is-size-7">
<p class="has-text-weight-semibold">{{ t('admin.pos.settings_wheel.self_serve_wash_attachment') }}</p>
<p>
<strong>{{ t('admin.pos.settings_wheel.self_serve_customer') }}:</strong>
#{{ getSelfServeWashPayload(attachment)?.customer_number || '-' }}
</p>
<p>
<strong>{{ t('admin.pos.settings_wheel.self_serve_driver') }}:</strong>
{{ formatSelfServeDriver(getSelfServeWashPayload(attachment) || {}) }}
</p>
<p>
<strong>{{ t('pos.license_plate') }}:</strong>
{{ getSelfServeWashPayload(attachment)?.license_plate || '-' }}
</p>
<p>
<strong>{{ t('admin.pos.settings_wheel.self_serve_elapsed') }}:</strong>
{{ formatElapsedMinutes(getSelfServeWashPayload(attachment)?.elapsed_wash_time_seconds) }}
</p>
</div>
</template>
<!-- If the other type is a URL, you can create a link -->
<template v-else-if="typeof getAttachmentContent(attachment).other === 'string' && getAttachmentContent(attachment).other.startsWith('http')">
<a :href="String(getAttachmentContent(attachment).other)" target="_blank" rel="noopener noreferrer">
{{ getAttachmentContent(attachment).other }}
</a>
</template>
<template v-else>
<span>{{ getAttachmentOtherText(attachment) }}</span>
</template>
</template>
<!-- NO PREVIEW -->
<template v-else>
<span>{{ t('admin.pos.attachments_no_preview') }}</span>
</template>
</template>
<template #footer>
<div class="card-footer-item">
{{ t('common.created') }}: {{ new Date(attachment.created_at).toLocaleString() }}
</div>
<a class="card-footer-item" v-if="!attachment.deleted_at" @click="onClickDelete(attachment.id)">
<!-- Delete button or action can be placed here -->
{{ t('admin.pos.delete') }}
</a>
<!-- Download, if applicable -->
<a
class="card-footer-item"
:data-testid="`pos-order-attachment-download-${attachment.id}`"
v-if="isAttachmentTypeIn(['image', 'document']) && (getAttachmentContent(attachment).image || getAttachmentContent(attachment).document)"
@click="$emit('download', attachment.id)"
>
{{ t('admin.pos.attachments_download_action') }}
</a>
</template>
</WhiteBoxCard>
</div>
</template>
</div>
</div>
</template>
<style scoped>
</style>