diff --git a/appsettings.js b/appsettings.js index ebd662f..f2e673b 100644 --- a/appsettings.js +++ b/appsettings.js @@ -3,4 +3,6 @@ module.exports = { oidcIssuer: process.env['OIDC_ISSUER'] ?? '', oidcClientId: process.env['OIDC_CLIENT_ID'] ?? '', oidcScope: process.env['OIDC_SCOPE'] ?? '', + backendApi: process.env['BACKEND_API'] ?? '', + }; diff --git a/components/DefautLayout.tsx b/components/DefautLayout.tsx index 973c396..9a515a1 100644 --- a/components/DefautLayout.tsx +++ b/components/DefautLayout.tsx @@ -36,71 +36,12 @@ const DefaultLayout: React.FC<{ menu.push( { - key: '#menu-1', - label: 'Menu 1', + key: '/main', + label: 'Main', icon: , - children: [ - { - key: '/dashboard', - label: 'Dashboard', - onClick: () => router.push('/dashboard') - }, - { - key: '/sub-menu-b', - label: 'Sub Menu B', - onClick: () => router.push('/') - }, - { - key: '/sub-menu-c', - label: 'Sub Menu C', - onClick: () => router.push('/') - } - ] }, - { - key: '#menu-2', - label: 'Menu 2', - icon: , - children: [ - { - key: '/sub-menu-d', - label: 'Sub Menu D', - onClick: () => router.push('/') - }, - { - key: '/sub-menu-e', - label: 'Sub Menu E', - onClick: () => router.push('/') - }, - { - key: '/sub-menu-f', - label: 'Sub Menu F', - onClick: () => router.push('/') - } - ] - }, - { - key: '#menu-3', - label: 'Menu 3', - icon: , - children: [ - { - key: '/sub-menu-g', - label: 'Sub Menu G', - onClick: () => router.push('/') - }, - { - key: '/sub-menu-h', - label: 'Sub Menu H', - onClick: () => router.push('/') - }, - { - key: '/sub-menu-i', - label: 'Sub Menu I', - onClick: () => router.push('/') - } - ] - } + + ); if (status === 'authenticated') { @@ -124,7 +65,7 @@ const DefaultLayout: React.FC<{ icon: , onClick: () => { nProgress.start(); - signIn('oidc'); + router.push('/login') } }); } diff --git a/pages/api/be/[...apiGateway].ts b/pages/api/be/[...apiGateway].ts index 3dea920..1fbacc9 100644 --- a/pages/api/be/[...apiGateway].ts +++ b/pages/api/be/[...apiGateway].ts @@ -5,7 +5,7 @@ import { AppSettings } from '../../../functions/AppSettings'; // Great way to avoid using CORS and making API calls from HTTPS pages to back-end HTTP servers // Recommendation for projects in Kubernetes cluster: set target to Service DNS name instead of public DNS name const server = Proxy.createProxyServer({ - target: AppSettings.current.backendApiHost, + target: AppSettings.current.backendApi, // changeOrigin to support name-based virtual hosting changeOrigin: true, xfwd: true, @@ -23,7 +23,7 @@ server.on('proxyReq', (proxyReq, req) => { } proxyReq.removeHeader('cookie'); // console.log(JSON.stringify(proxyReq.getHeaders(), null, 4)); - console.log('API Proxy:', req.url, '-->', AppSettings.current.backendApiHost + urlRewrite); + console.log('API Proxy:', req.url, '-->', AppSettings.current.backendApi+ urlRewrite); }); const apiGateway = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/pages/createOrder/index.tsx b/pages/createOrder/index.tsx new file mode 100644 index 0000000..21f5620 --- /dev/null +++ b/pages/createOrder/index.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; +import { WithDefaultLayout } from "@/components/DefautLayout"; +import { Page } from "@/types/Page"; + +const CreateOrderPage: Page = () => { + const [description, setDescription] = useState(""); + const [orderFrom, setOrderFrom] = useState(""); + const [orderTo, setOrderTo] = useState(""); + const [quantity, setQuantity] = useState(""); + const [orderAt, setOrderAt] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + try { + if (!description || !orderFrom || !orderTo || !quantity || !orderAt) { + throw new Error("Please fill in all fields."); + } + + if (description.length > 100) { + throw new Error("Description must be maximum 100 characters long."); + } + + const parsedQuantity = parseInt(quantity); + if (isNaN(parsedQuantity) || parsedQuantity <= 0) { + throw new Error("Quantity must be a positive number."); + } + + if (!isValidDate(orderAt)) { + throw new Error( + "Invalid date format for Order At. Please use YYYY-MM-DD format." + ); + } + + const response = await fetch("/api/be/api/v1/Order/CreateOrder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + description, + orderFrom, + orderTo, + quantity: parsedQuantity, + orderAt, + }), + }); + + if (!response.ok) { + throw new Error("Failed to add product."); + } + + // Reset form fields and error message on successful submission + setDescription(""); + setOrderFrom(""); + setOrderTo(""); + setQuantity(""); + setOrderAt(""); + setErrorMessage(""); + } catch (error) { + if (error instanceof Error) { + setErrorMessage(error.message); + } else { + setErrorMessage("An unknown error occurred."); + } + } + }; + + const isValidDate = (dateString: string): boolean => { + const regex = /^\d{4}-\d{2}-\d{2}$/; + return regex.test(dateString); + }; + + return ( +
+
+

Add Product

+
+
+ + setDescription(e.target.value)} + maxLength={100} + className="w-full border border-gray-300 rounded px-3 py-2" + /> +
+
+ + setOrderFrom(e.target.value)} + className="w-full border border-gray-300 rounded px-3 py-2" + /> +
+
+ + setOrderTo(e.target.value)} + className="w-full border border-gray-300 rounded px-3 py-2" + /> +
+
+ + setQuantity(e.target.value)} + className="w-full border border-gray-300 rounded px-3 py-2" + /> +
+
+ + setOrderAt(e.target.value)} + className="w-full border border-gray-300 rounded px-3 py-2" + /> +
+ +
+ {errorMessage &&

{errorMessage}

} +
+
+ ); +}; +CreateOrderPage.layout = WithDefaultLayout; +export default CreateOrderPage; diff --git a/pages/login/index.tsx b/pages/login/index.tsx new file mode 100644 index 0000000..0d9f84e --- /dev/null +++ b/pages/login/index.tsx @@ -0,0 +1,89 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useRouter } from "next/router"; + +const schema = z.object({ + email: z.string().nonempty(), + password: z.string().min(4, "Password at least 4 characters"), +}); + +type FormData = z.infer; + +const LoginPage: React.FC = () => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + }); + const router = useRouter(); + + const onSubmit = async (data: FormData) => { + try { + const response = await fetch("/api/be/api/v1/Auth/Login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error("Login failed. Please check your credentials."); + } + console.log("Login successful!"); + } catch (error: any) { + console.error("Login error:", error.message); + } + }; + + return ( +
+
+

Log In

+
+
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+
+ + + {errors.password && ( +

{errors.password.message}

+ )} +
+ +
+

+ No Account ?{" "} + + Register here + +

+
+
+ ); +}; + +export default LoginPage; diff --git a/pages/main.tsx b/pages/main.tsx new file mode 100644 index 0000000..0e144f7 --- /dev/null +++ b/pages/main.tsx @@ -0,0 +1,423 @@ +import React, { useState, useEffect } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { WithDefaultLayout } from "@/components/DefautLayout"; +import { Page } from "@/types/Page"; +import { + faFilter, + faEye, + faPenToSquare, + faTrash, + faChevronLeft, + faChevronRight, + faPlus, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; +import { useRouter } from "next/router"; + +const ViewPage: Page = () => { + const [data, setData] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(5); + const [selectedOrder, setSelectedOrder] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editedOrder, setEditedOrder] = useState({}); + const [validationErrors, setValidationErrors] = useState({}); + const [description, setDescription] = useState(""); + const [descriptionError, setDescriptionError] = useState(""); + const router = useRouter(); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + const fetchedData = []; + for (let i = 1; i <= 24; i++) { + const response = await fetch(`/api/be/api/v1/Order/OrderDetail/${i}`); + if (!response.ok) { + throw new Error(`Failed to fetch data for ID ${i}`); + } + const responseData = await response.json(); + fetchedData.push(responseData); + } + setData(fetchedData); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + + const nextPage = () => setCurrentPage(currentPage + 1); + const prevPage = () => setCurrentPage(currentPage - 1); + + const deleteItem = async (id) => { + try { + const response = await fetch(`/api/be/api/v1/Order/DeleteOrder/${id}`, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error(`Failed to delete item with ID ${id}`); + } + const newData = data.filter((item) => item.orderId !== id); + setData(newData); + // Adjust current page if it exceeds the new total number of pages + const totalPages = Math.ceil(newData.length / itemsPerPage); + if (currentPage > totalPages) { + setCurrentPage(totalPages); + } + } catch (error) { + console.error("Error deleting item:", error); + } + }; + + const openModal = (item) => { + setSelectedOrder(item); + setModalVisible(true); + }; + + const openEditModal = (item) => { + setEditedOrder({ + orderId: item.orderId, + orderName: item.orderName, + orderFrom: item.orderFrom, + orderTo: item.orderTo, + quantity: item.quantity, + }); + setEditModalVisible(true); + }; + + const closeModal = () => { + setSelectedOrder(null); + setModalVisible(false); + }; + + const closeEditModal = () => { + setEditedOrder({}); + setValidationErrors({}); + setEditModalVisible(false); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setEditedOrder({ ...editedOrder, [name]: value }); + }; + + const handleDescriptionChange = (e) => { + const { value } = e.target; + setDescription(value); + if (!value.trim()) { + setDescriptionError("Description cannot be empty"); + } else if (value.length > 100) { + setDescriptionError("Description must be at most 100 characters"); + } else { + setDescriptionError(""); + } + }; + + const updateOrder = async () => { + try { + if ( + !editedOrder.orderFrom || + !editedOrder.orderTo || + !description.trim() || + description.length > 100 + ) { + setValidationErrors({ + orderFrom: !editedOrder.orderFrom + ? "Order From cannot be empty" + : null, + orderTo: !editedOrder.orderTo ? "Order To cannot be empty" : null, + description: !description.trim() + ? "Description cannot be empty" + : description.length > 100 + ? "Description must be at most 100 characters" + : null, + }); + return; + } + const response = await fetch("/api/be/api/v1/Order/UpdateOrder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...editedOrder, + description, + }), + }); + + if (!response.ok) { + throw new Error("Failed to update order"); + } + const updatedData = data.map((item) => + item.orderId === selectedOrder.orderId + ? { ...item, ...editedOrder, description } + : item + ); + setData(updatedData); + setEditModalVisible(false); + router.push("/main"); + } catch (error) { + console.error("Error updating order:", error); + } + }; + + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = data.slice(indexOfFirstItem, indexOfLastItem); + const totalPages = Math.ceil(data.length / itemsPerPage); + + return ( +
+ {currentItems.length > 0 && ( + + + + + + + + + + + + + + {currentItems.map((item, index) => ( + + + + + + + + + + ))} + +
+ No. + + Order Name + + Order From + + Order To + + Order At + + Quantity + Action
{item.orderId}Order {item.orderId}{item.orderFrom}{item.orderTo}{item.orderedAt}{item.quantity} + openModal(item)} + /> + openEditModal(item)} + /> + deleteItem(item.orderId)} + /> +
+ )} + {modalVisible && ( +
+
+
+ +
+
+ {selectedOrder && ( +
+

Order Detail

+

+ Order ID:{" "} + {selectedOrder.orderId} +

+

+ Order Name:{" "} + {selectedOrder.orderName} +

+

+ Order From:{" "} + {selectedOrder.orderFrom} +

+

+ Order To:{" "} + {selectedOrder.orderTo} +

+

+ Ordered At:{" "} + {selectedOrder.orderedAt} +

+

+ Quantity:{" "} + {selectedOrder.quantity} +

+
+ )} +
+
+
+ )} + {editModalVisible && ( +
+
+
+ +
+
+

Edit Order

+
+ +