feat: frontend
This commit is contained in:
92
ordr-ui/app/components/ItemHistoryTable.tsx
Normal file
92
ordr-ui/app/components/ItemHistoryTable.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Mutex } from "async-mutex"
|
||||
import { useState } from "react"
|
||||
import useAsyncEffect from "use-async-effect"
|
||||
import { GetItemHistory } from "../client/controllers"
|
||||
import { ItemHistoryResponse } from "../client/response"
|
||||
import styled from "styled-components"
|
||||
|
||||
type ItemHistoryTableProps = {
|
||||
itemId: number
|
||||
}
|
||||
|
||||
const ItemHistoryTableStyle = styled.table`
|
||||
width: 100%
|
||||
`
|
||||
const ItemHistoryTableHead = styled.thead`
|
||||
background-color: #34067eff
|
||||
`
|
||||
const ItemHistoryTableItem = styled.td`
|
||||
align: center
|
||||
`
|
||||
|
||||
const ItemHistoryTH = styled.th`
|
||||
align: center
|
||||
`
|
||||
|
||||
const ItemHistoryTableBody = styled.tbody`
|
||||
> :nth-child(even) {
|
||||
background-color: #410041ff;
|
||||
}
|
||||
> :hover {
|
||||
background-color: #707070ff;
|
||||
}
|
||||
`
|
||||
|
||||
const ItemHistoryTableRow = styled.tr`
|
||||
|
||||
`
|
||||
|
||||
const itemHistoryMutex = new Mutex()
|
||||
|
||||
export const ItemHistoryTable = ({itemId}: ItemHistoryTableProps) => {
|
||||
const [itemPriceHistory, setItemPriceHistory] = useState<ItemHistoryResponse[]>([])
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if(itemPriceHistory.length === 0) {
|
||||
const release = await itemHistoryMutex.acquire()
|
||||
setItemPriceHistory(await GetItemHistory(itemId))
|
||||
await release()
|
||||
}
|
||||
}, [])
|
||||
|
||||
console.log(itemPriceHistory)
|
||||
|
||||
return (
|
||||
<ItemHistoryTableStyle>
|
||||
<ItemHistoryTableHead>
|
||||
<tr>
|
||||
<ItemHistoryTH>
|
||||
item
|
||||
</ItemHistoryTH>
|
||||
<ItemHistoryTH>
|
||||
price
|
||||
</ItemHistoryTH>
|
||||
<ItemHistoryTH>
|
||||
valid from
|
||||
</ItemHistoryTH>
|
||||
<ItemHistoryTH>
|
||||
valid to
|
||||
</ItemHistoryTH>
|
||||
</tr>
|
||||
</ItemHistoryTableHead>
|
||||
<ItemHistoryTableBody>
|
||||
{itemPriceHistory.map((iph) => (
|
||||
<ItemHistoryTableRow key = {iph.ItemId + new Date(iph.ValidFrom).getMilliseconds()}>
|
||||
<ItemHistoryTableItem>
|
||||
{iph.ItemName}
|
||||
</ItemHistoryTableItem>
|
||||
<ItemHistoryTableItem>
|
||||
{Math.trunc(parseInt(iph.ItemPrice) * 100) / 100}
|
||||
</ItemHistoryTableItem>
|
||||
<ItemHistoryTableItem>
|
||||
{new Date(iph.ValidFrom).toLocaleDateString()}
|
||||
</ItemHistoryTableItem>
|
||||
<ItemHistoryTableItem>
|
||||
{new Date(iph.ValidTo).toLocaleDateString()}
|
||||
</ItemHistoryTableItem>
|
||||
</ItemHistoryTableRow>
|
||||
))}
|
||||
</ItemHistoryTableBody>
|
||||
</ItemHistoryTableStyle>
|
||||
)
|
||||
}
|
||||
27
ordr-ui/app/components/ItemTableList.tsx
Normal file
27
ordr-ui/app/components/ItemTableList.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import useAsyncEffect from "use-async-effect"
|
||||
import { useItemStore } from "../providers/ItemsProvider"
|
||||
import { Mutex } from "async-mutex"
|
||||
import { ItemTableListRow } from "./ItemTableListRow"
|
||||
|
||||
const itemApiMutex = new Mutex()
|
||||
export const ItemTableList = () => {
|
||||
const itemStore = useItemStore((state) => state)
|
||||
|
||||
useAsyncEffect( async () => {
|
||||
if(itemStore.items.length === 0) {
|
||||
const release = await itemApiMutex.acquire()
|
||||
|
||||
await itemStore.sync()
|
||||
|
||||
await release()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{itemStore.items.map((i) => (
|
||||
<ItemTableListRow item={i} key={i.ItemId}/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
102
ordr-ui/app/components/ItemTableListRow.tsx
Normal file
102
ordr-ui/app/components/ItemTableListRow.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { ItemPriceResponse } from "../client/response"
|
||||
import { ItemHistoryTable } from "./ItemHistoryTable"
|
||||
import useAsyncEffect from "use-async-effect"
|
||||
import { useItemStore } from "../providers/ItemsProvider"
|
||||
import { Mutex } from "async-mutex"
|
||||
|
||||
type ItemTableListRowProps = {
|
||||
item: ItemPriceResponse
|
||||
}
|
||||
|
||||
const ItemRowContainer = styled.div`
|
||||
margin-bottom: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ItemFieldContainer = styled.div`
|
||||
display: inline-block;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
`
|
||||
|
||||
const ItemOverviewContainer = styled.div`
|
||||
display: inline-block;
|
||||
background-color: #410041ff;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: #920592ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const ItemDetailsContainer = styled.div`
|
||||
display: inline-block;
|
||||
background-color: #6e296eff;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: #da51daff;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
const itemTableListRowMutex = new Mutex()
|
||||
export const ItemTableListRow = ({item}: ItemTableListRowProps) => {
|
||||
const [shouldShowDetails, setShouldShowDetails] = useState<boolean>(false)
|
||||
|
||||
const itemStore = useItemStore((state) => state)
|
||||
const [newItemPrice, setNewItemPrice] = useState<number>(item.ItemPrice)
|
||||
|
||||
const [shouldPushNewItemPrice, setShouldPushNewItemPrice] = useState<boolean>(false)
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if(shouldPushNewItemPrice)
|
||||
{
|
||||
const release = await itemTableListRowMutex.acquire()
|
||||
setShouldPushNewItemPrice(false)
|
||||
await itemStore.setItemPrice(item.ItemId, newItemPrice)
|
||||
await release()
|
||||
}
|
||||
}, [shouldPushNewItemPrice])
|
||||
|
||||
return (
|
||||
<li>
|
||||
<ItemRowContainer>
|
||||
<ItemOverviewContainer onClick={() => {
|
||||
setShouldShowDetails(!shouldShowDetails)
|
||||
}}>
|
||||
<ItemFieldContainer>
|
||||
item: {item.ItemName}
|
||||
</ItemFieldContainer>
|
||||
<ItemFieldContainer>
|
||||
price: {Math.trunc(item.ItemPrice * 100) / 100}
|
||||
</ItemFieldContainer>
|
||||
</ItemOverviewContainer>
|
||||
{shouldShowDetails && (
|
||||
<>
|
||||
<ItemDetailsContainer>
|
||||
<h1 className="text-xl">Set Item Price</h1>
|
||||
<label>Price</label>
|
||||
<br />
|
||||
<input placeholder="price" defaultValue={(Math.trunc(item.ItemPrice * 100) / 100).toString()} onChange={(e) => {
|
||||
const newPrice = parseInt(e.currentTarget.value)
|
||||
if(!Number.isNaN(newPrice))
|
||||
setNewItemPrice(newPrice)
|
||||
}}/>
|
||||
<br />
|
||||
<button className="border p-2 mt-2" onClick={() => {setShouldPushNewItemPrice(true)}}>Set Price</button>
|
||||
</ItemDetailsContainer>
|
||||
<ItemHistoryTable itemId={item.ItemId} />
|
||||
</>
|
||||
)}
|
||||
</ItemRowContainer>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
13
ordr-ui/app/components/NavBar.tsx
Normal file
13
ordr-ui/app/components/NavBar.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import Link from "next/link"
|
||||
|
||||
export const NavBar = () => {
|
||||
return (
|
||||
<nav>
|
||||
<div className="flex items-center justify-center">
|
||||
<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/orders/0/0">Orders</Link>
|
||||
<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/users/0">Users</Link>
|
||||
<Link className=" pl-7 pr-7 pt-3 pb-3 hover:bg-purple-950" href="/items">Items</Link>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
143
ordr-ui/app/components/OrderItemTable.tsx
Normal file
143
ordr-ui/app/components/OrderItemTable.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useItemStore } from "../providers/ItemsProvider"
|
||||
import { useState } from "react"
|
||||
import useAsyncEffect from "use-async-effect"
|
||||
import styled from "styled-components"
|
||||
import { Mutex } from "async-mutex"
|
||||
|
||||
type OrderItemTableProps = {
|
||||
orderId: number
|
||||
}
|
||||
|
||||
|
||||
const OrderItemTableStyle = styled.table`
|
||||
width: 100%
|
||||
`
|
||||
const OrderItemTableHead = styled.thead`
|
||||
background-color: #34067eff
|
||||
`
|
||||
const OrderItemTableItem = styled.td`
|
||||
align: center
|
||||
`
|
||||
|
||||
const OrderItemTH = styled.th`
|
||||
align: center
|
||||
`
|
||||
|
||||
const OrderItemTableBody = styled.tbody`
|
||||
> :nth-child(even) {
|
||||
background-color: #410041ff;
|
||||
}
|
||||
> :hover {
|
||||
background-color: #707070ff;
|
||||
}
|
||||
`
|
||||
|
||||
const OrderItemTableRow = styled.tr`
|
||||
|
||||
`
|
||||
|
||||
const orderItemMutex = new Mutex()
|
||||
|
||||
export const OrderItemTable = ({orderId}: OrderItemTableProps) => {
|
||||
|
||||
const itemStore = useItemStore((state) => state)
|
||||
|
||||
const [orderItems, setOrderItems] = useState(itemStore.orderItems.filter((oi) => oi.OrderId === orderId))
|
||||
|
||||
const [itemName, setItemName] = useState("")
|
||||
const [itemQuantity, setItemQuantity] = useState(0)
|
||||
const [shouldPostData, setShouldPostData] = useState(false)
|
||||
useAsyncEffect(async () => {
|
||||
if (orderItems.length === 0) {
|
||||
const release = await orderItemMutex.acquire()
|
||||
setOrderItems(await itemStore.getOrderItems(orderId))
|
||||
await release()
|
||||
}
|
||||
if (itemStore.items.length === 0) {
|
||||
const release = await orderItemMutex.acquire()
|
||||
await itemStore.sync()
|
||||
await release()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
if(shouldPostData) {
|
||||
const items = itemStore.items.filter((i) => i.ItemName.toUpperCase().includes(itemName.toUpperCase()))
|
||||
|
||||
if(items.length > 0) {
|
||||
const release = await orderItemMutex.acquire()
|
||||
setOrderItems( [...orderItems, await itemStore.addItemToOrder(items[0].ItemId, orderId, itemQuantity)])
|
||||
await release()
|
||||
}
|
||||
setShouldPostData(false)
|
||||
}
|
||||
}, [shouldPostData])
|
||||
return (
|
||||
<>
|
||||
<OrderItemTableStyle>
|
||||
<OrderItemTableHead>
|
||||
<tr>
|
||||
<OrderItemTH>
|
||||
item
|
||||
</OrderItemTH>
|
||||
<OrderItemTH>
|
||||
needed
|
||||
</OrderItemTH>
|
||||
<OrderItemTH>
|
||||
made
|
||||
</OrderItemTH>
|
||||
<OrderItemTH>
|
||||
total
|
||||
</OrderItemTH>
|
||||
<OrderItemTH>
|
||||
unit
|
||||
</OrderItemTH>
|
||||
</tr>
|
||||
</OrderItemTableHead>
|
||||
<OrderItemTableBody>
|
||||
{orderItems.map((oi) => (
|
||||
<OrderItemTableRow key = {oi.ItemId}>
|
||||
<OrderItemTableItem>
|
||||
{oi.ItemName}
|
||||
</OrderItemTableItem>
|
||||
<OrderItemTableItem>
|
||||
<input className="w-10" defaultValue={oi.Quantity} onChange={async (e) => {
|
||||
if(!Number.isNaN(parseInt(e.currentTarget.value))) {
|
||||
await itemStore.setItemQuantity(oi.OrderId, oi.ItemId, parseInt(e.currentTarget.value))
|
||||
}
|
||||
}} />
|
||||
</OrderItemTableItem>
|
||||
<OrderItemTableItem>
|
||||
<input className="w-10" defaultValue={oi.Made} onChange={async (e) => {
|
||||
if(!Number.isNaN(parseInt(e.currentTarget.value))) {
|
||||
await itemStore.setItemMade(oi.OrderId, oi.ItemId, parseInt(e.currentTarget.value))
|
||||
}
|
||||
|
||||
}} />
|
||||
</OrderItemTableItem>
|
||||
<OrderItemTableItem>
|
||||
${Math.trunc(oi.TotalPrice * 100) / 100}
|
||||
</OrderItemTableItem>
|
||||
<OrderItemTableItem>
|
||||
${Math.trunc(oi.UnitPrice * 100) / 100}
|
||||
</OrderItemTableItem>
|
||||
</OrderItemTableRow>
|
||||
))}
|
||||
</OrderItemTableBody>
|
||||
</OrderItemTableStyle>
|
||||
<h1 className="text-xl font-bold">Add Item To Order</h1>
|
||||
<div className="inline-block">
|
||||
<input className="inline-block w-40" onChange={(e) => {
|
||||
setItemName(e.currentTarget.value)
|
||||
}} placeholder="item name"/>
|
||||
<input className="inline-block w-40" onChange={(e) => {
|
||||
setItemQuantity(parseInt(e.currentTarget.value))
|
||||
}} placeholder="needed" />
|
||||
</div>
|
||||
<br />
|
||||
<button className="border border-white p-1 mt-3" onClick={() => {
|
||||
setShouldPostData(true)
|
||||
}}>Add Item</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
44
ordr-ui/app/components/OrderTableList.tsx
Normal file
44
ordr-ui/app/components/OrderTableList.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'for client'
|
||||
import { useOrderStore } from "../providers/OrderProvider"
|
||||
import { useShallow } from "zustand/shallow"
|
||||
import useAsyncEffect from "use-async-effect"
|
||||
import { OrderTableRow } from "./OrderTableRow"
|
||||
import { Mutex } from "async-mutex"
|
||||
import styled from "styled-components"
|
||||
|
||||
type OrderTableProps = {
|
||||
page: number,
|
||||
filter: number,
|
||||
orderer: string,
|
||||
dateDue: string,
|
||||
datePlaced: string
|
||||
}
|
||||
|
||||
const OrderList = styled.ul`
|
||||
`
|
||||
|
||||
const mutex = new Mutex()
|
||||
|
||||
export const OrderTableList = ({page, filter, orderer, dateDue, datePlaced}: OrderTableProps) => {
|
||||
const orderStore = useOrderStore(useShallow((state) => ({
|
||||
...state
|
||||
})))
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const release = await mutex.acquire()
|
||||
await orderStore.sync(page, filter, {
|
||||
orderer: orderer,
|
||||
date_due: dateDue,
|
||||
date_placed: datePlaced
|
||||
})
|
||||
release()
|
||||
}, [page, filter, orderer, dateDue, datePlaced])
|
||||
|
||||
return (
|
||||
<OrderList>
|
||||
{orderStore.orders.map((o) => (
|
||||
<OrderTableRow key={o.Id} orderId={o.Id} orderer={o.Orderer} dateDue={o.DateDue} datePlaced={o.DatePlaced} amountPaid={o.AmountPaid} orderTotal={o.OrderTotal} amountDue={o.AmountDue} filled={o.Filled} delivered={o.Delivered} />
|
||||
))}
|
||||
</OrderList>
|
||||
)
|
||||
}
|
||||
91
ordr-ui/app/components/OrderTableRow.tsx
Normal file
91
ordr-ui/app/components/OrderTableRow.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useState } from "react"
|
||||
import styled from "styled-components"
|
||||
import { OrderItemTable } from "./OrderItemTable"
|
||||
|
||||
type OrderTableRowProps = {
|
||||
orderId: number
|
||||
orderer: string
|
||||
dateDue: string
|
||||
datePlaced: string
|
||||
amountPaid: number
|
||||
orderTotal: number
|
||||
amountDue: number
|
||||
filled: boolean
|
||||
delivered: boolean
|
||||
}
|
||||
|
||||
const OrderRowContainer = styled.div`
|
||||
margin-bottom: 10px;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const OrderFieldContainer = styled.div`
|
||||
display: inline-block;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
`
|
||||
|
||||
const OrderOverviewContainer = styled.div`
|
||||
display: inline-block;
|
||||
background-color: #410041ff;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: #920592ff;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const OrderDetailsContainer = styled.div`
|
||||
display: inline-block;
|
||||
background-color: #6e296eff;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: #da51daff;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
export const OrderTableRow = ({orderId, orderer, dateDue, datePlaced, amountPaid, orderTotal, amountDue, filled, delivered}: OrderTableRowProps) => {
|
||||
const dateDueDate = new Date(dateDue)
|
||||
const datePlacedDate = new Date(datePlaced)
|
||||
|
||||
const [shouldShowDetails, setShouldShowDetails] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<li>
|
||||
<OrderRowContainer>
|
||||
<OrderOverviewContainer onClick={() => {
|
||||
setShouldShowDetails(!shouldShowDetails)
|
||||
}}>
|
||||
<OrderFieldContainer>
|
||||
orderer: {orderer}
|
||||
</OrderFieldContainer>
|
||||
<OrderFieldContainer>
|
||||
due: {dateDueDate.toLocaleDateString()}
|
||||
</OrderFieldContainer>
|
||||
</OrderOverviewContainer>
|
||||
{shouldShowDetails && (
|
||||
<>
|
||||
<OrderDetailsContainer>
|
||||
<OrderFieldContainer>
|
||||
placed: {datePlacedDate.toLocaleDateString()}
|
||||
</OrderFieldContainer>
|
||||
<OrderFieldContainer>
|
||||
balance: {(Math.trunc(amountDue * 100) / 100).toString()}
|
||||
</OrderFieldContainer>
|
||||
<OrderFieldContainer>
|
||||
{filled ? delivered ? "delivered" : "undelivered" : "unfilled"}
|
||||
</OrderFieldContainer>
|
||||
</OrderDetailsContainer>
|
||||
<OrderItemTable orderId={orderId} />
|
||||
</>
|
||||
)}
|
||||
</OrderRowContainer>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
89
ordr-ui/app/components/UserTable.tsx
Normal file
89
ordr-ui/app/components/UserTable.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
import { useShallow } from "zustand/shallow"
|
||||
import { useUserStore } from "../providers/UsersProvider"
|
||||
import useAsyncEffect from "use-async-effect"
|
||||
import styled from "styled-components"
|
||||
import { useRef, useState } from "react"
|
||||
|
||||
type UserTableProps = {
|
||||
page: number
|
||||
}
|
||||
const UserTableStyle = styled.table`
|
||||
width: 100%
|
||||
`
|
||||
const UserTableHead = styled.thead`
|
||||
background-color: #34067eff
|
||||
`
|
||||
|
||||
const UserTH = styled.th`
|
||||
align: center
|
||||
`
|
||||
const UserTableItem = styled.td`
|
||||
align: center
|
||||
`
|
||||
|
||||
const UserTableBody = styled.tbody`
|
||||
> :nth-child(even) {
|
||||
background-color: #410041ff;
|
||||
}
|
||||
> :hover {
|
||||
background-color: #707070ff;
|
||||
}
|
||||
`
|
||||
|
||||
const UserTableRow = styled.tr`
|
||||
|
||||
`
|
||||
|
||||
export const UserTable = ({page}: UserTableProps) => {
|
||||
|
||||
const userStore = useUserStore(useShallow((state) => ({
|
||||
...state
|
||||
})))
|
||||
console.log(page)
|
||||
|
||||
const [callLock, setCallLock] = useState<boolean>(false)
|
||||
|
||||
const callLockRef = useRef(callLock)
|
||||
useAsyncEffect(async () => {
|
||||
if(!callLockRef.current) {
|
||||
callLockRef.current = true
|
||||
setCallLock(true)
|
||||
await userStore.sync(page)
|
||||
callLockRef.current = false
|
||||
setCallLock(false)
|
||||
}
|
||||
}, [page])
|
||||
console.log(userStore.tableUsers)
|
||||
return (
|
||||
<UserTableStyle>
|
||||
<UserTableHead>
|
||||
<UserTH>id</UserTH>
|
||||
<UserTH>name</UserTH>
|
||||
<UserTH>position</UserTH>
|
||||
<UserTH>active</UserTH>
|
||||
<UserTH>admin</UserTH>
|
||||
</UserTableHead>
|
||||
<UserTableBody>
|
||||
{userStore.tableUsers.map((u) => (
|
||||
<UserTableRow key={u.Id}>
|
||||
<UserTableItem>{u.Id}</UserTableItem>
|
||||
<UserTableItem>{u.Name}</UserTableItem>
|
||||
<UserTableItem>{u.JobPosition}</UserTableItem>
|
||||
<UserTableItem><input type="checkbox" defaultValue={u.Active ? "yes" : "no"} onChange={async (e) => {
|
||||
if(u.Active)
|
||||
await userStore.deactivateUser(u.Id)
|
||||
else
|
||||
await userStore.activateUser(u.Id)
|
||||
}}/></UserTableItem>
|
||||
<UserTableItem><input type="checkbox" value={u.Admin ? "yes" : "no"} onChange={async (e) => {
|
||||
if(u.Admin)
|
||||
await userStore.demoteUser(u.Id)
|
||||
else
|
||||
await userStore.promoteUser(u.Id)
|
||||
}}/></UserTableItem>
|
||||
</UserTableRow>))}
|
||||
</UserTableBody>
|
||||
</UserTableStyle>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user