Vitalik
4 months ago
29 changed files with 1101 additions and 1468 deletions
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import { IPagination } from "@/shared"; |
||||
|
||||
export interface IHideUsersPayload { |
||||
userIds: number[]; |
||||
} |
||||
|
||||
export interface IRevealUsersPayload { |
||||
userIds: number[]; |
||||
} |
||||
|
||||
export interface IGetUsersListParams extends IPagination {} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
import { IUser } from "@/shared"; |
||||
import http from "../http.service"; |
||||
import { ApiResponse } from "../http.types"; |
||||
import * as Req from "./requests.interfaces"; |
||||
|
||||
const hideUser = (data: Req.IHideUsersPayload): ApiResponse<void> => { |
||||
return http.post("admin/secret-mod-users/hide", data); |
||||
}; |
||||
|
||||
const revealUser = (data: Req.IRevealUsersPayload): ApiResponse<void> => { |
||||
return http.post("admin/secret-mod-users/reveal", data); |
||||
}; |
||||
|
||||
const getUsers = (params: Req.IGetUsersListParams) => { |
||||
return http.get<{ |
||||
items: IUser[]; |
||||
count: number; |
||||
}>("admin/secret-mod-users", { params }); |
||||
}; |
||||
|
||||
export const secretModApi = { |
||||
hideUser, |
||||
revealUser, |
||||
getUsers, |
||||
}; |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
import React from "react"; |
||||
import Modal from "../../../components/Modal"; |
||||
import { |
||||
secretUsersListSelectIds, |
||||
useAddUsersModalState, |
||||
useSecretUsersList, |
||||
} from "../states"; |
||||
import { UserSelectWithSearch } from "@/components/SmartComponents"; |
||||
import { EUsersListType, Loader } from "@/shared"; |
||||
import { Button } from "reactstrap"; |
||||
import { useSecretUsersEdit } from "../hooks"; |
||||
|
||||
export const AddUsersModal = () => { |
||||
const { close, usersIdsSelected, select, isOpen } = useAddUsersModalState(); |
||||
const { isLoading, add } = useSecretUsersEdit(); |
||||
|
||||
const hiddenUsersIds = useSecretUsersList((s) => |
||||
secretUsersListSelectIds(s.users) |
||||
); |
||||
|
||||
const save = () => { |
||||
add(usersIdsSelected.map(Number)); |
||||
close(); |
||||
}; |
||||
|
||||
return ( |
||||
<Modal show={isOpen} toggle={close} title="Додати користувачів"> |
||||
<div> |
||||
<UserSelectWithSearch |
||||
label="Оберіть користувачів" |
||||
type={EUsersListType.All} |
||||
value={usersIdsSelected} |
||||
onChange={select} |
||||
mode="multiple" |
||||
showCurrentUserOnTop={false} |
||||
excludeIds={hiddenUsersIds} |
||||
/> |
||||
|
||||
<div |
||||
style={{ display: "flex", justifyContent: "flex-end", marginTop: 20 }} |
||||
> |
||||
{isLoading ? ( |
||||
<Loader /> |
||||
) : ( |
||||
<Button color="primary" size="sm" onClick={save}> |
||||
Зберегти |
||||
</Button> |
||||
)} |
||||
</div> |
||||
</div> |
||||
</Modal> |
||||
); |
||||
}; |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
import React from "react"; |
||||
import { useAddUsersModalState, useSearchState } from "../states"; |
||||
import { SearchInput } from "@/shared"; |
||||
import { Button } from "reactstrap"; |
||||
|
||||
export const Header = () => { |
||||
const addUsers = useAddUsersModalState((s) => s.open); |
||||
const { value, onChange } = useSearchState(); |
||||
|
||||
return ( |
||||
<div |
||||
style={{ |
||||
justifyContent: "space-between", |
||||
alignItems: "center", |
||||
display: "flex", |
||||
marginBottom: 30, |
||||
}} |
||||
> |
||||
<div> |
||||
<h3>Приховані користувачі</h3> |
||||
</div> |
||||
<div |
||||
style={{ |
||||
display: "flex", |
||||
}} |
||||
> |
||||
<SearchInput |
||||
value={value} |
||||
onChange={onChange} |
||||
placeholder="Пошук" |
||||
style={{ height: 46, marginRight: "20px", width: 235 }} |
||||
/> |
||||
<Button color="primary" size="sm" onClick={addUsers}> |
||||
Додати |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from "./add-users-modal.component"; |
||||
export * from "./header.component"; |
||||
export * from "./users-table.component"; |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
|
||||
.secret-users-table .table-grid_custom .rdg { |
||||
height: 60vh; |
||||
} |
||||
|
||||
.secret-users-table { |
||||
.icon-trash { |
||||
width: 20px; |
||||
} |
||||
} |
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
import React, { useEffect } from "react"; |
||||
import { Table } from "@/components/TableGrid/Table"; |
||||
import { secretUsersTableColumns } from "../config"; |
||||
import { CustomTableRow } from "@/components/TableGrid/components"; |
||||
import { usePaginationList } from "@/shared"; |
||||
import { secretModApi } from "@/api/secret-mod/requests"; |
||||
import { useSecretUsersList } from "../states"; |
||||
import { useSecretEventsListener } from "../events"; |
||||
import "./style.scss"; |
||||
import { useSecretUsersEdit } from "../hooks"; |
||||
|
||||
export const SecretUsersTable = () => { |
||||
const navigateToUser = () => {}; |
||||
const { remove } = useSecretUsersEdit(); |
||||
|
||||
const defaultColumnsActive = ["avatarUrl", "name", "email", "actions"]; |
||||
|
||||
const paginationList = usePaginationList<any>({ |
||||
fetchItems: secretModApi.getUsers, |
||||
loadParams: { |
||||
limit: 99999, |
||||
page: 1, |
||||
sort: "ASC", |
||||
sortField: "lastName", |
||||
}, |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
useSecretUsersList.getState().setUsers(paginationList.items); |
||||
}, [paginationList.items]); |
||||
|
||||
useSecretEventsListener("addUsers", () => { |
||||
paginationList.resetFlatList(); |
||||
}); |
||||
|
||||
useSecretEventsListener("removeUsers", () => { |
||||
paginationList.resetFlatList(); |
||||
}); |
||||
|
||||
return ( |
||||
<div className="secret-users-table"> |
||||
<Table |
||||
tableName={"secret-users"} |
||||
columns={secretUsersTableColumns({ |
||||
onPressName: navigateToUser, |
||||
onPressRemove: (id) => { |
||||
remove([id]); |
||||
}, |
||||
})} |
||||
paginationList={paginationList} |
||||
activeColumns={defaultColumnsActive} |
||||
showActionBottomBar={false} |
||||
tableProps={{ |
||||
rowRenderer: (data: any) => ( |
||||
<CustomTableRow |
||||
rowData={data} |
||||
menuConfig={[]} |
||||
onOpenMenu={() => {}} |
||||
/> |
||||
), |
||||
}} |
||||
/> |
||||
</div> |
||||
); |
||||
}; |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from "./users-table-columns.config"; |
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
import React from "react"; |
||||
import { Avatar, createFullName, EUserStatus, IUser } from "@/shared"; |
||||
import _ from "lodash"; |
||||
import TrashSvgIcon from "../../../assets/img/trash-icon.svg"; |
||||
|
||||
export const secretUsersTableColumns = ({ |
||||
onPressName, |
||||
onPressRemove, |
||||
}): any => { |
||||
return [ |
||||
{ |
||||
name: "Аватар", |
||||
key: "avatarUrl", |
||||
filter: false, |
||||
width: 65, |
||||
maxWidth: 75, |
||||
formatter: ({ row }) => { |
||||
if (_.isEmpty(row)) return null; |
||||
|
||||
return <Avatar url={row?.info?.avatarUrl} />; |
||||
}, |
||||
}, |
||||
{ |
||||
name: "П.І.Б.", |
||||
key: "name", |
||||
resizable: true, |
||||
sortable: true, |
||||
sortKey: "lastName", |
||||
flex: 1, |
||||
filter: true, |
||||
filterType: "search", |
||||
formatter: ({ row }: { row: IUser }) => { |
||||
return ( |
||||
<div className="column-name" onClick={(e) => onPressName(row, e)}> |
||||
<div className="info"> |
||||
<div className="ellipsis"> |
||||
<span |
||||
className={ |
||||
row.status === EUserStatus.Active |
||||
? "full-name" |
||||
: "full-name-blocked" |
||||
} |
||||
> |
||||
{createFullName( |
||||
row.info?.firstName, |
||||
row.info?.middleName, |
||||
row.info?.lastName |
||||
)} |
||||
</span> |
||||
</div> |
||||
<span |
||||
className={`${ |
||||
row.status === EUserStatus.Active |
||||
? "position" |
||||
: "blocked-position" |
||||
} ellipsis`}
|
||||
> |
||||
{row.info?.position} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
); |
||||
}, |
||||
}, |
||||
{ |
||||
name: "Email", |
||||
dataIndex: "email", |
||||
key: "email", |
||||
cellClass: (row: IUser) => { |
||||
if (row.status === EUserStatus.Active) return ""; |
||||
return "blocked-text"; |
||||
}, |
||||
|
||||
resizable: true, |
||||
sortable: true, |
||||
filter: true, |
||||
filterType: "search", |
||||
flex: 1, |
||||
formatter: ({ row }) => { |
||||
return ( |
||||
<div className="ellipsis" onClick={(e) => onPressName(row, e)}> |
||||
<span>{row?.email}</span> |
||||
</div> |
||||
); |
||||
}, |
||||
}, |
||||
{ |
||||
name: "", |
||||
key: "actions", |
||||
width: 90, |
||||
formatter: ({ row }) => { |
||||
return ( |
||||
<img |
||||
src={TrashSvgIcon} |
||||
className="icon-trash" |
||||
onClick={() => onPressRemove(row.id)} |
||||
/> |
||||
); |
||||
}, |
||||
}, |
||||
]; |
||||
}; |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
import { Events } from "jet-tools"; |
||||
import { useEffect } from "react"; |
||||
|
||||
type SecretEvents = { |
||||
addUsers: {}; |
||||
removeUsers: {}; |
||||
}; |
||||
|
||||
export const secretEvents = new Events<SecretEvents>(); |
||||
|
||||
export const useSecretEventsListener = <T extends keyof SecretEvents>( |
||||
name: T, |
||||
action: (data: SecretEvents[T]) => void, |
||||
dependencies: any[] = [] |
||||
) => { |
||||
useEffect(() => { |
||||
const fn = (data: SecretEvents[T]) => action(data); |
||||
secretEvents.on(name, fn); |
||||
|
||||
return () => secretEvents.off(name, fn); |
||||
}, dependencies); |
||||
}; |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from "./use-secret-users-edit.hook"; |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
import { secretModApi } from "@/api/secret-mod/requests"; |
||||
import { useState } from "react"; |
||||
import { secretEvents } from "../events"; |
||||
import { notification } from "@/shared"; |
||||
|
||||
export const useSecretUsersEdit = () => { |
||||
const [isLoading, setLoading] = useState(false); |
||||
|
||||
const add = async (usersIdsSelected: number[]) => { |
||||
try { |
||||
setLoading(true); |
||||
if (usersIdsSelected.length) { |
||||
await secretModApi.hideUser({ userIds: usersIdsSelected.map(Number) }); |
||||
} |
||||
secretEvents.emit("addUsers", {}); |
||||
} catch (e) { |
||||
notification.showError( |
||||
"Помилка", |
||||
"Виникла помилка, спробуйте, будь ласка, пізніше." |
||||
); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
const remove = async (ids: number[]) => { |
||||
try { |
||||
setLoading(true); |
||||
if (ids.length) { |
||||
await secretModApi.revealUser({ userIds: ids.map(Number) }); |
||||
} |
||||
secretEvents.emit("addUsers", {}); |
||||
} catch (e) { |
||||
notification.showError( |
||||
"Помилка", |
||||
"Виникла помилка, спробуйте, будь ласка, пізніше." |
||||
); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
return { |
||||
isLoading, |
||||
setLoading, |
||||
add, |
||||
remove, |
||||
}; |
||||
}; |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
export * from "./secret-mod.page"; |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
import { Container, CardBody, Row } from "reactstrap"; |
||||
import React from "react"; |
||||
import { AddUsersModal, Header, SecretUsersTable } from "./components"; |
||||
|
||||
export const SecretModPage = () => { |
||||
return ( |
||||
<Container className="factory"> |
||||
<CardBody> |
||||
<Header /> |
||||
|
||||
<SecretUsersTable /> |
||||
</CardBody> |
||||
|
||||
<AddUsersModal /> |
||||
</Container> |
||||
); |
||||
}; |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
import _ from "lodash"; |
||||
import { create } from "zustand"; |
||||
|
||||
interface State { |
||||
isOpen: boolean; |
||||
usersIdsSelected: string[]; |
||||
|
||||
open: () => void; |
||||
close: () => void; |
||||
|
||||
select: (userIds: string[]) => void; |
||||
} |
||||
|
||||
export const useAddUsersModalState = create<State>()((set) => ({ |
||||
isOpen: false, |
||||
usersIdsSelected: [], |
||||
|
||||
open() { |
||||
set({ isOpen: true, usersIdsSelected: [] }); |
||||
}, |
||||
|
||||
close() { |
||||
set({ isOpen: false, usersIdsSelected: [] }); |
||||
}, |
||||
|
||||
select(userIds) { |
||||
set((state) => ({ |
||||
usersIdsSelected: _.uniq([...state.usersIdsSelected, ...userIds]), |
||||
})); |
||||
}, |
||||
})); |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export * from "./add-users-modal.state"; |
||||
export * from "./search.state"; |
||||
export * from "./secret-users-list.state"; |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
import { create } from "zustand"; |
||||
|
||||
interface State { |
||||
value: string; |
||||
onChange: (value: string) => void; |
||||
} |
||||
|
||||
export const useSearchState = create<State>()((set) => ({ |
||||
value: "", |
||||
onChange: (value) => set({ value }), |
||||
})); |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
import { secretModApi } from "@/api/secret-mod/requests"; |
||||
import { IUser, notification } from "@/shared"; |
||||
import { create } from "zustand"; |
||||
|
||||
interface State { |
||||
users: IUser[]; |
||||
|
||||
loadUsers: () => void; |
||||
setUsers: (users: IUser[]) => void; |
||||
} |
||||
|
||||
export const useSecretUsersList = create<State>()((set) => ({ |
||||
users: [], |
||||
async loadUsers() { |
||||
try { |
||||
const { data } = await secretModApi.getUsers({ |
||||
limit: 500, |
||||
sortField: "lastName", |
||||
sort: "ASC", |
||||
}); |
||||
set({ users: data.items }); |
||||
} catch (e) { |
||||
notification.showError( |
||||
"Помилка", |
||||
"Виникла помилка при завантаженні списку, спробуйте, будь ласка, пізніше." |
||||
); |
||||
} |
||||
}, |
||||
setUsers(users) { |
||||
set({ users }); |
||||
}, |
||||
})); |
||||
|
||||
export const secretUsersListSelectIds = (users: IUser[]) => |
||||
users.map((it) => it.id); |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
import React, { FC } from "react"; |
||||
|
||||
interface Props { |
||||
url: string; |
||||
style?: React.CSSProperties; |
||||
} |
||||
|
||||
export const Avatar: FC<Props> = ({ url, style = {} }) => { |
||||
return ( |
||||
<img |
||||
style={{ |
||||
height: 40, |
||||
width: 40, |
||||
borderRadius: 100, |
||||
objectFit: "fill", |
||||
...style, |
||||
}} |
||||
src={url || `${process.env.PUBLIC_URL}/img/default-avatar.jpg`} |
||||
alt={url ? "Avatar" : "Default avatar"} |
||||
/> |
||||
); |
||||
}; |
@ -1,6 +1,8 @@
@@ -1,6 +1,8 @@
|
||||
export * from "./loader.component"; |
||||
export * from "./icon.component"; |
||||
export * from "./avatar.component"; |
||||
export * from "./check-indicator.component"; |
||||
export * from "./selected-rows-menu.component"; |
||||
export * from "./selected-dropdown-menu.component"; |
||||
export * from "./cropper.component"; |
||||
export * from "./icon.component"; |
||||
export * from "./loader.component"; |
||||
export * from "./select-avatar.component"; |
||||
export * from "./selected-dropdown-menu.component"; |
||||
export * from "./selected-rows-menu.component"; |
||||
|
Loading…
Reference in new issue