ecom / client /src /pages /admin-seller.tsx
shashwatIDR's picture
Upload 147 files
b89a86e verified
import { useState, useRef } from "react";
import { useLocation } from "wouter";
import { useAuth } from "@/hooks/use-auth";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { sellerApi, categoriesApi, storesApi, productsApi, authApi, ordersApi } from "@/lib/api";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { Store, Package, Plus, Edit, Trash2, Upload, LogOut, Loader2, ShoppingCart, Clock, CheckCircle, MapPin, ExternalLink, Camera } from "lucide-react";
const sellerLoginSchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
});
const storeSchema = z.object({
name: z.string().min(1, "Store name is required"),
description: z.string().optional(),
});
const productSchema = z.object({
title: z.string().min(1, "Product title is required"),
description: z.string().min(1, "Product description is required"),
price: z.string().min(1, "Price is required"),
originalPrice: z.string().optional(),
stock: z.string().min(1, "Stock quantity is required"),
categoryId: z.string().min(1, "Category is required"),
});
type SellerLoginFormData = z.infer<typeof sellerLoginSchema>;
type StoreFormData = z.infer<typeof storeSchema>;
type ProductFormData = z.infer<typeof productSchema>;
export default function AdminSeller() {
const [, setLocation] = useLocation();
const { seller, userType, logout, sellerLogin } = useAuth();
const { toast } = useToast();
const queryClient = useQueryClient();
const [isStoreDialogOpen, setIsStoreDialogOpen] = useState(false);
const [isProductDialogOpen, setIsProductDialogOpen] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null);
const [capturedFiles, setCapturedFiles] = useState<File[]>([]);
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [isCameraOpen, setIsCameraOpen] = useState(false);
const [stream, setStream] = useState<MediaStream | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [activeTab, setActiveTab] = useState("dashboard");
// All useQuery hooks must be called at the top level - before any conditional returns
const { data: store, isLoading: storeLoading } = useQuery({
queryKey: ['/api/seller/store'],
queryFn: async () => {
const response = await sellerApi.getStore();
return response.json();
},
enabled: userType === 'seller' && !!seller, // Only run query when authenticated
});
const { data: products = [], isLoading: productsLoading } = useQuery({
queryKey: ['/api/seller/products'],
queryFn: async () => {
const response = await sellerApi.getProducts();
return response.json();
},
enabled: userType === 'seller' && !!seller, // Only run query when authenticated
});
const { data: categories = [] } = useQuery({
queryKey: ['/api/categories'],
queryFn: () => categoriesApi.getAll(),
enabled: userType === 'seller' && !!seller, // Only run query when authenticated
});
const { data: orders = [], isLoading: ordersLoading } = useQuery({
queryKey: ['/api/seller/orders'],
queryFn: async () => {
const response = await sellerApi.getOrders();
return response.json();
},
enabled: userType === 'seller' && !!seller, // Only run query when authenticated
});
// Query to get user location data
const { data: users = [] } = useQuery({
queryKey: ['/api/admin/users'],
queryFn: async () => {
const response = await fetch('/api/admin/users');
return response.json();
},
enabled: userType === 'seller' && !!seller,
});
// All useForm and useMutation hooks must also be at the top level
const sellerLoginForm = useForm<SellerLoginFormData>({
resolver: zodResolver(sellerLoginSchema),
defaultValues: {
username: "",
password: "",
},
});
const storeForm = useForm<StoreFormData>({
resolver: zodResolver(storeSchema),
defaultValues: {
name: store?.name || "",
description: store?.description || "",
},
});
const productForm = useForm<ProductFormData>({
resolver: zodResolver(productSchema),
defaultValues: {
title: "",
description: "",
price: "",
originalPrice: "",
stock: "",
categoryId: "",
},
});
const createStoreMutation = useMutation({
mutationFn: (data: StoreFormData) => {
const formData = new FormData();
formData.append('name', data.name);
if (data.description) formData.append('description', data.description);
if (selectedFiles) {
Array.from(selectedFiles).forEach((file, index) => {
if (index === 0) formData.append('bannerImage', file);
if (index === 1) formData.append('faceImage', file);
});
}
return storesApi.create(formData);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/seller/store'] });
setIsStoreDialogOpen(false);
storeForm.reset();
setSelectedFiles(null);
toast({
title: "Store created",
description: "Your store has been created successfully.",
});
},
onError: () => {
toast({
variant: "destructive",
title: "Error",
description: "Failed to create store.",
});
},
});
const updateStoreMutation = useMutation({
mutationFn: (data: StoreFormData) => {
const formData = new FormData();
formData.append('name', data.name);
if (data.description) formData.append('description', data.description);
if (selectedFiles) {
Array.from(selectedFiles).forEach((file, index) => {
if (index === 0) formData.append('bannerImage', file);
if (index === 1) formData.append('faceImage', file);
});
}
return storesApi.update(store.id, formData);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/seller/store'] });
setIsStoreDialogOpen(false);
setSelectedFiles(null);
toast({
title: "Store updated",
description: "Your store has been updated successfully.",
});
},
onError: () => {
toast({
variant: "destructive",
title: "Error",
description: "Failed to update store.",
});
},
});
const createProductMutation = useMutation({
mutationFn: (data: ProductFormData) => {
const formData = new FormData();
formData.append('title', data.title);
formData.append('description', data.description);
formData.append('price', data.price);
if (data.originalPrice) formData.append('originalPrice', data.originalPrice);
formData.append('stock', data.stock);
formData.append('categoryId', data.categoryId);
// Append selected files
if (selectedFiles) {
Array.from(selectedFiles).forEach((file) => {
formData.append('images', file);
});
}
// Append captured files
capturedFiles.forEach((file) => {
formData.append('images', file);
});
return productsApi.create(formData);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/seller/products'] });
setIsProductDialogOpen(false);
productForm.reset();
setSelectedFiles(null);
setCapturedFiles([]);
closeCamera();
toast({
title: "Product created",
description: "Your product has been created successfully.",
});
},
onError: () => {
toast({
variant: "destructive",
title: "Error",
description: "Failed to create product.",
});
},
});
const deleteProductMutation = useMutation({
mutationFn: (id: string) => productsApi.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/seller/products'] });
toast({
title: "Product deleted",
description: "Product has been deleted successfully.",
});
},
onError: () => {
toast({
variant: "destructive",
title: "Error",
description: "Failed to delete product.",
});
},
});
const updateOrderMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: any }) => sellerApi.updateOrder(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/seller/orders'] });
toast({
title: "Order updated",
description: "Order status has been updated successfully.",
});
},
onError: () => {
toast({
variant: "destructive",
title: "Error",
description: "Failed to update order status.",
});
},
});
const handleUpdateOrderStatus = (id: string, data: any) => {
updateOrderMutation.mutate({ id, data });
};
const onSellerLogin = async (data: SellerLoginFormData) => {
setIsLoading(true);
try {
const response = await authApi.sellerLogin(data);
const result = await response.json();
sellerLogin(result.token, result.seller);
toast({
title: "Welcome back!",
description: "You have been successfully logged in as a seller.",
});
} catch (error) {
toast({
variant: "destructive",
title: "Login failed",
description: "Invalid credentials. Please check with the administrator.",
});
} finally {
setIsLoading(false);
}
};
// Show login form if not authenticated as seller
if (userType !== 'seller' || !seller) {
return (
<div className="min-h-screen flex items-center justify-center page-gradient py-12 px-4 sm:px-6 lg:px-8">
<Card className="w-full max-w-md glass-card border-0">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold text-primary" data-testid="seller-auth-title">
Shoposphere Seller Login
</CardTitle>
</CardHeader>
<CardContent>
<Form {...sellerLoginForm}>
<form onSubmit={sellerLoginForm.handleSubmit(onSellerLogin)} className="space-y-4">
<FormField
control={sellerLoginForm.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} placeholder="Enter your username" data-testid="input-seller-username" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={sellerLoginForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} type="password" placeholder="Enter your password" data-testid="input-seller-password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="w-full"
disabled={isLoading}
data-testid="button-seller-login"
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Sign In
</Button>
</form>
</Form>
<p className="text-sm text-gray-600 text-center mt-4">
Seller accounts are created by administrators only
</p>
</CardContent>
</Card>
</div>
);
}
const onStoreSubmit = (data: StoreFormData) => {
if (store) {
updateStoreMutation.mutate(data);
} else {
createStoreMutation.mutate(data);
}
};
const onProductSubmit = (data: ProductFormData) => {
createProductMutation.mutate(data);
};
// Camera functions
const openCamera = async () => {
try {
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }, // Use back camera on mobile
audio: false
});
setStream(mediaStream);
setIsCameraOpen(true);
if (videoRef.current) {
videoRef.current.srcObject = mediaStream;
}
} catch (error) {
console.error('Error accessing camera:', error);
toast({
variant: "destructive",
title: "Camera Error",
description: "Unable to access camera. Please check permissions.",
});
}
};
const closeCamera = () => {
if (stream) {
stream.getTracks().forEach(track => track.stop());
setStream(null);
}
setIsCameraOpen(false);
};
const capturePhoto = () => {
if (videoRef.current && canvasRef.current) {
const video = videoRef.current;
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
if (context) {
context.drawImage(video, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const file = new File([blob], `captured-${Date.now()}.jpg`, { type: 'image/jpeg' });
setCapturedFiles(prev => [...prev, file]);
toast({
title: "Photo captured",
description: "Photo added to product images.",
});
}
}, 'image/jpeg', 0.8);
}
}
};
const removeCapturedPhoto = (index: number) => {
setCapturedFiles(prev => prev.filter((_, i) => i !== index));
};
const handleLogout = () => {
logout();
setLocation('/');
};
return (
<div className="min-h-screen page-gradient">
<header className="glass-strong border-0 shadow-2xl">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div>
<h1 className="text-2xl font-bold text-primary" data-testid="seller-title">
Shoposphere Seller
</h1>
<p className="text-sm text-muted-foreground">Welcome, {seller.username}</p>
</div>
<div className="flex items-center space-x-4">
<Button onClick={() => setLocation('/')} variant="outline" data-testid="back-to-site">
Back to Site
</Button>
<Button onClick={handleLogout} variant="ghost" data-testid="seller-logout">
<LogOut className="mr-2 h-4 w-4" />
Logout
</Button>
</div>
</div>
</div>
</header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-testid="admin-seller-page">
<Tabs defaultValue="dashboard" className="space-y-6">
<TabsList className="glass-card border-0 grid w-full grid-cols-4">
<TabsTrigger value="dashboard" data-testid="dashboard-tab">
<Package className="mr-2 h-4 w-4" />
Dashboard
</TabsTrigger>
<TabsTrigger value="store" data-testid="store-tab">
<Store className="mr-2 h-4 w-4" />
My Store
</TabsTrigger>
<TabsTrigger value="products" data-testid="products-tab">
<Package className="mr-2 h-4 w-4" />
Products
</TabsTrigger>
<TabsTrigger value="orders" data-testid="orders-tab">
<ShoppingCart className="mr-2 h-4 w-4" />
Orders ({orders.length})
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<Card className="glass-card border-0 p-6">
<div className="flex items-center space-x-4">
<Store className="h-8 w-8 text-blue-600" />
<div>
<h3 className="text-2xl font-bold">{store ? 1 : 0}</h3>
<p className="text-gray-600">Store</p>
</div>
</div>
</Card>
<Card className="glass-card border-0 p-6">
<div className="flex items-center space-x-4">
<Package className="h-8 w-8 text-blue-600" />
<div>
<h3 className="text-2xl font-bold">{products.length}</h3>
<p className="text-gray-600">Products Listed</p>
</div>
</div>
</Card>
<Card className="glass-card border-0 p-6">
<div className="flex items-center space-x-4">
<ShoppingCart className="h-8 w-8 text-green-600" />
<div>
<h3 className="text-2xl font-bold">{orders.length}</h3>
<p className="text-gray-600">Total Orders</p>
</div>
</div>
</Card>
</div>
{/* Customer Locations */}
<Card className="glass-card border-0 mb-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MapPin className="h-5 w-5" />
Customer Locations
</CardTitle>
</CardHeader>
<CardContent>
{users.length > 0 ? (
<div className="space-y-4" data-testid="customer-locations">
{users.slice(0, 5).map((user: any) => (
<div key={user.id} className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-1">
<h3 className="font-semibold" data-testid={`user-name-${user.id}`}>
{user.firstName} {user.lastName}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{user.email} • {user.phone}
</p>
{/* Location Information */}
<div className="mt-2">
{user.locationDetectedAutomatically && user.googleMapsUrl ? (
<div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 text-green-600" />
<span className="text-green-600" data-testid={`user-location-auto-${user.id}`}>
Auto-detected location
</span>
<Button
size="sm"
variant="outline"
className="h-6 px-2 text-xs"
onClick={() => window.open(user.googleMapsUrl, '_blank')}
data-testid={`view-location-${user.id}`}
>
<ExternalLink className="mr-1 h-3 w-3" />
View on Maps
</Button>
</div>
) : user.street && user.city ? (
<div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 text-blue-600" />
<span className="text-gray-600 dark:text-gray-400" data-testid={`user-location-manual-${user.id}`}>
{user.street}, {user.city}, {user.state} {user.pinCode}
</span>
</div>
) : (
<div className="flex items-center gap-2 text-sm">
<MapPin className="h-4 w-4 text-gray-400" />
<span className="text-gray-400" data-testid={`user-location-none-${user.id}`}>
No location provided
</span>
</div>
)}
</div>
</div>
<div className="text-right">
{user.latitude && user.longitude && (
<p className="text-xs text-gray-500 dark:text-gray-400" data-testid={`coordinates-${user.id}`}>
{Number(user.latitude).toFixed(4)}, {Number(user.longitude).toFixed(4)}
</p>
)}
<p className="text-xs text-gray-400 mt-1">
Joined: {new Date(user.createdAt).toLocaleDateString()}
</p>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8" data-testid="no-customers">
<MapPin className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No customer data available yet.</p>
</div>
)}
</CardContent>
</Card>
{/* Recent Orders */}
<Card className="glass-card border-0">
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
</CardHeader>
<CardContent>
{ordersLoading ? (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
) : orders.length === 0 ? (
<p className="text-gray-500 text-center py-8" data-testid="no-orders">
No orders yet. Start by creating products to sell!
</p>
) : (
<div className="space-y-4">
{orders.slice(0, 5).map((order: any) => (
<div key={order.id} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div>
<p className="font-semibold">Order #{order.id.slice(-8)}</p>
<p className="text-sm text-gray-600">₹{parseFloat(order.total).toFixed(2)} • {order.items?.length || 0} items</p>
</div>
<div className="text-right">
<Badge variant={order.status === 'delivered' ? 'default' : order.status === 'pending' ? 'outline' : 'secondary'}>
{order.status}
</Badge>
<p className="text-sm text-gray-500 mt-1">
{new Date(order.createdAt).toLocaleDateString()}
</p>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="store">
<Card className="glass-card border-0">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Store Management</CardTitle>
<Dialog open={isStoreDialogOpen} onOpenChange={setIsStoreDialogOpen}>
<DialogTrigger asChild>
<Button data-testid="manage-store-button">
{store ? <Edit className="mr-2 h-4 w-4" /> : <Plus className="mr-2 h-4 w-4" />}
{store ? 'Edit Store' : 'Create Store'}
</Button>
</DialogTrigger>
<DialogContent className="bg-white border border-black max-w-md">
<DialogHeader>
<DialogTitle>{store ? 'Edit Store' : 'Create Store'}</DialogTitle>
</DialogHeader>
<Form {...storeForm}>
<form onSubmit={storeForm.handleSubmit(onStoreSubmit)} className="space-y-4">
<FormField
control={storeForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Store Name</FormLabel>
<FormControl>
<Input {...field} data-testid="store-name-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={storeForm.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} data-testid="store-description-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Images</label>
<Input
type="file"
multiple
accept="image/*"
onChange={(e) => setSelectedFiles(e.target.files)}
data-testid="store-images-input"
/>
<p className="text-xs text-gray-500 mt-1">
Upload up to 2 images (banner and profile)
</p>
</div>
<Button type="submit" className="w-full" data-testid="submit-store">
{store ? 'Update Store' : 'Create Store'}
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
{storeLoading ? (
<div className="space-y-4">
<Skeleton className="h-32 w-full" />
<div className="flex items-center space-x-4">
<Skeleton className="w-16 h-16 rounded-full" />
<div>
<Skeleton className="h-6 w-48 mb-2" />
<Skeleton className="h-4 w-32" />
</div>
</div>
</div>
) : store ? (
<div className="space-y-6" data-testid="store-info">
{/* Store Banner */}
<div className="relative h-32 bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg overflow-hidden">
{store.bannerImage ? (
<img
src={store.bannerImage}
alt="Store banner"
className="w-full h-full object-cover"
data-testid="store-banner-image"
/>
) : (
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-blue-700" />
)}
</div>
{/* Store Info */}
<div className="flex items-start space-x-6">
<div className="w-16 h-16 rounded-full overflow-hidden bg-gray-100 flex-shrink-0">
{store.faceImage ? (
<img
src={store.faceImage}
alt="Store profile"
className="w-full h-full object-cover"
data-testid="store-face-image"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<Store className="h-6 w-6 text-gray-400" />
</div>
)}
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-gray-900" data-testid="store-name">
{store.name}
</h3>
{store.description && (
<p className="text-gray-600 mt-2" data-testid="store-description">
{store.description}
</p>
)}
<div className="mt-4">
<Button
variant="outline"
onClick={() => setLocation(`/store/${store.id}`)}
data-testid="view-store-public"
>
View Public Store
</Button>
</div>
</div>
</div>
</div>
) : (
<div className="text-center py-12" data-testid="no-store">
<Store className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500 mb-4">You haven't created a store yet.</p>
<p className="text-sm text-gray-400">Create your store to start selling products.</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="products">
<Card className="glass-card border-0">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Product Management</CardTitle>
<Dialog open={isProductDialogOpen} onOpenChange={setIsProductDialogOpen}>
<DialogTrigger asChild>
<Button disabled={!store} data-testid="create-product-button">
<Plus className="mr-2 h-4 w-4" />
Add Product
</Button>
</DialogTrigger>
<DialogContent className="bg-white border border-black max-w-md">
<DialogHeader>
<DialogTitle>Add New Product</DialogTitle>
</DialogHeader>
<Form {...productForm}>
<form onSubmit={productForm.handleSubmit(onProductSubmit)} className="space-y-4">
<FormField
control={productForm.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Product Title</FormLabel>
<FormControl>
<Input {...field} data-testid="product-title-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={productForm.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field} data-testid="product-description-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={productForm.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Price</FormLabel>
<FormControl>
<Input {...field} type="number" step="0.01" data-testid="product-price-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={productForm.control}
name="originalPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Original Price</FormLabel>
<FormControl>
<Input {...field} type="number" step="0.01" data-testid="product-original-price-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={productForm.control}
name="stock"
render={({ field }) => (
<FormItem>
<FormLabel>Stock Quantity</FormLabel>
<FormControl>
<Input {...field} type="number" data-testid="product-stock-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={productForm.control}
name="categoryId"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger data-testid="product-category-select">
<SelectValue placeholder="Select category" />
</SelectTrigger>
</FormControl>
<SelectContent>
{categories.map((category: any) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Product Images</label>
<div className="space-y-3">
<div className="flex space-x-2">
<Input
type="file"
multiple
accept="image/*"
onChange={(e) => setSelectedFiles(e.target.files)}
data-testid="product-images-input"
className="flex-1"
/>
<Button
type="button"
variant="outline"
onClick={openCamera}
data-testid="camera-button"
className="px-3"
>
<Camera className="h-4 w-4" />
</Button>
</div>
{isCameraOpen && (
<div className="border rounded-lg p-4 bg-gray-50">
<video
ref={videoRef}
autoPlay
playsInline
className="w-full h-48 object-cover rounded mb-2"
data-testid="camera-preview"
/>
<div className="flex space-x-2">
<Button
type="button"
onClick={capturePhoto}
data-testid="capture-photo-button"
className="flex-1"
>
Capture Photo
</Button>
<Button
type="button"
variant="outline"
onClick={closeCamera}
data-testid="close-camera-button"
>
Close Camera
</Button>
</div>
</div>
)}
{capturedFiles.length > 0 && (
<div className="grid grid-cols-3 gap-2">
{capturedFiles.map((file, index) => (
<div key={index} className="relative">
<img
src={URL.createObjectURL(file)}
alt={`Captured ${index + 1}`}
className="w-full h-20 object-cover rounded"
data-testid={`captured-image-${index}`}
/>
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => removeCapturedPhoto(index)}
className="absolute top-1 right-1 h-6 w-6 p-0"
data-testid={`remove-captured-${index}`}
>
×
</Button>
</div>
))}
</div>
)}
<canvas ref={canvasRef} className="hidden" />
</div>
<p className="text-xs text-gray-500 mt-1">
Upload files or take photos - up to 5 images total
</p>
</div>
<Button type="submit" className="w-full" data-testid="submit-product">
Add Product
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
</CardHeader>
<CardContent>
{!store ? (
<div className="text-center py-12" data-testid="create-store-first">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500 mb-4">Create your store first to add products.</p>
</div>
) : productsLoading ? (
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div key={i} className="flex items-center justify-between p-4 border rounded-lg">
<div className="flex items-center space-x-4">
<Skeleton className="w-16 h-16" />
<div>
<Skeleton className="h-6 w-48 mb-2" />
<Skeleton className="h-4 w-32" />
</div>
</div>
<Skeleton className="h-8 w-20" />
</div>
))}
</div>
) : products.length > 0 ? (
<div className="space-y-4" data-testid="products-list">
{products.map((product: any) => (
<div key={product.id} className="flex items-center justify-between p-4 border border-gray-200 rounded-lg" data-testid={`product-${product.id}`}>
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-gray-100 rounded overflow-hidden">
{product.images && product.images.length > 0 ? (
<img
src={product.images[0]}
alt={product.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<Package className="h-6 w-6 text-gray-400" />
</div>
)}
</div>
<div>
<h3 className="font-medium text-gray-900" data-testid={`product-title-${product.id}`}>
{product.title}
</h3>
<p className="text-sm text-gray-500" data-testid={`product-category-${product.id}`}>
{product.category?.name}
</p>
<div className="flex items-center space-x-2 mt-1">
<span className="text-sm font-semibold text-gray-900" data-testid={`product-price-${product.id}`}>
₹{parseFloat(product.price).toFixed(2)}
</span>
<Badge variant={product.isActive ? 'default' : 'secondary'} data-testid={`product-status-${product.id}`}>
{product.isActive ? 'Active' : 'Inactive'}
</Badge>
<span className="text-xs text-gray-500" data-testid={`product-stock-${product.id}`}>
Stock: {product.stock}
</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setLocation(`/product/${product.id}`)}
data-testid={`view-product-${product.id}`}
>
View
</Button>
<Button
variant="destructive"
size="sm"
onClick={() => deleteProductMutation.mutate(product.id)}
data-testid={`delete-product-${product.id}`}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12" data-testid="no-products">
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No products added yet.</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="orders">
<Card className="glass-card border-0">
<CardHeader>
<CardTitle>Order Management</CardTitle>
</CardHeader>
<CardContent>
{ordersLoading ? (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
) : orders.length === 0 ? (
<div className="text-center py-12" data-testid="no-orders-list">
<ShoppingCart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500">No orders received yet.</p>
</div>
) : (
<div className="space-y-4" data-testid="orders-list">
{orders.map((order: any) => (
<div key={order.id} className="border border-gray-200 rounded-lg p-6" data-testid={`order-${order.id}`}>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-semibold text-lg" data-testid={`order-id-${order.id}`}>
Order #{order.id.slice(-8)}
</h3>
<p className="text-sm text-gray-600" data-testid={`order-date-${order.id}`}>
Placed on {new Date(order.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</p>
</div>
<div className="text-right">
<div className="flex items-center space-x-2 mb-2">
<Badge
variant={
order.status === 'delivered' ? 'default' :
order.status === 'shipped' ? 'secondary' :
order.status === 'pending' ? 'outline' :
'destructive'
}
data-testid={`order-status-${order.id}`}
>
{order.status}
</Badge>
<Badge variant={order.paymentStatus === 'paid' ? 'default' : 'destructive'}>
{order.paymentStatus === 'paid' ? 'Paid' : 'Unpaid'}
</Badge>
</div>
<p className="text-lg font-semibold mt-1" data-testid={`order-total-${order.id}`}>
₹{parseFloat(order.total).toFixed(2)}
</p>
</div>
</div>
{/* Customer Info */}
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<h4 className="font-medium mb-2">Customer Information</h4>
{order.user && (
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<p className="text-sm"><span className="font-medium">Name:</span> {order.user.firstName} {order.user.lastName}</p>
<p className="text-sm"><span className="font-medium">Email:</span> {order.user.email}</p>
</div>
<div>
<p className="text-sm"><span className="font-medium">Phone:</span> {order.user.phone}</p>
<p className="text-sm"><span className="font-medium">Payment:</span> {order.paymentMethod === 'cod' ? 'Cash on Delivery' : 'UPI'}</p>
</div>
</div>
)}
<p className="text-sm text-gray-600"><span className="font-medium">Address:</span> {order.shippingAddress}</p>
</div>
{/* Order Management Actions */}
<div className="flex items-center space-x-2 mb-4 p-3 bg-blue-50 rounded-lg">
<div className="flex items-center space-x-2">
<label className="text-sm font-medium">Status:</label>
<select
className="px-3 py-1 text-sm border rounded"
value={order.status}
onChange={(e) => handleUpdateOrderStatus(order.id, { status: e.target.value })}
>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<Button
size="sm"
variant={order.paymentStatus === 'paid' ? 'outline' : 'default'}
onClick={() => handleUpdateOrderStatus(order.id, {
paymentStatus: order.paymentStatus === 'paid' ? 'unpaid' : 'paid'
})}
>
Mark as {order.paymentStatus === 'paid' ? 'Unpaid' : 'Paid'}
</Button>
</div>
{/* Order Items */}
<div>
<h4 className="font-medium mb-3">Items Ordered</h4>
<div className="space-y-2">
{order.items?.map((item: any, index: number) => (
<div key={index} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0" data-testid={`order-item-${order.id}-${index}`}>
<div className="flex items-center space-x-3">
<div className="w-12 h-12 bg-gray-100 rounded overflow-hidden">
{item.product?.images?.[0] ? (
<img
src={item.product.images[0]}
alt={item.product.title}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<Package className="h-4 w-4 text-gray-400" />
</div>
)}
</div>
<div>
<p className="font-medium text-sm">{item.product?.title}</p>
<p className="text-xs text-gray-500">Qty: {item.quantity}</p>
</div>
</div>
<div className="text-right">
<p className="font-medium text-sm">₹{parseFloat(item.price).toFixed(2)}</p>
<p className="text-xs text-gray-500">₹{(parseFloat(item.price) * item.quantity).toFixed(2)} total</p>
</div>
</div>
))}
</div>
</div>
<div className="flex justify-end mt-4 pt-4 border-t">
<div className="text-right">
<p className="text-sm text-gray-600">Subtotal: ₹{parseFloat(order.subtotal).toFixed(2)}</p>
<p className="text-sm text-gray-600">Tax: ₹{parseFloat(order.tax).toFixed(2)}</p>
<p className="text-sm text-gray-600">Shipping: ₹{parseFloat(order.shipping || '0').toFixed(2)}</p>
<p className="font-semibold text-lg">Total: ₹{parseFloat(order.total).toFixed(2)}</p>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
}