diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -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. diff --git a/app/dashboard/orders/[id]/page.tsx b/app/dashboard/orders/[id]/page.tsx new file mode 100644 index 0000000..51a0cba --- /dev/null +++ b/app/dashboard/orders/[id]/page.tsx @@ -0,0 +1,122 @@ +"use client" + +import { useParams } from "next/navigation" +import { useState } from "react" +import Layout from "@/components/kokonutui/layout" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ArrowLeft, Truck, DollarSign, Package, User } from "lucide-react" +import Link from "next/link" + +export default function OrderDetails() { + const params = useParams() + const orderId = params.id + + // In a real application, you would fetch the order details based on the orderId + const [trackingNumber, setTrackingNumber] = useState("") + + const order = { + id: orderId, + customer: "John Doe", + email: "john.doe@example.com", + date: "2023-05-01", + total: "$150.00", + status: "Processing", + tracking: "", + items: [ + { id: 1, name: "Product A", quantity: 2, price: "$50.00" }, + { id: 2, name: "Product B", quantity: 1, price: "$50.00" }, + ], + shippingAddress: "123 Main St, Anytown, AN 12345", + } + + const handleSaveTracking = () => { + // TODO: Implement API call to save tracking number + console.log("Tracking number saved:", trackingNumber) + } + + return ( + +
+
+

Order Details: {order.id}

+ + + +
+ +
+
+

+ + Customer Information +

+

Name: {order.customer}

+

Email: {order.email}

+

Shipping Address: {order.shippingAddress}

+
+ +
+

+ + Order Summary +

+

Order Date: {order.date}

+

Total: {order.total}

+

Status: {order.status}

+ + {/* Tracking Input Field */} +
+

Tracking Number:

+ setTrackingNumber(e.target.value)} + className="mt-2" + /> + +
+
+
+ +
+

Order Items

+ + + + + + + + + + {order.items.map((item) => ( + + + + + + ))} + +
ProductQuantityPrice
{item.name}{item.quantity}{item.price}
+
+ +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/dashboard/orders/page.tsx b/app/dashboard/orders/page.tsx new file mode 100644 index 0000000..c121649 --- /dev/null +++ b/app/dashboard/orders/page.tsx @@ -0,0 +1,21 @@ +import Dashboard from "@/components/kokonutui/dashboard"; +import { Package } from "lucide-react"; +import OrderTable from "@/components/order-table" + +export default function OrdersPage() { + return ( + +
+
+

+ + Orders +

+
+ + {/* ✅ Order Table Component */} + +
+
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..08a49e2 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,38 @@ +import Dashboard from "@/components/kokonutui/dashboard"; +import Content from "@/components/kokonutui/content"; +import { fetchWithAuthorization } from "@/lib/server-utils" + +// ✅ Corrected Vendor Type +interface Vendor { + _id: string; + username: string; + storeId: string; + pgpKey: string; + __v: number; +} + +interface User { + vendor: Vendor; +} + +interface OrderStats { + totalOrders: number; + pendingOrders: number; + completedOrders: number; + cancelledOrders: number; +} + +export default async function DashboardPage() { + const [userResponse, orderStats] = await Promise.all([ + fetchWithAuthorization("/auth/me"), + fetchWithAuthorization("/orders/stats"), + ]); + + const vendor = userResponse.vendor; + + return ( + + + + ); +} \ No newline at end of file diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx new file mode 100644 index 0000000..b0fa90d --- /dev/null +++ b/app/dashboard/products/page.tsx @@ -0,0 +1,203 @@ +"use client" + +import { useState, useEffect, ChangeEvent } from "react"; +import { useRouter } from "next/navigation"; +import Layout from "@/components/kokonutui/layout"; +import { Button } from "@/components/ui/button"; +import { Product } from "@/models/products"; +import { Plus } from "lucide-react"; +import { fetchProductData, saveProductData, deleteProductData } from "@/lib/productData"; +import { ProductModal } from "@/components/product-modal"; +import ProductTable from "@/components/product-table"; + +export default function ProductsPage() { + const router = useRouter(); + const [products, setProducts] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState(false); + const [imagePreview, setImagePreview] = useState(null); + const [productData, setProductData] = useState({ + name: "", + description: "", + unitType: "pcs", + category: "", + tieredPricing: [{ minQuantity: 1, pricePerUnit: 0 }], + image: null, + }); + + // Fetch products and categories + useEffect(() => { + const fetchDataAsync = async () => { + try { + const authToken = document.cookie.split("Authorization=")[1]; + + const [fetchedProducts, fetchedCategories] = await Promise.all([ + fetchProductData( + `${process.env.NEXT_PUBLIC_API_URL}/products`, + authToken + ), + fetchProductData( + `${process.env.NEXT_PUBLIC_API_URL}/categories`, + authToken + ), + ]); + + setProducts(fetchedProducts); + setCategories(fetchedCategories); + } catch (error) { + console.error("Error loading data:", error); + } finally { + setLoading(false); + } + }; + fetchDataAsync(); + }, []); + + // Handle input changes + const handleChange = ( + e: ChangeEvent + ) => setProductData({ ...productData, [e.target.name]: e.target.value }); + + // Handle tiered pricing changes + const handleTieredPricingChange = ( + e: ChangeEvent, + index: number + ) => { + const updatedPricing = [...productData.tieredPricing]; + updatedPricing[index][e.target.name] = e.target.value; + setProductData({ ...productData, tieredPricing: updatedPricing }); + }; + + // Save product data after modal form submission + const handleSaveProduct = async (data: Product) => { + const adjustedPricing = data.tieredPricing.map((tier) => ({ + minQuantity: tier.minQuantity, + pricePerUnit: typeof tier.pricePerUnit === "string" + ? parseFloat(tier.pricePerUnit) // Convert string to number + : tier.pricePerUnit, + })); + + const productToSave = { + ...data, + pricing: adjustedPricing, + imageBase64: imagePreview || "", + }; + + try { + const authToken = document.cookie.split("Authorization=")[1]; + const apiUrl = editing + ? `${process.env.NEXT_PUBLIC_API_URL}/products/${data._id}` + : `${process.env.NEXT_PUBLIC_API_URL}/products`; + + const savedProduct = await saveProductData( + apiUrl, + productToSave, + authToken, + editing ? "PUT" : "POST" + ); + + setProducts((prevProducts) => { + if (editing) { + return prevProducts.map((product) => + product._id === savedProduct._id ? savedProduct : product + ); + } else { + return [...prevProducts, savedProduct]; + } + }); + + setModalOpen(false); // Close modal after saving + } catch (error) { + console.error("Error saving product:", error); + } + }; + + // Handle delete product + const handleDeleteProduct = async (productId: string) => { + const authToken = document.cookie.split("Authorization=")[1]; + try { + await deleteProductData( + `${process.env.NEXT_PUBLIC_API_URL}/products/${productId}`, + authToken + ); + + // Remove the product from the state + setProducts((prevProducts) => + prevProducts.filter((product) => product._id !== productId) + ); + } catch (error) { + console.error("Error deleting product:", error); + } + }; + + // Edit product + const handleEditProduct = (product: Product) => { + setProductData({ + ...product, + tieredPricing: product.pricing.map((tier) => ({ + minQuantity: tier.minQuantity, + pricePerUnit: tier.pricePerUnit.toString(), + })), + }); + setEditing(true); + setModalOpen(true); + }; + + // Reset product data when adding a new product + const handleAddNewProduct = () => { + setProductData({ + name: "", + description: "", + unitType: "pcs", + category: "", + tieredPricing: [{ minQuantity: 1, pricePerUnit: 0 }], + image: null, + }); + setEditing(false); + setModalOpen(true); + }; + + // Get category name by ID + const getCategoryNameById = (categoryId: string): string => { + const category = categories.find((cat) => cat._id === categoryId); + return category ? category.name : "Unknown Category"; + }; + + return ( + +
+
+

+ Product Inventory +

+ +
+ + + + setModalOpen(false)} + onSave={handleSaveProduct} + productData={productData} + categories={categories} + editing={editing} + handleChange={handleChange} + handleTieredPricingChange={handleTieredPricingChange} + setProductData={setProductData} + /> +
+
+ ); +} diff --git a/app/dashboard/shipping/page.tsx b/app/dashboard/shipping/page.tsx new file mode 100644 index 0000000..f88993f --- /dev/null +++ b/app/dashboard/shipping/page.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useState, useEffect, ChangeEvent } from "react"; +import Layout from "@/components/kokonutui/layout"; +import { Edit, Plus, Trash } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { ShippingModal } from "@/components/shipping-modal"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + fetchShippingMethods, + addShippingMethod, + deleteShippingMethod, + updateShippingMethod, +} from "@/lib/shippingHelper"; + +interface ShippingMethod { + _id?: string; // Required after fetching + name: string; + price: number; +} + +interface ShippingData { + _id?: string; // Optional for new entry + name: string; + price: number; +} + +import { ShippingTable } from "@/components/shipping-table" + +export default function ShippingPage() { + const [shippingMethods, setShippingMethods] = useState([]); + const [newShipping, setNewShipping] = useState({ + name: "", + price: 0, + }); + const [loading, setLoading] = useState(true); + const [modalOpen, setModalOpen] = useState(false); + const [editing, setEditing] = useState(false); + + useEffect(() => { + const fetchShippingMethodsData = async () => { + try { + const authToken = document.cookie.split("Authorization=")[1]; + const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(authToken); + + setShippingMethods( + fetchedMethods.filter((method) => method._id) + ); + } catch (error) { + console.error("Error loading shipping options:", error); + } finally { + setLoading(false); + } + }; + + fetchShippingMethodsData(); + }, []); + + const handleAddShipping = async () => { + if (!newShipping.name || !newShipping.price) return; + + try { + const authToken = document.cookie.split("Authorization=")[1]; + const updatedMethods: ShippingMethod[] = await addShippingMethod(authToken, newShipping); + + setShippingMethods(updatedMethods); + setNewShipping({ name: "", price: 0 }); // No `_id` needed for new entry + setModalOpen(false); + } catch (error) { + console.error("Error adding shipping method:", error); + } + }; + + const handleUpdateShipping = async () => { + if (!newShipping.name || !newShipping.price || !newShipping._id) return; // Ensure `_id` exists + + try { + const authToken = document.cookie.split("Authorization=")[1]; + const updatedShipping: ShippingMethod = await updateShippingMethod(authToken, newShipping._id, newShipping); + + setShippingMethods((prevMethods) => + prevMethods.map((method) => (method._id === updatedShipping._id ? updatedShipping : method)) + ); + setNewShipping({ name: "", price: 0 }); + setEditing(false); + setModalOpen(false); + } catch (error) { + console.error("Error updating shipping method:", error); + } + }; + + const handleDeleteShipping = async (_id: string) => { + try { + const authToken = document.cookie.split("Authorization=")[1]; + const response = await deleteShippingMethod(authToken, _id); + if (response.success) { + setShippingMethods((prevMethods) => + prevMethods.filter((method) => method._id !== _id) + ); + } else { + console.error("Deletion was not successful."); + } + } catch (error) { + console.error("Error deleting shipping method:", error); + } + }; + + const handleEditShipping = (shipping: ShippingMethod) => { + setNewShipping({ ...shipping, _id: shipping._id ?? "" }); // Ensure _id is always a string + setEditing(true); + setModalOpen(true); + }; + + return ( + +
+
+

+ Manage Shipping Options +

+ +
+ + {/* Shipping Methods Table */} + +
+ + {/* Shipping Modal */} + setModalOpen(false)} + onSave={editing ? handleUpdateShipping : handleAddShipping} + shippingData={newShipping} + editing={editing} + handleChange={(e) => + setNewShipping({ ...newShipping, [e.target.name]: e.target.value }) + } + setShippingData={setNewShipping} + /> +
+ ); +} diff --git a/app/dashboard/storefront/page.tsx b/app/dashboard/storefront/page.tsx new file mode 100644 index 0000000..ca052ab --- /dev/null +++ b/app/dashboard/storefront/page.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState, useEffect, ChangeEvent } from "react"; +import { useRouter } from "next/navigation"; +import Layout from "@/components/kokonutui/layout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Save, Send, Key, MessageSquare, Shield } from "lucide-react"; +import { apiRequest } from "@/lib/storeHelper"; +import { toast, Toaster } from "sonner"; +import BroadcastDialog from "@/components/broadcast-dialog"; + +// ✅ Define the Storefront Type +interface Storefront { + pgpKey: string; + welcomeMessage: string; + telegramToken: string; +} + +export default function StorefrontPage() { + const router = useRouter(); + const [storefront, setStorefront] = useState({ + pgpKey: "", + welcomeMessage: "", + telegramToken: "", + }); + + const [broadcastOpen, setBroadcastOpen] = useState(false); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // ✅ Fetch Storefront Data + useEffect(() => { + const fetchStorefront = async () => { + try { + setLoading(true); + const data: Storefront = await apiRequest("/storefront"); + setStorefront(data); + } catch (error) { + toast.error("Failed to load storefront data."); + } finally { + setLoading(false); + } + }; + + fetchStorefront(); + }, []); + + // ✅ Handle Form Input Changes + const handleInputChange = (e: ChangeEvent) => { + setStorefront({ ...storefront, [e.target.name]: e.target.value }); + }; + + // ✅ Save Storefront Changes + const saveStorefront = async () => { + try { + setSaving(true); + await apiRequest("/storefront", "PUT", storefront); + toast.success("Storefront updated successfully!"); + } catch (error) { + toast.error("Failed to update storefront."); + } finally { + setSaving(false); + } + }; + + return ( + + {/* ✅ Dark Themed Toaster for Notifications */} + + + {/* Broadcast Dialog Component */} + + +
+ {/* PGP Key Section */} +
+
+ +

+ PGP Encryption Key +

+
+
+