hmm
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, ChangeEvent } from "react";
|
import { useState, useEffect, ChangeEvent } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -6,7 +6,11 @@ import Layout from "@/components/kokonutui/layout";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Product } from "@/models/products";
|
import { Product } from "@/models/products";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { fetchProductData, saveProductData, deleteProductData } from "@/lib/productData";
|
import {
|
||||||
|
fetchProductData,
|
||||||
|
saveProductData,
|
||||||
|
deleteProductData,
|
||||||
|
} from "@/lib/productData";
|
||||||
import { ProductModal } from "@/components/product-modal";
|
import { ProductModal } from "@/components/product-modal";
|
||||||
import ProductTable from "@/components/product-table";
|
import ProductTable from "@/components/product-table";
|
||||||
|
|
||||||
@@ -66,7 +70,8 @@ export default function ProductsPage() {
|
|||||||
index: number
|
index: number
|
||||||
) => {
|
) => {
|
||||||
const updatedPricing = [...productData.tieredPricing];
|
const updatedPricing = [...productData.tieredPricing];
|
||||||
updatedPricing[index][e.target.name] = e.target.value;
|
const name = e.target.name as "minQuantity" | "pricePerUnit";
|
||||||
|
updatedPricing[index][name] = e.target.valueAsNumber || 0;
|
||||||
setProductData({ ...productData, tieredPricing: updatedPricing });
|
setProductData({ ...productData, tieredPricing: updatedPricing });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,7 +79,8 @@ export default function ProductsPage() {
|
|||||||
const handleSaveProduct = async (data: Product) => {
|
const handleSaveProduct = async (data: Product) => {
|
||||||
const adjustedPricing = data.tieredPricing.map((tier) => ({
|
const adjustedPricing = data.tieredPricing.map((tier) => ({
|
||||||
minQuantity: tier.minQuantity,
|
minQuantity: tier.minQuantity,
|
||||||
pricePerUnit: typeof tier.pricePerUnit === "string"
|
pricePerUnit:
|
||||||
|
typeof tier.pricePerUnit === "string"
|
||||||
? parseFloat(tier.pricePerUnit) // Convert string to number
|
? parseFloat(tier.pricePerUnit) // Convert string to number
|
||||||
: tier.pricePerUnit,
|
: tier.pricePerUnit,
|
||||||
}));
|
}));
|
||||||
@@ -136,9 +142,9 @@ export default function ProductsPage() {
|
|||||||
const handleEditProduct = (product: Product) => {
|
const handleEditProduct = (product: Product) => {
|
||||||
setProductData({
|
setProductData({
|
||||||
...product,
|
...product,
|
||||||
tieredPricing: product.pricing.map((tier) => ({
|
tieredPricing: product.tieredPricing.map(tier => ({
|
||||||
minQuantity: tier.minQuantity,
|
minQuantity: tier.minQuantity,
|
||||||
pricePerUnit: tier.pricePerUnit.toString(),
|
pricePerUnit: tier.pricePerUnit
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
|
|||||||
@@ -21,17 +21,7 @@ import {
|
|||||||
updateShippingMethod,
|
updateShippingMethod,
|
||||||
} from "@/lib/shippingHelper";
|
} from "@/lib/shippingHelper";
|
||||||
|
|
||||||
interface ShippingMethod {
|
import { ShippingMethod, ShippingData } from "@/lib/types";
|
||||||
_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"
|
import { ShippingTable } from "@/components/shipping-table"
|
||||||
|
|
||||||
@@ -51,9 +41,13 @@ export default function ShippingPage() {
|
|||||||
const authToken = document.cookie.split("Authorization=")[1];
|
const authToken = document.cookie.split("Authorization=")[1];
|
||||||
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(authToken);
|
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(authToken);
|
||||||
|
|
||||||
setShippingMethods(
|
// Ensure `_id` is always a string
|
||||||
fetchedMethods.filter((method) => method._id)
|
const sanitizedMethods: ShippingMethod[] = fetchedMethods.map((method) => ({
|
||||||
);
|
...method,
|
||||||
|
_id: method._id ?? "", // Default to empty string if undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
setShippingMethods(sanitizedMethods);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading shipping options:", error);
|
console.error("Error loading shipping options:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -114,7 +108,10 @@ export default function ShippingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditShipping = (shipping: ShippingMethod) => {
|
const handleEditShipping = (shipping: ShippingMethod) => {
|
||||||
setNewShipping({ ...shipping, _id: shipping._id ?? "" }); // Ensure _id is always a string
|
setNewShipping({
|
||||||
|
...shipping,
|
||||||
|
_id: shipping._id ?? "", // ✅ Ensure _id is always a string
|
||||||
|
});
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import Layout from "@/components/kokonutui/layout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { Send } from "lucide-react";
|
|
||||||
|
|
||||||
export default function WithdrawalsPageClient({ balances, withdrawals }) {
|
|
||||||
const [withdrawalData, setWithdrawalData] = useState({
|
|
||||||
currency: "bitcoin",
|
|
||||||
address: "",
|
|
||||||
amount: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleWithdraw = async () => {
|
|
||||||
const { currency, address, amount } = withdrawalData;
|
|
||||||
|
|
||||||
if (!currency || !address || !amount || parseFloat(amount) <= 0) {
|
|
||||||
alert("Please provide valid withdrawal details.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const authToken = document.cookie.split("authToken=")[1];
|
|
||||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/withdraw`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${authToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
credentials: "include",
|
|
||||||
body: JSON.stringify({ currency, address, amount }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Failed to process withdrawal");
|
|
||||||
|
|
||||||
alert("Withdrawal request submitted successfully!");
|
|
||||||
setWithdrawalData({ currency: "bitcoin", address: "", amount: "" });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing withdrawal:", error);
|
|
||||||
alert("Failed to process withdrawal. Please try again.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setWithdrawalData({ ...withdrawalData, [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Withdraw Funds
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Balances Display */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<Card className="shadow-sm bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Bitcoin Balance</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{balances.bitcoin} BTC
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="shadow-sm bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Litecoin Balance</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{balances.litecoin} LTC
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="shadow-sm bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Monero Balance</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{balances.monero} XMR
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Withdrawal Form */}
|
|
||||||
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg shadow border border-gray-200 dark:border-gray-700 space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
Request Withdrawal
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Currency
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={withdrawalData.currency}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setWithdrawalData({ ...withdrawalData, currency: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Select Currency" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="bitcoin">Bitcoin</SelectItem>
|
|
||||||
<SelectItem value="litecoin">Litecoin</SelectItem>
|
|
||||||
<SelectItem value="monero">Monero</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Withdrawal Address
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
name="address"
|
|
||||||
placeholder="Enter the withdrawal address"
|
|
||||||
value={withdrawalData.address}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
Amount
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
name="amount"
|
|
||||||
placeholder="Enter the amount to withdraw"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="any"
|
|
||||||
value={withdrawalData.amount}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={handleWithdraw} className="w-full">
|
|
||||||
<Send className="mr-2 h-5 w-5" />
|
|
||||||
Request Withdrawal
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Withdrawals History */}
|
|
||||||
<div className="bg-white dark:bg-zinc-800 p-6 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
||||||
Withdrawals History
|
|
||||||
</h2>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Date</TableHead>
|
|
||||||
<TableHead>Currency</TableHead>
|
|
||||||
<TableHead>Amount</TableHead>
|
|
||||||
<TableHead>Address</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{withdrawals.length > 0 ? (
|
|
||||||
withdrawals.map((withdrawal) => (
|
|
||||||
<TableRow key={withdrawal.id}>
|
|
||||||
<TableCell>{withdrawal.date}</TableCell>
|
|
||||||
<TableCell>{withdrawal.currency.toUpperCase()}</TableCell>
|
|
||||||
<TableCell>{withdrawal.amount}</TableCell>
|
|
||||||
<TableCell>{withdrawal.address}</TableCell>
|
|
||||||
<TableCell>{withdrawal.status}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="text-center">
|
|
||||||
No withdrawals found.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import WithdrawalsPageClient from "./WithdrawalsPageClient";
|
|
||||||
|
|
||||||
export default async function WithdrawalsPage() {
|
|
||||||
const authToken = ""; // Fetch token from cookies if needed
|
|
||||||
const [balancesRes, withdrawalsRes] = await Promise.all([
|
|
||||||
fetch(`${process.env.NEXT_PUBLIC_API_URL}/balances`, {
|
|
||||||
headers: { Authorization: `Bearer ${authToken}` },
|
|
||||||
}),
|
|
||||||
fetch(`${process.env.NEXT_PUBLIC_API_URL}/withdrawals`, {
|
|
||||||
headers: { Authorization: `Bearer ${authToken}` },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const balances = balancesRes.ok ? await balancesRes.json() : {};
|
|
||||||
const withdrawals = withdrawalsRes.ok ? await withdrawalsRes.json() : [];
|
|
||||||
|
|
||||||
return <WithdrawalsPageClient balances={balances} withdrawals={withdrawals} />;
|
|
||||||
}
|
|
||||||
@@ -30,16 +30,14 @@ interface ProductModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (productData: Product) => void;
|
onSave: (productData: Product) => void;
|
||||||
productData: Product;
|
productData: Product;
|
||||||
categories: Category[]; // Define categories as an array of Category
|
categories: Category[];
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
handleChange: (
|
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||||
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
setProductData: React.Dispatch<React.SetStateAction<Product>>;
|
||||||
) => void;
|
|
||||||
handleTieredPricingChange: (
|
handleTieredPricingChange: (
|
||||||
e: ChangeEvent<HTMLInputElement>,
|
e: ChangeEvent<HTMLInputElement>,
|
||||||
index: number
|
index: number
|
||||||
) => void;
|
) => void;
|
||||||
setProductData: React.Dispatch<React.SetStateAction<Product>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProductModal = ({
|
export const ProductModal = ({
|
||||||
@@ -55,10 +53,10 @@ export const ProductModal = ({
|
|||||||
}: ProductModalProps) => {
|
}: ProductModalProps) => {
|
||||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||||
|
|
||||||
// Update image preview when product data changes (for editing purposes)
|
// Update image preview when product data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (productData.image && typeof productData.image === "string") {
|
if (productData.image && typeof productData.image === "string") {
|
||||||
setImagePreview(productData.image); // For existing product image
|
setImagePreview(productData.image);
|
||||||
}
|
}
|
||||||
}, [productData.image]);
|
}, [productData.image]);
|
||||||
|
|
||||||
@@ -73,21 +71,23 @@ export const ProductModal = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle tiered pricing change
|
// ✅ FIXED: Moved Inside the Component & Used Type Assertion
|
||||||
const handleTieredPricingChangeInternal = (
|
const handleTieredPricingChangeInternal = (
|
||||||
e: ChangeEvent<HTMLInputElement>,
|
e: ChangeEvent<HTMLInputElement>,
|
||||||
index: number
|
index: number
|
||||||
) => {
|
) => {
|
||||||
const updatedPricing = [...productData.tieredPricing];
|
const updatedPricing = [...productData.tieredPricing];
|
||||||
const updatedTier = updatedPricing[index];
|
const field = e.target.name as keyof (typeof productData.tieredPricing)[number];
|
||||||
|
|
||||||
// Ensure pricePerUnit is a number
|
updatedPricing[index] = {
|
||||||
const value = e.target.name === "pricePerUnit"
|
...updatedPricing[index],
|
||||||
? parseFloat(e.target.value) || 0 // If value is invalid, default to 0
|
[field]: e.target.valueAsNumber || 0,
|
||||||
: e.target.value;
|
};
|
||||||
|
|
||||||
updatedTier[e.target.name] = value;
|
setProductData({
|
||||||
setProductData({ ...productData, tieredPricing: updatedPricing });
|
...productData,
|
||||||
|
tieredPricing: updatedPricing,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,36 +98,21 @@ export const ProductModal = ({
|
|||||||
{editing ? "Edit Product" : "Add Product"}
|
{editing ? "Edit Product" : "Add Product"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Product Name</label>
|
<label className="text-sm font-medium">Product Name</label>
|
||||||
<Input
|
<Input name="name" placeholder="Product Name" value={productData.name} onChange={handleChange} />
|
||||||
name="name"
|
|
||||||
placeholder="Product Name"
|
|
||||||
value={productData.name}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Description</label>
|
<label className="text-sm font-medium">Description</label>
|
||||||
<Textarea
|
<Textarea name="description" placeholder="Product Description" value={productData.description} onChange={handleChange} rows={3} />
|
||||||
name="description"
|
|
||||||
placeholder="Product Description"
|
|
||||||
value={productData.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Category</label>
|
<label className="text-sm font-medium">Category</label>
|
||||||
<Select
|
<Select value={productData.category} onValueChange={(value) => setProductData({ ...productData, category: value })}>
|
||||||
value={productData.category}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setProductData({ ...productData, category: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a category" />
|
<SelectValue placeholder="Select a category" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -141,125 +126,23 @@ export const ProductModal = ({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{productData.category && (
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-sm font-medium">Tiered Pricing</h3>
|
<h3 className="text-sm font-medium">Tiered Pricing</h3>
|
||||||
{productData.tieredPricing.map((tier, index) => (
|
{productData.tieredPricing.map((tier, index) => (
|
||||||
<div key={index} className="flex gap-2">
|
<div key={index} className="flex gap-2">
|
||||||
<div className="flex-1 space-y-1">
|
<Input name="minQuantity" type="number" min="1" placeholder="Quantity" value={tier.minQuantity} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
|
||||||
<label className="text-xs text-muted-foreground">
|
<Input name="pricePerUnit" type="number" step="0.01" placeholder="Price" value={tier.pricePerUnit} onChange={(e) => handleTieredPricingChangeInternal(e, index)} />
|
||||||
Quantity
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
name="minQuantity"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
placeholder="Quantity"
|
|
||||||
value={tier.minQuantity}
|
|
||||||
onChange={(e) => handleTieredPricingChangeInternal(e, index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 space-y-1">
|
|
||||||
<label className="text-xs text-muted-foreground">
|
|
||||||
Price
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
name="pricePerUnit"
|
|
||||||
type="number"
|
|
||||||
step="0.01"
|
|
||||||
placeholder="Price"
|
|
||||||
value={tier.pricePerUnit}
|
|
||||||
onChange={(e) => handleTieredPricingChangeInternal(e, index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button variant="outline" size="sm" onClick={() => setProductData({ ...productData, tieredPricing: [...productData.tieredPricing, { minQuantity: 1, pricePerUnit: 0 }] })}>
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="mt-2"
|
|
||||||
onClick={() =>
|
|
||||||
setProductData({
|
|
||||||
...productData,
|
|
||||||
tieredPricing: [
|
|
||||||
...productData.tieredPricing,
|
|
||||||
{ minQuantity: 1, pricePerUnit: 0 }, // Initialize pricePerUnit as a number
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
+ Add Tier
|
+ Add Tier
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Unit Type</label>
|
|
||||||
<Select
|
|
||||||
value={productData.unitType}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setProductData({ ...productData, unitType: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select unit" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="pcs">Pieces</SelectItem>
|
|
||||||
<SelectItem value="gr">Grams</SelectItem>
|
|
||||||
<SelectItem value="kg">Kilograms</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Upload Section */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Product Image</label>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
className="file:text-foreground"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
/>
|
|
||||||
{imagePreview && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<img
|
|
||||||
src={imagePreview}
|
|
||||||
alt="Product preview"
|
|
||||||
className="rounded-md border w-32 h-32 object-cover"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Click the upload button above to change image
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!imagePreview &&
|
|
||||||
productData.image &&
|
|
||||||
typeof productData.image === "string" && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<img
|
|
||||||
src={productData.image}
|
|
||||||
alt="Existing product"
|
|
||||||
className="rounded-md border w-32 h-32 object-cover"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Existing product image
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Upload a product image (JPEG, PNG, WEBP)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="flex justify-end gap-2">
|
<DialogFooter className="flex justify-end gap-2">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>Cancel</Button>
|
||||||
Cancel
|
<Button onClick={() => onSave(productData)}>{editing ? "Update Product" : "Create Product"}</Button>
|
||||||
</Button>
|
|
||||||
<Button onClick={() => onSave(productData)}>
|
|
||||||
{editing ? "Update Product" : "Create Product"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -5,20 +5,17 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
interface ShippingData {
|
|
||||||
name: string;
|
import { ShippingData } from "@/lib/types";
|
||||||
price: number;
|
|
||||||
_id?: string; // Optional for new entry
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShippingModalProps {
|
interface ShippingModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (shippingData: ShippingData) => void;
|
onSave: (shippingData: ShippingData) => void; // ✅ Allow passing shippingData
|
||||||
shippingData: { name: string; price: number }; // Define type of shippingData
|
shippingData: ShippingData;
|
||||||
|
setShippingData: React.Dispatch<React.SetStateAction<ShippingData>>;
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
setShippingData: React.Dispatch<React.SetStateAction<{ name: string; price: 0.00 }>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShippingModal = ({
|
export const ShippingModal = ({
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Edit, Trash } from "lucide-react";
|
import { Edit, Trash } from "lucide-react";
|
||||||
|
|
||||||
interface ShippingMethod {
|
import { ShippingMethod } from "@/lib/types";
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
price: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ShippingTableProps {
|
interface ShippingTableProps {
|
||||||
shippingMethods: ShippingMethod[];
|
shippingMethods: ShippingMethod[];
|
||||||
@@ -46,9 +49,7 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
|
|||||||
className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70"
|
className="transition-colors hover:bg-gray-50 dark:hover:bg-zinc-800/70"
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium">{method.name}</TableCell>
|
<TableCell className="font-medium">{method.name}</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">£{method.price}</TableCell>
|
||||||
£{method.price}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -63,7 +64,7 @@ export const ShippingTable: React.FC<ShippingTableProps> = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-red-600 hover:text-red-700 dark:text-red-400"
|
className="text-red-600 hover:text-red-700 dark:text-red-400"
|
||||||
onClick={() => onDeleteShipping(method._id)}
|
onClick={() => onDeleteShipping(method._id ?? "")}
|
||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
12
lib/types.ts
Normal file
12
lib/types.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// lib/types.ts
|
||||||
|
export interface ShippingMethod {
|
||||||
|
_id?: string; // Optional before saving, required after fetching
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShippingData {
|
||||||
|
_id?: string; // Optional before saving
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
// In models/products.ts
|
||||||
export interface Product {
|
export interface Product {
|
||||||
_id?: string;
|
_id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
unitType: "pcs" | "gr" | "kg";
|
unitType: string;
|
||||||
category: string;
|
category: string;
|
||||||
tieredPricing: { minQuantity: number; pricePerUnit: number }[]; // Make pricePerUnit a number
|
tieredPricing: Array<{
|
||||||
image?: string | null;
|
minQuantity: number;
|
||||||
|
pricePerUnit: number;
|
||||||
|
}>;
|
||||||
|
image?: string | File | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
908
package-lock.json
generated
908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -63,6 +63,7 @@
|
|||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
|
|||||||
Reference in New Issue
Block a user