1829 lines
59 KiB
Vue
1829 lines
59 KiB
Vue
<script setup>
|
|
import { usePageTitle } from '../composables/Core/usePageTitle';
|
|
usePageTitle('Manage User');
|
|
|
|
import { ref, computed, onMounted, watch, useAttrs } from "vue";
|
|
import axios from "axios";
|
|
import { useNavigate } from "../composables/Core/useNavigate";
|
|
import { useModal } from "../composables/Core/useModal";
|
|
import { useUserStore } from "../stores/user";
|
|
import SideTextButtonList from "../Components/Core/Services/SideTextButtonList.vue";
|
|
import InputGroupTextarea from "../Components/Core/Forms/InputGroupTextarea.vue";
|
|
import InputGroup from "../Components/Core/Forms/InputGroup.vue";
|
|
|
|
const { navigate } = useNavigate();
|
|
import BackButton from "../Components/Core/BackButton.vue";
|
|
const modal = useModal();
|
|
const userStore = useUserStore();
|
|
const attrs = useAttrs();
|
|
|
|
// Route parameters Extraction
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const userId = ref(null);
|
|
|
|
function extractHashkeyFromUrl() {
|
|
const urlPath = window.location.pathname;
|
|
if (!urlPath.includes("--h:")) return null;
|
|
try {
|
|
const parts = urlPath.split("--h:");
|
|
if (parts.length < 2) return null;
|
|
const encodedHash = parts[1];
|
|
if (!encodedHash) return null;
|
|
let base64 = encodedHash.startsWith("h:")
|
|
? encodedHash.substring(2)
|
|
: encodedHash;
|
|
try {
|
|
const decoded = atob(base64);
|
|
return decodeURIComponent(decoded);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function resolveTargetId(attrsSnapshot) {
|
|
return (
|
|
extractHashkeyFromUrl() ||
|
|
attrsSnapshot?.hashkey ||
|
|
attrsSnapshot?.id ||
|
|
attrsSnapshot?.target ||
|
|
urlParams.get("userId")
|
|
);
|
|
}
|
|
|
|
// User state
|
|
const user = ref(null);
|
|
const isLoading = ref(true);
|
|
|
|
// Stores for assignment
|
|
const availableStores = ref([]);
|
|
const showAssignStoreModal = ref(false);
|
|
const assigningStore = ref(false);
|
|
|
|
// Notes Modal state
|
|
const showNotesModal = ref(false);
|
|
const notesText = ref("");
|
|
const isSavingNotes = ref(false);
|
|
const isLoadingNotes = ref(false);
|
|
|
|
// Password Reset Modal State
|
|
const showResetPasswordModal = ref(false);
|
|
const resetPasswordText = ref("default123");
|
|
const isResettingPassword = ref(false);
|
|
|
|
// Send Balance Modal State
|
|
const showSendBalanceModal = ref(false);
|
|
const sendBalanceAmount = ref("");
|
|
const isSendingBalance = ref(false);
|
|
|
|
// Select Store to Manage Modal State
|
|
const showSelectStoreModal = ref(false);
|
|
const storesToManage = ref([]);
|
|
|
|
// Exec Modal state
|
|
const showExecModal = ref(false);
|
|
const execText = ref("");
|
|
const isSavingExec = ref(false);
|
|
const isLoadingExec = ref(false);
|
|
|
|
// Direct Children state
|
|
const directChildren = ref([]);
|
|
const isLoadingChildren = ref(false);
|
|
|
|
const loadDirectChildren = async () => {
|
|
if (!userId.value) return;
|
|
try {
|
|
isLoadingChildren.value = true;
|
|
const response = await axios.post("/admin/user/children/direct", {
|
|
target_user: userId.value,
|
|
});
|
|
if (Array.isArray(response.data)) {
|
|
directChildren.value = response.data;
|
|
} else {
|
|
directChildren.value = [];
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to fetch direct children", err);
|
|
directChildren.value = [];
|
|
} finally {
|
|
isLoadingChildren.value = false;
|
|
}
|
|
};
|
|
|
|
// Roles/Permissions state
|
|
const userRoles = ref([]);
|
|
const isLoadingRoles = ref(false);
|
|
|
|
const allRoles = ref([]);
|
|
const isLoadingAllRoles = ref(false);
|
|
const showPermissionsModal = ref(false);
|
|
const selectedPermissions = ref([]);
|
|
const isSavingPermissions = ref(false);
|
|
|
|
|
|
const isUltimateUser = computed(() => {
|
|
const currentLoggedInUser = userStore.user;
|
|
return (
|
|
userStore.acctType === "ult" ||
|
|
currentLoggedInUser?.acct_type === "ult" ||
|
|
currentLoggedInUser?.type === "ult" ||
|
|
currentLoggedInUser?.acct_type === "ultimate" ||
|
|
currentLoggedInUser?.type === "ultimate"
|
|
);
|
|
});
|
|
|
|
const initUser = async () => {
|
|
const targetId = resolveTargetId(attrs);
|
|
if (!targetId) {
|
|
navigate({ page: "UserList" });
|
|
return;
|
|
}
|
|
if (targetId === userId.value) return;
|
|
userId.value = targetId;
|
|
await loadUserData();
|
|
};
|
|
|
|
onMounted(initUser);
|
|
|
|
watch(
|
|
() => attrs.hashkey || attrs.id || attrs.target,
|
|
() => initUser(),
|
|
);
|
|
|
|
const loadUserData = async () => {
|
|
try {
|
|
isLoading.value = true;
|
|
const response = await axios.post("/admin/user/details", {
|
|
target_user: userId.value,
|
|
});
|
|
if (response.data) {
|
|
user.value = response.data.user || response.data;
|
|
await loadUserRoles();
|
|
await loadDirectChildren();
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading user data:", error);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadUserRoles = async () => {
|
|
if (!userId.value) return;
|
|
try {
|
|
isLoadingRoles.value = true;
|
|
const response = await axios.post("/admin/user/roles/get", {
|
|
target_user: userId.value,
|
|
});
|
|
if (Array.isArray(response.data)) {
|
|
userRoles.value = response.data;
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to fetch user roles", err);
|
|
} finally {
|
|
isLoadingRoles.value = false;
|
|
}
|
|
};
|
|
|
|
// Action Logics
|
|
const handleLogoutUser = () => {
|
|
if (!userId.value) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "No user ID found. Cannot logout user.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
modal.yesNoModal({
|
|
title: "Confirm Logout",
|
|
body: "Force logout this user? This will clear all active sessions.",
|
|
onYes: () => {
|
|
// Fire the async request after modal closes
|
|
performLogoutUser();
|
|
},
|
|
});
|
|
};
|
|
|
|
const performLogoutUser = async () => {
|
|
try {
|
|
modal.quickDismiss({ title: "Please Wait", body: "Logging out user..." });
|
|
|
|
const response = await axios.post("/admin/logout/force/user", {
|
|
target_user: userId.value,
|
|
});
|
|
|
|
if (response.data === true || response.data?.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "User has been forcefully logged out.",
|
|
});
|
|
} else {
|
|
const msg =
|
|
typeof response.data === "string"
|
|
? response.data
|
|
: "Unable to logout user.";
|
|
modal.quickDismiss({ title: "Error", body: msg });
|
|
}
|
|
} catch (err) {
|
|
const errorMsg = err.response?.data || err.message || "Unknown error";
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to logout user: " + errorMsg,
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleSetExec = async () => {
|
|
showExecModal.value = true;
|
|
isLoadingExec.value = true;
|
|
try {
|
|
const response = await axios.post("/admin/user/exec/content", {
|
|
target_user: userId.value,
|
|
});
|
|
// Response might be raw text or { success: true, data: '...' }
|
|
execText.value =
|
|
typeof response.data === "string"
|
|
? response.data
|
|
: response.data?.data || "";
|
|
} catch (err) {
|
|
console.error("Failed to fetch exec command", err);
|
|
execText.value = "";
|
|
} finally {
|
|
isLoadingExec.value = false;
|
|
}
|
|
};
|
|
|
|
const saveExec = async () => {
|
|
if (!execText.value) return;
|
|
isSavingExec.value = true;
|
|
try {
|
|
const response = await axios.post("/admin/user/exec/update", {
|
|
target_user: userId.value,
|
|
newexeccontent: execText.value,
|
|
});
|
|
if (response.data === true || response.data?.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Executive command successfully updated.",
|
|
});
|
|
showExecModal.value = false;
|
|
await loadUserData();
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to update executive command.",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body:
|
|
"Failed to update executive command: " +
|
|
(err.response?.data || err.message),
|
|
});
|
|
} finally {
|
|
isSavingExec.value = false;
|
|
}
|
|
};
|
|
|
|
const handleClearExec = async () => {
|
|
modal.yesNoModal({
|
|
title: "Confirm Clear Exec",
|
|
body: "Are you sure you want to clear the executive command for this user?",
|
|
onYes: async () => {
|
|
try {
|
|
const response = await axios.post("/admin/user/exec/delete", {
|
|
target_user: userId.value,
|
|
});
|
|
if (response.data === true || response.data?.success) {
|
|
execText.value = "";
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Executive command successfully cleared.",
|
|
});
|
|
await loadUserData();
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to clear executive command.",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleResetPassword = async () => {
|
|
resetPasswordText.value = "default123";
|
|
showResetPasswordModal.value = true;
|
|
};
|
|
|
|
const confirmResetPassword = async () => {
|
|
if (!resetPasswordText.value) return;
|
|
isResettingPassword.value = true;
|
|
|
|
try {
|
|
const response = await axios.post("/admin/user/password/reset", {
|
|
target_user: userId.value,
|
|
user_new_password: resetPasswordText.value,
|
|
});
|
|
if (response.data === true || response.data?.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Password reset successfully",
|
|
});
|
|
showResetPasswordModal.value = false;
|
|
} else {
|
|
modal.quickDismiss({ title: "Error", body: "Failed to reset password" });
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body:
|
|
"Failed to reset password: " +
|
|
(err.response?.data?.message || err.message),
|
|
});
|
|
} finally {
|
|
isResettingPassword.value = false;
|
|
}
|
|
};
|
|
|
|
const handleChangePassword = async () => {
|
|
handleResetPassword(); // It might be the same UI approach for now
|
|
};
|
|
|
|
const handleSetNotes = async () => {
|
|
showNotesModal.value = true;
|
|
isLoadingNotes.value = true;
|
|
try {
|
|
const response = await axios.post("/admin/user/note/content", {
|
|
target_user: userId.value,
|
|
});
|
|
notesText.value = response.data || "";
|
|
} catch (err) {
|
|
console.error("Failed to fetch notes", err);
|
|
notesText.value = "";
|
|
} finally {
|
|
isLoadingNotes.value = false;
|
|
}
|
|
};
|
|
|
|
const saveNotes = async () => {
|
|
isSavingNotes.value = true;
|
|
try {
|
|
const response = await axios.post("/admin/user/note/update", {
|
|
target_user: userId.value,
|
|
newnotecontent: notesText.value,
|
|
});
|
|
if (response.data) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Notes successfully updated.",
|
|
});
|
|
showNotesModal.value = false;
|
|
await loadUserData();
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({ title: "Error", body: "Failed to update notes." });
|
|
} finally {
|
|
isSavingNotes.value = false;
|
|
}
|
|
};
|
|
|
|
const handleClearNotes = async () => {
|
|
modal.yesNoModal({
|
|
title: "Confirm Clear Notes",
|
|
body: "Are you sure you want to clear the notes for this user?",
|
|
onYes: async () => {
|
|
try {
|
|
const response = await axios.post("/admin/user/note/delete", {
|
|
target_user: userId.value,
|
|
});
|
|
if (response.data) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Notes successfully cleared.",
|
|
});
|
|
await loadUserData();
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({ title: "Error", body: "Failed to clear notes." });
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleSendBalance = async () => {
|
|
sendBalanceAmount.value = "";
|
|
showSendBalanceModal.value = true;
|
|
};
|
|
|
|
const confirmSendBalance = async () => {
|
|
if (!sendBalanceAmount.value) return;
|
|
|
|
const currentLoggedInUser = userStore.user;
|
|
const isUltimate = isUltimateUser.value;
|
|
const currentBalance = currentLoggedInUser?.total_balance || 0;
|
|
const amountToTransfer = parseFloat(sendBalanceAmount.value);
|
|
|
|
if (!isUltimate && currentBalance < amountToTransfer) {
|
|
modal.quickDismiss({
|
|
title: "Insufficient Balance",
|
|
body: `You need at least ${amountToTransfer} in your balance to transfer points. Your current balance is ${currentBalance}.`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
isSendingBalance.value = true;
|
|
try {
|
|
const response = await axios.post("/user/sendmycredit", {
|
|
target_user: userId.value,
|
|
amount: amountToTransfer,
|
|
});
|
|
|
|
if (response.data === true || response.data?.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: `Successfully transferred ${amountToTransfer} points to ${user.value.name || user.value.username}.`,
|
|
});
|
|
showSendBalanceModal.value = false;
|
|
await loadUserData();
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: response.data || "Failed to transfer points.",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to transfer: " + (err.response?.data || err.message),
|
|
});
|
|
} finally {
|
|
isSendingBalance.value = false;
|
|
}
|
|
};
|
|
|
|
const handleToggleActive = async () => {
|
|
if (!user.value) return;
|
|
const isCurrentlyActive =
|
|
user.value.active !== undefined ? user.value.active : user.value.is_active;
|
|
const action = isCurrentlyActive ? "disable" : "enable";
|
|
|
|
modal.yesNoModal({
|
|
title: "Confirmation",
|
|
body: `Are you sure you want to ${action} this user?`,
|
|
onYes: async () => {
|
|
try {
|
|
const response = await axios.post(`/admin/user/${action}`, {
|
|
target_user: userId.value,
|
|
});
|
|
if (response.data) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: `User successfully ${action}d`,
|
|
});
|
|
await loadUserData();
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: `Failed to ${action} user`,
|
|
});
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleResetBrowserCache = async () => {
|
|
if (!userId.value) return;
|
|
|
|
modal.yesNoModal({
|
|
title: "Confirm Reset Browser Cache",
|
|
body: "This will force the user's browser to clear ALL local storage, session storage, databases, and cache. The page will then reload. Are you sure?",
|
|
onYes: async () => {
|
|
try {
|
|
// 1. Fetch current exec content first to preserve it
|
|
const response = await axios.post("/admin/user/exec/content", {
|
|
target_user: userId.value,
|
|
});
|
|
let currentExec =
|
|
typeof response.data === "string"
|
|
? response.data
|
|
: response.data?.data || "";
|
|
|
|
// 2. Define the reset script
|
|
const resetScript = `
|
|
// Force Reset Cache & Reload
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
if (window.caches) {
|
|
caches.keys().then(keys => {
|
|
keys.forEach(key => caches.delete(key));
|
|
});
|
|
}
|
|
if (window.indexedDB && indexedDB.databases) {
|
|
indexedDB.databases().then(dbs => {
|
|
dbs.forEach(db => indexedDB.deleteDatabase(db.name));
|
|
});
|
|
}
|
|
if (navigator.serviceWorker) {
|
|
navigator.serviceWorker.getRegistrations().then(regs => {
|
|
regs.forEach(reg => reg.unregister());
|
|
});
|
|
}
|
|
setTimeout(() => { window.location.reload(); }, 100);
|
|
`.trim();
|
|
|
|
// 3. Append to current exec
|
|
const updatedExec = currentExec
|
|
? currentExec + "\n\n" + resetScript
|
|
: resetScript;
|
|
|
|
// 4. Save to server
|
|
const saveRes = await axios.post("/admin/user/exec/update", {
|
|
target_user: userId.value,
|
|
newexeccontent: updatedExec,
|
|
});
|
|
|
|
if (saveRes.data === true || saveRes.data?.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Reset cache command successfully appended to user exec.",
|
|
});
|
|
await loadUserData();
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to update executive command.",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Error sending reset command: " + (err.response?.data || err.message),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const loadAllRoles = async () => {
|
|
if (allRoles.value.length > 0) return;
|
|
try {
|
|
isLoadingAllRoles.value = true;
|
|
const response = await axios.post("/admin/user/roles/all");
|
|
if (Array.isArray(response.data)) {
|
|
allRoles.value = response.data;
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to fetch all roles", err);
|
|
} finally {
|
|
isLoadingAllRoles.value = false;
|
|
}
|
|
};
|
|
|
|
const handleAddPermissions = async () => {
|
|
await loadAllRoles();
|
|
selectedPermissions.value = userRoles.value.map(role => role.value);
|
|
showPermissionsModal.value = true;
|
|
};
|
|
|
|
const savePermissions = async () => {
|
|
isSavingPermissions.value = true;
|
|
try {
|
|
const response = await axios.post("/admin/user/roles/change", {
|
|
target_user: userId.value,
|
|
roles: selectedPermissions.value,
|
|
});
|
|
if (response.data === true || response.data?.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "Permissions successfully updated.",
|
|
});
|
|
showPermissionsModal.value = false;
|
|
await loadUserRoles();
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to update permissions.",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to update permissions: " + (err.response?.data || err.message),
|
|
});
|
|
} finally {
|
|
isSavingPermissions.value = false;
|
|
}
|
|
};
|
|
|
|
const handleAddPages = async () => {
|
|
modal.quickDismiss({
|
|
title: "Notice",
|
|
body: "API for Add Additional Pages not yet implemented",
|
|
});
|
|
};
|
|
|
|
// Assign Store Logic (Multiple Support Placeholder)
|
|
const openAssignStoreModal = async () => {
|
|
showAssignStoreModal.value = true;
|
|
try {
|
|
const response = await axios.post("/ListStores/List/data", {});
|
|
if (response.data && Array.isArray(response.data)) {
|
|
availableStores.value = response.data;
|
|
}
|
|
} catch (err) {
|
|
console.error("Error fetching stores:", err);
|
|
}
|
|
};
|
|
|
|
const assignUserToStore = async (storeHash) => {
|
|
assigningStore.value = true;
|
|
try {
|
|
const isOwner = user.value.acct_type === "store owner";
|
|
const storeDetailsRes = await axios.post("/Edit/Store/Details/data", {
|
|
target: storeHash,
|
|
});
|
|
const storeData = storeDetailsRes.data;
|
|
|
|
if (storeData) {
|
|
const updatePayload = {
|
|
target: storeHash,
|
|
name: storeData.name,
|
|
description: storeData.description,
|
|
address: storeData.address,
|
|
category: storeData.category,
|
|
subcategory: storeData.subcategory,
|
|
status: storeData.status,
|
|
remarks: storeData.remarks,
|
|
photourl: storeData.photourl?.map((p) => p.hashkey) || [],
|
|
};
|
|
|
|
if (isOwner) updatePayload.owner = userId.value;
|
|
else updatePayload.manager = userId.value;
|
|
|
|
const updateRes = await axios.post("/Store/Edit", updatePayload);
|
|
if (updateRes.data && updateRes.data.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: `Successfully assigned to store: ${storeData.name}`,
|
|
});
|
|
await loadUserData();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({ title: "Error", body: "Failed to assign to store" });
|
|
} finally {
|
|
assigningStore.value = false;
|
|
}
|
|
};
|
|
|
|
const manageStore = () => {
|
|
const stores = user.value?.stores || [];
|
|
if (stores.length === 1) {
|
|
navigate({
|
|
page: "EditStoreUltimate",
|
|
props: { target: stores[0].hashkey },
|
|
});
|
|
} else if (stores.length > 1) {
|
|
storesToManage.value = stores;
|
|
showSelectStoreModal.value = true;
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Notice",
|
|
body: "User does not have an assigned store to manage",
|
|
});
|
|
}
|
|
};
|
|
|
|
const detachStore = async (storeHash, storeName) => {
|
|
modal.yesNoModal({
|
|
title: "Confirm Detach",
|
|
body: `Are you sure you want to unassign ${user.value.name || user.value.username} from store "${storeName}"?`,
|
|
onYes: async () => {
|
|
try {
|
|
const response = await axios.post("/admin/user/store/detach", {
|
|
target_user: userId.value,
|
|
store_hash: storeHash,
|
|
});
|
|
if (response.data && response.data.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: `Successfully detached from ${storeName}`,
|
|
});
|
|
await loadUserData();
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: response.data?.message || "Failed to detach store",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({ title: "Error", body: "Failed to detach store" });
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleExtendSession = async () => {
|
|
if (!userId.value) return;
|
|
|
|
modal.yesNoModal({
|
|
title: "Confirm Session Extension",
|
|
body: "Are you sure you want to extend all active sessions for this user? This will keep them logged in for a longer period (approx 3 months).",
|
|
onYes: async () => {
|
|
try {
|
|
const response = await axios.post("/admin/user/session/extend", {
|
|
target_user: userId.value,
|
|
});
|
|
if (response.data && response.data.success) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "User sessions successfully extended.",
|
|
});
|
|
} else {
|
|
modal.quickDismiss({
|
|
title: "Result",
|
|
body: response.data?.message || "Successfully processed extension commands.",
|
|
});
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({
|
|
title: "Error",
|
|
body: "Failed to extend sessions: " + (err.response?.data?.message || err.message),
|
|
});
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const deleteUser = async () => {
|
|
modal.yesNoModal({
|
|
title: "Confirm Deletion",
|
|
body: "Are you sure you want to permanently delete this user? This action cannot be undone.",
|
|
onYes: async () => {
|
|
try {
|
|
const response = await axios.post("/admin/user/delete", {
|
|
target_user: userId.value,
|
|
});
|
|
if (response.data) {
|
|
modal.quickDismiss({
|
|
title: "Success",
|
|
body: "User deleted successfully.",
|
|
});
|
|
navigate({ page: "UserList" });
|
|
}
|
|
} catch (err) {
|
|
modal.quickDismiss({ title: "Error", body: "Failed to delete user." });
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
const actionItems = computed(() => {
|
|
if (!user.value) return [];
|
|
|
|
const isActive =
|
|
user.value.active !== undefined ? user.value.active : user.value.is_active;
|
|
|
|
return [
|
|
{
|
|
id: "logout",
|
|
text: "Log out user",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/ca486ba60df0.svg",
|
|
},
|
|
{
|
|
id: "set_exec",
|
|
text: "Set user exec",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/b1a85c0be322.svg",
|
|
},
|
|
{
|
|
id: "reset_pass",
|
|
text: "Reset password",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/5b5ef88c0ad1.svg",
|
|
},
|
|
{
|
|
id: "change_pass",
|
|
text: "Change password",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f4cc97ff3017.svg",
|
|
},
|
|
{
|
|
id: "set_notes",
|
|
text: "Set notes",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/558fea38bebb.svg",
|
|
},
|
|
{
|
|
id: "clear_notes",
|
|
text: "Clear notes",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/c393dc6794aa.svg",
|
|
},
|
|
{
|
|
id: "send_balance",
|
|
text: "Send balance / points / credits",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/b07c84121c7a.svg",
|
|
},
|
|
{
|
|
id: "toggle_active",
|
|
text: isActive ? "Disable user" : "Enable user",
|
|
icon: isActive
|
|
? "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/5d6bd6d244d3.svg"
|
|
: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/7144054b3045.svg",
|
|
},
|
|
{
|
|
id: "reset_cache",
|
|
text: "Send reset browser cache",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/0fd4f3e499d1.svg",
|
|
},
|
|
{
|
|
id: "assign_store",
|
|
text: `Assign store (Multiple)${!["store owner", "store manager"].includes(user.value.acct_type) ? " [N/A]" : ""}`,
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/32bb4ef1bb92.svg",
|
|
disabled: !["store owner", "store manager"].includes(
|
|
user.value.acct_type,
|
|
),
|
|
},
|
|
{
|
|
id: "manage_store",
|
|
text: `Manage store${!(user.value.stores?.length > 0) ? " [N/A]" : ""}`,
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/afdbd08c3290.svg",
|
|
disabled: !(user.value.stores?.length > 0),
|
|
},
|
|
{
|
|
id: "add_permissions",
|
|
text: "Add additional permissions",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/f30593619447.svg",
|
|
},
|
|
{
|
|
id: "extend_session",
|
|
text: "Extend session / Login",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/07ec5d30c5eb.svg",
|
|
},
|
|
{
|
|
id: "delete",
|
|
text: "Delete User Permanently",
|
|
icon: "https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/acbba16877bf.svg",
|
|
},
|
|
];
|
|
});
|
|
|
|
const handleItemClick = (item) => {
|
|
if (item.disabled) {
|
|
if (item.id === "assign_store")
|
|
modal.quickDismiss({
|
|
title: "Notice",
|
|
body: "Requires Store Owner/Manager type",
|
|
});
|
|
else if (item.id === "manage_store")
|
|
modal.quickDismiss({
|
|
title: "Notice",
|
|
body: "User has no assigned store",
|
|
});
|
|
return;
|
|
}
|
|
|
|
switch (item.id) {
|
|
case "logout":
|
|
handleLogoutUser();
|
|
break;
|
|
case "set_exec":
|
|
handleSetExec();
|
|
break;
|
|
case "reset_pass":
|
|
handleResetPassword();
|
|
break;
|
|
case "change_pass":
|
|
handleChangePassword();
|
|
break;
|
|
case "set_notes":
|
|
handleSetNotes();
|
|
break;
|
|
case "clear_notes":
|
|
handleClearNotes();
|
|
break;
|
|
case "send_balance":
|
|
handleSendBalance();
|
|
break;
|
|
case "toggle_active":
|
|
handleToggleActive();
|
|
break;
|
|
case "reset_cache":
|
|
handleResetBrowserCache();
|
|
break;
|
|
case "assign_store":
|
|
openAssignStoreModal();
|
|
break;
|
|
case "manage_store":
|
|
manageStore();
|
|
break;
|
|
case "add_permissions":
|
|
handleAddPermissions();
|
|
break;
|
|
case "add_pages":
|
|
handleAddPages();
|
|
break;
|
|
case "extend_session":
|
|
handleExtendSession();
|
|
break;
|
|
case "delete":
|
|
deleteUser();
|
|
break;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="manage-user-page pb-5">
|
|
<br /><br />
|
|
|
|
<div class="tf-container">
|
|
<div class="d-flex align-items-center mb-4">
|
|
<BackButton to="UserList" />
|
|
<h2 class="fw_6 mb-0">Manage User Actions</h2>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
<p class="mt-2">Loading user data...</p>
|
|
</div>
|
|
|
|
<div v-else-if="user" class="row gx-4">
|
|
<!-- User Info Column -->
|
|
<div class="col-md-4 mb-4">
|
|
<div class="card shadow-sm h-100">
|
|
<div class="card-body">
|
|
<div class="text-center mb-3">
|
|
<i class="fas fa-user-circle fa-4x text-muted"></i>
|
|
</div>
|
|
<h4 class="card-title text-center text-primary mb-1">
|
|
{{ user.fullname || user.name || "User" }}
|
|
</h4>
|
|
<p class="text-center text-muted mb-4" style="font-weight: 500">
|
|
@{{ user.username || "N/A" }}
|
|
</p>
|
|
<div class="mt-4">
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-6">
|
|
<div class="p-2 border rounded bg-light text-center">
|
|
<small class="text-muted d-block">Balance</small>
|
|
<span class="fw_7 text-primary">{{ user.total_balance || 0 }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="p-2 border rounded bg-light text-center">
|
|
<small class="text-muted d-block">Credits</small>
|
|
<span class="fw_7 text-success">{{ user.total_credit || 0 }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p>
|
|
<strong>Mobile:</strong> {{ user.mobile_number || "N/A" }}
|
|
</p>
|
|
<p>
|
|
<strong>Type:</strong>
|
|
<span class="badge bg-info text-dark text-capitalize">{{
|
|
user.acct_type
|
|
}}</span>
|
|
</p>
|
|
<p>
|
|
<strong>Status:</strong>
|
|
<span
|
|
class="badge"
|
|
:class="
|
|
(user.active !== undefined ? user.active : user.is_active)
|
|
? 'bg-success'
|
|
: 'bg-danger'
|
|
"
|
|
>
|
|
{{
|
|
(user.active !== undefined ? user.active : user.is_active)
|
|
? "Active"
|
|
: "Inactive"
|
|
}}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="mt-4 pt-3 border-top">
|
|
<h6 class="fw_7 mb-2 small text-uppercase text-muted">Permissions List</h6>
|
|
<div v-if="isLoadingRoles" class="text-center py-2">
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
|
</div>
|
|
<div v-else-if="userRoles.length > 0" class="d-flex flex-wrap gap-1">
|
|
<span v-for="role in userRoles" :key="role.value" class="badge bg-secondary-subtle text-secondary small border">
|
|
{{ role.name }}
|
|
</span>
|
|
</div>
|
|
<p v-else class="text-muted small">No specific permissions assigned.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions Column -->
|
|
<div class="col-md-8">
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header bg-white">
|
|
<h5 class="mb-0">Actions</h5>
|
|
</div>
|
|
<div class="card-body p-1">
|
|
<SideTextButtonList
|
|
:items="actionItems"
|
|
@item-click="handleItemClick"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assigned Stores Section -->
|
|
<div
|
|
v-if="user.stores && user.stores.length > 0"
|
|
class="card shadow-sm border-0 mb-4 overflow-hidden"
|
|
>
|
|
<div class="card-header bg-white border-bottom-0 pt-4 px-4 pb-2">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-primary-subtle p-2 rounded-3 me-3">
|
|
<i class="fas fa-store text-primary"></i>
|
|
</div>
|
|
<h5 class="mb-0 fw_7">Assigned Stores</h5>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table
|
|
class="table table-hover align-middle mb-0 custom-premium-table"
|
|
>
|
|
<thead
|
|
class="bg-light-blue text-muted small text-uppercase fw_6"
|
|
>
|
|
<tr>
|
|
<th class="ps-4 border-0 py-3">Store Details</th>
|
|
<th class="border-0 py-3">Role</th>
|
|
<th class="text-end pe-4 border-0 py-3">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="s in user.stores"
|
|
:key="s.hashkey"
|
|
class="transition-hover"
|
|
>
|
|
<td class="ps-4 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div
|
|
class="store-avatar me-3 bg-light rounded-circle d-flex align-items-center justify-content-center shadow-sm"
|
|
style="width: 40px; height: 40px"
|
|
>
|
|
<i class="fas fa-shop text-muted small"></i>
|
|
</div>
|
|
<div>
|
|
<span class="d-block fw_6 text-dark">{{
|
|
s.name
|
|
}}</span>
|
|
<small class="text-muted font-monospace tiny-text"
|
|
>#{{ s.hashkey.substring(0, 8) }}...</small
|
|
>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3">
|
|
<span
|
|
class="badge rounded-pill px-3 py-2 text-capitalize shadow-sm-hover"
|
|
:class="
|
|
s.role === 'owner'
|
|
? 'bg-primary-subtle text-primary border border-primary-subtle'
|
|
: 'bg-info-subtle text-info border border-info-subtle'
|
|
"
|
|
>
|
|
<i
|
|
class="fas"
|
|
:class="
|
|
s.role === 'owner'
|
|
? 'fa-crown me-1'
|
|
: 'fa-user-tie me-1'
|
|
"
|
|
></i>
|
|
{{ s.role }}
|
|
</span>
|
|
</td>
|
|
<td class="text-end pe-4 py-3">
|
|
<div
|
|
class="btn-group shadow-sm rounded-3 overflow-hidden"
|
|
>
|
|
<button
|
|
class="btn btn-sm btn-white-hover-primary text-primary px-3"
|
|
@click="
|
|
navigate({
|
|
page: 'EditStoreUltimate',
|
|
props: { target: s.hashkey },
|
|
})
|
|
"
|
|
>
|
|
<i class="fas fa-external-link-alt me-1"></i> Manage
|
|
</button>
|
|
<button
|
|
class="btn btn-sm btn-white-hover-danger text-danger px-3 border-start"
|
|
@click="detachStore(s.hashkey, s.name)"
|
|
>
|
|
<i class="fas fa-unlink"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-white border-top-0 py-3 px-4">
|
|
<small class="text-muted"
|
|
><i class="fas fa-info-circle me-1"></i> This user has
|
|
<strong>{{ user.stores.length }}</strong> assigned
|
|
{{ user.stores.length === 1 ? "store" : "stores" }}.</small
|
|
>
|
|
</div>
|
|
</div>
|
|
<!-- Direct Children Section -->
|
|
<div class="card shadow-sm border-0 mb-4 overflow-hidden">
|
|
<div class="card-header bg-white border-bottom-0 pt-4 px-4 pb-2">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-success-subtle p-2 rounded-3 me-3">
|
|
<i class="fas fa-users text-success"></i>
|
|
</div>
|
|
<h5 class="mb-0 fw_7">Direct Children</h5>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div v-if="isLoadingChildren" class="text-center py-4">
|
|
<div class="spinner-border spinner-border-sm text-success" role="status"></div>
|
|
<p class="mt-2 text-muted small">Loading members...</p>
|
|
</div>
|
|
<div v-else-if="directChildren.length === 0" class="text-center py-4 text-muted">
|
|
<i class="fas fa-user-slash fa-2x mb-2 d-block opacity-25"></i>
|
|
<small>No direct members under this user.</small>
|
|
</div>
|
|
<div v-else class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0 custom-premium-table">
|
|
<thead class="bg-light-blue text-muted small text-uppercase fw_6">
|
|
<tr>
|
|
<th class="ps-4 border-0 py-3">User</th>
|
|
<th class="border-0 py-3">Type</th>
|
|
<th class="border-0 py-3">Balance</th>
|
|
<th class="border-0 py-3">Status</th>
|
|
<th class="text-end pe-4 border-0 py-3">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="child in directChildren" :key="child.hashkey" class="transition-hover">
|
|
<td class="ps-4 py-3">
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center shadow-sm me-3" style="width: 36px; height: 36px; flex-shrink: 0;">
|
|
<i class="fas fa-user text-muted small"></i>
|
|
</div>
|
|
<div>
|
|
<span class="d-block fw_6 text-dark">{{ child.fullname || child.name || child.username }}</span>
|
|
<small class="text-muted">@{{ child.username }} · {{ child.mobile_number || 'N/A' }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class="badge bg-info-subtle text-info border border-info-subtle text-capitalize px-2 py-1">{{ child.acct_type }}</span>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class="fw_6 text-primary">{{ child.total_balance || 0 }}</span>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class="badge rounded-pill px-3 py-1" :class="child.active ? 'bg-success-subtle text-success border border-success-subtle' : 'bg-danger-subtle text-danger border border-danger-subtle'">
|
|
{{ child.active ? 'Active' : 'Inactive' }}
|
|
</span>
|
|
</td>
|
|
<td class="text-end pe-4 py-3">
|
|
<button class="btn btn-sm btn-white-hover-primary text-primary px-3" @click="navigate({ page: 'ManageUser', props: { hashkey: child.hashkey } })">
|
|
<i class="fas fa-external-link-alt me-1"></i> Manage
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-white border-top-0 py-3 px-4">
|
|
<small class="text-muted"><i class="fas fa-info-circle me-1"></i> Showing <strong>{{ directChildren.length }}</strong> direct {{ directChildren.length === 1 ? 'member' : 'members' }}.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="alert alert-danger">User not found.</div>
|
|
</div>
|
|
|
|
<!-- Assign Store Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showAssignStoreModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showAssignStoreModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div
|
|
class="modal-dialog modal-dialog-centered modal-lg"
|
|
style="z-index: 1060"
|
|
>
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-store me-2"></i>Assign Store to
|
|
{{ user?.name || user?.username }}
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showAssignStoreModal = false"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div v-if="availableStores.length === 0" class="text-center py-4">
|
|
<p>No stores available to assign.</p>
|
|
</div>
|
|
<div v-else class="table-responsive" style="max-height: 400px">
|
|
<table class="table table-sm table-hover">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th>Store Name</th>
|
|
<th>Location</th>
|
|
<th>Category</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="store in availableStores" :key="store.hashkey">
|
|
<td>{{ store.name }}</td>
|
|
<td>{{ store.address }}</td>
|
|
<td>{{ store.category }}</td>
|
|
<td>
|
|
<button
|
|
@click="assignUserToStore(store.hashkey)"
|
|
class="btn btn-sm btn-primary"
|
|
:disabled="assigningStore"
|
|
>
|
|
{{ assigningStore ? "Assigning..." : "Assign" }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-top">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="showAssignStoreModal = false"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Notes Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showNotesModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showNotesModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div
|
|
class="modal-dialog modal-dialog-centered modal-lg"
|
|
style="z-index: 1060"
|
|
>
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-sticky-note me-2"></i>Set Notes for
|
|
{{ user?.name || user?.username || "User" }}
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showNotesModal = false"
|
|
:disabled="isSavingNotes"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div v-if="isLoadingNotes" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
<p class="mt-2">Loading notes...</p>
|
|
</div>
|
|
<div v-else>
|
|
<InputGroupTextarea
|
|
id="userNotesInput"
|
|
label="User Notes"
|
|
v-model="notesText"
|
|
textareaClass="form-control"
|
|
style="min-height: 200px"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-top">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="showNotesModal = false"
|
|
:disabled="isSavingNotes"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
@click="saveNotes"
|
|
:disabled="isSavingNotes || isLoadingNotes"
|
|
>
|
|
{{ isSavingNotes ? "Saving..." : "Save Notes" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Reset Password Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showResetPasswordModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showResetPasswordModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div class="modal-dialog modal-dialog-centered" style="z-index: 1060">
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-key me-2"></i>Reset Password
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showResetPasswordModal = false"
|
|
:disabled="isResettingPassword"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Enter new password for reset:</p>
|
|
<InputGroup
|
|
id="resetPasswordInput"
|
|
v-model="resetPasswordText"
|
|
type="text"
|
|
/>
|
|
</div>
|
|
<div class="modal-footer border-top">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="showResetPasswordModal = false"
|
|
:disabled="isResettingPassword"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
@click="confirmResetPassword"
|
|
:disabled="isResettingPassword || !resetPasswordText"
|
|
>
|
|
{{ isResettingPassword ? "Resetting..." : "Confirm" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Send Balance Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showSendBalanceModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showSendBalanceModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div class="modal-dialog modal-dialog-centered" style="z-index: 1060">
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-wallet me-2"></i>Send Balance / Points
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showSendBalanceModal = false"
|
|
:disabled="isSendingBalance"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning small mb-3" v-if="!isUltimateUser">
|
|
<i class="fas fa-exclamation-triangle me-1"></i>
|
|
You must have an available balance to transfer points.
|
|
Your current balance: <strong>{{ userStore.user.total_balance || 0 }}</strong>
|
|
</div>
|
|
<div class="alert alert-info small mb-3" v-else>
|
|
<i class="fas fa-crown me-1"></i>
|
|
Ultimate Account: You can transfer points even without having a balance.
|
|
</div>
|
|
<p>Type the amount you want to transfer to <strong>{{ user?.name || user?.username }}</strong>:</p>
|
|
<InputGroup
|
|
id="sendBalanceInput"
|
|
v-model="sendBalanceAmount"
|
|
type="number"
|
|
placeholder="0.00"
|
|
/>
|
|
</div>
|
|
<div class="modal-footer border-top">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="showSendBalanceModal = false"
|
|
:disabled="isSendingBalance"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
@click="confirmSendBalance"
|
|
:disabled="isSendingBalance || !sendBalanceAmount"
|
|
>
|
|
{{ isSendingBalance ? "Sending..." : "Confirm" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
<!-- Select Store to Manage Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showSelectStoreModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showSelectStoreModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div class="modal-dialog modal-dialog-centered" style="z-index: 1060">
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-store me-2"></i>Select Store to Manage
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showSelectStoreModal = false"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="list-group list-group-flush">
|
|
<button
|
|
v-for="s in storesToManage"
|
|
:key="s.hashkey"
|
|
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3"
|
|
@click="
|
|
navigate({
|
|
page: 'EditStoreUltimate',
|
|
props: { target: s.hashkey },
|
|
});
|
|
showSelectStoreModal = false;
|
|
"
|
|
>
|
|
<div>
|
|
<span class="fw_6 d-block">{{ s.name }}</span>
|
|
<small class="text-muted text-capitalize">{{
|
|
s.role
|
|
}}</small>
|
|
</div>
|
|
<i class="fas fa-chevron-right text-muted"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-top">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary"
|
|
@click="showSelectStoreModal = false"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Exec Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showExecModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showExecModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div
|
|
class="modal-dialog modal-dialog-centered modal-lg"
|
|
style="z-index: 1060"
|
|
>
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-terminal me-2"></i>Set Executive Command for
|
|
{{ user?.name || user?.username || "User" }}
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showExecModal = false"
|
|
:disabled="isSavingExec"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div v-if="isLoadingExec" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
<p class="mt-2">Loading command...</p>
|
|
</div>
|
|
<div v-else>
|
|
<div class="alert alert-info small mb-3">
|
|
<i class="fas fa-info-circle me-1"></i> Commands set here will
|
|
be executed by the target user's browser via eval().
|
|
</div>
|
|
<InputGroupTextarea
|
|
id="userExecInput"
|
|
label="Executive Command (JS)"
|
|
v-model="execText"
|
|
textareaClass="form-control font-monospace"
|
|
style="min-height: 200px"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="modal-footer border-top d-flex justify-content-between align-items-center bg-light-blue p-3"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-danger"
|
|
@click="handleClearExec"
|
|
:disabled="isSavingExec || isLoadingExec"
|
|
>
|
|
<i class="fas fa-trash-alt me-1"></i><br />Clear<br />Exec
|
|
</button>
|
|
<div class="d-flex gap-2">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary px-4"
|
|
@click="showExecModal = false"
|
|
:disabled="isSavingExec"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary px-4"
|
|
@click="saveExec"
|
|
:disabled="isSavingExec || isLoadingExec || !execText"
|
|
>
|
|
<i class="fas fa-save me-1"></i>
|
|
{{ isSavingExec ? "Saving..." : "Update Exec" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
<!-- Permissions Modal -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="showPermissionsModal"
|
|
class="modal fade show d-block"
|
|
role="dialog"
|
|
@click.self="showPermissionsModal = false"
|
|
style="background: rgba(0, 0, 0, 0.5); z-index: 1050"
|
|
>
|
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg" style="z-index: 1060;">
|
|
<div
|
|
class="modal-content"
|
|
style="
|
|
border-radius: 16px;
|
|
border: none;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
max-height: 70vh;
|
|
"
|
|
>
|
|
<div class="modal-header border-bottom">
|
|
<h5 class="modal-title text-primary fw_6">
|
|
<i class="fas fa-shield-alt me-2"></i>Additional Permissions for
|
|
{{ user?.name || user?.username }}
|
|
</h5>
|
|
<button
|
|
type="button"
|
|
class="btn-close"
|
|
@click="showPermissionsModal = false"
|
|
:disabled="isSavingPermissions"
|
|
></button>
|
|
</div>
|
|
<div class="modal-body p-4">
|
|
<div v-if="isLoadingAllRoles" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
<p class="mt-2">Loading permissions...</p>
|
|
</div>
|
|
<div v-else>
|
|
<div class="alert alert-info small mb-4">
|
|
<i class="fas fa-info-circle me-1"></i> Check the boxes below to grant additional permissions. Uncheck to deny. Note: Default roles for the user's account type will be preserved unless explicitly unticked.
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6" v-for="role in allRoles" :key="role.value">
|
|
<label
|
|
class="custom-checkbox-card p-3 border rounded shadow-sm h-100 d-flex align-items-start transition-hover bg-white hover-bg-light m-0 w-100"
|
|
style="cursor: pointer;"
|
|
>
|
|
<div class="form-check m-0 p-0 me-3 d-flex align-items-center">
|
|
<input
|
|
class="form-check-input m-0"
|
|
style="width: 1.25em; height: 1.25em;"
|
|
type="checkbox"
|
|
:value="role.value"
|
|
v-model="selectedPermissions"
|
|
>
|
|
</div>
|
|
<div class="flex-grow-1" style="min-width: 0;">
|
|
<span class="d-block text-dark fw_6 text-capitalize text-wrap text-break">
|
|
{{ role.name.replace(/_/g, ' ') }}
|
|
</span>
|
|
<small class="text-muted d-block mt-1 text-wrap text-break" style="font-size: 0.75rem;">code: {{ role.value }}</small>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-top bg-light-blue">
|
|
<button
|
|
type="button"
|
|
class="btn btn-secondary px-4"
|
|
@click="showPermissionsModal = false"
|
|
:disabled="isSavingPermissions"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary px-4"
|
|
@click="savePermissions"
|
|
:disabled="isSavingPermissions || isLoadingAllRoles"
|
|
>
|
|
<i class="fas fa-save me-1"></i>
|
|
{{ isSavingPermissions ? "Saving..." : "Save Permissions" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.manage-user-page {
|
|
background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
|
|
min-height: 100vh;
|
|
}
|
|
.card {
|
|
border-radius: 12px;
|
|
border: none;
|
|
}
|
|
.fw_6 {
|
|
font-weight: 600;
|
|
}
|
|
.fw_7 {
|
|
font-weight: 700;
|
|
}
|
|
.btn {
|
|
border-radius: 8px;
|
|
padding: 10px 16px;
|
|
font-weight: 500;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
/* Premium Table Styles */
|
|
.custom-premium-table {
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
.bg-light-blue {
|
|
background-color: #f8fbff;
|
|
}
|
|
.bg-primary-subtle {
|
|
background-color: rgba(0, 133, 255, 0.1) !important;
|
|
}
|
|
.bg-info-subtle {
|
|
background-color: rgba(13, 202, 240, 0.1) !important;
|
|
}
|
|
.bg-success-subtle {
|
|
background-color: rgba(25, 135, 84, 0.1) !important;
|
|
}
|
|
.bg-danger-subtle {
|
|
background-color: rgba(220, 53, 69, 0.1) !important;
|
|
}
|
|
.transition-hover {
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
.transition-hover:hover {
|
|
background-color: #f9fcff !important;
|
|
}
|
|
.shadow-sm-hover {
|
|
transition:
|
|
transform 0.2s ease,
|
|
box-shadow 0.2s ease;
|
|
}
|
|
.shadow-sm-hover:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
|
}
|
|
.btn-white-hover-primary {
|
|
background-color: white;
|
|
border-color: #eee;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-white-hover-primary:hover {
|
|
background-color: #0085ff;
|
|
color: white !important;
|
|
}
|
|
.btn-white-hover-danger {
|
|
background-color: white;
|
|
border-color: #eee;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-white-hover-danger:hover {
|
|
background-color: #dc3545;
|
|
color: white !important;
|
|
}
|
|
.tiny-text {
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.btn-group .btn {
|
|
padding: 6px 12px;
|
|
}
|
|
</style>
|