diff --git a/.env.development b/.env.development index 58ed8bd..5fdce69 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,6 @@ NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=e01b7895a403fa7364061b2f01a650fc -BACKEND_API_HOST=https://demo.duendesoftware.com +BACKEND_API_HOST=https://new-dev.accelist.com:1234 OIDC_ISSUER=https://demo.duendesoftware.com OIDC_CLIENT_ID=interactive.public.short OIDC_SCOPE=openid profile email api offline_access diff --git a/components/DefautLayout.tsx b/components/DefautLayout.tsx index 973c396..aaade45 100644 --- a/components/DefautLayout.tsx +++ b/components/DefautLayout.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import Head from 'next/head'; import { Avatar, Button, ConfigProvider, Drawer, Layout, Menu, MenuProps } from "antd"; -import { faBars, faSignOut, faSignIn, faHome, faCubes, faUser, faUsers, faFlaskVial } from '@fortawesome/free-solid-svg-icons' +import { faBars, faSignOut, faSignIn, faHome, faCubes, faUser, faUsers, faFlaskVial, faPlus } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useRouter } from "next/router"; import { useSession, signIn, signOut } from "next-auth/react"; @@ -102,36 +102,46 @@ const DefaultLayout: React.FC<{ ] } ); - + if (status === 'authenticated') { menu.push({ - key: '/sign-out', - label: 'Sign out', - icon: , - onClick: () => { - nProgress.start(); - signOut(); - // HINT: use this method call if need to end SSO server authentication session: - // signOut({ - // callbackUrl: '/api/end-session' - // }); - } + key: '/post-order', + label: 'Post Order', + icon: , + onClick: () => { + nProgress.start(); + router.push('/be-orders/orderpostpage'); + }, }); - } else { + menu.push({ - key: '/sign-in', - label: 'Sign in', - icon: , - onClick: () => { - nProgress.start(); - signIn('oidc'); - } + key: '/sign-out', + label: 'Sign out', + icon: , + onClick: () => { + nProgress.start(); + signOut(); + // HINT: use this method call if need to end SSO server authentication session: + // signOut({ + // callbackUrl: '/api/end-session' + // }); + }, }); + } else { + menu.push({ + key: '/sign-in', + label: 'Sign in', + icon: , + onClick: () => { + nProgress.start(); + signIn('oidc'); + }, + }); + } + + return menu; } - return menu; - } - const displayUserName = session?.user?.name; function renderAvatar() { diff --git a/package-lock.json b/package-lock.json index 9344003..4cb792d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,13 @@ "@fortawesome/react-fontawesome": "0.2.0", "@hookform/error-message": "2.0.1", "@hookform/resolvers": "3.0.1", + "@tanstack/react-query": "^5.32.0", "antd": "5.4.0", "dayjs": "1.11.7", "http-proxy": "1.18.1", "jotai": "2.0.3", "next": "13.3.0", - "next-auth": "4.22.0", + "next-auth": "^4.22.0", "nprogress": "0.2.0", "openid-client": "5.4.0", "react": "18.2.0", @@ -671,6 +672,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.32.0.tgz", + "integrity": "sha512-Z3flEgCat55DRXU5UMwYU1U+DgFZKA3iufyOKs+II7iRAo0uXkeU7PH5e6sOH1CGEag0IpKmZxlUFpCg6roSKw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.32.0.tgz", + "integrity": "sha512-+E3UudQtarnx9A6xhpgMZapyF+aJfNBGFMgI459FnduEZqT/9KhOWnMOneZahLRt52yzskSA0AuOyLkXHK0yBA==", + "dependencies": { + "@tanstack/query-core": "5.32.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/http-proxy": { "version": "1.17.10", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", diff --git a/package.json b/package.json index 3d75a9a..6ab4a40 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,13 @@ "@fortawesome/react-fontawesome": "0.2.0", "@hookform/error-message": "2.0.1", "@hookform/resolvers": "3.0.1", + "@tanstack/react-query": "^5.32.0", "antd": "5.4.0", "dayjs": "1.11.7", "http-proxy": "1.18.1", "jotai": "2.0.3", "next": "13.3.0", - "next-auth": "4.22.0", + "next-auth": "^4.22.0", "nprogress": "0.2.0", "openid-client": "5.4.0", "react": "18.2.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 57444b8..f52162a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -6,6 +6,7 @@ import Router from 'next/router'; import NProgress from 'nprogress'; import { SessionProvider } from 'next-auth/react'; import { SessionErrorHandler } from '../components/SessionErrorHandler'; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // Add this line // https://fontawesome.com/v5/docs/web/use-with/react#next-js import { config } from '@fortawesome/fontawesome-svg-core'; @@ -26,16 +27,21 @@ function CustomApp({ Component, pageProps: { session, ...pageProps } }: AppPropsWithLayout): JSX.Element { + const queryClient = new QueryClient(); + // https://nextjs.org/docs/basic-features/layouts#per-page-layouts const withLayout = Component.layout ?? (page => page); return ( - // https://next-auth.js.org/getting-started/client#sessionprovider - - - {withLayout()} - - + // Wrap your application in the QueryClientProvider + + {/* https://next-auth.js.org/getting-started/client#sessionprovider */} + + + {withLayout()} + + + ); } @@ -48,3 +54,4 @@ Router.events.on('routeChangeComplete', NProgress.done); Router.events.on('routeChangeError', NProgress.done); export default CustomApp; + diff --git a/pages/be-orders/orderdeletepage.tsx b/pages/be-orders/orderdeletepage.tsx new file mode 100644 index 0000000..01d5eda --- /dev/null +++ b/pages/be-orders/orderdeletepage.tsx @@ -0,0 +1,38 @@ +import { useRouter } from 'next/router'; +import { WithDefaultLayout } from '@/components/DefautLayout'; + +export default function DeleteOrderPage() { + const router = useRouter(); + const { id } = router.query; //You get this from the URL, which you get from the MainMenu. + + const handleDelete = async () => { + const confirmDelete = window.confirm('Are you sure you want to delete this order?'); + if (!confirmDelete) { + return; + } + + // Make a DELETE request to your API + const response = await fetch(`api/be/api/v1/Order/DeleteOrder/${id}`, { + method: 'DELETE', + }); + + if (response.ok) { + // If the order was successfully deleted, redirect to the main menu + router.push('/MainMenu'); + } else { + // Handle error + alert('Failed to delete order'); + } + }; + + return ( +
+

Delete Order

+ +
+ ); +} + +DeleteOrderPage.layout = WithDefaultLayout; diff --git a/pages/be-orders/orderdetailpage.tsx b/pages/be-orders/orderdetailpage.tsx new file mode 100644 index 0000000..ca0bf7b --- /dev/null +++ b/pages/be-orders/orderdetailpage.tsx @@ -0,0 +1,58 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { WithDefaultLayout } from '@/components/DefautLayout'; + +export default function OrderDetailPage() { + const router = useRouter(); + const { id } = router.query; + const [order, setOrder] = useState(null); + + interface Order { + orderId: number; + description: string; + orderFrom: string; + orderTo: string; + total: number; + quantity: number; + orderedAt: string; + } + + useEffect(() => { + const fetchOrder = async () => { + const response = await fetch(`api/be/api/v1/Order/OrderDetail/${id}`); + const data = await response.json(); + setOrder(data); + }; + + if (id) { + fetchOrder(); + } + }, [id]); + + if (!order) { + return null; + } + + return ( +
+

Order Detail

+
+
Order ID:
+
{order.orderId}
+
Description:
+
{order.description}
+
Order From:
+
{order.orderFrom}
+
Order To:
+
{order.orderTo}
+
Ordered At:
+
{order.orderedAt}
+
Quantity:
+
{order.quantity}
+
+
+ ); +} + +OrderDetailPage.layout = WithDefaultLayout; + diff --git a/pages/be-orders/orderpostpage.tsx b/pages/be-orders/orderpostpage.tsx new file mode 100644 index 0000000..9bc4621 --- /dev/null +++ b/pages/be-orders/orderpostpage.tsx @@ -0,0 +1,110 @@ +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/router'; +import { WithDefaultLayout } from '@/components/DefautLayout'; + +export default function PostOrderPage() { + const [orderFrom, setOrderFrom] = useState(''); + const [orderTo, setOrderTo] = useState(''); + const [total, setTotal] = useState(''); + const [quantity, setQuantity] = useState(1); + const [orderedAt, setOrderedAt] = useState(''); + const router = useRouter(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + // Add your validation logic here + if (!orderFrom || orderFrom.length < 1) { + alert('Invalid order from'); + return; + } + if (!orderTo || orderTo.length < 1) { + alert('Invalid order to'); + return; + } + if (!total || isNaN(Number(total))) { + alert('Invalid total'); + return; + } + if (!quantity || quantity < 1 || quantity > 99) { + alert('Invalid quantity'); + return; + } + if (!orderedAt || new Date(orderedAt) < new Date()) { + alert('Invalid ordered at date'); + return; + } + + // Make a POST request to your API + const response = await fetch('api/be/api/v1/Order/CreateOrder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + orderFrom: orderFrom, + orderTo: orderTo, + total: total, + quantity: quantity, + orderedAt: orderedAt, + }), + }); + + if (response.ok) { + // If the order was successfully posted, redirect to the main menu + router.push('/MainMenu'); + } else { + // Handle error + alert('Failed to post order'); + } + }; + + return ( +
+
+

Post Order

+
+
+
+ + setOrderFrom(e.target.value)} required className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"/> +
+
+ + setOrderTo(e.target.value)} required className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> +
+
+ + setTotal(e.target.value)} required className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> +
+
+ + setQuantity(Number(e.target.value))} + required + className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" + /> +
+
+ + setOrderedAt(e.target.value)} required className="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" /> +
+
+
+ +
+
+
+
+); +} + +PostOrderPage.layout = WithDefaultLayout; + + diff --git a/pages/be-orders/orderupdatepage.tsx b/pages/be-orders/orderupdatepage.tsx new file mode 100644 index 0000000..cba91e2 --- /dev/null +++ b/pages/be-orders/orderupdatepage.tsx @@ -0,0 +1,94 @@ +import { useState, FormEvent } from 'react'; +import { useRouter } from 'next/router'; +import { WithDefaultLayout } from '@/components/DefautLayout'; + +export default function UpdateOrderPage() { + const [description, setDescription] = useState(''); + const [orderFrom, setOrderFrom] = useState(''); + const [orderTo, setOrderTo] = useState(''); + const [quantity, setQuantity] = useState(1); + const router = useRouter(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + + // Add your validation logic here + if (!description || description.length > 100) { + alert('Invalid description'); + return; + } + if (!orderFrom || orderFrom.length < 1) { + alert('Invalid order from'); + return; + } + if (!orderTo || orderTo.length < 1) { + alert('Invalid order to'); + return; + } + if (!quantity || quantity < 1 || quantity > 99) { + alert('Invalid quantity'); + return; + } + + // Make a PUT request to your API + const response = await fetch(`api/be/api/v1/Order/UpdateOrder`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + description: description, + orderFrom: orderFrom, + orderTo: orderTo, + quantity: quantity, + }), + }); + + if (response.ok) { + // If the order was successfully updated, redirect to the main menu + router.push('/MainMenu'); + } else { + // Handle error + alert('Failed to update order'); + } + }; + + return ( +
+

Update Order

+
+
+ + +
+
+ + setOrderFrom(e.target.value)} required minLength={1} /> +
+
+ + setOrderTo(e.target.value)} required minLength={1} /> +
+
+ + setQuantity(Number(e.target.value))} required min={1} max={99} /> +
+
+ +
+
+
+ ); +} + +UpdateOrderPage.layout = WithDefaultLayout; diff --git a/pages/index.tsx b/pages/index.tsx index 6c0943a..74c7293 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,15 +1,126 @@ +import { useRouter } from 'next/router'; import { WithDefaultLayout } from '../components/DefautLayout'; -import { Title } from '../components/Title'; -import { Page } from '../types/Page'; - -const IndexPage: Page = () => { - return ( -
- Home - Hello World! -
- ); +import { useEffect, useState } from 'react'; + +interface Order { + id: number; + name: string; + from: string; + to: string; + at: string; + quantity: number; } -IndexPage.layout = WithDefaultLayout; -export default IndexPage; +export default function MainMenu() { +// const [orders, setOrders] = useState([]); + +// useEffect(() => { +// const fetchOrders = async () => { +// const orderIds = [1, 2, 3, 4, 5]; + +// const fetchOrderDetails = async (id: number) => { +// const response = await fetch(`api/be/api/v1/Order/OrderDetail/${id}`); +// if (!response.ok) { +// console.error(`Failed to fetch order ${id}: ${response.statusText}`); +// return null; +// } +// const data = await response.json(); +// return data; +// }; + +// const orders = await Promise.all(orderIds.map(fetchOrderDetails)); +// setOrders(orders.filter(order => order !== null)); +// }; + +// fetchOrders(); +// }, []); + +// const router = useRouter(); + +const [orders, setOrders] = useState([]); + +useEffect(() => { + // Replace fetchOrders with dummy data + const dummyOrders: Order[] = [ + { id: 1, name: 'Order 1', from: 'Location 1', to: 'Location 2', at: '2022-01-01', quantity: 10 }, + { id: 2, name: 'Order 2', from: 'Location 2', to: 'Location 3', at: '2022-02-01', quantity: 20 }, + { id: 3, name: 'Order 3', from: 'Location 3', to: 'Location 4', at: '2022-03-01', quantity: 30 }, + { id: 4, name: 'Order 4', from: 'Location 4', to: 'Location 5', at: '2022-04-01', quantity: 40 }, + { id: 5, name: 'Order 5', from: 'Location 5', to: 'Location 6', at: '2022-05-01', quantity: 50 }, + ]; + setOrders(dummyOrders); +}, []); + +const router = useRouter(); + + const handleView = (order: Order) => { + // Navigate to the Order Detail page for the clicked order + router.push(`/be-orders/orderdetailpage/${order.id}`); + }; + + const handleUpdate = (order: Order) => { + // Handle update action here... + router.push(`/be-orders/orderupdatepage/${order.id}`); + }; + + const handleDelete = async (order: Order) => { + const confirmDelete = window.confirm('Are you sure you want to delete this order?'); + if (!confirmDelete) { + return; + } + + // Make a DELETE request to your API + const response = await fetch(`api/be/api/v1/Order/DeleteOrder/${order.id}`, { + method: 'DELETE', + }); + + if (response.ok) { + // If the order was successfully deleted, remove it from the orders state + setOrders(orders.filter(o => o.id !== order.id)); + } else { + // Handle error + alert('Failed to delete order'); + } + }; + + return ( +
+

Main Menu

+
+ + + + + + + + + + + + + + {orders.map((order) => ( + + + + + + + + + + ))} + +
OrderNameFromToAtQuantityAction
{order.id}{order.name}{order.from}{order.to}{order.at}{order.quantity} +
handleView(order)}>View
+
handleUpdate(order)}>Update
+
handleDelete(order)}>Delete
+
+
+
+ ); +} + +MainMenu.layout = WithDefaultLayout; + diff --git a/pages/user/login.tsx b/pages/user/login.tsx new file mode 100644 index 0000000..f600293 --- /dev/null +++ b/pages/user/login.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { WithDefaultLayout } from '@/components/DefautLayout'; + +export default function LoginPage() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const router = useRouter(); + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Make a POST request to your API + const response = await fetch('api/be/api/v1/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: username, + password: password, + }), + }); + + if (response.ok) { + // If the login was successful, redirect to the main menu + router.push('/MainMenu'); + } else { + // Handle error + alert('Failed to log in'); + } + }; + + return ( +
+
+

+ Login Page +

+
+ +
+
+ + setUsername(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
+ +
+ +
+
+
+
+ ); +} + + +LoginPage.layout = WithDefaultLayout; \ No newline at end of file diff --git a/pages/user/register.tsx b/pages/user/register.tsx new file mode 100644 index 0000000..687c496 --- /dev/null +++ b/pages/user/register.tsx @@ -0,0 +1,214 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import { WithDefaultLayout } from '@/components/DefautLayout'; + +export default function RegisterPage() { + const [email, setEmail] = useState(''); + const [dob, setDob] = useState(''); + const [gender, setGender] = useState(''); + const [address, setAddress] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const router = useRouter(); + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Add your validation logic here + if (!email || !email.includes('@')) { + alert('Invalid email'); + return; + } + + const age = new Date().getFullYear() - new Date(dob).getFullYear(); + if (age < 14) { + alert('You must be at least 14 years old'); + return; + } + + if (!['M', 'P', 'Other'].includes(gender)) { + alert('Invalid gender'); + return; + } + + if (!address || address.length > 255) { + alert('Invalid address'); + return; + } + + if (!username || username.length > 20) { + alert('Invalid username'); + return; + } + + if (!password || password.length < 8 || password.length > 64) { + alert('Invalid password'); + return; + } + + // Make a POST request to your API + const response = await fetch('api/be/api/v1/auth/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: email, + dob: dob, + gender: gender, + address: address, + username: username, + password: password, + }), + }); + + if (response.ok) { + // If the registration was successful, redirect to the login page + router.push('/LoginPage'); + } else { + // Handle error + alert('Failed to register'); + } + }; + + return ( +
+
+

+ Register Page +

+
+ +
+ {/* Email Field */} +
+ + setEmail(e.target.value)} + /> +
+ {/* DoB Field */} +
+ + setDob(e.target.value)} + /> +
+ {/* Gender Field */} +
+ Gender + + + +
+ {/* Address Field */} +
+ +