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,136 @@
import axios from 'axios'
import {CreateItemQuery} from '../queries/CreateItemQuery'
import {ItemPriceResponse} from '../response/ItemPriceResponse'
import {OrderItemPriceResponse} from '../response/OrderItemPriceResponse'
import {SetItemPriceQuery} from '../queries/SetItemPriceQuery'
import {GetCurrentPriceQuery} from '../queries/GetCurrentPriceQuery'
import {AddItemToOrderQuery} from '../queries/AddItemToOrderQuery'
import {GetOrderItemsQuery} from '../queries/GetOrderItemsQuery'
import {SetItemMadeRequest} from '../request/SetItemMadeRequest'
import {OrderFilledResponse} from '../response/OrderFilledResponse'
import {SetItemQuantityRequest} from '../request/SetItemQuantityRequest'
import {DeleteOrderItemRequest} from '../request/DeleteOrderItemRequest'
import { ItemHistoryResponse } from '../response/ItemHistoryResponse'
export const CreateItem = async (query: CreateItemQuery): Promise<ItemPriceResponse> => {
const queryParams = new URLSearchParams({
item_name: query.item_name,
in_season: query.in_season,
item_price: query.item_price
})
const res = await axios.post(process.env.API_URL + `/item/create?${queryParams.toString()}`)
if (res.data.Location) {
window.location.href = res.data.Location
}
return res.data
}
export const SetItemPrice = async (query: SetItemPriceQuery) => {
const queryParams = new URLSearchParams({
item_id: query.item_id,
item_price: query.item_price
})
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/item/price?${queryParams.toString()}`, {}, {withCredentials: true})
if (res.data.Location) {
window.location.href = res.data.Location
}
}
export const GetCurrentPrice = async (query: GetCurrentPriceQuery): Promise<ItemPriceResponse> => {
const queryParams = new URLSearchParams({
item_id: query.item_id
})
const res = await axios.get(process.env.API_URL + `/item/price?${queryParams.toString()}`)
if (res.data.Location) {
window.location.href = res.data.Location
}
return res.data
}
export const AddItemToOrder = async (query: AddItemToOrderQuery): Promise<OrderItemPriceResponse> => {
const queryParams = new URLSearchParams({
item_id: query.item_id,
order_id: query.order_id,
quantity: query.quantity
})
console.log(process.env.NEXT_PUBLIC_API_URL + `/order/item?${queryParams.toString()}`)
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/order/item?${queryParams.toString()}`, {}, {withCredentials: true})
if (res.data?.Location) {
window.location.href = res.data.Location
}
return res.data
}
export const GetOrderItems = async (query: GetOrderItemsQuery): Promise<OrderItemPriceResponse[]> => {
const queryParams = new URLSearchParams({
order_id: query.order_id
})
const res = await axios.get(process.env.NEXT_PUBLIC_API_URL + `/order/items?${queryParams.toString()}`, {withCredentials: true})
if (res.data?.Location) {
window.location.href = res.data.Location
}
return res.data || []
}
export const SetItemMade = async (request: SetItemMadeRequest): Promise<OrderFilledResponse> => {
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + '/item/made', request, {withCredentials: true})
if (res.data?.Location) {
window.location.href = res.data.Location
}
return res.data
}
export const SetItemQuantity = async (request: SetItemQuantityRequest): Promise<OrderFilledResponse> => {
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/item/quantity`, request, {withCredentials: true})
if (res.data.Location) {
window.location.href = res.data.Location
}
return res.data
}
export const DeleteOrderItem = async (request: DeleteOrderItemRequest) => {
const res = await axios.delete(process.env.API_URL + `/order/item?order_id=${request.order_id}&item_id=${request.item_id}`)
if (res.data.Location) {
window.location.href = res.data.Location
}
}
export const DeleteItem = async (itemId: number) => {
const queryParams = new URLSearchParams({
item_id: itemId.toString()
})
const res = await axios.delete(process.env.API_URL + `/item?${queryParams.toString()}`)
if (res.data.Location) {
window.location.href = res.data.Location
}
}
export const GetItems = async (): Promise<ItemPriceResponse[]> => {
const res = await axios.get(process.env.NEXT_PUBLIC_API_URL + `/items`, {withCredentials: true})
if (res.data.Location) {
window.location.href = res.data.Location
}
return res.data
}
export const GetItemHistory = async (itemId: number): Promise<ItemHistoryResponse[]> => {
const res = await axios.get(process.env.NEXT_PUBLIC_API_URL + `/item/history?item_id=${itemId.toString()}`, {withCredentials: true})
return res.data
}

View File

@@ -0,0 +1,56 @@
import axios from 'axios'
import {OrderResponse} from '../response/OrderResponse'
import {OrderTableQuery} from '../request/GetOrderTableRequest'
axios.interceptors.response.use(response => {
return response;
}, async error => {
if (error.response.status === 401) {
const resp = await axios.get(process.env.NEXT_PUBLIC_API_URL + "/auth/login", {withCredentials: true})
window.location.href = resp.data.Location
}
return error
})
export const CreateOrder = async (orderer: string, dateDue: string): Promise<OrderResponse> => {
const queryParams = new URLSearchParams({
orderer: orderer,
date_due: dateDue,
date_placed: new Date().toISOString()
})
const res = await axios.post(process.env.NEXT_PUBLIC_API_URL + `/order/create?${queryParams.toString()}`, {}, {withCredentials: true})
return res.data as OrderResponse
}
export const GetOrderById = async (orderId: number): Promise<OrderResponse> => {
const queryParams = new URLSearchParams({
order_id: orderId.toString()
})
const res = await axios.get(process.env.API_URL + `/order?${queryParams.toString()}`)
return res.data as OrderResponse
}
export const GetOrderTable = async (page: number, filter: number, searchParams: OrderTableQuery): Promise<OrderResponse[]> => {
const queryParams = new URLSearchParams({
page: page.toString(),
filter: filter.toString()
})
console.log(page)
console.log(filter)
console.log(searchParams)
console.log(queryParams.toString())
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/order/table?${queryParams.toString()}`, searchParams, {withCredentials: true})
if(res.data?.Location) {
window.location.href = res.data.Location
}
return res.data || []
}
export const DeleteOrder = async (orderId: number) => {
const queryParams = new URLSearchParams({
order_id: orderId.toString()
})
await axios.delete(process.env.API_URL + `/order?${queryParams}`)
}

View File

@@ -0,0 +1,81 @@
import axios, { HttpStatusCode } from 'axios'
import { UserResponse } from '../response/UserResponse'
import { UserTable } from '../queries/UserTableQuery'
import { LoginRedirectResponse } from '../response'
// import { cookies } from 'next/headers'
export const GetCurrentUser = async (): Promise<UserResponse | undefined> => {
console.log(process.env.NEXT_PUBLIC_API_URL + "/user/current")
const res = await axios.get(process.env.NEXT_PUBLIC_API_URL + "/user/current", {
maxRedirects: 0,
validateStatus: (status) => {
return status >= 200 && status < 400
}});
if(res.data.Location) {
window.location.href = res.data.Location
}
return res.data;
};
export const GetUserTable = async (page: number): Promise<UserResponse[]> => {
const res = await axios.put(process.env.NEXT_PUBLIC_API_URL + `/users?page=${page.toString()}`, {
name: "",
jobPosition: ""
}, {
withCredentials: true
});
if (res.data.Location)
window.location.href = res.data.Location
return res.data || []
}
export const SetUserName = async (userName: string) => {
const queryParams = new URLSearchParams({
user_name: userName
});
await axios.put(process.env.NEXT_PUBLIC_API_URL + `/user/name?${queryParams.toString()}`)
}
export const PromoteUser = async (userId: number) => {
const queryParams = new URLSearchParams({
user_id: userId.toString()
});
await axios.put(process.env.NEXT_PUBLIC_API_URL + `/user/promote?${queryParams.toString()}`, {}, {withCredentials: true});
}
export const DemoteUser = async (userId: number) => {
const queryParams = new URLSearchParams({
user_id: userId.toString()
})
await axios.put(process.env.NEXT_PUBLIC_API_URL + `/user/demote?${queryParams.toString()}`, {}, {withCredentials: true})
}
export const DeactivateUser = async (userId: number) => {
const queryParams = new URLSearchParams({
user_id: userId.toString()
})
await axios.delete(process.env.NEXT_PUBLIC_API_URL + `/user/deactivate?${queryParams.toString()}`, {withCredentials: true})
}
export const ActivateUser = async (userId: number) => {
const queryParams = new URLSearchParams({
user_id: userId.toString()
})
await axios.put(process.env.NEXT_PUBLIC_API_URL + `/user/activate?${queryParams.toString()}`, {}, {withCredentials: true})
}
export const CreatePosition = async (positionName: string) => {
const queryParams = new URLSearchParams({
position_name: positionName
})
await axios.post(process.env.NEXT_PUBLIC_API_URL + `/position/create?` + queryParams.toString())
}
export const SetUserPosition = async (userId: number, positionName: string) => {
const queryParams = new URLSearchParams({
user_id: userId.toString(),
position: positionName
})
await axios.put(process.env.NEXT_PUBLIC_API_URL + `/user/position?${queryParams.toString}`)
}

View File

@@ -0,0 +1,3 @@
export * from './ItemController'
export * from './OrderController'
export * from './UserController'

View File

@@ -0,0 +1,9 @@
/*
AddItemToOrder
PUT: {{baseURL}}/order/item?item_id=2&order_id=2&quantity=8
*/
export interface AddItemToOrderQuery {
item_id: string;
order_id: string;
quantity: string;
}

View File

@@ -0,0 +1,9 @@
/*
CreateItem
POST: {{baseURL}}/item/create?item_name=4 Berry Pie&in_season=1&item_price=35.00
*/
export interface CreateItemQuery {
item_name: string;
in_season: string;
item_price: string;
}

View File

@@ -0,0 +1,8 @@
/*
CreateOrder
POST: {{baseURL}}/order/create?orderer=john roe&date_due=2026-01-01T23:59:59-07:00
*/
export interface CreateOrder {
orderer: string;
date_due: string;
}

View File

@@ -0,0 +1,7 @@
/*
CreatePosition
POST: {{baseURL}}/position/create?position_name=Manager
*/
export interface CreatePosition {
position_name: string;
}

View File

@@ -0,0 +1,7 @@
/*
DeactivateUser
DELETE: {{baseURL}}/user/deactivate?user_id=4
*/
export interface DeactivateUser {
user_id: string;
}

View File

@@ -0,0 +1,7 @@
/*
DemoteUser
PUT: {{baseURL}}/user/demote?user_id=1
*/
export interface DemoteUser {
user_id: string;
}

View File

@@ -0,0 +1,7 @@
/*
GetCurrentPrice
GET: {{baseURL}}/item/price/current?item_id=1
*/
export interface GetCurrentPriceQuery {
item_id: string;
}

View File

@@ -0,0 +1,7 @@
/*
GetOrderById
GET: http://localhost:8080/order?order_id=8
*/
export interface GetOrderById {
order_id: string;
}

View File

@@ -0,0 +1,7 @@
/*
GetOrderItems
GET: http://localhost:8080/order/items?order_id=1
*/
export interface GetOrderItemsQuery {
order_id: string;
}

View File

@@ -0,0 +1,8 @@
/*
GetOrderTable
GET: http://localhost:8080/order/table?page=0&filter=257
*/
export interface GetOrderTableQuery {
page: string;
filter: string;
}

View File

@@ -0,0 +1,7 @@
/*
PromoteUser
PUT: {{baseURL}}/user/promote?user_id=1
*/
export interface PromoteUser {
user_id: string;
}

View File

@@ -0,0 +1,8 @@
/*
SetItemPrice
PUT: {{baseURL}}/item/price?item_id=1&item_price=36.99
*/
export interface SetItemPriceQuery {
item_id: string;
item_price: string;
}

View File

@@ -0,0 +1,8 @@
/*
SetUserPosition
PUT: {{baseURL}}/user/position?user_id=2&position=Manager
*/
export interface SetUserPosition {
user_id: string;
position: string;
}

View File

@@ -0,0 +1,7 @@
/*
UserName
PUT: {{baseURL}}/user/name?user_name=Ada%20Conway
*/
export interface UserName {
user_name: string;
}

View File

@@ -0,0 +1,7 @@
/*
UserTable
GET: {{baseURL}}/users?page=1
*/
export interface UserTable {
page: string;
}

View File

@@ -0,0 +1,8 @@
/*
DeleteOrderItem
DELETE: {{baseURL}}/order/item
*/
export interface DeleteOrderItemRequest {
item_id: number;
order_id: number;
}

View File

@@ -0,0 +1,9 @@
/*
GetOrderTable
GET: http://localhost:8080/order/table?page=0&filter=257
*/
export interface OrderTableQuery {
orderer: string;
date_due: string;
date_placed: string;
}

View File

@@ -0,0 +1,9 @@
/*
SetItemMade
PUT: {{baseURL}}/item/made
*/
export interface SetItemMadeRequest {
order_id: number;
item_id: number;
made: number;
}

View File

@@ -0,0 +1,5 @@
export interface SetItemQuantityRequest {
order_id: number;
item_id: number;
quantity: number;
}

View File

@@ -0,0 +1,7 @@
/*
UserName
PUT: {{baseURL}}/user/name?user_name=Ada%20Conway
*/
export interface UserName {
name: string;
}

View File

@@ -0,0 +1,7 @@
export type ItemHistoryResponse = {
ItemId : string
ItemName : string
ItemPrice: string
ValidFrom: string
ValidTo : string
}

View File

@@ -0,0 +1,6 @@
export interface ItemPriceResponse {
ItemId: number;
ItemName: string;
ItemPrice: number;
InSeason: boolean
}

View File

@@ -0,0 +1,4 @@
export type LoginRedirectResponse = {
status: string,
location: string
};

View File

@@ -0,0 +1,4 @@
export interface OrderFilledResponse{
OrderId: number;
Filled: boolean;
}

View File

@@ -0,0 +1,10 @@
export interface OrderItemPriceResponse{
ItemId: number;
OrderId: number;
ItemName: string;
Quantity: number;
Made: number;
CreatedAt: Date;
TotalPrice: number;
UnitPrice: number;
}

View File

@@ -0,0 +1,12 @@
export interface OrderResponse{
Id: number;
UserId: number;
Orderer: string;
DateDue: string;
DatePlaced: string;
AmountPaid: number;
OrderTotal: number;
AmountDue: number;
Filled: boolean;
Delivered: boolean;
}

View File

@@ -0,0 +1,7 @@
export interface UserResponse{
Id: number;
Name: string;
JobPosition: string;
Active: boolean;
Admin: boolean;
}

View File

@@ -0,0 +1,7 @@
export type {ItemPriceResponse} from './ItemPriceResponse'
export type {OrderFilledResponse} from './OrderFilledResponse'
export type {OrderItemPriceResponse} from './OrderItemPriceResponse'
export type {OrderResponse} from './OrderResponse'
export type {UserResponse} from './UserResponse'
export type { LoginRedirectResponse} from './LoginRedirectResponse'
export type {ItemHistoryResponse} from './ItemHistoryResponse'

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

BIN
ordr-ui/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
ordr-ui/app/globals.css Normal file
View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -0,0 +1,11 @@
'use client'
import { ItemTableList } from "../components/ItemTableList"
const Items = () => {
return (
<ItemTableList />
)
}
export default Items

36
ordr-ui/app/layout.tsx Normal file
View File

@@ -0,0 +1,36 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { NavBar } from "./components/NavBar";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<NavBar />
{children}
</body>
</html>
);
}

View File

@@ -0,0 +1,89 @@
'use client'
import {useParams} from 'next/navigation'
import { useState } from 'react'
import { OrderTableList } from '@/app/components/OrderTableList'
import useAsyncEffect from 'use-async-effect'
import DatePicker from 'react-datepicker'
import "react-datepicker/dist/react-datepicker.css"
import { useOrderStore } from '@/app/providers/OrderProvider'
const OrderTable = () => {
const {page, filter} = useParams()
console.log(filter)
const [tempOrderer, setTempOrderer] = useState("")
const [tempDateDue, setTempDateDue] = useState("")
const [tempDatePlaced, setTempDatePlaced] = useState("")
const [orderer, setOrderer] = useState("")
const [dateDue, setDateDue] = useState("")
const [datePlaced, setDatePlaced] = useState("")
const orderStore = useOrderStore((state) => state)
const [shouldPushUpdates, setShouldPushUpdates] = useState(false)
const [shouldPushNewOrder, setShouldPushNewOrder] = useState(false)
const [newOrderOrderer, setNewOrderOrderer] = useState("")
const [newOrderDue, setNewOrderDue] = useState<Date | null>(null)
useAsyncEffect(async () => {
if(shouldPushUpdates) {
setOrderer(tempOrderer)
setDateDue(tempDateDue)
setDatePlaced(tempDatePlaced)
setShouldPushUpdates(false)
}
}, [shouldPushUpdates])
useAsyncEffect(async () => {
if(shouldPushNewOrder) {
if(newOrderDue)
orderStore.createOrder(newOrderOrderer, newOrderDue.toISOString())
setShouldPushNewOrder(false)
}
}, [shouldPushNewOrder])
return (
<>
<h1 className="text-xl font-bold mt-5">Search</h1>
<input type='text' placeholder="orderer" onChange={(e) => {
console.log(e.currentTarget.value)
setTempOrderer(e.currentTarget.value)
}} />
<input type='text' placeholder="date due" onChange={(e) => {
setTempDateDue(e.currentTarget.value)
}} />
<input type='text' placeholder="date placed" onChange={(e) => {
setTempDatePlaced(e.currentTarget.value)
}} />
<br />
<button className="border border-white p-1 mt-3 mb-5" onClick={() => {
setShouldPushUpdates(true)
}}>Search</button>
<hr className="mb-5"/>
<OrderTableList page={parseInt(page as string)} filter={parseInt(filter as string)} orderer={orderer} dateDue={dateDue} datePlaced={datePlaced}/>
<h1 className="text-xl font-bold">Create new order</h1>
<div className="inline-block w-100">
<input className="inline-block w-20" placeholder="orderer" onChange={(e) => {
setNewOrderOrderer(e.currentTarget.value)
}}/>
<DatePicker
selected={newOrderDue}
onChange={(date: Date | null) => setNewOrderDue(date)}
dateFormat="MM/dd/yyyy" // Example format
placeholderText="Due date"
/>
<br />
<button onClick={() => {
setShouldPushNewOrder(true)
}}>Create</button>
</div>
</>
)
}
export default OrderTable

32
ordr-ui/app/page.tsx Normal file
View File

@@ -0,0 +1,32 @@
'use client'
import Image from "next/image";
import { useAsyncEffect } from 'use-async-effect'
import { useShallow } from 'zustand/react/shallow'
import { UserResponse } from './client/response'
import { useCurrentAuthenticatedUserStore, UserActions } from './providers'
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Home() {
const authenticatedUserStore: UserResponse & UserActions = useCurrentAuthenticatedUserStore(useShallow((state) => ({
...state
})))
useAsyncEffect(async () => {
if(authenticatedUserStore.Id === -1) {
await authenticatedUserStore.sync()
}
})
const router = useRouter()
useEffect(() => {
router.push('/orders/0/0')
}, [router])
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
</main>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { create } from 'zustand'
import { UserResponse } from '../client/response'
import { GetCurrentUser, SetUserName } from '../client/controllers'
export type UserActions = {
sync: () => Promise<UserResponse | undefined>
updateName: (name: string) => Promise<void>
}
export const useCurrentAuthenticatedUserStore = create<UserResponse & UserActions>((set) => ({
Id: -1,
Name: '',
JobPosition: '',
Active: false,
Admin: false,
sync: async () => {
const authUser = await GetCurrentUser()
set((state) => ({
...authUser,
...state
}))
return authUser
},
updateName: async (name: string) => {
await SetUserName(name)
}
}))

View File

@@ -0,0 +1,188 @@
import { create } from 'zustand'
import { ItemPriceResponse, OrderItemPriceResponse, OrderFilledResponse } from '../client/response'
import * as ItemController from '../client/controllers/ItemController'
import { CreateItemQuery } from '../client/queries/CreateItemQuery'
import { SetItemPriceQuery } from '../client/queries/SetItemPriceQuery'
import { blob } from 'stream/consumers'
export type ItemData = {
items: ItemPriceResponse[],
orderItems: OrderItemPriceResponse[]
}
export type ItemActions = {
sync: () => Promise<void>
createItem: (itemName: string, inSeason: boolean, itemPrice: number) => Promise<ItemPriceResponse>
setItemPrice: (itemId: number, price: number) => Promise<void>
getCurrentPrice: (itemId: number) => Promise<ItemPriceResponse>
addItemToOrder: (itemId: number, orderId: number, quantity: number) => Promise<OrderItemPriceResponse>
getOrderItems: (orderId: number) => Promise<OrderItemPriceResponse[]>
setItemMade: (orderId: number, itemId: number, made: number) => Promise<OrderFilledResponse>
setItemQuantity: (orderId: number, itemId: number, quantity: number) => Promise<OrderFilledResponse>
deleteOrderItem: (orderId: number, itemId: number) => Promise<void>
deleteItem: (itemId: number) => Promise<void>
}
export const useItemStore = create<ItemData & ItemActions>((set, get) => ({
items: [],
orderItems: [],
sync: async (): Promise<void> => {
const itemPrices = await ItemController.GetItems()
set((state) => ({
...state,
items: itemPrices
}))
},
createItem: async (itemName: string, inSeason: boolean, itemPrice: number): Promise<ItemPriceResponse> => {
const itemQuery: CreateItemQuery = {
item_name: itemName,
in_season: inSeason ? "1" : "0",
item_price: itemPrice.toString()
}
const itemResponse = await ItemController.CreateItem(itemQuery)
set((state) => ({
...state,
items: [...state.items, itemResponse]
}))
return itemResponse
},
setItemPrice: async (itemId: number, price: number): Promise<void> => {
const itemPriceQuery: SetItemPriceQuery = {
item_id: itemId.toString(),
item_price: price.toString()
}
await ItemController.SetItemPrice(itemPriceQuery)
set((state) => {
const item = state.items.filter((i) => i.ItemId === itemId)[0]
let itemsWithoutItem = state.items.filter((i) => i.ItemId !== itemId)
item.ItemPrice = price
if(!Array.isArray(itemsWithoutItem))
itemsWithoutItem = [itemsWithoutItem]
return {
...state,
items: [...itemsWithoutItem, item].sort((a,b) => {
if(a.InSeason && !b.InSeason)
return 1
if(!a.InSeason && b.InSeason)
return -1
return a.ItemId - b.ItemId
})
}
})
},
getCurrentPrice: async (itemId: number): Promise<ItemPriceResponse> => {
const store = get()
const priceObjectArray = store.items.filter((i: ItemPriceResponse) => i.ItemId === itemId)
if(priceObjectArray.length > 0) {
return priceObjectArray[0]
}
const resp = await ItemController.GetCurrentPrice({item_id: itemId.toString()})
set((state) => ({
...state,
items: [...state.items, resp]
}))
return resp
},
addItemToOrder: async (itemId: number, orderId: number, quantity: number): Promise<OrderItemPriceResponse> => {
const resp = await ItemController.AddItemToOrder({item_id: itemId.toString(), order_id: orderId.toString(), quantity: quantity.toString()})
set((state) => ({
...state,
orderItems: [...state.orderItems, resp]
}))
return resp
},
getOrderItems: async (orderId: number): Promise<OrderItemPriceResponse[]> => {
const state = get()
const fetchedOrderItems = state.orderItems
const orderOrderItems = fetchedOrderItems.filter((i) => i.OrderId === orderId)
if(orderOrderItems.length > 0) {
return orderOrderItems
}
const resp = await ItemController.GetOrderItems({order_id: orderId.toString()})
set((state) => ({
orderItems: [...state.orderItems, ...resp]
}))
return resp
},
setItemMade: async (orderId: number, itemId: number, made: number): Promise<OrderFilledResponse> => {
const order_filled: OrderFilledResponse = await ItemController.SetItemMade({
order_id: orderId,
item_id: itemId,
made: made
})
set((state) => {
// TODO: Update order filled once order store is made
const orderItem = state.orderItems.filter((oi) => oi.ItemId === itemId && oi.OrderId === orderId)[0]
const orderItemsWithoutOrderItem = state.orderItems.filter((oi) => oi.ItemId !== itemId || oi.OrderId !== orderId)
orderItem.Made = made
return {
...state,
orderItems: [...orderItemsWithoutOrderItem, orderItem]
}
})
return order_filled
},
setItemQuantity: async (orderId: number, itemId: number, quantity: number): Promise<OrderFilledResponse> => {
const order_filled: OrderFilledResponse = await ItemController.SetItemQuantity({
order_id: orderId,
item_id: itemId,
quantity: quantity
})
set((state) => {
// TODO: Update order filled once order store is made
const orderItem = state.orderItems.filter((oi) => oi.ItemId === itemId && oi.OrderId === orderId)[0]
const orderItemsWithoutOrderItem = state.orderItems.filter((oi) => oi.ItemId !== itemId || oi.OrderId !== orderId)
orderItem.Quantity = quantity
return {
...state,
orderItems: [...orderItemsWithoutOrderItem, orderItem]
}
})
return order_filled
},
deleteOrderItem: async (orderId: number, itemId: number): Promise<void> => {
await ItemController.DeleteOrderItem({order_id: orderId, item_id: itemId})
set((state) => ({
...state,
orderItems: [...(state.orderItems.filter((i) => i.OrderId !== orderId || i.ItemId !== itemId))]
}))
},
deleteItem: async (itemId: number): Promise<void> => {
await ItemController.DeleteItem(itemId)
set((state) => {
const itemsWithoutItem = state.items.filter((i) => i.ItemId !== itemId)
const orderItemsWithoutItem = state.orderItems.filter((i) => i.ItemId !== itemId)
return {
...state,
items: itemsWithoutItem,
orderItems: orderItemsWithoutItem
}
})
}
}))

View File

@@ -0,0 +1,43 @@
import { create } from 'zustand'
import { OrderResponse } from '../client/response'
import { OrderTableQuery } from '../client/request/GetOrderTableRequest'
import * as OrderController from '../client/controllers/OrderController'
type OrderData = {
orders: OrderResponse[]
}
type OrderActions = {
sync: (page: number, filter: number, searchParams: OrderTableQuery) => Promise<OrderResponse[]>
createOrder: (orderer: string, dateDue: string) => Promise<OrderResponse>
getOrderById: (orderId: number) => Promise<OrderResponse>
deleteOrder: (orderId: number) => Promise<void>
}
export const useOrderStore = create<OrderData & OrderActions>((set, get) => ({
orders: [],
sync: async (page: number, filter: number, searchParams: OrderTableQuery): Promise<OrderResponse[]> => {
const resp = await OrderController.GetOrderTable(page, filter, searchParams)
set((state) => ({
...state,
orders: resp
}))
return resp
},
createOrder: async (orderer: string, dateDue: string): Promise<OrderResponse> => {
const resp = await OrderController.CreateOrder(orderer, dateDue)
console.log(resp)
set((state) => ({
...state,
orders: [...state.orders, resp]
}))
console.log(get().orders)
return resp
},
getOrderById: async (orderId: number): Promise<OrderResponse> => {
return await OrderController.GetOrderById(orderId)
},
deleteOrder: async (orderId): Promise<void> => {
await OrderController.DeleteOrder(orderId)
}
}))

View File

@@ -0,0 +1,113 @@
import { create } from 'zustand'
import { UserResponse } from '../client/response'
import { GetUserTable, SetUserPosition, PromoteUser, DemoteUser, DeactivateUser, CreatePosition, ActivateUser } from '../client/controllers'
type UserData = {
tableUsers: UserResponse[]
}
type UsersActions = {
sync: (page: number) => Promise<UserResponse[]>
setUserPosition: (userId: number, positionName: string) => Promise<void>
promoteUser: (userId: number) => Promise<void>
demoteUser: (userId: number) => Promise<void>
deactivateUser: (userId: number) => Promise<void>
activateUser: (userId: number) => Promise<void>
createPosition: (positionName: string) => Promise<void>
}
export const useUserStore = create<UserData & UsersActions>((set) => ({
sync: async (page: number): Promise<UserResponse[]> => {
const users_in_page = await GetUserTable(page)
set((state) => ({
... state,
tableUsers: users_in_page,
}))
return users_in_page
},
tableUsers: [] as UserResponse[],
setUserPosition: async (userId, positionName): Promise<void> => {
await SetUserPosition(userId, positionName)
set((state) => {
const match_user = state.tableUsers.filter((u) => u.Id === userId)[0]
match_user.JobPosition = positionName
return {
... state,
tableUsers: [...state.tableUsers, match_user]
}
})
},
promoteUser: async (userId: number): Promise<void> => {
await PromoteUser(userId)
set((state) => {
const users = state.tableUsers.filter((u) => u.Id === userId)
if(users.length > 0)
{
const user = users[0]
const tableUsersWithoutUser = state.tableUsers.filter((u) => u.Id !== userId)
user.Admin = true
return {
...state,
tableUsers: [...tableUsersWithoutUser, user]
}
}
return state
})
},
demoteUser: async (userId: number): Promise<void> => {
await DemoteUser(userId)
set((state) => {
const users = state.tableUsers.filter((u) => u.Id === userId)
if(users.length > 0)
{
const user = users[0]
const tableUsersWithoutUser = state.tableUsers.filter((u) => u.Id !== userId)
user.Admin = false
return {
...state,
tableUsers: [...tableUsersWithoutUser, user]
}
}
return state
})
},
deactivateUser: async (userId: number): Promise<void> => {
await DeactivateUser(userId)
set((state) => {
const users = state.tableUsers.filter((u) => u.Id === userId)
if(users.length > 0)
{
const user = users[0]
const tableUsersWithoutUser = state.tableUsers.filter((u) => u.Id !== userId)
user.Active = false
return {
...state,
tableUsers: [...tableUsersWithoutUser, user]
}
}
return state
})
},
activateUser: async (userId: number): Promise<void> => {
await ActivateUser(userId)
set((state) => {
const users = state.tableUsers.filter((u) => u.Id === userId)
if(users.length > 0)
{
const user = users[0]
const tableUsersWithoutUser = state.tableUsers.filter((u) => u.Id !== userId)
user.Active = true
return {
...state,
tableUsers: [...tableUsersWithoutUser, user]
}
}
return state
})
},
createPosition: async (positionName: string): Promise<void> => {
await CreatePosition(positionName)
}
}))

View File

@@ -0,0 +1 @@
export * from './AuthenticationProvider'

View File

@@ -0,0 +1,15 @@
'use client'
import { useParams } from 'next/navigation'
import { UserTable } from '@/app/components/UserTable'
const Users = () => {
const {page} = useParams()
return (
<>
<UserTable page={parseInt(page as string)} />
</>
)
}
export default Users