import { Dialog, QSelect, Notify, QNotifyCreateOptions, useQuasar, } from 'quasar'; import { Ref, ref } from 'vue'; import axios, { AxiosInstance, AxiosProgressEvent } from 'axios'; import { ComposerTranslation } from 'vue-i18n'; import arr from 'src/utils/arr'; import { getRole } from 'src/services/keycloak'; import GlobalDialog from 'components/GlobalDialog.vue'; import DialogDuplicateData from 'components/DialogDuplicateData.vue'; import useOptionStore from '../options'; import { CustomerBranch } from '../customer/types'; import { CustomerBranchRelation } from '../quotations'; import { Employee } from '../employee/types'; import { CreatedBy } from '../types'; export const baseUrl = import.meta.env.VITE_API_BASE_URL; export function dialog(opts: { title: string; message: string; color?: string; icon?: string; persistent?: boolean; actionText?: string; cancelText?: string; enablei18n?: boolean; action?: (...args: unknown[]) => unknown; cancel?: (...args: unknown[]) => unknown; }) { Dialog.create({ component: GlobalDialog, componentProps: opts, }); } export function dialogCheckData(opts: { checkData: (...args: unknown[]) => { oldData: { nameField: string; value: string }[]; newData: { nameField: string; value: string }[]; }; action?: (...args: unknown[]) => unknown; cancel?: (...args: unknown[]) => unknown; }) { Dialog.create({ component: DialogDuplicateData, componentProps: opts, }); } export function dialogWarningClose( t: ComposerTranslation, opts: { message?: string; actionText?: string; action?: (...args: unknown[]) => unknown; cancel?: (...args: unknown[]) => unknown; }, ) { dialog({ color: 'warning', icon: 'mdi-alert', title: t('form.warning.title'), actionText: opts.actionText || t('dialog.action.ok'), message: opts.message || t('dialog.message.close'), action: async () => { if (opts.action) opts.action(); }, cancel: () => { if (opts.cancel) opts.cancel(); }, }); } export function notify( type: 'create', message?: string, opts?: QNotifyCreateOptions | string, ) { if (type === 'create') { Notify.create({ icon: 'mdi-check-circle', iconColor: 'white', iconSize: '42px', color: 'positive', message: message, position: 'bottom', timeout: 1000, group: false, actions: [ { icon: 'mdi-close', color: 'white', round: true, handler: () => {}, }, ], }); } if (opts) { Notify.create(opts); } } export function moveItemUp(items: T, index: number) { arr.move(items, index, -1); } export function moveItemDown(items: T, index: number) { arr.move(items, index, 1); } export function deleteItem(items: unknown[], index: number) { if (!items) return; if (index >= 0 && index < items.length) { items.splice(index, 1); } } export function formatNumberDecimal(num: number, point: number = 2): string { return (num || 0).toLocaleString('eng', { minimumFractionDigits: point, maximumFractionDigits: point, }); } export function selectFilterOptionRefMod( source: Ref[]>, destination: Ref[]>, filterField?: string, ) { destination.value = source.value; const filter = ((value, update) => { if (value === '') update(() => (destination.value = source.value)); else update( () => { destination.value = source.value.filter((v) => { const label = v[filterField || 'label']; if (!label) return; if (typeof label !== 'string') { throw new Error('Label must be of type string.'); } return ( label.toLocaleLowerCase().indexOf(value.toLocaleLowerCase()) > -1 ); }); }, (ref) => { if (value !== '' && destination.value.length > 0) { ref.setOptionIndex(-1); ref.moveOptionSelection(1, true); } }, ); }) satisfies QSelect['onFilter']; return filter; } export function selectOptionFilter( list: Record[], filterField?: string, prepare?: ( ...args: unknown[] ) => Record[] | Promise[]>, ) { const options = ref([]); (async () => { const data = await prepare?.(); if (data) options.value = list = data; })(); const filter = ((value, update) => { if (value === '') update(() => (options.value = list)); else update(() => { options.value = list.filter((v) => { const label = v[filterField || 'label']; if (typeof label !== 'string') { throw new Error('Label must be of type string.'); } return ( label.toLocaleLowerCase().indexOf(value.toLocaleLowerCase()) > -1 ); }); }); }) satisfies QSelect['onFilter']; return { options, filter }; } export function checkTabBeforeAdd(data: unknown[], except?: string[]) { if (!data) return; const lastData = data[data.length - 1]; if (typeof lastData !== 'object') return false; let canAdd = false; for (const [key, val] of Object.entries( lastData as Record, )) { if (except?.includes(key)) continue; canAdd = val !== '' && val !== null; if (canAdd) break; } return canAdd; } export function isRoleInclude(role2check: string[]): boolean { const roles = getRole() ?? []; const filterRole = roles.filter( (role: string) => role !== 'offline_access' && role !== 'uma_authorization' && !role.startsWith('default-roles'), ); const isIncluded = role2check.some((r) => filterRole.includes(r)); return isIncluded; } const allRoles = [ 'head_of_admin', 'admin', 'executive', 'accountant', 'branch_admin', 'branch_manager', 'branch_accountant', 'head_of_sale', 'sale', 'data_entry', 'document_checker', 'messenger', 'corporate_customer', 'agency', ]; const permissions = { branch: { edit: allRoles.slice(0, 7), view: allRoles.slice(0, 7), }, personnel: { edit: allRoles.slice(0, 6).filter((r) => r !== 'accountant'), view: allRoles.slice(0, 6).filter((r) => r !== 'accountant'), }, product: { edit: allRoles.slice(0, 7), view: allRoles, }, workflow: { edit: allRoles.slice(0, 6), view: allRoles.filter((r) => r !== 'branch_accountant'), }, customer: { edit: allRoles.slice(0, 9).filter((r) => r !== 'branch_accountant'), view: allRoles.slice(0, 9), }, agencies: { edit: allRoles.slice(0, 7), view: allRoles, }, quotation: { edit: allRoles.slice(0, 9).filter((r) => r !== 'branch_accountant'), view: allRoles.slice(0, 9), }, related: { // ใช้กับหลายเมนู edit: allRoles.slice(0, 6), view: allRoles, }, // account: { // edit: allRoles.slice(0, 6), // view: allRoles.slice(0, 7), // }, uploadSlip: { edit: allRoles.slice(0, 6), view: allRoles.filter((r) => r !== 'head_of_sale' && r !== 'sale'), }, dashBoard: { edit: allRoles.slice(0, 6).filter((r) => r !== 'admin'), view: allRoles.filter((r) => r !== 'admin'), }, }; export function canAccess( menu: keyof typeof permissions, action: 'edit' | 'view' = 'view', ): boolean { // uma_authorization = all roles const roles = getRole() ?? []; if (roles.includes('system')) return true; const allowedRoles = permissions[menu]?.[action] || []; return allowedRoles.some((role: string) => roles.includes(role)); } export function resetScrollBar(elementId: string) { const element = document.getElementById(elementId); if (element) { element.scrollTop = 0; } } export function manageAttachment( api: AxiosInstance, base: string, option?: { onUploadProgress?: (e: AxiosProgressEvent) => void }, ) { return { listAttachment: async (opts: { parentId: string }) => { const res = await api.get( `/${base}/${opts.parentId}/attachment`, ); if (res.status >= 400) return false; return res.data; }, headAttachment: async (opts: { parentId: string; fileId: string }) => { const url = `/${base}/${opts.parentId}/attachment/${opts.fileId}`; const res = await api.head(url); if (res.status < 400) return res.headers; return false; }, getAttachment: async (opts: { parentId: string; name: string; download?: boolean; }) => { const url = `/${base}/${opts.parentId}/attachment/${opts.name}`; const res = await api.get(url); if (opts.download) { fetch(res.data) .then(async (res) => await res.blob()) .then((blob) => { const a = document.createElement('a'); a.download = opts.name; a.href = window.URL.createObjectURL(blob); a.click(); a.remove(); }); } return res.data; }, putAttachment: async (opts: { parentId: string; name: string; file: File; onUploadProgress?: (e: AxiosProgressEvent) => void; abortController?: AbortController; }) => { const urlRes = await api.put( `/${base}/${opts.parentId}/attachment/${opts.name}`, ); if (urlRes.status >= 400) return false; // NOTE: Must use axios instance or else CORS error. const uploadRes = await axios.put(urlRes.data, opts.file, { headers: { 'Content-Type': opts.file.type }, onUploadProgress: opts.onUploadProgress ? opts.onUploadProgress : option?.onUploadProgress ? option.onUploadProgress : (e) => console.log(e), signal: opts.abortController?.signal, }); if (uploadRes.status >= 400) return false; return true; }, delAttachment: async (opts: { parentId: string; name: string }) => { const res = await api.delete( `/${base}/${opts.parentId}/attachment/${opts.name}`, ); if (res.status < 400) return true; return false; }, }; } export function manageFile( api: AxiosInstance, base: string, option?: { onUploadProgress?: (e: AxiosProgressEvent) => void }, ) { return { listFile: async (opts: { group: T; parentId: string }) => { const res = await api.get( `/${base}/${opts.parentId}/file-${opts.group}`, ); if (res.status >= 400) return false; return res.data; }, headFile: async (opts: { group: T; parentId: string; fileId: string }) => { const url = `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`; const res = await api.head(url); if (res.status < 400) return res.headers; return false; }, getFile: async (opts: { group: T; parentId: string; fileId: string; download?: boolean; }) => { const url = `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`; const res = await api.get(url); if (opts.download) { fetch(res.data) .then(async (res) => await res.blob()) .then((blob) => { const a = document.createElement('a'); a.download = opts.fileId; a.href = window.URL.createObjectURL(blob); a.click(); a.remove(); }); } return res.data; }, putFile: async (opts: { group: T; parentId: string; fileId: string; file: File; uploadUrl?: boolean; onUploadProgress?: (e: AxiosProgressEvent) => void; abortController?: AbortController; }) => { const res = opts.uploadUrl ? await api.put( `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`, ) : await api.put( `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`, opts.file, { headers: { 'Content-Type': opts.file.type }, onUploadProgress: opts.onUploadProgress ? opts.onUploadProgress : option?.onUploadProgress ? option.onUploadProgress : (e) => console.log(e), signal: opts.abortController?.signal, }, ); if (res.status >= 400) return false; if (opts.uploadUrl && typeof res.data === 'string') { // NOTE: Must use axios instance or else CORS error. const uploadRes = await axios.put(res.data, opts.file, { headers: { 'Content-Type': opts.file.type }, onUploadProgress: opts.onUploadProgress ? opts.onUploadProgress : option?.onUploadProgress ? option.onUploadProgress : (e) => console.log(e), signal: opts.abortController?.signal, }); if (uploadRes.status >= 400) return true; } return true; }, delFile: async (opts: { group: T; parentId: string; fileId: string }) => { const res = await api.delete( `/${base}/${opts.parentId}/file-${opts.group}/${opts.fileId}`, ); if (res.status < 400) return true; return false; }, }; } export function manageMeta(api: AxiosInstance, base: string) { return { postMeta: async (opts: { group: T; parentId: string; meta: any; file?: File; }) => { const url = `${base}/${opts.parentId}/${opts.group}`; const res = await api.post(url, opts.meta); if (res.status < 400) { if (opts.file !== undefined) { await manageFile(api, base).putFile({ parentId: opts.parentId, group: opts.group, fileId: res.data.id, file: opts.file, }); } return res.data; } }, getMetaList: async (opts: { group: T; parentId: string }) => { const url = `${base}/${opts.parentId}/${opts.group}`; const res = await api.get(url); return res.data; }, getMeta: async (opts: { group: T; parentId: string; metaId: string }) => { const url = `${base}/${opts.parentId}/${opts.group}/${opts.metaId}`; const res = await api.get(url); return res.data; }, putMeta: async (opts: { group: T; parentId: string; metaId: string; meta: any; file?: File; }) => { const res = await api.put( `${base}/${opts.parentId}/${opts.group}/${opts.metaId}`, opts.meta, ); if (res.status < 400) { if (opts.file !== undefined) await manageFile(api, base).putFile({ parentId: opts.parentId, group: opts.group, fileId: res.data.id, file: opts.file, }); return res.data; } return false; }, delMeta: async (opts: { group: T; parentId: string; metaId: string }) => { const res = await api.delete( `${base}/${opts.parentId}/${opts.group}/${opts.metaId}`, ); if (res.status < 400) return true; return false; }, }; } export async function waitAll[]>(arr: T) { return await Promise.all(arr); } export function commaInput( text: string, type: 'string' | 'number' = 'number', ): string { if (typeof text !== 'string') return '0'; if (!text) return '0'; const num = Number(text.replace(/,/gi, '')); const parts = num.toString().split('.'); const integerPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); const decimalPart = parts[1]?.slice(0, 2) || (type === 'number' && '00'); if (integerPart === 'NaN') return '0'; return integerPart + (decimalPart ? `.${decimalPart}` : ''); } export function convertFileSize(size: number): string { if (size === undefined) return 'Unknow size'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; let sizeNumber = typeof size === 'string' ? parseFloat(size) : size; while (sizeNumber >= 1024 && i++ < units.length - 1) { sizeNumber /= 1024; } return sizeNumber.toFixed(2) + ' ' + units[i]; } export async function getAttachmentHead(api: AxiosInstance, url: string) { const res = await api.head(`${url}`); if (res) return res.headers; } export function calculateDaysUntilExpire(expireDate: Date | string): number { const today = new Date(); const expire = new Date(expireDate); const diffInTime = expire.getTime() - today.getTime(); const diffInDays = Math.ceil(diffInTime / (1000 * 60 * 60 * 24)); return diffInDays; } export function formatNumberWithCommas(value: string) { const [integer, decimal] = value.split('.'); const integerWithCommas = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ','); return decimal ? `${integerWithCommas}.${decimal}` : integerWithCommas; } export function createDataRefBase( defaultPage = 1, defaultPageMax = 1, defaultPageSize = 30, ) { return { data: ref([]), page: ref(defaultPage), pageMax: ref(defaultPageMax), pageSize: ref(defaultPageSize), }; } export function changeMode(mode: string) { const $q = useQuasar(); if (mode === 'light') { localStorage.setItem('currentTheme', 'light'); $q.dark.set(false); return; } if (mode === 'dark') { localStorage.setItem('currentTheme', 'dark'); $q.dark.set(true); return; } if (mode === 'baseOnDevice') { localStorage.setItem('currentTheme', 'baseOnDevice'); if ( window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ) { $q.dark.set(true); } else { $q.dark.set(false); } return; } } export function capitalizeFirstLetter(val: string) { return String(val).charAt(0).toUpperCase() + String(val).slice(1); } export function getCustomerName( record: CustomerBranch | CustomerBranchRelation, opts?: { locale?: string; noCode?: boolean; }, ) { const customer = record; return ( { ['CORP']: { ['eng']: customer.registerNameEN, ['tha']: customer.registerName, }[opts?.locale || 'eng'], ['PERS']: { ['eng']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstNameEN} ${customer?.lastNameEN}`, ['tha']: `${useOptionStore().mapOption(customer?.namePrefix || '')} ${customer?.firstName} ${customer?.lastName}`, }[opts?.locale || 'eng'] || '-', }[customer.customer.customerType] + (opts?.noCode ? '' : ' ' + `(${customer.code})`) ); } export function getEmployeeName( record: Employee | CreatedBy, opts?: { locale?: string; }, ) { const employee = record; return { ['eng']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstNameEN} ${employee.lastNameEN}`, ['tha']: `${typeof employee.namePrefix === 'string' ? useOptionStore().mapOption(employee.namePrefix) : ''} ${employee.firstName || employee.firstNameEN} ${employee.lastName}`, }[opts?.locale || 'eng']; } export function setPrefixName( record: { namePrefix: string; firstName: string; lastName: string; firstNameEN: string; lastNameEN: string; }, opts?: { locale?: string; }, ) { const data = record; return { ['eng']: `${typeof data.namePrefix === 'string' ? useOptionStore().mapOption(data.namePrefix) : ''} ${data.firstNameEN} ${data.lastNameEN}`, ['tha']: `${typeof data.namePrefix === 'string' ? useOptionStore().mapOption(data.namePrefix) : ''} ${data.firstName} ${data.lastName}`, }[opts?.locale || 'eng']; } export function toCamelCase(text: string): string { return text .replace(/[^a-zA-Z0-9]+(.)/g, (match, chr) => chr.toUpperCase()) .replace(/^[A-Z]/, (match) => match.toLowerCase()); }