Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
222 lines
8.2 KiB
TypeScript
222 lines
8.2 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, ChangeEvent } from "react"
|
|
import Link from "next/link"
|
|
import { motion } from "framer-motion"
|
|
import {
|
|
PlusCircle,
|
|
Truck,
|
|
BarChart3,
|
|
MessageSquare,
|
|
} from "lucide-react"
|
|
import { Card, CardContent } from "@/components/common/card"
|
|
import dynamic from "next/dynamic"
|
|
import { Product } from "@/lib/models/products"
|
|
import { Category } from "@/lib/models/categories"
|
|
import { clientFetch } from "@/lib/api"
|
|
import { toast } from "sonner"
|
|
|
|
const ProductModal = dynamic(() => import("@/components/modals/product-modal").then(mod => ({ default: mod.ProductModal })), {
|
|
loading: () => null
|
|
});
|
|
|
|
const actions = [
|
|
{
|
|
title: "Add Product",
|
|
icon: PlusCircle,
|
|
href: "/dashboard/products/new", // Fallback text
|
|
color: "bg-blue-500/10 text-blue-500",
|
|
description: "Create a new listing",
|
|
action: "modal"
|
|
},
|
|
{
|
|
title: "Process Orders",
|
|
icon: Truck,
|
|
href: "/dashboard/orders?status=paid",
|
|
color: "bg-emerald-500/10 text-emerald-500",
|
|
description: "Ship pending orders"
|
|
},
|
|
{
|
|
title: "Analytics",
|
|
icon: BarChart3,
|
|
href: "/dashboard/analytics",
|
|
color: "bg-purple-500/10 text-purple-500",
|
|
description: "View sales performance"
|
|
},
|
|
{
|
|
title: "Messages",
|
|
icon: MessageSquare,
|
|
href: "/dashboard/chats",
|
|
color: "bg-amber-500/10 text-amber-500",
|
|
description: "Chat with customers"
|
|
}
|
|
]
|
|
|
|
export default function QuickActions() {
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [categories, setCategories] = useState<Category[]>([]);
|
|
const [productData, setProductData] = useState<Product>({
|
|
name: "",
|
|
description: "",
|
|
unitType: "pcs",
|
|
category: "",
|
|
pricing: [{ minQuantity: 1, pricePerUnit: 0 }],
|
|
image: null,
|
|
costPerUnit: 0,
|
|
});
|
|
|
|
// Fetch categories on mount
|
|
useEffect(() => {
|
|
const fetchCategories = async () => {
|
|
try {
|
|
const data = await clientFetch('/categories');
|
|
setCategories(data);
|
|
} catch (error) {
|
|
console.error("Failed to fetch categories:", error);
|
|
}
|
|
};
|
|
fetchCategories();
|
|
}, []);
|
|
|
|
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
setProductData({ ...productData, [e.target.name]: e.target.value });
|
|
};
|
|
|
|
const handleTieredPricingChange = (e: ChangeEvent<HTMLInputElement>, index: number) => {
|
|
const updatedPricing = [...productData.pricing];
|
|
const name = e.target.name as "minQuantity" | "pricePerUnit";
|
|
updatedPricing[index][name] = e.target.valueAsNumber || 0;
|
|
setProductData({ ...productData, pricing: updatedPricing });
|
|
};
|
|
|
|
const handleAddTier = () => {
|
|
setProductData((prev) => ({
|
|
...prev,
|
|
pricing: [...prev.pricing, { minQuantity: 1, pricePerUnit: 0 }],
|
|
}));
|
|
};
|
|
|
|
const handleRemoveTier = (index: number) => {
|
|
setProductData((prev) => ({
|
|
...prev,
|
|
pricing: prev.pricing.filter((_, i) => i !== index),
|
|
}));
|
|
};
|
|
|
|
const handleSaveProduct = async (data: Product, file?: File | null) => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Prepare the product data
|
|
const payload = {
|
|
...data,
|
|
stockTracking: data.stockTracking ?? true,
|
|
currentStock: data.currentStock ?? 0,
|
|
lowStockThreshold: data.lowStockThreshold ?? 10,
|
|
stockStatus: data.stockStatus ?? 'out_of_stock'
|
|
};
|
|
|
|
const productResponse = await clientFetch("/products", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (file) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/products/${productResponse._id}/image`, {
|
|
method: "PUT",
|
|
headers: {
|
|
Authorization: `Bearer ${document.cookie.split("; ").find((row) => row.startsWith("Authorization="))?.split("=")[1]}`,
|
|
},
|
|
body: formData,
|
|
});
|
|
}
|
|
|
|
setModalOpen(false);
|
|
setProductData({
|
|
name: "",
|
|
description: "",
|
|
unitType: "pcs",
|
|
category: "",
|
|
pricing: [{ minQuantity: 1, pricePerUnit: 0 }],
|
|
image: null,
|
|
costPerUnit: 0,
|
|
});
|
|
toast.success("Product added successfully");
|
|
|
|
// Optional: trigger a refresh of products or stats if needed
|
|
// currently just closing modal
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast.error("Failed to save product");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{actions.map((action, index) => {
|
|
const isModalAction = action.action === "modal";
|
|
|
|
const CardContentWrapper = () => (
|
|
<Card className="h-full border-none bg-black/40 backdrop-blur-xl hover:bg-black/60 transition-all duration-300 group overflow-hidden relative">
|
|
<div className="absolute inset-0 border border-white/10 rounded-xl pointer-events-none group-hover:border-white/20 transition-colors" />
|
|
<div className={`absolute inset-0 bg-gradient-to-br ${action.color.split(' ')[0].replace('/10', '/5')} opacity-0 group-hover:opacity-100 transition-opacity duration-500`} />
|
|
|
|
<CardContent className="p-6 flex flex-col items-center text-center relative z-10">
|
|
<div className={`p-4 rounded-2xl ${action.color.replace('bg-', 'bg-opacity-10 bg-')} mb-4 group-hover:scale-110 transition-transform duration-300 shadow-lg shadow-black/20`}>
|
|
<action.icon className="h-6 w-6" />
|
|
</div>
|
|
<h3 className="font-bold text-lg text-white group-hover:text-primary transition-colors">{action.title}</h3>
|
|
<p className="text-sm text-zinc-400 mt-1">{action.description}</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
|
|
return (
|
|
<motion.div
|
|
key={action.title}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
whileHover={{ y: -5 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
>
|
|
{isModalAction ? (
|
|
<div onClick={() => setModalOpen(true)} className="cursor-pointer h-full">
|
|
<CardContentWrapper />
|
|
</div>
|
|
) : (
|
|
<Link href={action.href} className="h-full block">
|
|
<CardContentWrapper />
|
|
</Link>
|
|
)}
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<ProductModal
|
|
open={modalOpen}
|
|
onClose={() => setModalOpen(false)}
|
|
onSave={handleSaveProduct}
|
|
productData={productData}
|
|
categories={categories}
|
|
editing={false}
|
|
handleChange={handleChange}
|
|
handleTieredPricingChange={handleTieredPricingChange}
|
|
handleAddTier={handleAddTier}
|
|
handleRemoveTier={handleRemoveTier}
|
|
setProductData={setProductData}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
|