export default defineNuxtPlugin((nuxtApp) => { const module = "application"; const mode = "dev"; const paths = [ { name: "dev", url: "https://erpapi.bigdatatech.vn/" }, { name: "local", url: "http://localhost:8000/" }, { name: "prod", url: "https://erpapi.bigdatatech.vn/" }, ]; const path = paths.find((v) => v.name === mode).url; const { $copy, $clone, $updateSeriesFields, $snackbar, $store, $remove, $dialog } = nuxtApp; const requestLogin = function () { $store.commit("login", undefined); window.location.href = `https://${mode === "dev" ? "dev." : ""}login.utopia.com.vn/signin?module=${module}&link=${window.location.origin}`; }; const getpath = function (name) { return name ? paths.find((v) => v.name === name).url : path; }; const findapi = function (name) { const result = Array.isArray(name) ? apis.filter((v) => name.findIndex((x) => v.name === x) >= 0) : apis.find((v) => v.name === name); if (!result) { console.error(`API "${name}" doesn't exist`); } return $copy(result); }; const readyapi = function (apiNames) { const apisWithReady = []; apiNames.forEach((apiName) => { const found = apis.find((v) => v.name === apiName); if (found) { const apiWithReady = { ...$copy(found), ready: Boolean($store[apiName]), }; apisWithReady.push(apiWithReady); } }); return apisWithReady; }; const getapi = async function (list) { try { const arr = list.map((v) => { const found = apis.find((api) => api.name === v.name); const url = (v.path ? paths.find((x) => x.name === v.path).url : path) + (v.url ? v.url : found.url); let params = v.params ? v.params : found.params === undefined ? {} : found.params; params.login = $store.login ? $store.login.id : undefined; return { url, params }; }); let data = await Promise.all(arr.map((v) => $fetch(v.url, { params: v.params }))); data.map((v, i) => { list[i].data = v; if (list[i].commit) { let data = v.rows ? v.rows : v; $store.commit(list[i].commit, data); } }); return list; } catch (err) { console.log(err); return "error"; } }; const insertapi = async function (name, { data, values, notify = true } = {}) { try { const api = findapi(name); const curpath = api.path ? paths.find((x) => x.name === api.path).url : path; let rs; if (Array.isArray(data)) { rs = await $fetch(`${curpath}import-data/${api.url.substring(5, api.url.length - 1)}/`, { method: "POST", body: data, params: { values, action: "import" }, }); } else { rs = await $fetch(`${curpath}${api.url}`, { method: "POST", body: data, params: { values }, }); } // update store if (api.commit) { if ($store[api.commit]) { const copy = $copy($store[api.commit]); const rows = Array.isArray(rs) ? rs : [rs]; rows.forEach((v) => { if (v.id && !v.error) { const idx = copy.findIndex((x) => x.id === v.id); if (idx >= 0) copy[idx] = v; else copy.push(v); } }); $store.commit(api.commit, copy); } } if (notify) { $snackbar( $store.lang === "en" ? "Data has been successfully saved to the system." : "Dữ liệu đã được lưu vào hệ thống", "Success", ); } return rs; } catch (err) { console.error(err); return "error"; } }; const patchapi = async function (name, data, values, notify) { try { const api = findapi(name); const curpath = api.path ? paths.find((x) => x.name === api.path).url : path; const updateUrl = api.url_detail || api.url; const rs = await $fetch(`${curpath}${updateUrl}${data.id}/`, { method: "PATCH", body: data, params: { values: values || api.params?.values }, }); if (api.commit) { const index = $store[api.commit] ? $store[api.commit].findIndex((v) => v.id === rs.id) : -1; if (index >= 0) { const copy = $copy($store[api.commit]); if (Array.isArray(rs)) { rs.forEach((r) => { const index = copy.findIndex((v) => v.id === r.id); if (index >= 0) copy[index] = r; }); } else { copy[index] = rs; } $store.commit(api.commit, copy); } } if (notify) { $snackbar( $store.lang === "en" ? "Data has been successfully saved to the system." : "Dữ liệu đã được lưu vào hệ thống", "Success", ); } return rs; } catch (err) { console.error(err); return "error"; } }; const findpage = function (arr) { var copy = $copy($store.pagetrack); var doFind = function () { let found = undefined; for (let i = 1; i <= 30; i++) { let name = `pagedata${i}`; if (!copy[name]) { found = name; copy[name] = true; break; } } if (!found) console.log("pagename not found"); return found; }; let result; if (arr) { result = []; arr.map((v) => { result.push({ name: v, value: doFind() }); }); } else { result = doFind(copy); } $store.commit("pagetrack", copy); return result; }; const getpage = function (showFilter) { return { data: [], fields: [], filters: [], update: undefined, action: undefined, filterby: undefined, api: { full_data: true }, origin_api: { full_data: true }, tablesetting: undefined, setting: undefined, tabfield: true, setpage: {}, showFilter: !showFilter ? true : showFilter, }; }; const setpage = function (pagename, row, api) { let json = row.detail; let fields = $updateSeriesFields(json.fields); let copy = $store[pagename] || getpage(); copy.fields = fields; copy.setting = $copy(row); if (json.filters) copy.filters = $copy(json.filters); if (json.tablesetting) copy.tablesetting = json.tablesetting; if (api) { let copyApi = $copy(api); delete copyApi.data; copyApi.full_data = api.data.full_data; copyApi.total_rows = api.data.total_rows; copy.api = copyApi; copy.origin_api = copy; } $store.commit(pagename, copy); return copy; }; const getdata = async function (name, { filter, params, first = false } = {}) { const api = findapi(name); if (params) api.params = params; else if (filter) api.params.filter = filter; const rs = await getapi([api]); const { data } = rs[0]; if (data) { if (data.rows) { return first ? data.rows[0] : data.rows; } else { const rows = Array.isArray(data) ? data : [data]; return first ? (rows.length > 0 ? rows[0] : data) : data; } } else { const rows = Array.isArray(data) ? data : [data]; return first ? (rows.length > 0 ? rows[0] : data) : data; } }; const subscribe = (name, filter, callback) => { const apiConfig = findapi(name); if (!apiConfig) { console.error(`[subscribe] API config for '${name}' not found.`); return; } const urlParts = apiConfig.url.split("/").filter((p) => p); // Capitalize the model name for the backend consumer const modelName = urlParts.length > 1 ? urlParts[1].charAt(0).toUpperCase() + urlParts[1].slice(1) : null; if (!modelName) { console.error(`[subscribe] Could not determine model name from URL for '${name}'.`); return; } const params = $clone(apiConfig.params) || {}; // List of parameters that need to be stringified if they are objects const paramsToJSONify = ["filter_or", "exclude", "distinct_values", "calculation", "final_filter", "final_exclude"]; // Apply the filter passed to subscribe function if (filter) { params.filter = JSON.stringify(filter); } // Stringify other dictionary-like parameters if they exist and are objects for (const key of paramsToJSONify) { if (params[key] && typeof params[key] === "object" && !Array.isArray(params[key])) { params[key] = JSON.stringify(params[key]); } } const payload = { name: modelName, params: params, }; subscribeToData(payload, callback); }; const insertrow = async function (name, data, values, pagename, notify) { let result = await insertapi(name, { data, values, notify }); if (result === "error" || !pagename || !$store[pagename]) return result; let copy = $clone($store[pagename]); copy.update = { refresh: true }; $store.commit(pagename, copy); return result; }; const updaterow = async function (name, data, values, pagename, notify) { let result = await patchapi(name, data, values, notify); if (result === "error" || !pagename || !$store[pagename]) return result; let copy = $clone($store[pagename]); copy.update = { refresh: true }; $store.commit(pagename, copy); return result; }; // delete data const deleteapi = async function (name, id) { try { const found = findapi(name); let rs; if (!Array.isArray(id)) { rs = await $fetch(`${path}${found.url_detail}${id}`, { method: "delete", }); } else { rs = await $fetch(`${path}import-data/${found.url.substring(5, found.url.length - 1)}/`, id, { params: { action: "delete" }, }); } if (found.commit) { const copy = $copy($store[found.commit]); if (!Array.isArray(id)) { const index = copy.findIndex((v) => v.id === id); if (index >= 0) $remove(copy, index); } else { rs.forEach((element) => { const index = copy.findIndex((v) => v.id === element.id); if (index >= 0) $remove(copy, index); }); } $store.commit(found.name, copy); } return id; } catch (err) { console.error(err); if (err.response) { let content = `Đã xảy ra lỗi, xóa dữ liệu không thành công`; if (err.response.data) content += `

Chi tiết

${JSON.stringify(err.response.data)}

`; $dialog(content, "Lỗi", "Error"); } return "error"; } }; // delete row const deleterow = async function (name, id, pagename) { let result = await deleteapi(name, id); if (result === "error" || !pagename || !$store[pagename]) return result; let copy = $clone($store[pagename]); copy.update = { refresh: true }; $store.commit(pagename, copy); return result; }; // update page const updatepage = function (pagename, row, action) { let copy = $clone($store[pagename]); let rows = Array.isArray(row) ? row : [row]; rows.map((x) => { let idx = copy.data.findIndex((v) => v.id === x.id); if (action === "delete") { if (idx >= 0) $remove(copy.data, idx); } else { idx >= 0 ? (copy.data[idx] = x) : copy.data.unshift(x); } }); copy.update = { data: copy.data }; $store.commit(pagename, copy); }; const buildFileUrl = (file) => { if (!file || typeof file !== "string") { console.error(`Invalid file__file: ${file}`); return; } return `${getpath()}static/files/${encodeURIComponent(file)}`; }; const generateDocument = async (params) => { const apiBaseUrl = path; const url = new URL(`${apiBaseUrl}generate-document/`); if (params) { for (const key in params) { if (Object.hasOwnProperty.call(params, key)) { const value = params[key]; if (value !== undefined && value !== null) { url.searchParams.append(key, value); } } } } try { const response = await $fetch(url.toString()); if (response && response.pdf) { const pdfUrl = `${apiBaseUrl}static/contract/${response.pdf}`; return { success: true, pdfUrl: pdfUrl, data: response }; } else { console.error("generateDocument: Định dạng phản hồi API không hợp lệ.", response); return { success: false, error: "Định dạng phản hồi API không hợp lệ.", }; } } catch (error) { console.error("Lỗi khi tạo tài liệu:", error); return { success: false, error: error.data?.message || error.message }; } }; const accountEntry = async (params) => { const apiBaseUrl = path; try { const response = await $fetch(`${apiBaseUrl}account-entry/`, { method: "POST", body: params, headers: { "Content-Type": "application/json", }, }); if (response) { return { success: true, data: response }; } else { console.error("Định dạng phản hồi API không hợp lệ.", response); return { success: false, error: "Định dạng phản hồi API không hợp lệ.", }; } } catch (error) { console.error("Lỗi khi tạo accountEntry:", error); return { success: false, error: error.data?.message || error.message }; } }; /** * @param {'edit' | 'view'} right * @param {{ code?: string, category?: string }} options * `code` & `category` from `Biz_Setting` * - If passed options, returns edit rights for `code` (useful for components used in modals in many different tabs) * - If not passed, returns edit rights for current tab & subtab * @returns {boolean} */ const getEditRights = (right = "edit", { code, category } = {}) => { if (!["edit", "view"].includes(right)) throw new Error(`right ${right} is not one of ['edit', 'view']`); const getRight = (rightObj) => { return right === "edit" ? rightObj && rightObj.is_edit : Boolean(rightObj); }; if ($store.rights.length === 0) return true; // full rights if (code && category) { // if passed, must pass both const foundRight = $store.rights.find( ({ setting__category, setting__code }) => setting__category === category && setting__code === code, ); return getRight(foundRight); } else { const { tab, subtab } = $store.tabinfo; let isTabEdit; let isSubTabEdit; const tabRight = $store.rights.find((rights) => rights.setting === tab.id); isTabEdit = getRight(tabRight); if (!subtab) isSubTabEdit = false; else { const subTabRight = $store.rights.find((rights) => rights.setting === subtab.id); isSubTabEdit = getRight(subTabRight); } return isTabEdit || isSubTabEdit; } }; // --- WebSocket Integration --- let socket = null; const requests = {}; // Store callbacks for subscription responses const connectWebSocket = () => { if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { return; } // Use wss for https, ws for http const wsProtocol = window.location.protocol === "https:" ? "wss:" : "wss:"; // Construct base URL without http/https protocol const baseUrl = path.replace(/^https?:\/\//, ""); const wsUrl = `${wsProtocol}//${baseUrl}ws/data/`; socket = new WebSocket(wsUrl); socket.onopen = () => { console.log("WebSocket connection established"); }; socket.onmessage = (event) => { const response = JSON.parse(event.data); // Handle initial subscription responses with specific callbacks if (response.type === "subscription_response" && response.request_id && requests[response.request_id]) { const callback = requests[response.request_id]; callback(response.data); delete requests[response.request_id]; // Clean up callback after use } else { // For all other messages (like realtime_update), dispatch a global event // This decouples the plugin from the store logic window.dispatchEvent(new CustomEvent("ws_message", { detail: response })); } }; socket.onclose = (event) => { console.log("WebSocket connection closed:", event); socket = null; // Attempt to reconnect after a delay setTimeout(connectWebSocket, 5000); }; socket.onerror = (error) => { console.error("WebSocket error:", error); }; }; const subscribeToData = (payload, callback) => { if (!socket || socket.readyState !== WebSocket.OPEN) { console.warn("WebSocket is not connected. Attempting to connect and retry."); connectWebSocket(); // Retry subscription after a short delay setTimeout(() => subscribeToData(payload, callback), 1000); return; } const requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; if (callback) { requests[requestId] = callback; } const request = { action: "subscribe", request_id: requestId, payload: payload, }; socket.send(JSON.stringify(request)); console.log("[WebSocket] Sent subscription request:", request); }; return { provide: { mode, getpath, findapi, readyapi, getapi, getdata, subscribe, insertapi, patchapi, updaterow, findpage, getpage, setpage, insertrow, deleteapi, deleterow, updatepage, requestLogin, buildFileUrl, generateDocument, accountEntry, getEditRights, connectWebSocket, subscribeToData, }, }; });