Files
lucia-frontend/src/views/AccountManagement/AccountAdmin/AccountAdmin.vue
2026-03-08 12:11:57 +08:00

466 lines
16 KiB
Vue

<template>
<div class="flex justify-center pt-2">
<div class="flex w-[1216px] flex-col items-center">
<div class="flex w-full justify-between py-2 items-center">
<button
id="create_new_acct_btn"
class="flex rounded-full bg-primary text-[#ffffff] flex justify-center items-center px-6 py-[10px]"
@click="onCreateNewClick"
>
{{ i18next.t("AcctMgmt.CreateNew") }}
</button>
<SearchBar
@on-search-account-button-click="onSearchAccountButtonClick"
/>
</div>
<div
id="acct_mgmt_data_grid"
class="flex w-full overflow-y-auto h-[570px]"
@scroll="handleScroll"
>
<DataTable
:value="accountSearchResults"
dataKey="username"
tableClass="w-full mt-4 text-sm relative table-fixed"
:rowClass="getRowClass"
>
<Column
field="username"
:header="i18next.t('AcctMgmt.Account')"
bodyClass="font-medium"
sortable
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<div
@dblclick="onAcctDoubleClick(slotProps.data.username)"
class="account-cell cursor-pointer whitespace-nowrap overflow-hidden text-ellipsis"
>
{{ slotProps.data.username }}
</div>
<span
id="just_create_badge"
class="flex ml-4"
v-if="
isOneAccountJustCreate &&
slotProps.data.username === justCreateUsername
"
>
<img src="@/assets/icon-new.svg" alt="New" />
</span>
</div>
</template>
</Column>
<Column
field="name"
:header="i18next.t('AcctMgmt.FullName')"
bodyClass="text-neutral-500"
sortable
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<div
class="fullname-cell whitespace-nowrap overflow-hidden text-ellipsis"
>
{{ slotProps.data.name }}
</div>
</div>
</template>
</Column>
<Column
field="is_admin"
:header="i18next.t('AcctMgmt.AdminRights')"
bodyClass="text-neutral-500 flex justify-center"
headerClass="header-center"
>
<template #body="slotProps">
<div
v-if="!slotProps.data.isCurrentLoggedIn"
class="row-container flex-w-full-hoverable flex w-full justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<img
v-if="slotProps.data.is_admin"
src="@/assets/radioOn.svg"
alt="Radio On"
class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, false)"
/>
<img
v-else
src="@/assets/radioOff.svg"
alt="Radio Off"
class="btn-admin cursor-pointer flex justify-center"
@click="onAdminInputClick(slotProps.data, true)"
/>
</div>
<div
v-else
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"
></div>
</template>
</Column>
<Column
field="is_active"
:header="i18next.t('AcctMgmt.AccountActivation')"
bodyClass="text-neutral-500"
headerClass="header-center"
>
<template #body="slotProps">
<div
v-if="!slotProps.data.isCurrentLoggedIn"
class="row-container flex-w-full-hoverable w-full"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<div class="w-full flex justify-center">
<img
v-if="slotProps.data.is_active"
src="@/assets/radioOn.svg"
alt="Radio On"
class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, false)"
/>
<img
v-else
src="@/assets/radioOff.svg"
alt="Radio Off"
class="btn-activate cursor-pointer flex"
@click="setIsActiveInput(slotProps.data, true)"
/>
</div>
</div>
<div
v-else
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"
></div>
</template>
</Column>
<Column
:header="i18next.t('AcctMgmt.Detail')"
bodyClass="text-neutral-500"
headerClass="header-center"
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full flex justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
>
<img
:src="
slotProps.data.isDetailHovered
? iconDetailOn
: iconDetailOff
"
alt="Detail"
class="btn-detail cursor-pointer"
@click="onDetailBtnClick(slotProps.data.username)"
@mouseover="handleDetailMouseOver(slotProps.data.username)"
@mouseout="handleDetailMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column
:header="i18next.t('AcctMgmt.Edit')"
bodyClass="text-neutral-500"
headerClass="header-center"
>
<template #body="slotProps">
<div
class="row-container flex-w-full-hoverable w-full flex justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
>
<img
:src="slotProps.data.isEditHovered ? iconEditOn : iconEditOff"
alt="Edit"
class="btn-edit cursor-pointer"
@click="onEditButtonClick(slotProps.data.username)"
@mouseover="handleEditMouseOver(slotProps.data.username)"
@mouseout="handleEditMouseOut(slotProps.data.username)"
/>
</div>
</template>
</Column>
<Column
:header="i18next.t('AcctMgmt.Delete')"
bodyClass="text-neutral-500 flex justify-center"
headerClass="header-center"
>
<template #body="slotProps">
<div
v-if="!slotProps.data.isCurrentLoggedIn"
class="row-container flex-w-full-hoverable w-full flex justify-center"
@mouseenter="handleRowMouseOver(slotProps.data.username)"
>
<img
:src="
slotProps.data.isDeleteHovered
? iconDeleteRed
: iconDeleteGray
"
:alt="
slotProps.data.isDeleteHovered ? 'hovered' : 'not-hovered'
"
class="delete-account cursor-pointer flex"
@mouseover="handleDeleteMouseOver(slotProps.data.username)"
@mouseout="handleDeleteMouseOut(slotProps.data.username)"
@click="onDeleteBtnClick(slotProps.data.username)"
/>
</div>
<div
v-else
@mouseenter="handleRowMouseOver(slotProps.data.username)"
@mouseout="handleRowMouseOut(slotProps.data.username)"
class="row-container flex-w-full-hoverable flex w-full h-6"
></div>
</template>
</Column>
</DataTable>
</div>
</div>
</div>
</template>
<script setup>
// The Lucia project.
// Copyright 2024-2026 DSP, inc. All rights reserved.
// Authors:
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/**
* @module views/AccountManagement/AccountAdmin Account
* administration page with user list, search, create/edit/
* delete actions, admin toggle, and infinite scroll.
*/
import { ref, computed, onMounted, watch } from "vue";
import { useLoadingStore } from "@/stores/loading";
import { useModalStore } from "@/stores/modal";
import { useAcctMgmtStore } from "@/stores/acctMgmt";
import { useLoginStore } from "@/stores/login";
import SearchBar from "../../../components/AccountMenu/SearchBar.vue";
import i18next from "@/i18n/i18n.js";
import { useToast } from "vue-toast-notification";
import {
MODAL_CREATE_NEW,
MODAL_ACCT_EDIT,
MODAL_ACCT_INFO,
MODAL_DELETE,
ONCE_RENDER_NUM_OF_DATA,
} from "@/constants/constants.js";
import iconDeleteGray from "@/assets/icon-delete-gray.svg";
import iconDeleteRed from "@/assets/icon-delete-red.svg";
import iconEditOff from "@/assets/icon-edit-off.svg";
import iconEditOn from "@/assets/icon-edit-on.svg";
import iconDetailOn from "@/assets/icon-detail-on.svg";
import iconDetailOff from "@/assets/icon-detail-card.svg";
const toast = useToast();
const acctMgmtStore = useAcctMgmtStore();
const loadingStore = useLoadingStore();
const modalStore = useModalStore();
const loginStore = useLoginStore();
const infiniteStart = ref(0);
const shouldUpdateList = computed(() => acctMgmtStore.shouldUpdateList);
const allAccountResponsive = computed(() => acctMgmtStore.allUserAccountList);
const infiniteAcctData = computed(() =>
allAccountResponsive.value.slice(
0,
infiniteStart.value + ONCE_RENDER_NUM_OF_DATA,
),
);
const loginUserData = ref(null);
const isOneAccountJustCreate = computed(
() => acctMgmtStore.isOneAccountJustCreate,
);
const justCreateUsername = computed(() => acctMgmtStore.justCreateUsername);
const inputQuery = ref("");
const fetchLoginUserData = async () => {
await loginStore.getUserData();
loginUserData.value = loginStore.userData;
};
const moveJustCreateUserToFirstRow = () => {
if (infiniteAcctData.value && infiniteAcctData.value.length) {
const index = acctMgmtStore.allUserAccountList.findIndex(
(user) => user.username === acctMgmtStore.justCreateUsername,
);
if (index !== -1) {
const [justCreateUser] = acctMgmtStore.allUserAccountList[index];
infiniteAcctData.value.unshift(justCreateUser);
}
}
};
const accountSearchResults = computed(() => {
if (!inputQuery.value) {
return infiniteAcctData.value;
}
return acctMgmtStore.allUserAccountList.filter((user) =>
user.username.includes(inputQuery.value),
);
});
const onCreateNewClick = () => {
acctMgmtStore.clearCurrentViewingUser();
modalStore.openModal(MODAL_CREATE_NEW);
};
const onAcctDoubleClick = (username) => {
acctMgmtStore.setCurrentViewingUser(username);
modalStore.openModal(MODAL_ACCT_INFO);
};
const handleDeleteMouseOver = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, true);
};
const handleDeleteMouseOut = (username) => {
acctMgmtStore.changeIsDeleteHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const handleRowMouseOver = (username) => {
acctMgmtStore.changeIsRowHoveredByUser(username, true);
};
const handleRowMouseOut = (username) => {
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const handleEditMouseOver = (username) => {
acctMgmtStore.changeIsEditHoveredByUser(username, true);
};
const handleEditMouseOut = (username) => {
acctMgmtStore.changeIsEditHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const handleDetailMouseOver = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, true);
};
const handleDetailMouseOut = (username) => {
acctMgmtStore.changeIsDetailHoveredByUser(username, false);
acctMgmtStore.changeIsRowHoveredByUser(username, false);
};
const onEditButtonClick = (userNameToEdit) => {
acctMgmtStore.setCurrentViewingUser(userNameToEdit);
modalStore.openModal(MODAL_ACCT_EDIT);
};
const onDeleteBtnClick = (usernameToDelete) => {
acctMgmtStore.setCurrentViewingUser(usernameToDelete);
modalStore.openModal(MODAL_DELETE);
};
const getRowClass = (curData) => {
return curData?.isRowHovered ? "bg-[#F1F5F9]" : "";
};
const onDetailBtnClick = (dataKey) => {
acctMgmtStore.setCurrentViewingUser(dataKey);
modalStore.openModal(MODAL_ACCT_INFO);
};
watch(shouldUpdateList, async (newShouldUpdateList) => {
if (newShouldUpdateList) {
await acctMgmtStore.getAllUserAccounts();
moveJustCreateUserToFirstRow();
}
acctMgmtStore.setShouldUpdateList(false);
});
const onSearchAccountButtonClick = (inputQueryString) => {
inputQuery.value = inputQueryString;
};
const setIsActiveInput = async (userData, inputIsActiveToSet) => {
const userDataToReplace = {
username: userData.username,
name: userData.name,
is_active: inputIsActiveToSet,
};
await acctMgmtStore.editAccount(userData.username, userDataToReplace);
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
};
const onAdminInputClick = async (userData, inputIsAdminOn) => {
const ADMIN_ROLE_NAME = "admin";
switch (inputIsAdminOn) {
case true:
await acctMgmtStore.addRoleToUser(userData.username, ADMIN_ROLE_NAME);
break;
case false:
await acctMgmtStore.deleteRoleToUser(userData.username, ADMIN_ROLE_NAME);
break;
default:
break;
}
const userDataToReplace = {
username: userData.username,
name: userData.name,
is_admin: inputIsAdminOn,
};
acctMgmtStore.updateSingleAccountPiniaState(userDataToReplace);
toast.success(i18next.t("AcctMgmt.MsgAccountEdited"));
};
/**
* Loads more account data when the user scrolls near the bottom.
* @param {Event} event - The scroll event from the data grid.
*/
const handleScroll = (event) => {
const container = event.target;
const smallValue = 3;
const isOverScrollHeight =
container.scrollTop + container.clientHeight >=
container.scrollHeight - smallValue;
if (isOverScrollHeight) {
fetchMoreDataVue3();
}
};
const fetchMoreDataVue3 = () => {
if (infiniteAcctData.value.length < acctMgmtStore.allUserAccountList.length) {
infiniteStart.value += ONCE_RENDER_NUM_OF_DATA;
}
};
onMounted(async () => {
loadingStore.setIsLoading(false);
await fetchLoginUserData();
await acctMgmtStore.getAllUserAccounts();
});
</script>
<style>
/* Center column text so that radio buttons are also centered */
.header-center .p-column-header-content {
justify-content: center;
}
</style>