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

41
ordr-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
ordr-ui/README.md Normal file
View File

@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

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

18
ordr-ui/eslint.config.mjs Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

7
ordr-ui/next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

6589
ordr-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
ordr-ui/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "ordr-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@uidotdev/usehooks": "^2.4.1",
"async-mutex": "^0.5.0",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
"dotenv": "^17.2.3",
"next": "16.0.2",
"react": "19.2.0",
"react-date-picker": "^12.0.1",
"react-datepicker": "^8.9.0",
"react-dom": "19.2.0",
"styled-components": "^6.1.19",
"tailwind": "^4.0.0",
"use-async-effect": "^2.2.7",
"zustand": "^5.0.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.2",
"tailwindcss": "^4",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
ordr-ui/public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
ordr-ui/public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
ordr-ui/public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

37
ordr-ui/tsconfig.json Normal file
View File

@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
},
{
"name": "@styled/typescript-styled-plugin"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}

4532
ordr-ui/yarn.lock Normal file

File diff suppressed because it is too large Load Diff