feat: frontend

This commit is contained in:
2025-11-17 21:07:51 -07:00
parent dd0ab39985
commit e1396e2d24
87 changed files with 13616 additions and 148 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
</>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}