@zenify-omni/chat-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/api/campaign.js +19 -0
- package/api/chat.js +518 -0
- package/api/chatroom.js +254 -0
- package/api/contact.js +45 -0
- package/api/crm-organization.js +101 -0
- package/api/customer.js +143 -0
- package/api/facebook-labels.js +11 -0
- package/api/field.js +11 -0
- package/api/file.js +17 -0
- package/api/filter.js +50 -0
- package/api/group.js +11 -0
- package/api/iam.js +23 -0
- package/api/index.js +54 -0
- package/api/orgTree.js +45 -0
- package/api/post.js +111 -0
- package/api/queue.js +15 -0
- package/api/report.js +422 -0
- package/api/send.js +21 -0
- package/api/session-event.js +30 -0
- package/api/session.js +249 -0
- package/api/settings/assign-task.js +10 -0
- package/api/settings/channel.js +136 -0
- package/api/settings/chat-layout.js +37 -0
- package/api/settings/chat-timeout.js +10 -0
- package/api/settings/common.js +32 -0
- package/api/settings/config.js +19 -0
- package/api/settings/index.js +24 -0
- package/api/settings/ivr.js +25 -0
- package/api/settings/queue-v3.js +56 -0
- package/api/settings/queue.js +48 -0
- package/api/settings/routing.js +32 -0
- package/api/settings/service.js +31 -0
- package/api/settings.js +567 -0
- package/api/shipping.js +19 -0
- package/api/shopee.js +21 -0
- package/api/smart-qc/index.js +4 -0
- package/api/smart-qc/keyword.js +10 -0
- package/api/tag.js +11 -0
- package/api/user.js +177 -0
- package/client.js +391 -0
- package/config/env.js +76 -0
- package/constants/advance-filter.js +358 -0
- package/constants/auto-assign-tag.js +13 -0
- package/constants/chat-message.js +423 -0
- package/constants/chat-room-tag.js +7 -0
- package/constants/chat-source.js +68 -0
- package/constants/chatbot.js +58 -0
- package/constants/common.js +4 -0
- package/constants/crm-organization.js +75 -0
- package/constants/distribute.js +8 -0
- package/constants/distribution-log.js +14 -0
- package/constants/events.js +27 -0
- package/constants/file-upload.js +86 -0
- package/constants/lead.js +111 -0
- package/constants/mask-data-config.js +16 -0
- package/constants/month.js +78 -0
- package/constants/quick-filter.js +49 -0
- package/constants/regex.js +34 -0
- package/constants/setting.js +62 -0
- package/constants/shipping.js +12 -0
- package/constants/social.js +21 -0
- package/constants/sortable-fields.js +146 -0
- package/constants/tenant_config.js +154 -0
- package/constants/use-query-key.js +470 -0
- package/constants/user.js +293 -0
- package/exceptions/axios-error.js +24 -0
- package/exceptions/https-error.js +28 -0
- package/exceptions/validator-error.js +20 -0
- package/lib/avatar-color.js +31 -0
- package/lib/i18n/index.js +85 -0
- package/lib/i18n/language-store.js +74 -0
- package/lib/i18n/locales/en/campaign.json +3 -0
- package/lib/i18n/locales/en/chat.json +656 -0
- package/lib/i18n/locales/en/common.json +1846 -0
- package/lib/i18n/locales/en/customer.json +20 -0
- package/lib/i18n/locales/en/lead.json +5 -0
- package/lib/i18n/locales/en/lumi.json +4 -0
- package/lib/i18n/locales/en/notification.json +107 -0
- package/lib/i18n/locales/en/org.json +4 -0
- package/lib/i18n/locales/en/posts.json +8 -0
- package/lib/i18n/locales/en/settings.json +39 -0
- package/lib/i18n/locales/vi/campaign.json +3 -0
- package/lib/i18n/locales/vi/chat.json +660 -0
- package/lib/i18n/locales/vi/common.json +1846 -0
- package/lib/i18n/locales/vi/customer.json +20 -0
- package/lib/i18n/locales/vi/lead.json +5 -0
- package/lib/i18n/locales/vi/lumi.json +4 -0
- package/lib/i18n/locales/vi/notification.json +107 -0
- package/lib/i18n/locales/vi/org.json +4 -0
- package/lib/i18n/locales/vi/posts.json +8 -0
- package/lib/i18n/locales/vi/settings.json +39 -0
- package/lib/i18n/server-label.js +33 -0
- package/lib/phone.js +70 -0
- package/lib/user-avatar.js +21 -0
- package/package.json +45 -0
- package/socket/index.js +30 -0
- package/transport-registry.js +77 -0
- package/utils/access-detail.js +119 -0
- package/utils/auth.js +205 -0
- package/utils/chunk-reload-guard.js +4 -0
- package/utils/concurrency-limiter.js +30 -0
- package/utils/crm-organization.js +0 -0
- package/utils/currency.js +14 -0
- package/utils/customers.js +42 -0
- package/utils/data.js +178 -0
- package/utils/date.js +144 -0
- package/utils/debounced-invalidate.js +15 -0
- package/utils/emoticon.js +134 -0
- package/utils/export.js +7 -0
- package/utils/field-layout.js +33 -0
- package/utils/filter-adapters.js +9 -0
- package/utils/filter.js +18 -0
- package/utils/formatters.js +6 -0
- package/utils/image-compression.js +169 -0
- package/utils/leads.js +13 -0
- package/utils/microlink.js +20 -0
- package/utils/org-tree.js +108 -0
- package/utils/request.js +461 -0
- package/utils/session-end.js +19 -0
- package/utils/string.js +154 -0
- package/utils/table-display-config.js +80 -0
- package/utils/validation.js +49 -0
package/utils/auth.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { UserRole, UserRoleContext } from "../constants/user.js";
|
|
2
|
+
import { envSet } from "../config/env.js";
|
|
3
|
+
const SessionEndReason = Object.freeze({
|
|
4
|
+
// The server-side session row is gone: signed out elsewhere, kicked by the
|
|
5
|
+
// parallel-session limit, or revoked by an admin. Refreshing cannot help.
|
|
6
|
+
REVOKED: "revoked",
|
|
7
|
+
// The credential simply aged out past the refresh window.
|
|
8
|
+
EXPIRED: "expired",
|
|
9
|
+
// Token minted by the legacy be `/v2/login` (no `sid`); ended by the FE
|
|
10
|
+
// migration nudge to move the user onto a new-issuer `sid` session.
|
|
11
|
+
LEGACY: "legacy"
|
|
12
|
+
});
|
|
13
|
+
const SESSION_END_REASON_KEY = "sessionEndReason";
|
|
14
|
+
const markSessionEnded = (reason) => {
|
|
15
|
+
try {
|
|
16
|
+
if (!sessionStorage.getItem(SESSION_END_REASON_KEY)) {
|
|
17
|
+
sessionStorage.setItem(SESSION_END_REASON_KEY, reason);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const getSessionEndReason = () => {
|
|
23
|
+
try {
|
|
24
|
+
return sessionStorage.getItem(SESSION_END_REASON_KEY);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const clearSessionEndReason = () => {
|
|
30
|
+
try {
|
|
31
|
+
sessionStorage.removeItem(SESSION_END_REASON_KEY);
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const login = (user) => {
|
|
36
|
+
clearSessionEndReason();
|
|
37
|
+
localStorage.setItem("zenify-sdk-user", JSON.stringify(user));
|
|
38
|
+
};
|
|
39
|
+
const setUser = (user) => {
|
|
40
|
+
localStorage.setItem("zenify-sdk-user", JSON.stringify(user));
|
|
41
|
+
};
|
|
42
|
+
const logout = () => {
|
|
43
|
+
localStorage.removeItem("zenify-sdk-user");
|
|
44
|
+
localStorage.removeItem("smartView");
|
|
45
|
+
localStorage.removeItem("queueId");
|
|
46
|
+
localStorage.removeItem("activeWorkspaceId");
|
|
47
|
+
sessionStorage.removeItem("viewAsUserData");
|
|
48
|
+
};
|
|
49
|
+
const DEVICE_ID_KEY = "deviceId";
|
|
50
|
+
const generateDeviceId = () => typeof globalThis.crypto?.randomUUID === "function" ? globalThis.crypto.randomUUID() : `web-${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
|
|
51
|
+
const getDeviceId = () => {
|
|
52
|
+
let deviceId = localStorage.getItem(DEVICE_ID_KEY);
|
|
53
|
+
if (!deviceId) {
|
|
54
|
+
deviceId = generateDeviceId();
|
|
55
|
+
localStorage.setItem(DEVICE_ID_KEY, deviceId);
|
|
56
|
+
}
|
|
57
|
+
return deviceId;
|
|
58
|
+
};
|
|
59
|
+
const isAdmin = () => {
|
|
60
|
+
const user = getUser();
|
|
61
|
+
return !user ? false : user?.role === UserRole.ADMINISTRATOR;
|
|
62
|
+
};
|
|
63
|
+
const isSupervisor = () => {
|
|
64
|
+
const user = getUser();
|
|
65
|
+
return !user ? false : user?.role === UserRole.SUPERVISOR;
|
|
66
|
+
};
|
|
67
|
+
function isRoleTeamLeader() {
|
|
68
|
+
const user = getUser();
|
|
69
|
+
return !user ? false : user?.role === UserRole.TEAM_LEADER;
|
|
70
|
+
}
|
|
71
|
+
function isRoleTeamLeaderSale() {
|
|
72
|
+
const user = getUser();
|
|
73
|
+
if (!user) return false;
|
|
74
|
+
const { role, roles = [] } = user;
|
|
75
|
+
if (roles?.length > 0) {
|
|
76
|
+
return roles.includes(UserRole.SALE_TEAM_LEADER);
|
|
77
|
+
}
|
|
78
|
+
return role === UserRole.SALE_TEAM_LEADER;
|
|
79
|
+
}
|
|
80
|
+
function isRoleSale() {
|
|
81
|
+
const user = getUser();
|
|
82
|
+
if (!user) return false;
|
|
83
|
+
const { role, roles = [] } = user;
|
|
84
|
+
if (roles?.length > 0) {
|
|
85
|
+
return roles.includes(UserRole.SALE);
|
|
86
|
+
}
|
|
87
|
+
return role === UserRole.SALE;
|
|
88
|
+
}
|
|
89
|
+
function isRoleAgent() {
|
|
90
|
+
const user = getUser();
|
|
91
|
+
return !user ? false : user?.role === UserRole.AGENT;
|
|
92
|
+
}
|
|
93
|
+
function isRoleSaleOrSaleLeader() {
|
|
94
|
+
const user = getUser();
|
|
95
|
+
if (!user) return false;
|
|
96
|
+
const { role, roles = [] } = user;
|
|
97
|
+
if (roles?.length > 0) {
|
|
98
|
+
return roles.includes(UserRole.SALE) || roles.includes(UserRole.SALE_TEAM_LEADER);
|
|
99
|
+
}
|
|
100
|
+
return role === UserRole.SALE || role === UserRole.SALE_TEAM_LEADER;
|
|
101
|
+
}
|
|
102
|
+
function checkIsViewAsMode() {
|
|
103
|
+
return !!sessionStorage.getItem("viewAsUserData");
|
|
104
|
+
}
|
|
105
|
+
let userCache = {
|
|
106
|
+
sessionRaw: void 0,
|
|
107
|
+
localRaw: void 0,
|
|
108
|
+
hasValue: false,
|
|
109
|
+
value: null
|
|
110
|
+
};
|
|
111
|
+
const getUser = () => {
|
|
112
|
+
let sessionRaw = null;
|
|
113
|
+
let localRaw = null;
|
|
114
|
+
try {
|
|
115
|
+
sessionRaw = sessionStorage.getItem("viewAsUserData");
|
|
116
|
+
} catch {
|
|
117
|
+
sessionRaw = null;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
localRaw = localStorage.getItem("zenify-sdk-user");
|
|
121
|
+
} catch {
|
|
122
|
+
localRaw = null;
|
|
123
|
+
}
|
|
124
|
+
if (userCache.hasValue && userCache.sessionRaw === sessionRaw && userCache.localRaw === localRaw) {
|
|
125
|
+
return userCache.value;
|
|
126
|
+
}
|
|
127
|
+
let value;
|
|
128
|
+
try {
|
|
129
|
+
value = sessionRaw ? JSON.parse(sessionRaw) : JSON.parse(localRaw);
|
|
130
|
+
} catch {
|
|
131
|
+
value = null;
|
|
132
|
+
}
|
|
133
|
+
userCache = { sessionRaw, localRaw, hasValue: true, value };
|
|
134
|
+
return value;
|
|
135
|
+
};
|
|
136
|
+
const renderUserRoleContext = (user) => {
|
|
137
|
+
if (!user) {
|
|
138
|
+
user = getUser();
|
|
139
|
+
}
|
|
140
|
+
const { roles = [], role } = user;
|
|
141
|
+
const userRoles = roles.length > 0 ? roles : [role].filter(Boolean);
|
|
142
|
+
return [...new Set(userRoles)].map((r) => UserRoleContext[r]).filter(Boolean).join(", ");
|
|
143
|
+
};
|
|
144
|
+
const decodeTokenPayload = (token) => {
|
|
145
|
+
try {
|
|
146
|
+
const seg = token?.split(".")?.[1];
|
|
147
|
+
if (!seg) return null;
|
|
148
|
+
const json = atob(seg.replace(/-/g, "+").replace(/_/g, "/"));
|
|
149
|
+
return JSON.parse(json);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const getRealTokenIdentity = () => {
|
|
155
|
+
let token;
|
|
156
|
+
try {
|
|
157
|
+
token = JSON.parse(localStorage.getItem("zenify-sdk-user") || "null")?.token;
|
|
158
|
+
} catch {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
const p = decodeTokenPayload(token);
|
|
162
|
+
return {
|
|
163
|
+
sid: p?.sid,
|
|
164
|
+
user_id: (p?.user_id ?? p?.sub) != null ? String(p?.user_id ?? p?.sub) : void 0,
|
|
165
|
+
tenant_id: p?.tenant_id != null ? String(p.tenant_id) : void 0
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
const matchNamespacePrefix = (perm, prefix) => perm === prefix || perm.startsWith(prefix + ":");
|
|
169
|
+
const isHelpDeskPlanOnly = () => {
|
|
170
|
+
const user = getUser();
|
|
171
|
+
if (!user) return false;
|
|
172
|
+
return envSet("HELP_DESK_PLAN_TENANT_IDS").has(user.tenant_id);
|
|
173
|
+
};
|
|
174
|
+
const ASAHI_TENANT_IDS = /* @__PURE__ */ new Set([
|
|
175
|
+
"6858fc5277924f901d276d32",
|
|
176
|
+
"646ae9cffe32b43426f45165"
|
|
177
|
+
]);
|
|
178
|
+
const MISA_TENANT_IDS = /* @__PURE__ */ new Set(["69bcd013c49df515076b109a"]);
|
|
179
|
+
const GOOGLE_TENANT_IDS = /* @__PURE__ */ new Set([
|
|
180
|
+
"68ef2ff8cc7ace4872d0641f",
|
|
181
|
+
"646ae9cffe32b43426f45165"
|
|
182
|
+
]);
|
|
183
|
+
export {
|
|
184
|
+
SessionEndReason,
|
|
185
|
+
checkIsViewAsMode,
|
|
186
|
+
clearSessionEndReason,
|
|
187
|
+
decodeTokenPayload,
|
|
188
|
+
getDeviceId,
|
|
189
|
+
getRealTokenIdentity,
|
|
190
|
+
getSessionEndReason,
|
|
191
|
+
getUser,
|
|
192
|
+
isAdmin,
|
|
193
|
+
isHelpDeskPlanOnly,
|
|
194
|
+
isRoleAgent,
|
|
195
|
+
isRoleSale,
|
|
196
|
+
isRoleSaleOrSaleLeader,
|
|
197
|
+
isRoleTeamLeader,
|
|
198
|
+
isRoleTeamLeaderSale,
|
|
199
|
+
isSupervisor,
|
|
200
|
+
login,
|
|
201
|
+
logout,
|
|
202
|
+
markSessionEnded,
|
|
203
|
+
renderUserRoleContext,
|
|
204
|
+
setUser
|
|
205
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "../config/env.js";
|
|
2
|
+
const WINDOW_MS = 1e4;
|
|
3
|
+
const DYNAMIC_IMPORT_RE = /(dynamically imported module|Loading chunk .* failed|error loading dynamically imported module|Importing a module script failed)/i;
|
|
4
|
+
const ROUTER_STALE_CHUNK_RE = /(Failed to load module script|reading '(component|errorComponent|pendingComponent|notFoundComponent)')/i;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function createConcurrencyLimiter(limit) {
|
|
2
|
+
let active = 0;
|
|
3
|
+
const queue = [];
|
|
4
|
+
const next = () => {
|
|
5
|
+
if (active >= limit || queue.length === 0) return;
|
|
6
|
+
active++;
|
|
7
|
+
const { fn, resolve, reject } = queue.shift();
|
|
8
|
+
Promise.resolve().then(fn).then(resolve, reject).finally(() => {
|
|
9
|
+
active--;
|
|
10
|
+
next();
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
return (fn) => new Promise((resolve, reject) => {
|
|
14
|
+
queue.push({ fn, resolve, reject });
|
|
15
|
+
next();
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function createSingleFlight() {
|
|
19
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
20
|
+
return (key, fn) => {
|
|
21
|
+
if (inflight.has(key)) return inflight.get(key);
|
|
22
|
+
const p = Promise.resolve().then(fn).finally(() => inflight.delete(key));
|
|
23
|
+
inflight.set(key, p);
|
|
24
|
+
return p;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
createConcurrencyLimiter,
|
|
29
|
+
createSingleFlight
|
|
30
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function formatVND(value, scale = 1) {
|
|
2
|
+
if (value == null || isNaN(value)) return "0";
|
|
3
|
+
const num = Math.floor(Number(value) * scale);
|
|
4
|
+
return `${num.toLocaleString("vi-VN")} \u20AB`;
|
|
5
|
+
}
|
|
6
|
+
const usdFormatter = new Intl.NumberFormat("en-US", {
|
|
7
|
+
style: "currency",
|
|
8
|
+
currency: "USD",
|
|
9
|
+
minimumFractionDigits: 2,
|
|
10
|
+
maximumFractionDigits: 2
|
|
11
|
+
});
|
|
12
|
+
export {
|
|
13
|
+
formatVND
|
|
14
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import i18n from "../lib/i18n/index.js";
|
|
2
|
+
import { UserRole } from "../constants/user.js";
|
|
3
|
+
import { getUser } from "./auth.js";
|
|
4
|
+
const sourceDetailNames = {
|
|
5
|
+
get CALL_INBOUND() {
|
|
6
|
+
return i18n.t("common:label.customerCalling", {
|
|
7
|
+
defaultValue: "Kh\xE1ch g\u1ECDi \u0111i\u1EC7n"
|
|
8
|
+
});
|
|
9
|
+
},
|
|
10
|
+
CALL_INTERNAL: "",
|
|
11
|
+
get CALL_OUTBOUND() {
|
|
12
|
+
return i18n.t("common:action.makeOutboundCall", {
|
|
13
|
+
defaultValue: "G\u1ECDi \u0111i\u1EC7n ra"
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
get chat() {
|
|
17
|
+
return i18n.t("common:label.webMessage", { defaultValue: "Tin nh\u1EAFn web" });
|
|
18
|
+
},
|
|
19
|
+
email: "Email",
|
|
20
|
+
get facebook() {
|
|
21
|
+
return i18n.t("common:label.facebookChannel", {
|
|
22
|
+
defaultValue: "K\xEAnh Facebook"
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
get hand() {
|
|
26
|
+
return i18n.t("common:action.addNew", { defaultValue: "Th\xEAm m\u1EDBi" });
|
|
27
|
+
},
|
|
28
|
+
import: "",
|
|
29
|
+
sms: "",
|
|
30
|
+
zalo: "Zalo"
|
|
31
|
+
};
|
|
32
|
+
const isCustomerEditableByUser = (customer) => {
|
|
33
|
+
const user = getUser();
|
|
34
|
+
if (!customer || !user) return false;
|
|
35
|
+
if ([UserRole.ADMINISTRATOR, UserRole.SUPERVISOR].includes(user.role)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return String(user._id) === String(customer.contact_owner_id);
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
isCustomerEditableByUser
|
|
42
|
+
};
|
package/utils/data.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { DataTypes, KeyApplyTo } from "../constants/mask-data-config.js";
|
|
2
|
+
import { getUser } from "./auth.js";
|
|
3
|
+
import { EMAIL_REGEX } from "../constants/regex.js";
|
|
4
|
+
import { replacePhonesInText } from "../lib/phone.js";
|
|
5
|
+
function isInArray(value, array) {
|
|
6
|
+
if (!array || !Array.isArray(array)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (array.length === 0) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (value === null || value === void 0) {
|
|
13
|
+
return array.some((item) => item === value);
|
|
14
|
+
}
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
const stringValue = JSON.stringify(value);
|
|
17
|
+
return array.some((item) => JSON.stringify(item) === stringValue);
|
|
18
|
+
}
|
|
19
|
+
return array.includes(value);
|
|
20
|
+
}
|
|
21
|
+
const maskDataConfigMap = /* @__PURE__ */ new Map();
|
|
22
|
+
const setMaskDataConfig = (maskDataConfig = []) => {
|
|
23
|
+
if (!Array.isArray(maskDataConfig)) {
|
|
24
|
+
console.warn("maskDataConfig should be an array");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
maskDataConfigMap.clear();
|
|
28
|
+
maskDataConfig.forEach((config) => {
|
|
29
|
+
if (config && config.data_type) {
|
|
30
|
+
maskDataConfigMap.set(config.data_type, config);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const getMaskDataConfig = (type) => {
|
|
35
|
+
return maskDataConfigMap.get(type);
|
|
36
|
+
};
|
|
37
|
+
const iamRoleIdOf = (user) => {
|
|
38
|
+
const iamRole = user?.iam_role;
|
|
39
|
+
if (!iamRole) return "";
|
|
40
|
+
return String(iamRole._id ?? iamRole);
|
|
41
|
+
};
|
|
42
|
+
const matchUserRole = (user, values) => {
|
|
43
|
+
if (!Array.isArray(values)) return false;
|
|
44
|
+
const wanted = values.map(String);
|
|
45
|
+
if (wanted.includes(String(user.role ?? ""))) return true;
|
|
46
|
+
const iamRoleId = iamRoleIdOf(user);
|
|
47
|
+
if (iamRoleId && wanted.includes(iamRoleId)) return true;
|
|
48
|
+
return Array.isArray(user.roles) && user.roles.some((role) => wanted.includes(String(role)));
|
|
49
|
+
};
|
|
50
|
+
const checkMaskAppliedData = (applyTo) => {
|
|
51
|
+
const user = getUser();
|
|
52
|
+
if (!user) return false;
|
|
53
|
+
let check = false;
|
|
54
|
+
applyTo.forEach((item) => {
|
|
55
|
+
if (item.key === KeyApplyTo.USER && item?.value?.includes(user._id)) {
|
|
56
|
+
check = true;
|
|
57
|
+
}
|
|
58
|
+
if (item.key === KeyApplyTo.ROLE && matchUserRole(user, item?.value)) {
|
|
59
|
+
check = true;
|
|
60
|
+
}
|
|
61
|
+
if (item.key === KeyApplyTo.GROUP && item?.value?.map(String)?.includes(String(user.group_id))) {
|
|
62
|
+
check = true;
|
|
63
|
+
}
|
|
64
|
+
if (item.key === KeyApplyTo.ORGANIZATION && item?.value?.map(String)?.includes(String(user.organize_id))) {
|
|
65
|
+
check = true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return check;
|
|
69
|
+
};
|
|
70
|
+
const maskPhoneNumber = (data, pattern) => {
|
|
71
|
+
if (!data || !pattern) {
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
const maskCount = (pattern.match(/\*/g) || []).length;
|
|
75
|
+
const visibleCount = data.length - maskCount;
|
|
76
|
+
const startsWithStar = pattern.startsWith("#");
|
|
77
|
+
const endsWithStar = pattern.endsWith("#");
|
|
78
|
+
if (pattern?.replace(/\*/g, "") === pattern) {
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
if (pattern?.replace(/#/g, "") === pattern) {
|
|
82
|
+
return "*".repeat(data.length);
|
|
83
|
+
}
|
|
84
|
+
const longestHashRun = Math.max(
|
|
85
|
+
...(pattern.match(/#+/g) || [""]).map((run) => run.length)
|
|
86
|
+
);
|
|
87
|
+
if (longestHashRun >= 2) {
|
|
88
|
+
const showCount = (pattern.match(/#/g) || []).length;
|
|
89
|
+
if (showCount >= data.length) {
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
const hideCount = data.length - showCount;
|
|
93
|
+
if (pattern.startsWith("*")) {
|
|
94
|
+
return "*".repeat(hideCount) + data.slice(data.length - showCount);
|
|
95
|
+
}
|
|
96
|
+
return data.slice(0, showCount) + "*".repeat(hideCount);
|
|
97
|
+
}
|
|
98
|
+
if (startsWithStar && !endsWithStar) {
|
|
99
|
+
return data.substring(0, visibleCount) + "*".repeat(maskCount);
|
|
100
|
+
}
|
|
101
|
+
if (!startsWithStar && endsWithStar) {
|
|
102
|
+
return "*".repeat(maskCount) + data.substring(maskCount);
|
|
103
|
+
}
|
|
104
|
+
if (startsWithStar && endsWithStar) {
|
|
105
|
+
const leftVisible = Math.ceil(visibleCount / 2);
|
|
106
|
+
const rightVisible = visibleCount - leftVisible;
|
|
107
|
+
return data.substring(0, leftVisible) + "*".repeat(maskCount) + data.substring(data.length - rightVisible);
|
|
108
|
+
}
|
|
109
|
+
let result = "";
|
|
110
|
+
for (let i = 0; i < data.length; i++) {
|
|
111
|
+
result += pattern[i] === "*" ? "*" : data[i];
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
const maskEmail = (data, pattern) => {
|
|
116
|
+
const [local, domain] = data.split("@");
|
|
117
|
+
const atIndex = pattern.indexOf("@");
|
|
118
|
+
const leftPattern = pattern.substring(0, atIndex);
|
|
119
|
+
const rightPattern = pattern.substring(atIndex + 1);
|
|
120
|
+
const leftMaskCount = (leftPattern.match(/\*/g) || []).length;
|
|
121
|
+
const rightMaskCount = (rightPattern.match(/\*/g) || []).length;
|
|
122
|
+
let maskedLocal = local.length <= leftMaskCount ? "*".repeat(local.length) : local.slice(0, local.length - leftMaskCount) + "*".repeat(leftMaskCount);
|
|
123
|
+
let maskedDomain = domain.length <= rightMaskCount ? "*".repeat(domain.length) : "*".repeat(rightMaskCount) + domain.slice(rightMaskCount);
|
|
124
|
+
return maskedLocal + "@" + maskedDomain;
|
|
125
|
+
};
|
|
126
|
+
function isHasPhoneNumberMask() {
|
|
127
|
+
return !!getMaskDataConfig(DataTypes.PHONE_NUMBER);
|
|
128
|
+
}
|
|
129
|
+
function maskData(input) {
|
|
130
|
+
if (!input || typeof input !== "string") {
|
|
131
|
+
return input;
|
|
132
|
+
}
|
|
133
|
+
let result = input;
|
|
134
|
+
const maskingPhoneNumberConfig = getMaskDataConfig(DataTypes.PHONE_NUMBER);
|
|
135
|
+
const maskingEmailConfig = getMaskDataConfig(DataTypes.EMAIL);
|
|
136
|
+
if (maskingPhoneNumberConfig) {
|
|
137
|
+
const { masking_pattern, apply_to } = maskingPhoneNumberConfig;
|
|
138
|
+
if (checkMaskAppliedData(apply_to)) {
|
|
139
|
+
result = replacePhonesInText(result, ({ raw }) => {
|
|
140
|
+
const digits = raw.replace(/\D/g, "");
|
|
141
|
+
return maskPhoneNumber(digits, masking_pattern);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (maskingEmailConfig) {
|
|
146
|
+
const { masking_pattern, apply_to } = maskingEmailConfig;
|
|
147
|
+
if (checkMaskAppliedData(apply_to) && typeof result === "string") {
|
|
148
|
+
result = result?.replace(EMAIL_REGEX, (match) => {
|
|
149
|
+
return maskEmail(match, masking_pattern);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
function truncateText(text, maxLength = 30) {
|
|
156
|
+
if (!text) return "";
|
|
157
|
+
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
|
|
158
|
+
}
|
|
159
|
+
const hasNonEmpty = (obj, keys) => {
|
|
160
|
+
if (!obj || typeof obj !== "object") return false;
|
|
161
|
+
if (!Array.isArray(keys) || keys.length === 0) return false;
|
|
162
|
+
return keys.some((key) => {
|
|
163
|
+
const val = obj[key];
|
|
164
|
+
return val !== void 0 && val !== null && val !== "";
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
const normalizeText = (text) => text?.toLowerCase().normalize("NFD")?.replace(/[\u0300-\u036f]/g, "").trim();
|
|
168
|
+
export {
|
|
169
|
+
hasNonEmpty,
|
|
170
|
+
isHasPhoneNumberMask,
|
|
171
|
+
isInArray,
|
|
172
|
+
maskData,
|
|
173
|
+
maskEmail,
|
|
174
|
+
maskPhoneNumber,
|
|
175
|
+
normalizeText,
|
|
176
|
+
setMaskDataConfig,
|
|
177
|
+
truncateText
|
|
178
|
+
};
|
package/utils/date.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
differenceInCalendarDays,
|
|
3
|
+
differenceInHours,
|
|
4
|
+
differenceInMinutes,
|
|
5
|
+
format,
|
|
6
|
+
isValid
|
|
7
|
+
} from "date-fns";
|
|
8
|
+
import { TZDate } from "@date-fns/tz";
|
|
9
|
+
import i18n from "../lib/i18n/index.js";
|
|
10
|
+
let tenantTz = "Asia/Ho_Chi_Minh";
|
|
11
|
+
const isValidTz = (tz) => {
|
|
12
|
+
if (typeof tz !== "string" || !tz) return false;
|
|
13
|
+
try {
|
|
14
|
+
Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const setTenantTz = (tz) => {
|
|
21
|
+
if (isValidTz(tz)) tenantTz = tz;
|
|
22
|
+
};
|
|
23
|
+
const getTenantTz = () => tenantTz;
|
|
24
|
+
const formatDay = (date, st) => {
|
|
25
|
+
if (!date) return "";
|
|
26
|
+
const d = new TZDate(date, tenantTz);
|
|
27
|
+
if (!isValid(d)) return "";
|
|
28
|
+
return format(d, st || "dd/MM/yyyy");
|
|
29
|
+
};
|
|
30
|
+
const formatDayWithTime = (date) => {
|
|
31
|
+
if (!date) return "";
|
|
32
|
+
const d = new TZDate(date, tenantTz);
|
|
33
|
+
if (!isValid(d)) return "";
|
|
34
|
+
return format(d, "dd/MM/yyyy HH:mm");
|
|
35
|
+
};
|
|
36
|
+
const formatTime = (date) => {
|
|
37
|
+
if (!date) return "";
|
|
38
|
+
const d = new TZDate(date, tenantTz);
|
|
39
|
+
if (!isValid(d)) return "";
|
|
40
|
+
return format(d, "HH:mm");
|
|
41
|
+
};
|
|
42
|
+
const formatDayWithTimeFromTimestamps = (timestamp) => {
|
|
43
|
+
if (!timestamp) return "";
|
|
44
|
+
const d = new TZDate(timestamp * 1e3, tenantTz);
|
|
45
|
+
if (!isValid(d)) return "";
|
|
46
|
+
return format(d, "dd/MM/yyyy HH:mm");
|
|
47
|
+
};
|
|
48
|
+
const getVNDate = (date) => new TZDate(date, tenantTz);
|
|
49
|
+
const generateLastMsgTimeText = (time) => {
|
|
50
|
+
if (!time) return "";
|
|
51
|
+
const currentTime = getVNDate(/* @__PURE__ */ new Date());
|
|
52
|
+
const datedLastMsgTime = getVNDate(time);
|
|
53
|
+
if (!isValid(datedLastMsgTime)) return "";
|
|
54
|
+
const diffDays = differenceInCalendarDays(currentTime, datedLastMsgTime);
|
|
55
|
+
const diffHours = differenceInHours(currentTime, datedLastMsgTime);
|
|
56
|
+
const diffMinutes = differenceInMinutes(currentTime, datedLastMsgTime);
|
|
57
|
+
if (diffDays === 0) {
|
|
58
|
+
if (diffHours === 0) {
|
|
59
|
+
if (diffMinutes === 0) {
|
|
60
|
+
return i18n.t("common:time.justNow", { defaultValue: "V\u1EEBa xong" });
|
|
61
|
+
} else {
|
|
62
|
+
return i18n.t("common:time.minutesAgo", {
|
|
63
|
+
count: diffMinutes,
|
|
64
|
+
defaultValue: "{count} ph\xFAt tr\u01B0\u1EDBc"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} else if (diffHours === 1) {
|
|
68
|
+
return i18n.t("common:time.oneHourAgo", { defaultValue: "1 gi\u1EDD tr\u01B0\u1EDBc" });
|
|
69
|
+
} else if (diffHours === 2) {
|
|
70
|
+
return i18n.t("common:time.twoHoursAgo", { defaultValue: "2 gi\u1EDD tr\u01B0\u1EDBc" });
|
|
71
|
+
} else {
|
|
72
|
+
return i18n.t("common:time.today", { defaultValue: "H\xF4m nay" }) + " " + format(datedLastMsgTime, "HH:mm");
|
|
73
|
+
}
|
|
74
|
+
} else if (diffDays === 1) {
|
|
75
|
+
return i18n.t("common:time.yesterday", { defaultValue: "H\xF4m qua" }) + " " + format(datedLastMsgTime, "HH:mm");
|
|
76
|
+
} else {
|
|
77
|
+
return format(datedLastMsgTime, "dd/MM/yyyy HH:mm");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const generateReminderTimeText = (reminderTime) => {
|
|
81
|
+
if (!reminderTime) return "";
|
|
82
|
+
const currentTime = getVNDate(/* @__PURE__ */ new Date());
|
|
83
|
+
const datedReminderTime = getVNDate(reminderTime);
|
|
84
|
+
if (!isValid(datedReminderTime)) return "";
|
|
85
|
+
const diffDays = differenceInCalendarDays(datedReminderTime, currentTime);
|
|
86
|
+
const diffHours = differenceInHours(datedReminderTime, currentTime);
|
|
87
|
+
const diffMinutes = differenceInMinutes(datedReminderTime, currentTime);
|
|
88
|
+
if (diffDays === 0) {
|
|
89
|
+
if (diffHours === 0) {
|
|
90
|
+
if (diffMinutes <= 0) {
|
|
91
|
+
return format(datedReminderTime, "dd/MM/yyyy HH:mm");
|
|
92
|
+
} else {
|
|
93
|
+
return i18n.t("common:time.minutesRemaining", {
|
|
94
|
+
count: diffMinutes,
|
|
95
|
+
defaultValue: "C\xF2n {count} ph\xFAt"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
} else if (diffHours === 1) {
|
|
99
|
+
return i18n.t("common:time.oneHourRemaining", {
|
|
100
|
+
defaultValue: "C\xF2n 1 gi\u1EDD"
|
|
101
|
+
});
|
|
102
|
+
} else if (diffHours === 2) {
|
|
103
|
+
return i18n.t("common:time.twoHoursRemaining", {
|
|
104
|
+
defaultValue: "C\xF2n 2 gi\u1EDD"
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
return i18n.t("common:time.today", { defaultValue: "H\xF4m nay" }) + ", " + format(datedReminderTime, "dd/MM/yyyy HH:mm");
|
|
108
|
+
}
|
|
109
|
+
} else if (diffDays === 1) {
|
|
110
|
+
return i18n.t("common:time.tomorrow", { defaultValue: "Ng\xE0y mai" }) + ", " + format(datedReminderTime, "dd/MM/yyyy HH:mm");
|
|
111
|
+
} else {
|
|
112
|
+
return format(datedReminderTime, "dd/MM/yyyy HH:mm");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const asTenant = (date) => new TZDate(date ?? /* @__PURE__ */ new Date(), getTenantTz());
|
|
116
|
+
const toInstant = (tzdate) => new Date(tzdate.getTime());
|
|
117
|
+
const tenantInstantFromWallClock = (date) => {
|
|
118
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
119
|
+
if (!isValid(d)) return d;
|
|
120
|
+
return new Date(
|
|
121
|
+
new TZDate(
|
|
122
|
+
d.getFullYear(),
|
|
123
|
+
d.getMonth(),
|
|
124
|
+
d.getDate(),
|
|
125
|
+
d.getHours(),
|
|
126
|
+
d.getMinutes(),
|
|
127
|
+
d.getSeconds(),
|
|
128
|
+
d.getMilliseconds(),
|
|
129
|
+
getTenantTz()
|
|
130
|
+
).getTime()
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
export {
|
|
134
|
+
formatDay,
|
|
135
|
+
formatDayWithTime,
|
|
136
|
+
formatDayWithTimeFromTimestamps,
|
|
137
|
+
formatTime,
|
|
138
|
+
generateLastMsgTimeText,
|
|
139
|
+
generateReminderTimeText,
|
|
140
|
+
getTenantTz,
|
|
141
|
+
getVNDate,
|
|
142
|
+
setTenantTz,
|
|
143
|
+
tenantInstantFromWallClock
|
|
144
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const timers = /* @__PURE__ */ new Map();
|
|
2
|
+
function debouncedInvalidate(queryClient, queryKey, options = {}) {
|
|
3
|
+
const { delay = 300, ...invalidateOptions } = options;
|
|
4
|
+
const mapKey = JSON.stringify(queryKey);
|
|
5
|
+
const existing = timers.get(mapKey);
|
|
6
|
+
if (existing) clearTimeout(existing);
|
|
7
|
+
const timer = setTimeout(() => {
|
|
8
|
+
timers.delete(mapKey);
|
|
9
|
+
queryClient.invalidateQueries({ queryKey, ...invalidateOptions });
|
|
10
|
+
}, delay);
|
|
11
|
+
timers.set(mapKey, timer);
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
debouncedInvalidate
|
|
15
|
+
};
|