-
-
+
+ {isAuthenticated && user ? (
+ <>
+
+
+ {getInitial()}
+
+
+
{user.name}
+
{user.role}
+
-
Profile
-
-
-
-
-
- Sign Out
-
+
+
+ >
+ ) : (
+
+
+ Sign In
+
+ )}
>
diff --git a/frontend/src/lib/mockData.js b/frontend/src/lib/mockData.js
deleted file mode 100644
index bb30d14..0000000
--- a/frontend/src/lib/mockData.js
+++ /dev/null
@@ -1,493 +0,0 @@
-export const mockShops = [
- {
- _id: 'shop_1',
- owner: 'user_seller_1',
- name: 'The Iron Loom',
- description: 'Forging heritage into every thread. We specialize in heavy-gauge industrial weaving techniques blended with traditional patterns from the southern highlands.',
- category: 'Textiles',
- coverUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85f82e?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1544441893-675973e31985?w=400&q=80',
- location: { type: 'Point', coordinates: [125.6128, 7.0731] },
- address: 'Davao City, Davao del Sur',
- isActive: true,
- rating: 4.9
- },
- {
- _id: 'shop_2',
- owner: 'user_seller_2',
- name: 'Maranao Woodworks',
- description: 'Exquisite okir wood carvings and handcrafted furniture that bring the rich history of Mindanao into your modern home. Master craftsmen preserve centuries of wood sculpting.',
- category: 'Woodwork',
- coverUrl: 'https://images.unsplash.com/photo-1504148455328-c376907d081c?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=400&q=80',
- location: { type: 'Point', coordinates: [124.2863, 8.0210] },
- address: 'Marawi City, Lanao del Sur',
- isActive: true,
- rating: 3.9
- },
- {
- _id: 'shop_3',
- owner: 'user_seller_3',
- name: 'Isla Pottery',
- description: 'Hand-thrown ceramics made from locally sourced clay, baked in wood-fired kilns for a unique, earthy finish. Every piece tells the story of the earth it came from.',
- category: 'Ceramics',
- coverUrl: 'https://images.unsplash.com/photo-1565193566173-7a0ee3dbe261?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1578500494198-246f612b3b3d?w=400&q=80',
- location: { type: 'Point', coordinates: [124.6461, 8.4822] },
- address: 'Cagayan de Oro, Misamis Oriental',
- isActive: true,
- rating: 4.7
- },
- {
- _id: 'shop_4',
- owner: 'user_seller_4',
- name: 'Lumad Metalcraft',
- description: 'Hand-forged brass and copper works inspired by ancestral Lumad patterns. Each piece is fire-tempered using traditional bellows techniques passed down through generations.',
- category: 'Metalwork',
- coverUrl: 'https://images.unsplash.com/photo-1571898223901-5ff77a0f7af5?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&q=80',
- location: { type: 'Point', coordinates: [125.1716, 6.1164] },
- address: 'General Santos City, South Cotabato',
- isActive: true,
- rating: 3.2
- },
- {
- _id: 'shop_5',
- owner: 'user_seller_5',
- name: 'Agusan Rattan Guild',
- description: 'Handcrafted rattan and bamboo furniture, combining tribal Agusan patterns with modern minimalist design.',
- category: 'Woodwork',
- coverUrl: 'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1540518614846-7eded433c457?w=400&q=80',
- location: { type: 'Point', coordinates: [125.5392, 8.9475] },
- address: 'Butuan City, Agusan del Norte',
- isActive: true,
- rating: 4.8
- },
- {
- _id: 'shop_6',
- owner: 'user_seller_6',
- name: 'Yakan Heritage Weavers',
- description: 'Authentic Yakan handwoven textiles featuring striking geometric patterns and bold colors, handmade by master weavers from Basilan and Zamboanga.',
- category: 'Textiles',
- coverUrl: 'https://images.unsplash.com/photo-1528459801416-a9e53bbf4e17?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1584917865442-de89df76afd3?w=400&q=80',
- location: { type: 'Point', coordinates: [122.0790, 6.9214] },
- address: 'Zamboanga City, Zamboanga del Sur',
- isActive: true,
- rating: 4.9
- },
- {
- _id: 'shop_7',
- owner: 'user_seller_7',
- name: 'Inaul Legacy Textiles',
- description: 'Preserving the legacy of the Maguindanaon Inaul. Exquisite handwoven silk fabrics featuring vibrant, royal colors and traditional geometric grids.',
- category: 'Textiles',
- coverUrl: 'https://images.unsplash.com/photo-1544441893-675973e31985?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85f82e?w=400&q=80',
- location: { type: 'Point', coordinates: [124.2422, 7.2207] },
- address: 'Cotabato City, Maguindanao',
- isActive: true,
- rating: 4.6
- },
- {
- _id: 'shop_8',
- owner: 'user_seller_8',
- name: 'Maria Cristina Metal Arts',
- description: 'Master blacksmiths crafting legendary steel bolos, Kampilan swords, and custom carbon steel kitchen blades. Heavy-duty utility meets ancestral artistry.',
- category: 'Metalwork',
- coverUrl: 'https://images.unsplash.com/photo-1509062522246-3755977927d7?w=1200&q=80',
- logoUrl: 'https://images.unsplash.com/photo-1518895949257-7621c3c786d7?w=400&q=80',
- location: { type: 'Point', coordinates: [124.2405, 8.2280] },
- address: 'Iligan City, Lanao del Norte',
- isActive: true,
- rating: 4.5
- },
-];
-
-export const mockProducts = [
- // ── The Iron Loom (shop_1) ──────────────────────────────────────────────
- {
- _id: 'prod_1',
- shop: 'shop_1',
- name: 'Ochre Highlands Scarf',
- description: 'A beautifully patterned handwoven scarf featuring natural cotton and silk blends. Hand-dyed using organic highland flora.',
- price: 18500,
- category: 'Scarves',
- imageUrl: 'https://images.unsplash.com/photo-1601924638867-3a6de6b7a500?w=800&q=80',
- inStock: true,
- colors: ['Natural Ochre', 'Slate Grey', 'Deep Rust'],
- sizes: ['S / 60×90cm', 'M / 80×120cm', 'L / 100×150cm'],
- rating: 4.9,
- reviewCount: 42,
- stockQuantity: 15,
- },
- {
- _id: 'prod_2',
- shop: 'shop_1',
- name: 'Iron Slate Runner',
- description: 'A heavy-weight runner blending hemp and steel-thread accents. Perfect for industrial-chic dining settings.',
- price: 24000,
- category: 'Runners',
- imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85f82e?w=800&q=80',
- inStock: true,
- colors: ['Slate Grey', 'Coal Black'],
- sizes: ['M / 40×150cm', 'L / 40×200cm'],
- rating: 4.8,
- reviewCount: 28,
- stockQuantity: 8,
- },
- {
- _id: 'prod_3',
- shop: 'shop_1',
- name: 'Desert Loom Blanket',
- description: 'Cozy and oversized, this natural alpaca blend blanket features hand-tied tassels and intricate geometric patterns.',
- price: 32000,
- category: 'Blankets',
- imageUrl: 'https://images.unsplash.com/photo-1612160609504-334bce3fc71e?w=800&q=80',
- inStock: true,
- colors: ['Desert Sand', 'Terracotta', 'Sage'],
- sizes: ['Queen', 'King'],
- rating: 5.0,
- reviewCount: 19,
- stockQuantity: 5,
- },
-
- // ── Maranao Woodworks (shop_2) ──────────────────────────────────────────
- {
- _id: 'prod_4',
- shop: 'shop_2',
- name: 'Okir Carved Baul',
- description: 'A traditional wooden chest adorned with intricate okir motifs. Perfect for storing heirlooms and valuables.',
- price: 850000,
- category: 'Storage',
- imageUrl: 'https://images.unsplash.com/photo-1506439773649-6e0eb8cfb237?w=800&q=80',
- inStock: true,
- colors: ['Dark Mahogany', 'Natural Nara'],
- sizes: ['Medium', 'Large'],
- rating: 4.9,
- reviewCount: 12,
- stockQuantity: 2,
- },
- {
- _id: 'prod_5',
- shop: 'shop_2',
- name: 'Mahogany Lounge Chair',
- description: 'A low-slung lounge chair carved from single pieces of sustainably sourced mahogany. Smooth, organic curves.',
- price: 1200000,
- category: 'Furniture',
- imageUrl: 'https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=800&q=80',
- inStock: true,
- colors: ['Cherry Mahogany', 'Espresso'],
- sizes: ['Standard'],
- rating: 4.7,
- reviewCount: 31,
- stockQuantity: 4,
- },
- {
- _id: 'prod_6',
- shop: 'shop_2',
- name: 'Kulintang Stand',
- description: 'Hand-carved wooden stand designed for traditional kulintang gongs. Features elegant swooping lines.',
- price: 450000,
- category: 'Instruments',
- imageUrl: 'https://images.unsplash.com/photo-1504148455328-c376907d081c?w=800&q=80',
- inStock: false,
- colors: ['Red Lauan', 'Golden Yakal'],
- sizes: ['Standard 8-Gong'],
- rating: 4.9,
- reviewCount: 9,
- stockQuantity: 0,
- },
-
- // ── Isla Pottery (shop_3) ───────────────────────────────────────────────
- {
- _id: 'prod_7',
- shop: 'shop_3',
- name: 'Speckled Coffee Mug',
- description: 'A charming, imperfectly round coffee mug with a rustic speckled glaze. Holds about 300 ml.',
- price: 65000,
- category: 'Drinkware',
- imageUrl: 'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?w=800&q=80',
- inStock: true,
- colors: ['Speckled White', 'Celadon Green', 'Iron Blue'],
- sizes: ['300ml', '450ml'],
- rating: 4.9,
- reviewCount: 128,
- stockQuantity: 45,
- },
- {
- _id: 'prod_8',
- shop: 'shop_3',
- name: 'Terracotta Planter',
- description: 'Large terracotta pot ideal for indoor monsteras or outdoor ferns. Hand-thrown and naturally porous.',
- price: 120000,
- category: 'Garden',
- imageUrl: 'https://images.unsplash.com/photo-1485955900006-10f4d324d411?w=800&q=80',
- inStock: true,
- colors: ['Classic Clay', 'Whitewash'],
- sizes: ['8-inch', '10-inch', '12-inch'],
- rating: 4.6,
- reviewCount: 54,
- stockQuantity: 22,
- },
- {
- _id: 'prod_9',
- shop: 'shop_3',
- name: 'Matte Charcoal Vase',
- description: 'A minimalist vase with a dark, matte charcoal finish. Perfect for dried flowers or standalone display.',
- price: 95000,
- category: 'Decor',
- imageUrl: 'https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?w=800&q=80',
- inStock: true,
- colors: ['Matte Charcoal', 'Sandstone'],
- sizes: ['Small', 'Medium', 'Large'],
- rating: 4.8,
- reviewCount: 67,
- stockQuantity: 14,
- },
-
- // ── Lumad Metalcraft (shop_4) ───────────────────────────────────────────
- {
- _id: 'prod_10',
- shop: 'shop_4',
- name: 'Hammered Copper Bowl',
- description: 'A statement serving bowl hand-hammered from a single sheet of copper. Food-safe lacquer interior with a raw patina exterior.',
- price: 185000,
- category: 'Kitchenware',
- imageUrl: 'https://images.unsplash.com/photo-1590479773265-7464e5d48118?w=800&q=80',
- inStock: true,
- colors: ['Raw Copper', 'Aged Patina'],
- sizes: ['Small', 'Medium'],
- rating: 4.8,
- reviewCount: 36,
- stockQuantity: 12,
- },
- {
- _id: 'prod_11',
- shop: 'shop_4',
- name: 'Brass Wind Chime',
- description: 'Melodic brass tubes tuned to a pentatonic scale, suspended from a hand-carved driftwood hanger.',
- price: 95000,
- category: 'Decor',
- imageUrl: 'https://images.unsplash.com/photo-1534126511673-b6899657816a?w=800&q=80',
- inStock: true,
- colors: ['Polished Brass', 'Antique Bronze'],
- sizes: ['Standard'],
- rating: 4.7,
- reviewCount: 22,
- stockQuantity: 18,
- },
- {
- _id: 'prod_12',
- shop: 'shop_4',
- name: 'Forged Iron Candle Holder',
- description: 'A dramatic three-arm candelabra forged from raw iron with twisted vine detailing. Perfect centerpiece for rustic dining.',
- price: 320000,
- category: 'Lighting',
- imageUrl: 'https://images.unsplash.com/photo-1512238972498-ef2bd6f24df0?w=800&q=80',
- inStock: false,
- colors: ['Matte Black', 'Rust Finish'],
- sizes: ['3-Arm', '5-Arm'],
- rating: 4.9,
- reviewCount: 15,
- stockQuantity: 0,
- },
-
- // ── Agusan Rattan Guild (shop_5) ─────────────────────────────────────────
- {
- _id: 'prod_13',
- shop: 'shop_5',
- name: 'Agusan Rattan Armchair',
- description: 'Sleek, lightweight rattan armchair featuring traditional weaving patterns on the backrest. Excellent durability.',
- price: 550000,
- category: 'Furniture',
- imageUrl: 'https://images.unsplash.com/photo-1567538096630-e0c55bd6374c?w=800&q=80',
- inStock: true,
- colors: ['Natural Tan', 'Honey Brown'],
- sizes: ['Standard'],
- rating: 4.8,
- reviewCount: 14,
- stockQuantity: 6,
- },
- {
- _id: 'prod_14',
- shop: 'shop_5',
- name: 'Bamboo Nesting Tables',
- description: 'Set of three minimalist nesting tables constructed from solid, sustainable structural bamboo.',
- price: 320000,
- category: 'Furniture',
- imageUrl: 'https://images.unsplash.com/photo-1533090161767-e6ffed986c88?w=800&q=80',
- inStock: true,
- colors: ['Natural Bamboo', 'Carbonized Dark'],
- sizes: ['Set of 3'],
- rating: 4.7,
- reviewCount: 9,
- stockQuantity: 8,
- },
-
- // ── Yakan Heritage Weavers (shop_6) ──────────────────────────────────────
- {
- _id: 'prod_15',
- shop: 'shop_6',
- name: 'Yakan Seputangan Tapestry',
- description: 'A masterpiece of Yakan weaving, this tight geometric cotton tapestry can be styled as a table centerpiece or wall art.',
- price: 450000,
- category: 'Decor',
- imageUrl: 'https://images.unsplash.com/photo-1528459801416-a9e53bbf4e17?w=800&q=80',
- inStock: true,
- colors: ['Multicolor Bold', 'Harvest Autumn'],
- sizes: ['100×100cm'],
- rating: 5.0,
- reviewCount: 22,
- stockQuantity: 3,
- },
- {
- _id: 'prod_16',
- shop: 'shop_6',
- name: 'Geometric Table Runner',
- description: 'Introduce color and heritage with this heavy Yakan weave runner. Handloomed over three weeks.',
- price: 180000,
- category: 'Decor',
- imageUrl: 'https://images.unsplash.com/photo-1584917865442-de89df76afd3?w=800&q=80',
- inStock: true,
- colors: ['Primary Red/Yellow', 'Teal/Copper'],
- sizes: ['35×180cm'],
- rating: 4.8,
- reviewCount: 17,
- stockQuantity: 12,
- },
-
- // ── Inaul Legacy Textiles (shop_7) ───────────────────────────────────────
- {
- _id: 'prod_17',
- shop: 'shop_7',
- name: 'Royal Purple Inaul Malong',
- description: 'Traditional Maguindanaon tubular garment handwoven in premium silk threads with golden metallic highlights.',
- price: 500000,
- category: 'Clothing',
- imageUrl: 'https://images.unsplash.com/photo-1544441893-675973e31985?w=800&q=80',
- inStock: true,
- colors: ['Royal Purple & Gold', 'Emerald & Bronze'],
- sizes: ['One Size (Tubular)'],
- rating: 4.9,
- reviewCount: 11,
- stockQuantity: 4,
- },
- {
- _id: 'prod_18',
- shop: 'shop_7',
- name: 'Golden Weave Inaul Sash',
- description: 'A ceremonial sash highlighting the intricate checkered and floral patterns native to Cotabato weaving.',
- price: 120000,
- category: 'Accessories',
- imageUrl: 'https://images.unsplash.com/photo-1558618666-fcd25c85f82e?w=800&q=80',
- inStock: true,
- colors: ['Crimson & Gold', 'Slate & Gold'],
- sizes: ['15×150cm'],
- rating: 4.5,
- reviewCount: 8,
- stockQuantity: 15,
- },
-
- // ── Maria Cristina Metal Arts (shop_8) ───────────────────────────────────
- {
- _id: 'prod_19',
- shop: 'shop_8',
- name: 'Hand-Forged Carbon Steel Bolo',
- description: 'A heavy-duty agricultural and utility blade forged from spring steel with a Kamagong hardwood handle.',
- price: 250000,
- category: 'Tools',
- imageUrl: 'https://images.unsplash.com/photo-1509062522246-3755977927d7?w=800&q=80',
- inStock: true,
- colors: ['Natural Steel'],
- sizes: ['12-inch Blade', '14-inch Blade'],
- rating: 4.7,
- reviewCount: 33,
- stockQuantity: 7,
- },
- {
- _id: 'prod_20',
- shop: 'shop_8',
- name: 'Damascus Steel Chef Knife',
- description: 'Precision kitchen knife showing beautiful ripple patterns of layered high-carbon steel. Exceptional edge retention.',
- price: 600000,
- category: 'Kitchenware',
- imageUrl: 'https://images.unsplash.com/photo-1518895949257-7621c3c786d7?w=800&q=80',
- inStock: true,
- colors: ['Damascus Pattern'],
- sizes: ['8-inch Gyuto'],
- rating: 4.6,
- reviewCount: 21,
- stockQuantity: 5,
- },
-];
-
-export const mockOrders = [
- {
- _id: 'order_1',
- buyer: 'user_buyer_1',
- shop: 'shop_3',
- items: [
- {
- product: 'prod_7',
- name: 'Speckled Coffee Mug',
- price: 65000,
- quantity: 2,
- },
- ],
- total: 130000,
- deliveryAddress: '123 Mango Avenue, Cebu City, Cebu 6000',
- trackingNumber: 'JNT123456789',
- courier: 'J&T',
- paymentMethod: 'cod',
- status: 'shipped',
- createdAt: '2026-06-10T10:30:00Z',
- },
- {
- _id: 'order_2',
- buyer: 'user_buyer_1',
- shop: 'shop_1',
- items: [
- {
- product: 'prod_1',
- name: 'Ochre Highlands Scarf',
- price: 18500,
- quantity: 1,
- },
- ],
- total: 18500,
- deliveryAddress: '123 Mango Avenue, Cebu City, Cebu 6000',
- trackingNumber: null,
- courier: null,
- paymentMethod: 'paymongo',
- status: 'pending',
- createdAt: '2026-06-12T14:15:00Z',
- },
- {
- _id: 'order_3',
- buyer: 'user_buyer_1',
- shop: 'shop_3',
- items: [
- {
- product: 'prod_7',
- name: 'Speckled Coffee Mug',
- price: 65000,
- quantity: 1,
- },
- {
- product: 'prod_9',
- name: 'Matte Charcoal Vase',
- price: 95000,
- quantity: 1,
- },
- ],
- total: 160000,
- deliveryAddress: '123 Mango Avenue, Cebu City, Cebu 6000',
- trackingNumber: 'ENT778899001',
- courier: 'Enterprise Cargo',
- paymentMethod: 'cod',
- status: 'delivered',
- createdAt: '2026-06-08T09:00:00Z',
- },
-];
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index b9a1a6d..d9cc93c 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -1,10 +1,10 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.jsx'
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import './index.css';
+import App from './App.jsx';
createRoot(document.getElementById('root')).render(
- ,
-)
+
+);
diff --git a/frontend/src/pages/admin/categories.jsx b/frontend/src/pages/admin/categories.jsx
new file mode 100644
index 0000000..ea5f3a1
--- /dev/null
+++ b/frontend/src/pages/admin/categories.jsx
@@ -0,0 +1,223 @@
+import React, { useState, useEffect, useMemo } from "react";
+import { Plus, Edit, Trash2, FolderPlus, ListCollapse, Loader2, Eye } from 'lucide-react';
+import AdminStatCard from "../../components/admin/dashboard/AdminStatCard";
+import { categoriesAPI } from "../../services/api";
+import DataTable from "../../components/ui/DataTable";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogClose } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import ConfirmDialog from "../../components/ui/confirm-dialog";
+import toast from "react-hot-toast";
+
+const CategoriesPage = () => {
+ const [categories, setCategories] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [error, setError] = useState("");
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [editingCategory, setEditingCategory] = useState(null);
+ const [formData, setFormData] = useState({ name:'', description:'' });
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [categoryToDelete, setCategoryToDelete] = useState(null);
+
+ const loadCategories = async () => {
+ try {
+ setIsLoading(true);
+ setError('');
+ const res = await categoriesAPI.getCategories();
+ setCategories(res?.data || []);
+ } catch (err) {
+ setError(err.message ||'Failed to load categories.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => { loadCategories(); }, []);
+
+ const handleOpenAddModal = () => {
+ setEditingCategory(null);
+ setFormData({ name:'', description:'' });
+ setIsModalOpen(true);
+ };
+
+ const handleOpenEditModal = (cat) => {
+ setEditingCategory(cat);
+ setFormData({ name: cat.name, description: cat.description ||'' });
+ setIsModalOpen(true);
+ };
+
+ const confirmDelete = (cat) => {
+ setCategoryToDelete(cat);
+ setConfirmOpen(true);
+ };
+
+ const executeDelete = async () => {
+ if (!categoryToDelete) return;
+ try {
+ await categoriesAPI.deleteCategory(categoryToDelete._id);
+ toast.success('Category deleted successfully');
+ await loadCategories();
+ } catch (err) {
+ toast.error(err.message ||'Failed to delete category.');
+ } finally {
+ setCategoryToDelete(null);
+ setConfirmOpen(false);
+ }
+ };
+
+ const handleFormSubmit = async (e) => {
+ e.preventDefault();
+ if (!formData.name.trim()) return;
+ try {
+ setIsSaving(true);
+ setError('');
+ if (editingCategory) {
+ await categoriesAPI.updateCategory(editingCategory._id, formData);
+ toast.success('Category updated successfully');
+ } else {
+ await categoriesAPI.createCategory(formData);
+ toast.success('Category created successfully');
+ }
+ setIsModalOpen(false);
+ await loadCategories();
+ } catch (err) {
+ toast.error(err.message ||'Failed to save category.');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const columns = useMemo(() => [
+ {
+ header: 'Category Name',
+ accessorKey: 'name',
+ cell: ({ row }) => (
+
{row.original.name}
+ )
+ },
+ {
+ header: 'Description',
+ accessorKey: 'description',
+ cell: ({ row }) => (
+
+ {row.original.description || 'No description listed.'}
+
+ )
+ },
+ {
+ header: 'Created On',
+ accessorKey: 'createdAt',
+ cell: ({ row }) => (
+
+ {new Date(row.original.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+ )
+ },
+ {
+ header: 'Actions',
+ id: 'actions',
+ meta: { headerClassName: 'text-center', cellClassName: 'flex justify-center' },
+ cell: ({ row }) => (
+
+ )
+ }
+ ], []);
+
+ if (isLoading) {
+ return (
+
+
+
Loading categories...
+
+ );
+ }
+
+ return (
+
+ {/* Page Header */}
+
+
+
Category Management
+
Manage product and shop categories across the Artisan Hub ecosystem.
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Stats Grid */}
+
+
+
+
+
+
+ {/* Add/Edit Modal */}
+
+
+
setConfirmOpen(false)}
+ onConfirm={executeDelete}
+ title="Delete Category"
+ message={`Are you sure you want to delete the category "${categoryToDelete?.name}"? This might affect products using it.`}
+ isDestructive={true}
+ confirmText="Delete"
+ />
+
+ );
+};
+export default CategoriesPage;
diff --git a/frontend/src/pages/admin/disputes.jsx b/frontend/src/pages/admin/disputes.jsx
deleted file mode 100644
index eaba21a..0000000
--- a/frontend/src/pages/admin/disputes.jsx
+++ /dev/null
@@ -1,192 +0,0 @@
-import React, { useState } from 'react';
-import { Scale, Clock, BadgeCheck, AlertCircle, TrendingUp, Filter } from 'lucide-react';
-import AdminHeader from '../../components/admin/AdminHeader';
-import AdminStatCard from '../../components/admin/AdminStatCard';
-import ActiveDisputeQueue from '../../components/admin/ActiveDisputeQueue';
-import DisputeReasonsCard from '../../components/admin/DisputeReasonsCard';
-import RecentActivityCard from '../../components/admin/RecentActivityCard';
-import AdminActionModal from '../../components/admin/AdminActionModal';
-
-// --- Dummy Data (Data Contract for Backend) ---
-const pageData = {
- stats: {
- open: { value: '24', subtext: '12% VS LAST WEEK' },
- avgResolution: { value: '3.2 Days', subtext: '-0.4 DAYS IMPROVEMENT' },
- resolutionRate: { value: '94%', subtext: 'TARGET: 90%' },
- appeals: { value: '8', subtext: 'REQUIRES SENIOR ADMIN' }
- },
- queue: [
- {
- id: '#DISP-9021',
- shopName: 'Terra Ceramics',
- shopImg: 'https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=100&q=80',
- customerName: 'Elena Rodriguez',
- reason: 'DAMAGED',
- amount: '$145.00'
- },
- {
- id: '#DISP-8942',
- shopName: 'Loom & Leaf',
- shopImg: 'https://images.unsplash.com/photo-1590736969955-71cc94801759?w=100&q=80',
- customerName: 'Jameson Blake',
- reason: 'DELIVERY',
- amount: '$89.20'
- },
- {
- id: '#DISP-8710',
- shopName: 'Silver Lining',
- shopImg: 'https://images.unsplash.com/photo-1611085583191-a3b181a88401?w=100&q=80',
- customerName: 'Sarah Jenkins',
- reason: 'QUALITY',
- amount: '$320.00'
- },
- {
- id: '#DISP-8604',
- shopName: 'Kiln & Kin',
- shopImg: 'https://images.unsplash.com/photo-1578749556568-bc2c40e68b61?w=100&q=80',
- customerName: 'David Miller',
- reason: 'DAMAGED',
- amount: '$64.00'
- }
- ],
- reasons: [
- { reason: 'DAMAGED ITEM', percentage: 42 },
- { reason: 'ITEM NOT RECEIVED', percentage: 28 },
- { reason: 'QUALITY ISSUE', percentage: 20 }
- ],
- trendAnalysis: "Damaged item reports are up by 8%. Recommend updating 'Packaging Guidelines'.",
- activities: [
- {
- type: 'resolution',
- text: '
#DISP-8590 was resolved in favor of the customer.',
- timeAgo: '2 hours ago'
- },
- {
- type: 'message',
- text: '
Elena R. added a photo to dispute #DISP-9021.',
- timeAgo: '4 hours ago'
- }
- ]
-};
-
-const DisputesPage = () => {
- const [modalState, setModalState] = useState({ isOpen: false, title: '', message: '' });
-
- const openModal = (title, message) => {
- setModalState({ isOpen: true, title, message });
- };
-
- const closeModal = () => {
- setModalState(prev => ({ ...prev, isOpen: false }));
- };
-
- return (
-
-
openModal('Export Report', 'Download comprehensive dispute records.')}
- secondaryActionText="Filters"
- secondaryActionIcon={Filter}
- onSecondaryActionClick={() => openModal('Advanced Filters', 'Select date ranges and status filters.')}
- />
-
- {/* Header Section */}
-
-
-
- Disputes & Complaints
-
-
- Review and resolve customer conflicts within the artisan marketplace.
-
-
-
-
- {/* Stats Grid */}
-
-
-
- {pageData.stats.open.subtext}
-
- }
- icon={Scale}
- />
-
- {pageData.stats.avgResolution.subtext}
-
- }
- icon={Clock}
- />
-
- {pageData.stats.resolutionRate.subtext}
-
- }
- icon={BadgeCheck}
- />
-
- {pageData.stats.appeals.subtext}
-
- }
- icon={AlertCircle}
- iconBgClass="bg-[#F8E2DF]"
- iconColorClass="text-destructive"
- accentClass="border-l-4 border-l-destructive"
- />
-
-
- {/* Main Content Area (2-column layout) */}
-
-
- {/* Left Column: Active Dispute Queue */}
-
-
openModal('Filter Queue', 'Filter table by dispute reason or amount.')}
- onSortClick={() => openModal('Sort Queue', 'Sort table by newest or oldest.')}
- onRowClick={(id) => openModal('Review Dispute', `Opening full workspace for dispute ${id}.`)}
- />
-
-
- {/* Right Column: Sidebar Cards */}
-
-
- openModal('Activity Log', 'Viewing complete timeline of dispute events.')}
- />
-
-
-
-
- {/* Action Modal */}
-
-
- );
-};
-
-export default DisputesPage;
\ No newline at end of file
diff --git a/frontend/src/pages/admin/logs.jsx b/frontend/src/pages/admin/logs.jsx
index 9eca169..0d9d66a 100644
--- a/frontend/src/pages/admin/logs.jsx
+++ b/frontend/src/pages/admin/logs.jsx
@@ -1,168 +1,105 @@
-import React, { useState } from 'react';
-import { Banknote, CheckCircle, Hourglass, AlertCircle, TrendingUp, Calendar } from 'lucide-react';
-import AdminHeader from '../../components/admin/AdminHeader';
-import AdminStatCard from '../../components/admin/AdminStatCard';
-import TransactionLogsTable from '../../components/admin/TransactionLogsTable';
-import AuditSummaryCard from '../../components/admin/AuditSummaryCard';
-import PayoutHealthCard from '../../components/admin/PayoutHealthCard';
-import AdminActionModal from '../../components/admin/AdminActionModal';
+import React, { useState, useEffect } from 'react';
+import { Banknote, CheckCircle, Hourglass, TrendingUp, Loader2 } from 'lucide-react';
+import AdminStatCard from '../../components/admin/dashboard/AdminStatCard';
+import TransactionLogsTable from '../../components/admin/finance/TransactionLogsTable';
+import { adminAPI } from '../../services/api';
+import { formatPrice } from '../../utils/formatters';
-// --- Dummy Data (Data Contract for Backend) ---
-const pageData = {
- stats: {
- volume: { value: 'P142,850.00', subtext: '12% increase' },
- successRate: { value: '99.4%', subtext: 'STABLE GROWTH' },
- pending: { value: 'P12,400.00', subtext: '86 PROCESSING' },
- disputes: { value: '14', subtext: 'IMMEDIATE ACTION' }
- },
- transactions: [
- {
- date: 'Oct 24, 2023',
- id: 'TXN-9021834',
- shopName: 'Ancient Earth Crafts',
- type: 'SALE',
- amount: '+P245.00',
- status: 'COMPLETED'
- },
- {
- date: 'Oct 23, 2023',
- id: 'TXN-9021835',
- shopName: 'Indigo Loom Textiles',
- type: 'PAYOUT',
- amount: '-P1,200.00',
- status: 'PENDING'
- },
- {
- date: 'Oct 23, 2023',
- id: 'TXN-9021836',
- shopName: 'Obsidian Fire Glass',
- type: 'SALE',
- amount: '+P68.00',
- status: 'DISPUTED'
- },
- {
- date: 'Oct 22, 2023',
- id: 'TXN-9021837',
- shopName: 'The Iron Forge',
- type: 'REFUND',
- amount: '-P142.50',
- status: 'COMPLETED'
- }
- ],
- payoutHealth: [
- { name: 'BANK TRANSFER (ACH)', health: 99.8 },
- { name: 'STRIPE CONNECT', health: 98.9 },
- { name: 'PAYPAL GLOBAL', health: 94.2 }
- ]
-};
const LogsPage = () => {
- const [modalState, setModalState] = useState({ isOpen: false, title: '', message: '' });
- const openModal = (title, message) => {
- setModalState({ isOpen: true, title, message });
- };
- const closeModal = () => {
- setModalState(prev => ({ ...prev, isOpen: false }));
- };
+ const [orders, setOrders] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchOrders = async () => {
+ try {
+ const res = await adminAPI.getOrders();
+ if (res?.data) setOrders(res.data);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchOrders();
+ }, []);
+
+ const totalOrders = orders.length;
+ const deliveredCount = orders.filter(o => o.status === 'delivered' || o.status === 'completed').length;
+ const successRate = totalOrders > 0 ? ((deliveredCount / totalOrders) * 100).toFixed(1) : 0;
+ const totalRevenue = orders.reduce((sum, o) =>
+ sum + (o.status === 'delivered' || o.status === 'completed' ? (o.total || 0) : 0), 0);
+ const pendingCount = orders.filter(o => o.status === 'pending').length;
+
+ const formattedTransactions = orders.map(o => ({
+ date: o.createdAt ? new Date(o.createdAt).toLocaleDateString() : 'Unknown Date',
+ id: o._id ? o._id.substring(o._id.length - 8).toUpperCase() : 'UNKNOWN',
+ shopName: o.shop?.name || 'Unknown Shop',
+ type: 'SALE',
+ amount: formatPrice(o.total || 0),
+ status: o.status ? o.status.toUpperCase() : 'UNKNOWN'
+ }));
+
+ if (loading) {
+ return (
+
+
+
Loading transaction logs...
+
+ );
+ }
+
return (
-
-
openModal('Export CSV', 'Download comprehensive financial records.')}
- secondaryActionText="Filters"
- secondaryActionIcon={Calendar}
- onSecondaryActionClick={() => openModal('Advanced Filters', 'Select date ranges and transaction types.')}
- />
- {/* Header Section */}
-
-
-
- Transaction Logs
-
-
- Monitor and audit all platform financial activity from artisanal crafts to raw material sales.
-
-
+
+
+
+
+
+ Transaction Logs
+
+
+ Monitor and audit all platform financial activity.
+
+
{/* Stats Grid */}
-
+
-
- {pageData.stats.volume.subtext}
+
+ From delivered orders
}
icon={Banknote}
/>
- {pageData.stats.successRate.subtext}
+
+ {deliveredCount} of {totalOrders} ORDERS
}
icon={CheckCircle}
/>
- {pageData.stats.pending.subtext}
+
+ AWAITING PROCESSING
}
icon={Hourglass}
/>
-
- {pageData.stats.disputes.subtext}
-
- }
- icon={AlertCircle}
- iconBgClass="bg-[#F8E2DF]"
- iconColorClass="text-destructive"
- accentClass="border-l-4 border-l-destructive"
- />
- {/* Main Table Content */}
-
- openModal(`${filterName} Filter`, `Select options to filter the logs by ${filterName}.`)}
- onActionClick={(actionName, txnId) => openModal(`Action: ${actionName}`, `Viewing full details for transaction ${txnId}.`)}
- />
-
- {/* Bottom Widgets */}
-
-
-
openModal('Review Flags', 'Opening the AI audit flags queue for manual review.')}
- onDownloadClick={() => openModal('Download Audit', 'Generating the full automated audit PDF report.')}
- />
-
-
-
- {/* Action Modal */}
-
+
+ {/* Transaction Table */}
+
);
};
+
export default LogsPage;
\ No newline at end of file
diff --git a/frontend/src/pages/admin/moderate.jsx b/frontend/src/pages/admin/moderate.jsx
index 7602dfb..56c9711 100644
--- a/frontend/src/pages/admin/moderate.jsx
+++ b/frontend/src/pages/admin/moderate.jsx
@@ -1,159 +1,238 @@
-import React, { useState } from 'react';
-import {
- Hourglass,
- Flag,
- Clock,
- CheckCircle,
- TrendingDown
-} from 'lucide-react';
-import AdminHeader from '../../components/admin/AdminHeader';
-import AdminStatCard from '../../components/admin/AdminStatCard';
-import ModerationTable from '../../components/admin/ModerationTable';
-// import BulkActionOverlay from '../../components/admin/BulkActionOverlay';
-import AdminActionModal from '../../components/admin/AdminActionModal';
-
-// --- Dummy Data (Data Contract for Backend) ---
-const pageData = {
- stats: {
- pending: { value: '124', subtext: '12% less', isPositive: false },
- flagged: { value: '42', subtext: '8 urgent escalations' },
- avgTime: { value: '4.2h', subtext: 'Within SLA (6h)' },
- resolved: { value: '89', subtext: 'TARGET MET' }
- },
- tickets: [
- {
- productName: 'Hand-Carved Earthen Vase',
- id: '#PRD-9842',
- image: 'https://images.unsplash.com/photo-1610701596007-11502861dcfa?w=400&q=80',
- shopName: 'Ancient Earth Crafts',
- violation: 'COPYRIGHT',
- flaggedDate: 'Oct 24, 2023',
- flaggedTime: '10:42 AM',
- severity: 88,
- status: 'ESCALATED'
- },
- {
- productName: 'Indigo Weave Runner',
- id: '#PRD-7210',
- image: 'https://images.unsplash.com/photo-1596526131083-e8c633c948d2?w=400&q=80',
- shopName: 'Studio Indigo',
- violation: 'QUALITY',
- flaggedDate: 'Oct 24, 2023',
- flaggedTime: '09:15 AM',
- severity: 45,
- status: 'UNDER REVIEW'
- },
- {
- productName: 'Botanical Soap Set',
- id: '#PRD-3301',
- image: 'https://images.unsplash.com/photo-1600857062241-98e5dba7f214?w=400&q=80',
- shopName: "Nature's Lather",
- violation: 'PROHIBITED',
- flaggedDate: 'Oct 23, 2023',
- flaggedTime: '04:30 PM',
- severity: 12,
- status: 'PENDING'
- }
- ]
+import React, { useState, useEffect, useMemo } from 'react';
+import { Hourglass, Flag, CheckCircle, Eye, EyeOff, X, Loader2, ExternalLink } from 'lucide-react';
+import AdminStatCard from '../../components/admin/dashboard/AdminStatCard';
+import { adminAPI } from '../../services/api';
+import DataTable from '../../components/ui/DataTable';
+import { Button } from '@/components/ui/button';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import toast from 'react-hot-toast';
+
+
+const ProductDetailModal = ({ product, onClose, onToggleVisibility }) => {
+ if (!product) return null;
+
+ return (
+
+ );
};
+// ─── Main Page ─────────────────────────────────────────────────────────────────
const Moderate = () => {
- const [modalState, setModalState] = useState({ isOpen: false, title: '', message: '' });
+ const [products, setProducts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedProduct, setSelectedProduct] = useState(null);
+ const [actionLoading, setActionLoading] = useState(false);
- const openModal = (title, message) => {
- setModalState({ isOpen: true, title, message });
+ const fetchProducts = async () => {
+ try {
+ setLoading(true);
+ const res = await adminAPI.getProducts();
+ if (res?.data) setProducts(res.data);
+ } catch (err) {
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
};
- const closeModal = () => {
- setModalState(prev => ({ ...prev, isOpen: false }));
+ useEffect(() => { fetchProducts(); }, []);
+
+ const handleToggleVisibility = async (product) => {
+ try {
+ setActionLoading(true);
+ await adminAPI.toggleProduct(product.rawId);
+ toast.success(`Product ${product.status === 'ACTIVE' ? 'hidden' : 'restored'} successfully.`);
+ setSelectedProduct(null);
+ fetchProducts();
+ } catch (err) {
+ toast.error(err.message || 'Failed to update product visibility.');
+ } finally {
+ setActionLoading(false);
+ }
};
+ const flaggedCount = products.filter(p => !p.isActive).length;
+ const activeCount = products.filter(p => p.isActive).length;
+
+ const formattedProducts = products.map(p => ({
+ rawId: p._id,
+ productName: p.name,
+ id: p._id.substring(p._id.length - 8).toUpperCase(),
+ image: p.imageUrl || 'https://placehold.co/400x400/EBE5D9/8C5233?text=Product',
+ shopName: p.shop?.name || 'Unknown Shop',
+ flaggedDate: new Date(p.createdAt).toLocaleDateString(),
+ status: p.isActive ? 'ACTIVE' : 'HIDDEN',
+ }));
+
+ const columns = useMemo(() => [
+ {
+ header: 'Product',
+ accessorKey: 'product',
+ cell: ({ row }) => (
+
+ {row.original.productName}
+
+ )
+ },
+ {
+ header: 'Shop',
+ accessorKey: 'shopName',
+ cell: ({ row }) =>
{row.original.shopName}
+ },
+ {
+ header: 'Listed On',
+ accessorKey: 'flaggedDate',
+ cell: ({ row }) =>
{row.original.flaggedDate}
+ },
+ {
+ header: 'Status',
+ accessorKey: 'status',
+ cell: ({ row }) => (
+
+
+ {row.original.status}
+
+ )
+ },
+ {
+ header: 'Actions',
+ id: 'actions',
+ meta: { headerClassName: 'text-center', cellClassName: 'flex justify-center' },
+ cell: ({ row }) => (
+
+ )
+ }
+ ], []);
+
+ if (loading) {
+ return (
+
+
+
Loading product listings...
+
+ );
+ }
+
return (
-
-
openModal('Bulk Resolve', 'The bulk resolution wizard will open here.')}
- />
-
- {/* Header Section */}
-
-
-
- Product Moderation
-
-
- Review and manage reported items from the Artisan community.
-
-
+
+
+ {/* Header */}
+
+
Product Moderation
+
+ Review and manage listings from the Artisan community.
+
{/* Stats Grid */}
-
-
-
- {pageData.stats.pending.subtext}
-
- }
- icon={Hourglass}
- />
+
+
- {pageData.stats.flagged.subtext}
-
- }
+ title="HIDDEN / FLAGGED"
+ value={flaggedCount.toString()}
+ subtext={Currently Hidden}
icon={Flag}
iconBgClass="bg-[#F8E2DF]"
iconColorClass="text-destructive"
/>
-
-
- {pageData.stats.resolved.subtext}
-
- }
- icon={CheckCircle}
- />
+
- {/* Main Table Content */}
-
-
openModal(`${filterName} Filter`, `Select options to filter the moderation queue by ${filterName}.`)}
- onDownloadClick={() => openModal('Download Report', 'Download the current moderation queue data.')}
- onReviewClick={(productName) => openModal('Review Ticket', `Review full moderation details for ${productName}.`)}
+ {/* Products Table */}
+
+
0 && (
+
+ Showing {formattedProducts.length} products
+
+ )
+ }
/>
- {/* Floating Bulk Action Overlay (Mockup State) */}
- {/* openModal(`Bulk Action: ${actionName}`, `Applying ${actionName} to 4 selected tickets.`)}
- /> */}
-
- {/* Action Modal */}
-
+ {/* Product Detail Modal */}
+ {selectedProduct && (
+ setSelectedProduct(null)}
+ onToggleVisibility={handleToggleVisibility}
+ />
+ )}
);
};
diff --git a/frontend/src/pages/admin/overview.jsx b/frontend/src/pages/admin/overview.jsx
index 5d52134..5908d4d 100644
--- a/frontend/src/pages/admin/overview.jsx
+++ b/frontend/src/pages/admin/overview.jsx
@@ -1,199 +1,137 @@
-import React, { useState } from 'react';
-import {
- Store,
- Banknote,
- BadgeCheck,
- AlertTriangle,
- TrendingUp,
- TrendingDown,
- Armchair,
- Gem,
- Palette
-} from 'lucide-react';
-import AdminHeader from '../../components/admin/AdminHeader';
-import AdminStatCard from '../../components/admin/AdminStatCard';
-import PlatformGrowthChart from '../../components/admin/PlatformGrowthChart';
-import SystemAlerts from '../../components/admin/SystemAlerts';
-import CategoryPerformanceTable from '../../components/admin/CategoryPerformanceTable';
-import AdminActionModal from '../../components/admin/AdminActionModal';
-
-// --- Dummy Data (Data Contract for Backend) ---
-const pageData = {
- stats: {
- shops: { value: '4,281', subtext: '+12%', isPositive: true },
- gmv: { value: 'P1.2M', subtext: '+8.4%', isPositive: true },
- verifications: { value: '84', subtext: 'REVIEW QUEUE' },
- disputes: { value: '12', subtext: '-2.1%', isPositive: false }
- },
- growthData: [
- { label: 'OCT 01', showLabel: true, value: 30 },
- { label: '', showLabel: false, value: 40 },
- { label: '', showLabel: false, value: 35 },
- { label: 'OCT 10', showLabel: true, value: 50 },
- { label: '', showLabel: false, value: 65 },
- {
- label: '', showLabel: false, value: 100,
- isHighlighted: true,
- tooltip: { value: 'P42,800.00', date: 'Oct 12, 2023' }
- },
- { label: 'OCT 20', showLabel: true, value: 55 },
- { label: '', showLabel: false, value: 45 },
- { label: '', showLabel: false, value: 75 },
- { label: 'OCT 30', showLabel: true, value: 60 }
- ],
- systemAlerts: [
- {
- title: 'New verification request',
- message: 'The "Highland Loom" shop has submitted premium papers.',
- timeAgo: '2 MINUTES AGO',
- isUrgent: false
- },
- {
- title: 'Dispute Escalation',
- message: 'Order #8821 from "Ceramic Echoes" marked as undelivered.',
- timeAgo: '45 MINUTES AGO',
- isUrgent: true
- },
- {
- title: 'Server maintenance',
- message: 'Database optimization scheduled for 02:00 UTC.',
- timeAgo: '2 HOURS AGO',
- isUrgent: false
- }
- ],
- categoryPerformance: [
- {
- category: { name: 'Handcrafted Furniture', icon: Armchair },
- activeShops: '1,120',
- avgSales: 'P4,250',
- growth: '+14.2%',
- status: 'TRENDING'
- },
- {
- category: { name: 'Artisanal Jewelry', icon: Gem },
- activeShops: '845',
- avgSales: 'P1,980',
- growth: '+5.8%',
- status: 'STABLE'
- },
- {
- category: { name: 'Ceramics & Pottery', icon: Palette },
- activeShops: '562',
- avgSales: 'P2,110',
- growth: '-1.2%',
- status: 'STABLE'
- }
- ]
-};
+import React, { useState, useEffect } from 'react';
+import { Store, Banknote, BadgeCheck, Users, Loader2 } from 'lucide-react';
+import AdminStatCard from '../../components/admin/dashboard/AdminStatCard';
+import SystemAlerts from '../../components/admin/dashboard/SystemAlerts';
+import CategoryPerformanceTable from '../../components/admin/analytics/CategoryPerformanceTable';
+import { adminAPI } from '../../services/api';
+import { formatPrice } from '../../utils/formatters';
const Overview = () => {
- const [modalState, setModalState] = useState({ isOpen: false, title: '', message: '' });
+ const [stats, setStats] = useState({
+ users: 0,
+ shops: 0,
+ orders: 0,
+ revenue: 0,
+ pendingVerifications: 0,
+ growthData: [],
+ categoryPerformance: []
+ });
+ const [recentShops, setRecentShops] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState('');
- const openModal = (title, message) => {
- setModalState({ isOpen: true, title, message });
- };
+ const loadOverviewData = async () => {
+ try {
+ setIsLoading(true);
+ setError('');
+ const [statsRes, shopsRes] = await Promise.all([
+ adminAPI.getStats(),
+ adminAPI.getShops()
+ ]);
+
+ const shopsList = shopsRes?.data || [];
+ const pendingCount = shopsList.filter(s => s.verificationStatus === 'pending').length;
- const closeModal = () => {
- setModalState(prev => ({ ...prev, isOpen: false }));
+ setStats({
+ users: statsRes?.data?.users || 0,
+ shops: statsRes?.data?.shops || 0,
+ orders: statsRes?.data?.orders || 0,
+ revenue: statsRes?.data?.revenue || 0,
+ pendingVerifications: pendingCount,
+ growthData: statsRes?.data?.growthData || [],
+ categoryPerformance: statsRes?.data?.categoryPerformance || []
+ });
+
+ const pendingShops = shopsList.filter(s => s.verificationStatus === 'pending').slice(0, 4);
+ setRecentShops(pendingShops);
+ } catch (err) {
+ setError(err.message || 'Failed to load stats.');
+ } finally {
+ setIsLoading(false);
+ }
};
+ useEffect(() => { loadOverviewData(); }, []);
+
+ const systemAlerts = recentShops.length > 0
+ ? recentShops.map(shop => ({
+ title: shop.name,
+ message: `Submitted verification documents and is waiting for review.`,
+ timeAgo: new Date(shop.createdAt).toLocaleDateString(),
+ isUrgent: false
+ }))
+ : [{
+ title: 'No pending verifications',
+ message: 'All shop verification requests have been reviewed.',
+ timeAgo: 'Up to date',
+ isUrgent: false
+ }];
+
+ if (isLoading) {
+ return (
+
+
+
Loading dashboard...
+
+ );
+ }
+
return (
-
-
openModal('Export Report', 'The report generation and export feature is coming soon.')} />
+
- {/* Header Section */}
-
-
-
- System Overview
-
-
- Real-time health and performance metrics for the Artisan Hub ecosystem.
-
-
+
+
+
+ Dashboard
+
+
+ A live overview of the Artisan Hub platform.
+
- {/* Stats Grid */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Stats */}
-
- {pageData.stats.shops.subtext}
-
- }
+ title="Total Shops"
+ value={stats.shops}
+ subtext="Registered stores"
icon={Store}
/>
-
- {pageData.stats.gmv.subtext}
-
- }
+ title="Total Revenue"
+ value={formatPrice(stats.revenue)}
+ subtext="From delivered orders"
icon={Banknote}
/>
- {pageData.stats.verifications.subtext}
-
+ Needs review
}
icon={BadgeCheck}
/>
-
- {pageData.stats.disputes.subtext}
-
- }
- icon={AlertTriangle}
- iconBgClass="bg-[#F8E2DF]"
- iconColorClass="text-destructive"
+ title="Total Users"
+ value={stats.users}
+ subtext="Buyers and sellers"
+ icon={Users}
/>
- {/* Main Content Layout */}
-
- {/* Left Column (Chart) */}
-
-
- {/* Right Column (Alerts) */}
-
- openModal('System Activity Log', 'Full system activity log and filters will be displayed here.')}
- />
-
-
-
- {/* Bottom Layout (Table) */}
-
-
openModal('Region Filter', 'Geographic filtering options will be available soon.')}
- onViewDetailedAnalyticsClick={() => openModal('Detailed Analytics', 'Comprehensive category analytics dashboard is under construction.')}
- />
+ {/* Alerts */}
+
+
- {/* Action Modal */}
-
+ {/* Category Table */}
+
);
};
diff --git a/frontend/src/pages/admin/users.jsx b/frontend/src/pages/admin/users.jsx
index 885d1a8..d0c48ff 100644
--- a/frontend/src/pages/admin/users.jsx
+++ b/frontend/src/pages/admin/users.jsx
@@ -1,123 +1,174 @@
-import React, { useState } from 'react';
-import {
- Users,
- Store,
- Ban,
- UserPlus,
- TrendingUp
-} from 'lucide-react';
-import AdminHeader from '../../components/admin/AdminHeader';
-import AdminStatCard from '../../components/admin/AdminStatCard';
-import UserDirectoryTable from '../../components/admin/UserDirectoryTable';
-import AdminActionModal from '../../components/admin/AdminActionModal';
+import React, { useState, useEffect } from 'react';
+import { Users, Store, Ban, UserPlus, Loader2 } from 'lucide-react';
+import AdminStatCard from '../../components/admin/dashboard/AdminStatCard';
+import UserDirectoryTable from '../../components/admin/users/UserDirectoryTable';
+import { adminAPI } from '../../services/api';
+import ConfirmDialog from '../../components/ui/confirm-dialog';
+import toast from 'react-hot-toast';
-// --- Dummy Data (Data Contract for Backend) ---
-const pageData = {
- stats: {
- totalUsers: { value: '24,892', subtext: '12% increase' },
- activeShops: { value: '1,402', subtext: 'Active Now' },
- suspended: { value: '43', subtext: 'Immediate Action' },
- newReg: { value: '128', subtext: 'LAST 24H' }
- },
- directory: [
- {
- name: 'Aurelia James',
- subtext: 'EARTH & FIRE STUDIO',
- avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=400&q=80',
- role: 'SELLER',
- status: 'VERIFIED',
- joinDate: 'Oct 12, 2023'
- },
- {
- name: 'Marcus Smith',
- subtext: 'MARCUS.VIBE@EMAIL.COM',
- avatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=400&q=80',
- role: 'CUSTOMER',
- status: 'PENDING',
- joinDate: 'Jan 05, 2024'
- },
- {
- name: 'Liam Weaver',
- subtext: 'THE LOOM & HIDE',
- avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=400&q=80',
- role: 'SELLER',
- status: 'SUSPENDED',
- joinDate: 'Nov 22, 2023'
+const UsersPage = () => {
+ const [users, setUsers] = useState([]);
+ const [shops, setShops] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [confirmState, setConfirmState] = useState({
+ title: '',
+ message: '',
+ isDestructive: false,
+ confirmText: 'Confirm',
+ onConfirm: () => {}
+ });
+
+ const loadUserData = async () => {
+ try {
+ setIsLoading(true);
+ setError('');
+ const [usersRes, shopsRes] = await Promise.all([
+ adminAPI.getUsers(),
+ adminAPI.getShops()
+ ]);
+ setUsers(usersRes?.data || []);
+ setShops(shopsRes?.data || []);
+ } catch (err) {
+ setError(err.message || 'Failed to load user records.');
+ } finally {
+ setIsLoading(false);
}
- ]
-};
+ };
-const UsersPage = () => {
- const [modalState, setModalState] = useState({ isOpen: false, title: '', message: '' });
+ useEffect(() => {
+ loadUserData();
+ }, []);
- const openModal = (title, message) => {
- setModalState({ isOpen: true, title, message });
+ const handleActionClick = async (actionName, row) => {
+ if (actionName === 'Suspend' || actionName === 'Restore') {
+ setConfirmState({
+ title: `${actionName} User`,
+ message: `Are you sure you want to ${actionName.toLowerCase()} ${row.name}?`,
+ isDestructive: actionName === 'Suspend',
+ confirmText: actionName,
+ onConfirm: async () => {
+ try {
+ await adminAPI.toggleUser(row.id);
+ toast.success(`User ${actionName.toLowerCase()}d successfully.`);
+ const usersRes = await adminAPI.getUsers();
+ setUsers(usersRes?.data || []);
+ } catch (err) {
+ toast.error(err.message || 'Failed to update user status.');
+ }
+ }
+ });
+ setConfirmOpen(true);
+ } else if (actionName === 'View Shop') {
+ const userShop = shops.find(s => s.owner?._id === row.id || s.owner === row.id);
+ if (!userShop) { toast.error('No shop associated with this seller.'); }
+ }
};
- const closeModal = () => {
- setModalState(prev => ({ ...prev, isOpen: false }));
+ const handleDeleteUser = async (row) => {
+ setConfirmState({
+ title: 'Delete User',
+ message: `This will permanently delete "${row.name}" and all their data. This cannot be undone. Continue?`,
+ isDestructive: true,
+ confirmText: 'Delete',
+ onConfirm: async () => {
+ try {
+ await adminAPI.deleteUser(row.id);
+ toast.success('User deleted successfully.');
+ const usersRes = await adminAPI.getUsers();
+ setUsers(usersRes?.data || []);
+ } catch (err) {
+ toast.error(err.message || 'Failed to delete user.');
+ }
+ }
+ });
+ setConfirmOpen(true);
};
+ const directory = users.map(user => {
+ const isSeller = user.role === 'seller';
+ const shopOfUser = isSeller ? shops.find(s => s.owner?._id === user._id) : null;
+ return {
+ id: user._id,
+ name: user.name,
+ subtext: shopOfUser ? shopOfUser.name.toUpperCase() : user.email,
+ avatar: user.avatarUrl || `https://ui-avatars.com/api/?name=${encodeURIComponent(user.name)}&background=EBE5D9&color=8C5233&size=128`,
+ role: user.role.toUpperCase(),
+ status: user.isActive ? 'VERIFIED' : 'SUSPENDED',
+ joinDate: new Date(user.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
+ };
+ });
+
+ const totalUsersCount = users.length;
+ const activeShopsCount = shops.filter(s => s.isActive).length;
+ const suspendedCount = users.filter(u => !u.isActive).length;
+ const newRegCount = users.filter(u => new Date(u.createdAt) > new Date(Date.now() - 24 * 60 * 60 * 1000)).length;
+
+ if (isLoading) {
+ return (
+
+
+
Loading directory...
+
+ );
+ }
+
return (
-
-
openModal('Export Data', 'Download comprehensive user and store reports.')}
- secondaryActionText="Add New User"
- secondaryActionIcon={UserPlus}
- onSecondaryActionClick={() => openModal('Add New User', 'The user creation wizard will open here.')}
- />
+
- {/* Header Section */}
-
+
+
-
+
User & Store Management
-
+
Oversee Artisan Hub's ecosystem of creators and customers.
+ {error && (
+
+ {error}
+
+ )}
+
{/* Stats Grid */}
-
- {pageData.stats.totalUsers.subtext}
+
+ System Record
}
icon={Users}
/>
- {pageData.stats.suspended.subtext}
+
+ Requires Action
}
icon={Ban}
- iconBgClass="bg-[#F8E2DF]"
- iconColorClass="text-destructive"
/>
- {pageData.stats.newReg.subtext}
+
+ LAST 24H
}
icon={UserPlus}
@@ -126,19 +177,18 @@ const UsersPage = () => {
{/* Main Table Content */}
- openModal(`${filterName} Filter`, `Select options to filter the user directory by ${filterName}.`)}
- onActionClick={(actionName, userName) => openModal(`Action: ${actionName}`, `Applying ${actionName} to user ${userName}.`)}
+
- {/* Action Modal */}
- setConfirmOpen(false)}
+ {...confirmState}
/>
);
diff --git a/frontend/src/pages/admin/verify.jsx b/frontend/src/pages/admin/verify.jsx
index 425526d..c5236bd 100644
--- a/frontend/src/pages/admin/verify.jsx
+++ b/frontend/src/pages/admin/verify.jsx
@@ -1,155 +1,335 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import {
ClipboardList,
Clock,
Percent,
BadgeCheck,
- TrendingUp,
+ Loader2,
Palette,
- Gem,
- Hammer,
- Scissors
+ FileText,
+ ExternalLink,
+ Check,
+ X as XIcon,
+ AlertCircle,
+ User
} from 'lucide-react';
-import AdminHeader from '../../components/admin/AdminHeader';
-import AdminStatCard from '../../components/admin/AdminStatCard';
-import ApplicationsTable from '../../components/admin/ApplicationsTable';
-import BatchVerifyFAB from '../../components/admin/BatchVerifyFAB';
-import AdminActionModal from '../../components/admin/AdminActionModal';
-
-// --- Dummy Data (Data Contract for Backend) ---
-const pageData = {
- stats: {
- pending: { value: '42', subtext: '+5', isPositive: true },
- avgReviewTime: { value: '18 hrs', subtext: 'Goal: 12h' },
- approvalRate: { value: '76%', subtext: 'Target: 80%' },
- verifiedToday: { value: '12', subtext: 'DAILY STATS' }
- },
- applications: [
- {
- name: 'Earthbound Arts',
- id: '#SHOP-9041',
- icon: Palette,
- category: 'Ceramics & Clay',
- appliedOn: 'Oct 24, 2023',
- score: 92,
- status: 'UNDER REVIEW'
- },
- {
- name: 'Silver Lining Studio',
- id: '#SHOP-8822',
- icon: Gem,
- category: 'Jewelry Design',
- appliedOn: 'Oct 25, 2023',
- score: 48,
- status: 'FLAGGED'
- },
- {
- name: 'Wildwood Craft',
- id: '#SHOP-9128',
- icon: Hammer,
- category: 'Woodworking',
- appliedOn: 'Oct 26, 2023',
- score: 85,
- status: 'NEW'
- },
- {
- name: 'Northern Fabrics',
- id: '#SHOP-9233',
- icon: Scissors,
- category: 'Textiles',
- appliedOn: 'Oct 26, 2023',
- score: 78,
- status: 'NEW'
- }
- ]
-};
+import AdminStatCard from '../../components/admin/dashboard/AdminStatCard';
+import ApplicationsTable from '../../components/admin/shops/ApplicationsTable';
+import { adminAPI } from '../../services/api';
+import { Button } from '@/components/ui/button';
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+import ConfirmDialog from '../../components/ui/confirm-dialog';
+import toast from 'react-hot-toast';
const Verify = () => {
- const [modalState, setModalState] = useState({ isOpen: false, title: '', message: '' });
+ const [shops, setShops] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState('');
+ const [selectedShop, setSelectedShop] = useState(null);
+ const [isActioning, setIsActioning] = useState(false);
+ const [confirmOpen, setConfirmOpen] = useState(false);
+ const [confirmState, setConfirmState] = useState({ shopId: null, status: null });
- const openModal = (title, message) => {
- setModalState({ isOpen: true, title, message });
+ const loadShops = async () => {
+ try {
+ setIsLoading(true);
+ setError('');
+ const res = await adminAPI.getShops();
+ setShops(res?.data || []);
+ } catch (err) {
+ setError(err.message || 'Failed to load shops queue.');
+ } finally {
+ setIsLoading(false);
+ }
};
- const closeModal = () => {
- setModalState(prev => ({ ...prev, isOpen: false }));
+ useEffect(() => {
+ loadShops();
+ }, []);
+
+ const confirmVerifyAction = (shopId, status) => {
+ setConfirmState({ shopId, status });
+ setConfirmOpen(true);
};
+ const executeVerifyAction = async () => {
+ const { shopId, status } = confirmState;
+ if (!shopId) return;
+
+ try {
+ setIsActioning(true);
+ await adminAPI.verifyShop(shopId, status);
+ toast.success(`Shop has been successfully ${status}.`);
+ setSelectedShop(null);
+ await loadShops();
+ } catch (err) {
+ toast.error(err.message || 'Failed to update shop status.');
+ } finally {
+ setIsActioning(false);
+ setConfirmOpen(false);
+ }
+ };
+
+
+ const pendingShops = shops.filter(s => s.verificationStatus === 'pending');
+ const verifiedShopsCount = shops.filter(s => s.verificationStatus === 'verified').length;
+ const rejectedShopsCount = shops.filter(s => s.verificationStatus === 'rejected').length;
+ const totalApplications = shops.filter(s => s.verificationStatus && s.verificationStatus !== 'none').length;
+ const approvalRate = totalApplications > 0
+ ? Math.round((verifiedShopsCount / totalApplications) * 100)
+ : 100;
+
+
+ const tableData = shops.map(shop => {
+ let displayStatus = 'NEW';
+ if (shop.verificationStatus === 'pending') displayStatus = 'UNDER REVIEW';
+ if (shop.verificationStatus === 'verified') displayStatus = 'VERIFIED';
+ if (shop.verificationStatus === 'rejected') displayStatus = 'FLAGGED';
+
+ return {
+ _id: shop._id,
+ name: shop.name,
+ id: `#SHOP-${shop._id.substring(shop._id.length - 4).toUpperCase()}`,
+ icon: Palette,
+ category: shop.category || 'General Craft',
+ appliedOn: new Date(shop.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
+ score: shop.businessPermitUrl && shop.governmentIdUrl ? 95 : 45,
+ status: displayStatus,
+ ownerName: shop.owner?.name || 'Unknown Owner',
+ ownerEmail: shop.owner?.email || '',
+ description: shop.description || 'No description listed.',
+ businessPermitUrl: shop.businessPermitUrl,
+ governmentIdUrl: shop.governmentIdUrl,
+ verificationStatus: shop.verificationStatus
+ };
+ });
+
+ if (isLoading) {
+ return (
+
+
+
Loading verification queue...
+
+ );
+ }
+
return (
-
-
openModal('Export Report', 'Verification queue reports can be exported here.')} />
+
- {/* Header Section */}
-
+
+
-
- Verified Shops Queue
+
+ Seller Verification
-
- Review and verify pending shop applications to maintain marketplace quality.
+
+ Review shop applications and approve or reject seller accounts.
+ {error && (
+
+ {error}
+
+ )}
+
{/* Stats Grid */}
-
- {pageData.stats.pending.subtext}
-
- }
+ title="Pending Applications"
+ value={pendingShops.length}
+ subtext="Awaiting Moderation"
icon={ClipboardList}
/>
- {pageData.stats.verifiedToday.subtext}
-
- }
+ title="Verified Shops"
+ value={verifiedShopsCount}
+ subtext="Active Verified Accounts"
icon={BadgeCheck}
/>
- {/* Main Table Content */}
+
openModal('Filter Applications', 'Filter by status, category, or application date.')}
- onDownloadClick={() => openModal('Download Queue', 'Download the current verification queue data.')}
- onRowClick={(shopName) => openModal('Review Application', `Review full application details for ${shopName}.`)}
+ data={tableData}
+ onRowClick={(shop) => setSelectedShop(shop)}
/>
- {/* Action Modal */}
-
+
+
- {/* Floating Action Button */}
-
openModal('Batch Verify', 'Select multiple shops to verify them simultaneously.')}
+ setConfirmOpen(false)}
+ onConfirm={executeVerifyAction}
+ title="Verify Shop Application"
+ message={`Are you sure you want to set this shop status to ${confirmState.status?.toUpperCase()}?`}
+ isDestructive={confirmState.status === 'rejected'}
/>
);
diff --git a/frontend/src/pages/admin/withdrawals.jsx b/frontend/src/pages/admin/withdrawals.jsx
new file mode 100644
index 0000000..7a3b19d
--- /dev/null
+++ b/frontend/src/pages/admin/withdrawals.jsx
@@ -0,0 +1,215 @@
+import { useState, useEffect, useMemo } from 'react';
+import { Search, CheckCircle, XCircle, Loader2, Banknote, Eye } from 'lucide-react';
+import { adminAPI } from '../../services/api';
+import { StatusBadge } from '../../components/admin/common/StatusBadge';
+import DataTable from '../../components/ui/DataTable';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
+
+const WithdrawalDetailModal = ({ withdrawal, onClose, onUpdateStatus }) => {
+ if (!withdrawal) return null;
+ return (
+
+ );
+};
+
+const Withdrawals = () => {
+ const [withdrawals, setWithdrawals] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [selectedWithdrawal, setSelectedWithdrawal] = useState(null);
+
+ useEffect(() => {
+ fetchWithdrawals();
+ }, []);
+
+ const fetchWithdrawals = async () => {
+ try {
+ setLoading(true);
+ const res = await adminAPI.getWithdrawals();
+ if (res?.data) setWithdrawals(res.data);
+ } catch (error) {
+ console.error('Failed to load withdrawals:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleUpdateStatus = async (id, status) => {
+ try {
+ await adminAPI.updateWithdrawalStatus(id, status, `Payout ${status} by admin`);
+ fetchWithdrawals();
+ } catch (error) {
+ console.error(`Failed to mark withdrawal as ${status}:`, error);
+ }
+ };
+
+ // Map payout status to our unified badge vocabulary
+ const normalizeStatus = (s) => {
+ const map = { completed: 'COMPLETED', rejected: 'REJECTED', approved: 'APPROVED', pending: 'PENDING' };
+ return map[s] || s.toUpperCase();
+ };
+
+ const columns = useMemo(() => [
+ {
+ header: 'Requested On',
+ accessorKey: 'createdAt',
+ cell: ({ row }) => (
+
+ {new Date(row.original.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
+
+ )
+ },
+ {
+ header: 'Shop',
+ accessorKey: 'shop',
+ cell: ({ row }) => (
+
{row.original.shop?.name || 'Unknown Shop'}
+ )
+ },
+ {
+ header: 'Payout Method',
+ accessorKey: 'method',
+ cell: ({ row }) => (
+
+ {row.original.method}
+
+ )
+ },
+ {
+ header: 'Account Name',
+ accessorKey: 'accountName',
+ cell: ({ row }) => (
+
+ {row.original.accountName}
+
+ )
+ },
+ {
+ header: 'Account Number',
+ accessorKey: 'accountNumber',
+ cell: ({ row }) => (
+
+ {row.original.accountNumber}
+
+ )
+ },
+ {
+ header: 'Amount',
+ accessorKey: 'amount',
+ cell: ({ row }) => (
+
+ {(row.original.amount / 100).toLocaleString('en-PH', { style: 'currency', currency: 'PHP' })}
+
+ )
+ },
+ {
+ header: 'Status',
+ accessorKey: 'status',
+ cell: ({ row }) => (
+
+ )
+ },
+ {
+ header: 'Actions',
+ id: 'actions',
+ meta: { headerClassName: 'text-center', cellClassName: 'flex justify-center' },
+ cell: ({ row }) => (
+
+ )
+ }
+ ], [handleUpdateStatus]);
+
+ if (loading) {
+ return (
+
+
+
Loading withdrawal requests...
+
+ );
+ }
+
+ return (
+
+
+ {/* Page Header */}
+
+
+
+ Payout Requests
+
+
+ Review, approve, and process seller withdrawal requests.
+
+
+
+
+
+
+
+
+
setSelectedWithdrawal(null)}
+ onUpdateStatus={handleUpdateStatus}
+ />
+
+ );
+};
+
+export default Withdrawals;
diff --git a/frontend/src/pages/buyer/BuyerProfile.jsx b/frontend/src/pages/buyer/BuyerProfile.jsx
index dea4d82..8637ed1 100644
--- a/frontend/src/pages/buyer/BuyerProfile.jsx
+++ b/frontend/src/pages/buyer/BuyerProfile.jsx
@@ -5,15 +5,16 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Map, MapMarker, MapControls, MarkerContent, MarkerLabel } from '@/components/ui/map';
+import { usePHLocations } from '../../hooks/usePHLocations';
const BuyerProfile = () => {
const navigate = useNavigate();
const [isSaved, setIsSaved] = useState(false);
-
+
const [formData, setFormData] = useState(() => {
const saved = localStorage.getItem('buyerProfile');
if (saved) {
- try { return JSON.parse(saved); } catch (e) {}
+ try { return JSON.parse(saved); } catch (e) { }
}
return {
firstName: '',
@@ -42,10 +43,25 @@ const BuyerProfile = () => {
const handleInputChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value });
- const handleAddressChange = (e) => setFormData({
- ...formData,
- address: { ...formData.address, [e.target.name]: e.target.value }
- });
+ const { provinces, cities, getCities, loadingProvinces, loadingCities } = usePHLocations();
+
+ const handleAddressChange = (e) => {
+ const { name, value } = e.target;
+ if (name === 'state') {
+ setFormData(prev => ({
+ ...prev,
+ address: { ...prev.address, state: value, city: '' }
+ }));
+ const prov = provinces.find(p => p.name === value);
+ if (prov) getCities(prov.code);
+ else getCities(null);
+ } else {
+ setFormData(prev => ({
+ ...prev,
+ address: { ...prev.address, [name]: value }
+ }));
+ }
+ };
const handleFileChange = (e, field) => {
if (e.target.files && e.target.files[0]) {
@@ -75,7 +91,7 @@ const BuyerProfile = () => {
};
const handleLogout = () => {
- // Clear any auth tokens or user context here if they exist
+
navigate('/login');
};
@@ -83,19 +99,19 @@ const BuyerProfile = () => {
return (
- {/* Header Banner */}
+
+
- {/* Left Sidebar Profile Summary */}
+
- {/* Avatar */}
@@ -136,9 +152,9 @@ const BuyerProfile = () => {
-