first commit
This commit is contained in:
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
out/
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Mac / Linux / Windows system files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary
|
||||
*.tmp
|
||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Access Hub Homepage Design</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4248
package-lock.json
generated
Normal file
4248
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
package.json
Normal file
65
package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Access Hub Homepage Design",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-menubar": "^1.1.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slider": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toggle": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.1",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "*",
|
||||
"cmdk": "^1.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.487.0",
|
||||
"motion": "*",
|
||||
"next-themes": "^0.4.6",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.2",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "*",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"vaul": "^1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"vite": "6.3.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
}
|
||||
}
|
||||
5
postcss.config.mjs
Normal file
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
}
|
||||
}
|
||||
54
src/AdminApp.tsx
Normal file
54
src/AdminApp.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { AdminShell } from "./components/admin/AdminShell";
|
||||
import { OverviewPage } from "./components/admin/pages/OverviewPage";
|
||||
import { UsersRetailersPage } from "./components/admin/pages/UsersRetailersPage";
|
||||
import { UsersManufacturersPage } from "./components/admin/pages/UsersManufacturersPage";
|
||||
import { UsersPage } from "./components/admin/pages/UsersPage";
|
||||
import { ContentPage } from "./components/admin/pages/ContentPage";
|
||||
import { AnalyticsPage } from "./components/admin/pages/AnalyticsPage";
|
||||
import { SettingsPage } from "./components/admin/pages/SettingsPage";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
|
||||
export default function AdminApp() {
|
||||
const [currentPage, setCurrentPage] = useState("overview");
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
const renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case "overview":
|
||||
return <OverviewPage onNavigate={setCurrentPage} />;
|
||||
case "users-retailers":
|
||||
return <UsersRetailersPage />;
|
||||
case "users-manufacturers":
|
||||
return <UsersManufacturersPage />;
|
||||
case "users-customers":
|
||||
return <UsersPage />;
|
||||
case "content":
|
||||
return <ContentPage />;
|
||||
case "analytics":
|
||||
return <AnalyticsPage />;
|
||||
case "settings":
|
||||
return <SettingsPage />;
|
||||
default:
|
||||
return <OverviewPage onNavigate={setCurrentPage} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<AdminShell currentPage={currentPage} onNavigate={setCurrentPage}>
|
||||
{renderPage()}
|
||||
</AdminShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
439
src/App.tsx
Normal file
439
src/App.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
import { useState, useEffect, Suspense, lazy } from "react";
|
||||
import {
|
||||
Shield,
|
||||
ShoppingBag,
|
||||
Store,
|
||||
Factory,
|
||||
HelpCircle,
|
||||
ArrowRight,
|
||||
Moon,
|
||||
Sun,
|
||||
Globe,
|
||||
Sparkles,
|
||||
Users,
|
||||
Award,
|
||||
Gem,
|
||||
} from "lucide-react";
|
||||
import { PortalTile } from "./components/PortalTile";
|
||||
import { PortalOverlay } from "./components/PortalOverlay";
|
||||
import { HelpOverlay } from "./components/HelpOverlay";
|
||||
import { Footer } from "./components/Footer";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Card } from "./components/ui/card";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./components/ui/select";
|
||||
|
||||
const AdminApp = lazy(() => import("./AdminApp"));
|
||||
const CustomerApp = lazy(() => import("./CustomerApp"));
|
||||
const RetailerApp = lazy(() => import("./RetailerApp"));
|
||||
const ManufacturerApp = lazy(() => import("./ManufacturerApp"));
|
||||
|
||||
const PORTAL_DATA = {
|
||||
admin: {
|
||||
icon: Shield,
|
||||
title: "Admin Console",
|
||||
description: "Control tenants, roles, compliance, analytics.",
|
||||
features: [
|
||||
"Manage user roles and permissions",
|
||||
"Monitor platform analytics",
|
||||
"Configure compliance settings",
|
||||
],
|
||||
keyActions: [
|
||||
"Create and manage tenant organizations",
|
||||
"Assign and revoke user permissions",
|
||||
"View system-wide analytics and reports",
|
||||
"Configure DPDP compliance settings",
|
||||
],
|
||||
requiresLogin: false,
|
||||
tenantScope: "Super Admin or Tenant Admin",
|
||||
},
|
||||
customer: {
|
||||
icon: ShoppingBag,
|
||||
title: "Customer App",
|
||||
description: "Browse collections, wishlist, chat, book appointments.",
|
||||
features: [
|
||||
"Explore jewellery collections",
|
||||
"Create wishlists and favorites",
|
||||
"Book in-store appointments",
|
||||
],
|
||||
keyActions: [
|
||||
"Browse curated jewellery catalogs",
|
||||
"Save items to personal wishlist",
|
||||
"Chat with retailers and designers",
|
||||
"Schedule showroom visits",
|
||||
],
|
||||
requiresLogin: false,
|
||||
tenantScope: "Individual Customer",
|
||||
},
|
||||
retailer: {
|
||||
icon: Store,
|
||||
title: "Retailers' Shop",
|
||||
description: "Curate catalogues, publish virtual stores, manage CRM.",
|
||||
features: [
|
||||
"Build and customize virtual store",
|
||||
"Manage customer relationships",
|
||||
"Track inventory and orders",
|
||||
],
|
||||
keyActions: [
|
||||
"Create and publish product catalogs",
|
||||
"Manage customer data and preferences",
|
||||
"Process orders and track fulfillment",
|
||||
"Generate sales reports",
|
||||
],
|
||||
requiresLogin: false,
|
||||
tenantScope: "Retailer Tenant",
|
||||
},
|
||||
manufacturer: {
|
||||
icon: Factory,
|
||||
title: "Manufacturers' Shop",
|
||||
description: "Sync inventory, share with retailers, handle custom orders.",
|
||||
features: [
|
||||
"Synchronize production inventory",
|
||||
"Share catalogs with retailers",
|
||||
"Manage custom order requests",
|
||||
],
|
||||
keyActions: [
|
||||
"Upload and sync inventory data",
|
||||
"Share product listings with retail partners",
|
||||
"Accept and process custom orders",
|
||||
"Track production schedules",
|
||||
],
|
||||
requiresLogin: false,
|
||||
tenantScope: "Manufacturer Tenant",
|
||||
},
|
||||
};
|
||||
|
||||
const TRANSLATIONS = {
|
||||
en: {
|
||||
hero_title: "Your Jewellery Ecosystem, One Hub",
|
||||
hero_description: "A unified platform connecting admins, customers, retailers, and manufacturers in the jewellery industry. Streamline operations, enhance collaboration, and grow your business.",
|
||||
get_started: "Get Started",
|
||||
learn_more: "Learn More",
|
||||
portals_title: "Specialized Portals for Every Role",
|
||||
portals_description: "Each portal is designed specifically for its users, providing the exact tools and features needed for success",
|
||||
cta_title: "Ready to Transform Your Jewellery Business?",
|
||||
cta_description: "Join hundreds of businesses already using Hello Jewellers to streamline their operations and grow their sales",
|
||||
get_started_today: "Get Started Today",
|
||||
view_docs: "View Documentation",
|
||||
},
|
||||
hi: {
|
||||
hero_title: "आपका आभूषण इकोसिस्टम, एक हब",
|
||||
hero_description: "आभूषण उद्योग में एडमिन, ग्राहकों, रिटेलर्स और निर्माताओं को जोड़ने वाला एक एकीकृत मंच। संचालन को सुव्यवस्थित करें, सहयोग बढ़ाएं और अपने व्यवसाय को बढ़ाएं।",
|
||||
get_started: "शुरू करें",
|
||||
learn_more: "और जानें",
|
||||
portals_title: "हर भूमिका के लिए विशेष पोर्टल",
|
||||
portals_description: "प्रत्येक पोर्टल विशेष रूप से अपने उपयोगकर्ताओं के लिए डिज़ाइन किया गया है",
|
||||
cta_title: "अपने आभूषण व्यवसाय को बदलने के लिए तैयार हैं?",
|
||||
cta_description: "सैकड़ों व्यवसाय पहले से ही हैलो ज्वेलर्स का उपयोग कर रहे हैं",
|
||||
get_started_today: "आज ही शुरू करें",
|
||||
view_docs: "दस्तावेज़ देखें",
|
||||
},
|
||||
mr: {
|
||||
hero_title: "तुमचा दागिना परिसंस्था, एक केंद्र",
|
||||
hero_description: "दागिना उद्योगात प्रशासक, ग्राहक, किरकोळ विक्रेते आणि उत्पादकांना जोडणारे एकीकृत व्यासपीठ. ऑपरेशन सुव्यवस्थित करा, सहयोग वाढवा आणि आपला व्यवसाय वाढवा।",
|
||||
get_started: "सुरू करा",
|
||||
learn_more: "अधिक जाणून घ्या",
|
||||
portals_title: "प्रत्येक भूमिकेसाठी विशेष पोर्टल",
|
||||
portals_description: "प्रत्येक पोर्टल विशेषतः त्याच्या वापरकर्त्यांसाठी डिझाइन केलेले आहे",
|
||||
cta_title: "आपला दागिना व्यवसाय बदलण्यास तयार आहात?",
|
||||
cta_description: "शेकडो व्यवसाय आधीच हॅलो ज्वेलर्स वापरत आहेत",
|
||||
get_started_today: "आज सुरू करा",
|
||||
view_docs: "कागदपत्रे पहा",
|
||||
},
|
||||
gu: {
|
||||
hero_title: "તમારું દાગીના ઇકોસિસ્ટમ, એક હબ",
|
||||
hero_description: "દાગીના ઉદ્યોગમાં એડમિન, ગ્રાહકો, રિટેલર્સ અને ઉત્પાદકોને જોડતું એકીકૃત પ્લેટફોર્મ. કામગીરીને સુવ્યવસ્થિત કરો, સહયોગ વધારો અને તમારા વ્યવસાયને વધારો.",
|
||||
get_started: "પ્રારંભ કરો",
|
||||
learn_more: "વધુ જાણો",
|
||||
portals_title: "દરેક ભૂમિકા માટે વિશિષ્ટ પોર્ટલ",
|
||||
portals_description: "દરેક પોર્ટલ ખાસ કરીને તેના વપરાશકર્તાઓ માટે ડિઝાઇન કરવામાં આવ્યું છે",
|
||||
cta_title: "તમારા દાગીના વ્યવસાયને પરિવર્તિત કરવા તૈયાર છો?",
|
||||
cta_description: "સેંકડો વ્યવસાયો પહેલેથી જ હેલો જ્વેલર્સનો ઉપયોગ કરી રહ્યા છે",
|
||||
get_started_today: "આજે શરૂ કરો",
|
||||
view_docs: "દસ્તાવેજો જુઓ",
|
||||
},
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
const [language, setLanguage] = useState("en");
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [activePortalOverlay, setActivePortalOverlay] = useState<string | null>(null);
|
||||
const [currentView, setCurrentView] = useState<"home" | "admin" | "customer" | "retailer" | "manufacturer">("home");
|
||||
|
||||
const t = TRANSLATIONS[language as keyof typeof TRANSLATIONS] || TRANSLATIONS.en;
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// If in admin view, show admin console
|
||||
if (currentView === "admin") {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Loading Admin Console...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<AdminApp />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// If in customer view, show customer app
|
||||
if (currentView === "customer") {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center max-w-[390px] mx-auto">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Loading Customer App...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<CustomerApp />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// If in retailer view, show retailer portal
|
||||
if (currentView === "retailer") {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Loading Retailer Portal...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<RetailerApp />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// If in manufacturer view, show manufacturer portal
|
||||
if (currentView === "manufacturer") {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Loading Manufacturer Portal...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<ManufacturerApp />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Marketing page view
|
||||
const handlePortalContinue = (portalKey: string) => {
|
||||
if (portalKey === "admin") {
|
||||
setCurrentView("admin");
|
||||
} else if (portalKey === "customer") {
|
||||
setCurrentView("customer");
|
||||
} else if (portalKey === "retailer") {
|
||||
setCurrentView("retailer");
|
||||
} else if (portalKey === "manufacturer") {
|
||||
setCurrentView("manufacturer");
|
||||
}
|
||||
setActivePortalOverlay(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page text-text-primary">
|
||||
<Toaster />
|
||||
|
||||
{/* Enhanced Header with Theme & Language */}
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-[10px] bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground">H</span>
|
||||
</div>
|
||||
<span>Hello Jewellers</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Language Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger className="w-[140px] h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="hi">हिन्दी</SelectItem>
|
||||
<SelectItem value="mr">मराठी</SelectItem>
|
||||
<SelectItem value="gu">ગુજરાતી</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Moon className="w-5 h-5" />
|
||||
) : (
|
||||
<Sun className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Help Button */}
|
||||
<Button onClick={() => setHelpOpen(true)} variant="ghost">
|
||||
<HelpCircle className="w-5 h-5 mr-2" />
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-4 lg:px-8 py-12">
|
||||
{/* Hero Section with Indian-themed vectors */}
|
||||
<div className="max-w-6xl mx-auto mb-20">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-[48px] leading-[56px]">
|
||||
{t.hero_title}
|
||||
</h1>
|
||||
<p className="text-[18px] leading-[28px] text-text-secondary">
|
||||
{t.hero_description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button size="lg" onClick={() => window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })}>
|
||||
{t.get_started}
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" onClick={() => setHelpOpen(true)}>
|
||||
{t.learn_more}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Indian-themed vector illustration */}
|
||||
<div className="hidden lg:flex items-center justify-center">
|
||||
<div className="relative w-full max-w-md aspect-square">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-secondary/20 via-primary/20 to-accent-positive/20 rounded-full blur-3xl" />
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="w-24 h-24 rounded-[20px] bg-gradient-to-br from-secondary/20 to-secondary/10 flex items-center justify-center transition-transform hover:scale-105 border border-secondary/30">
|
||||
<Gem className="w-12 h-12 text-secondary" />
|
||||
</div>
|
||||
<div className="w-24 h-24 rounded-[20px] bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center transition-transform hover:scale-105 border border-primary/30">
|
||||
<Sparkles className="w-12 h-12 text-primary" />
|
||||
</div>
|
||||
<div className="w-24 h-24 rounded-[20px] bg-gradient-to-br from-accent-positive/20 to-accent-positive/10 flex items-center justify-center transition-transform hover:scale-105 border border-accent-positive/30">
|
||||
<Award className="w-12 h-12 text-accent-positive" />
|
||||
</div>
|
||||
<div className="w-24 h-24 rounded-[20px] bg-gradient-to-br from-accent-warn/20 to-accent-warn/10 flex items-center justify-center transition-transform hover:scale-105 border border-accent-warn/30">
|
||||
<Users className="w-12 h-12 text-accent-warn" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Portal Tiles */}
|
||||
<div className="max-w-6xl mx-auto mb-20">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-[36px] leading-[44px] mb-3">{t.portals_title}</h2>
|
||||
<p className="text-[16px] leading-[24px] text-text-secondary max-w-2xl mx-auto">
|
||||
{t.portals_description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{Object.entries(PORTAL_DATA).map(([key, portal]) => (
|
||||
<PortalTile
|
||||
key={key}
|
||||
icon={portal.icon}
|
||||
title={portal.title}
|
||||
description={portal.description}
|
||||
features={portal.features}
|
||||
requiresLogin={false}
|
||||
ctaText="Learn More"
|
||||
onCtaClick={() => setActivePortalOverlay(key)}
|
||||
onLearnMore={() => setActivePortalOverlay(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Section */}
|
||||
<div className="max-w-6xl mx-auto mb-12">
|
||||
<Card className="p-12 text-center space-y-6 bg-gradient-to-br from-primary/5 to-secondary/5">
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-[36px] leading-[44px]">{t.cta_title}</h2>
|
||||
<p className="text-[16px] leading-[24px] text-text-secondary max-w-2xl mx-auto">
|
||||
{t.cta_description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3 justify-center">
|
||||
<Button size="lg">
|
||||
{t.get_started_today}
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" onClick={() => setHelpOpen(true)}>
|
||||
<HelpCircle className="w-5 h-5 mr-2" />
|
||||
{t.view_docs}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer
|
||||
theme={theme}
|
||||
onThemeToggle={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
/>
|
||||
|
||||
{/* Modals */}
|
||||
<HelpOverlay open={helpOpen} onClose={() => setHelpOpen(false)} />
|
||||
|
||||
{/* Portal Overlays */}
|
||||
{activePortalOverlay && (
|
||||
<PortalOverlay
|
||||
open={true}
|
||||
onClose={() => setActivePortalOverlay(null)}
|
||||
icon={PORTAL_DATA[activePortalOverlay as keyof typeof PORTAL_DATA].icon}
|
||||
title={PORTAL_DATA[activePortalOverlay as keyof typeof PORTAL_DATA].title}
|
||||
description={PORTAL_DATA[activePortalOverlay as keyof typeof PORTAL_DATA].description}
|
||||
keyActions={PORTAL_DATA[activePortalOverlay as keyof typeof PORTAL_DATA].keyActions}
|
||||
requiresLogin={false}
|
||||
tenantScope={PORTAL_DATA[activePortalOverlay as keyof typeof PORTAL_DATA].tenantScope}
|
||||
onContinue={() => handlePortalContinue(activePortalOverlay)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/Attributions.md
Normal file
3
src/Attributions.md
Normal file
@@ -0,0 +1,3 @@
|
||||
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
|
||||
|
||||
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
|
||||
341
src/CustomerApp.tsx
Normal file
341
src/CustomerApp.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { MobileShell } from "./components/customer/MobileShell";
|
||||
import { SplashScreen } from "./components/customer/screens/SplashScreen";
|
||||
import { OnboardingScreen } from "./components/customer/screens/OnboardingScreen";
|
||||
import { LoginScreen } from "./components/customer/screens/LoginScreen";
|
||||
import { InviteScreen } from "./components/customer/screens/InviteScreen";
|
||||
import { ConfirmRetailerScreen } from "./components/customer/screens/ConfirmRetailerScreen";
|
||||
import { ProfileSetupScreen } from "./components/customer/screens/ProfileSetupScreen";
|
||||
import { HomeScreen } from "./components/customer/screens/HomeScreen";
|
||||
import { CollectionsScreen } from "./components/customer/screens/CollectionsScreen";
|
||||
import { ProductGridScreen } from "./components/customer/screens/ProductGridScreen";
|
||||
import { ProductDetailScreen } from "./components/customer/screens/ProductDetailScreen";
|
||||
import { WishlistScreen } from "./components/customer/screens/WishlistScreen";
|
||||
import { ChatScreen } from "./components/customer/screens/ChatScreen";
|
||||
import { AppointmentsScreen } from "./components/customer/screens/AppointmentsScreen";
|
||||
import { BookAppointmentScreen } from "./components/customer/screens/BookAppointmentScreen";
|
||||
import { InterestConfirmScreen } from "./components/customer/screens/InterestConfirmScreen";
|
||||
import { NotificationsScreen } from "./components/customer/screens/NotificationsScreen";
|
||||
import { AccountScreen } from "./components/customer/screens/AccountScreen";
|
||||
import { QuotesScreen } from "./components/customer/screens/QuotesScreen";
|
||||
import { OrdersScreen } from "./components/customer/screens/OrdersScreen";
|
||||
import { MyRetailersScreen } from "./components/customer/screens/MyRetailersScreen";
|
||||
import { RetailerStorefrontScreen } from "./components/customer/screens/RetailerStorefrontScreen";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
export type CustomerScreen =
|
||||
| "splash"
|
||||
| "onboarding"
|
||||
| "login"
|
||||
| "invite"
|
||||
| "confirm-retailer"
|
||||
| "profile-setup"
|
||||
| "home"
|
||||
| "collections"
|
||||
| "product-grid"
|
||||
| "product-detail"
|
||||
| "wishlist"
|
||||
| "chat"
|
||||
| "appointments"
|
||||
| "book-appointment"
|
||||
| "interest-confirm"
|
||||
| "notifications"
|
||||
| "account"
|
||||
| "quotes"
|
||||
| "orders"
|
||||
| "my-retailers"
|
||||
| "retailer-storefront";
|
||||
|
||||
interface CustomerState {
|
||||
isAuthenticated: boolean;
|
||||
hasRetailer: boolean;
|
||||
retailer: {
|
||||
name: string;
|
||||
location: string;
|
||||
associate: string;
|
||||
} | null;
|
||||
wishlist: string[];
|
||||
preferences: {
|
||||
occasions: string[];
|
||||
categories: string[];
|
||||
budgetRange: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export default function CustomerApp() {
|
||||
const [currentScreen, setCurrentScreen] = useState<CustomerScreen>("splash");
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
const [customerState, setCustomerState] = useState<CustomerState>({
|
||||
isAuthenticated: false,
|
||||
hasRetailer: false,
|
||||
retailer: null,
|
||||
wishlist: ["1", "2", "3", "5", "7"], // Pre-populated wishlist for demo
|
||||
preferences: null,
|
||||
});
|
||||
|
||||
// Context data for navigation
|
||||
const [selectedCollection, setSelectedCollection] = useState<string | null>(null);
|
||||
const [selectedProduct, setSelectedProduct] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
// Simulate splash screen
|
||||
useEffect(() => {
|
||||
if (currentScreen === "splash") {
|
||||
const timer = setTimeout(() => {
|
||||
setCurrentScreen("onboarding");
|
||||
}, 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [currentScreen]);
|
||||
|
||||
const handleLogin = () => {
|
||||
setCustomerState({ ...customerState, isAuthenticated: true });
|
||||
setCurrentScreen("invite");
|
||||
};
|
||||
|
||||
const handleSkipRetailer = () => {
|
||||
// Skip retailer connection and go to profile setup with generic retailer
|
||||
setCustomerState({
|
||||
...customerState,
|
||||
hasRetailer: false,
|
||||
retailer: {
|
||||
name: "Hello Jewellers",
|
||||
location: "Browse All",
|
||||
associate: "Platform",
|
||||
},
|
||||
});
|
||||
setCurrentScreen("profile-setup");
|
||||
};
|
||||
|
||||
const handleRetailerConfirm = (retailerData: { name: string; location: string; associate: string }) => {
|
||||
setCustomerState({
|
||||
...customerState,
|
||||
hasRetailer: true,
|
||||
retailer: retailerData,
|
||||
});
|
||||
setCurrentScreen("profile-setup");
|
||||
};
|
||||
|
||||
const handleProfileComplete = (preferences: any) => {
|
||||
setCustomerState({
|
||||
...customerState,
|
||||
preferences,
|
||||
});
|
||||
setCurrentScreen("home");
|
||||
};
|
||||
|
||||
const handleNavigate = (screen: CustomerScreen) => {
|
||||
setCurrentScreen(screen);
|
||||
};
|
||||
|
||||
const handleAddToWishlist = (productId: string) => {
|
||||
setCustomerState({
|
||||
...customerState,
|
||||
wishlist: [...customerState.wishlist, productId],
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFromWishlist = (productId: string) => {
|
||||
setCustomerState({
|
||||
...customerState,
|
||||
wishlist: customerState.wishlist.filter((id) => id !== productId),
|
||||
});
|
||||
};
|
||||
|
||||
// Screens that don't use the mobile shell
|
||||
const splashScreens: CustomerScreen[] = ["splash", "onboarding", "login", "invite", "confirm-retailer", "profile-setup"];
|
||||
|
||||
const renderScreen = () => {
|
||||
switch (currentScreen) {
|
||||
case "splash":
|
||||
return <SplashScreen />;
|
||||
case "onboarding":
|
||||
return <OnboardingScreen onComplete={() => setCurrentScreen("login")} />;
|
||||
case "login":
|
||||
return <LoginScreen onLogin={handleLogin} />;
|
||||
case "invite":
|
||||
return (
|
||||
<InviteScreen
|
||||
onSuccess={() => setCurrentScreen("confirm-retailer")}
|
||||
onSkip={handleSkipRetailer}
|
||||
/>
|
||||
);
|
||||
case "confirm-retailer":
|
||||
return (
|
||||
<ConfirmRetailerScreen
|
||||
onConfirm={() =>
|
||||
handleRetailerConfirm({
|
||||
name: "Nova Jewels",
|
||||
location: "Mumbai",
|
||||
associate: "Aditi Rao",
|
||||
})
|
||||
}
|
||||
onSkip={handleSkipRetailer}
|
||||
/>
|
||||
);
|
||||
case "profile-setup":
|
||||
return <ProfileSetupScreen onComplete={handleProfileComplete} />;
|
||||
case "home":
|
||||
return (
|
||||
<HomeScreen
|
||||
retailer={customerState.retailer!}
|
||||
onNavigate={handleNavigate}
|
||||
onCollectionClick={(id) => {
|
||||
setSelectedCollection(id);
|
||||
setCurrentScreen("product-grid");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "collections":
|
||||
return (
|
||||
<CollectionsScreen
|
||||
onCollectionClick={(id) => {
|
||||
setSelectedCollection(id);
|
||||
setCurrentScreen("product-grid");
|
||||
}}
|
||||
onBack={() => setCurrentScreen("home")}
|
||||
/>
|
||||
);
|
||||
case "product-grid":
|
||||
return (
|
||||
<ProductGridScreen
|
||||
collectionId={selectedCollection}
|
||||
onProductClick={(id) => {
|
||||
setSelectedProduct(id);
|
||||
setCurrentScreen("product-detail");
|
||||
}}
|
||||
onBack={() => setCurrentScreen("home")}
|
||||
/>
|
||||
);
|
||||
case "product-detail":
|
||||
return (
|
||||
<ProductDetailScreen
|
||||
productId={selectedProduct}
|
||||
isWishlisted={customerState.wishlist.includes(selectedProduct || "")}
|
||||
onAddToWishlist={() => {
|
||||
handleAddToWishlist(selectedProduct!);
|
||||
toast.success("Added to wishlist");
|
||||
}}
|
||||
onRemoveFromWishlist={() => {
|
||||
handleRemoveFromWishlist(selectedProduct!);
|
||||
toast.success("Removed from wishlist");
|
||||
}}
|
||||
onInquire={() => setCurrentScreen("chat")}
|
||||
onBookAppointment={() => setCurrentScreen("book-appointment")}
|
||||
onBack={() => setCurrentScreen("product-grid")}
|
||||
/>
|
||||
);
|
||||
case "wishlist":
|
||||
return (
|
||||
<WishlistScreen
|
||||
wishlistIds={customerState.wishlist}
|
||||
onProductClick={(id) => {
|
||||
setSelectedProduct(id);
|
||||
setCurrentScreen("product-detail");
|
||||
}}
|
||||
onRemoveFromWishlist={handleRemoveFromWishlist}
|
||||
/>
|
||||
);
|
||||
case "chat":
|
||||
return (
|
||||
<ChatScreen
|
||||
retailer={customerState.retailer!}
|
||||
attachedProduct={selectedProduct}
|
||||
/>
|
||||
);
|
||||
case "interest-confirm":
|
||||
return (
|
||||
<InterestConfirmScreen
|
||||
productName="Heritage Bridal Ring" // In real app, get from selectedProduct
|
||||
onGoHome={() => setCurrentScreen("home")}
|
||||
onContinueChat={() => setCurrentScreen("chat")}
|
||||
onBookAppointment={() => setCurrentScreen("book-appointment")}
|
||||
/>
|
||||
);
|
||||
case "notifications":
|
||||
return (
|
||||
<NotificationsScreen
|
||||
onBack={() => setCurrentScreen("home")}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
);
|
||||
case "appointments":
|
||||
return (
|
||||
<AppointmentsScreen
|
||||
retailer={customerState.retailer!}
|
||||
onBookNew={() => setCurrentScreen("book-appointment")}
|
||||
/>
|
||||
);
|
||||
case "book-appointment":
|
||||
return (
|
||||
<BookAppointmentScreen
|
||||
retailer={customerState.retailer!}
|
||||
onBack={() => setCurrentScreen("appointments")}
|
||||
onSuccess={() => setCurrentScreen("appointments")}
|
||||
/>
|
||||
);
|
||||
case "account":
|
||||
return (
|
||||
<AccountScreen
|
||||
retailer={customerState.retailer!}
|
||||
preferences={customerState.preferences}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
);
|
||||
case "my-retailers":
|
||||
return (
|
||||
<MyRetailersScreen
|
||||
onBack={() => setCurrentScreen("account")}
|
||||
onViewStore={(retailerId) => {
|
||||
setSelectedProduct(retailerId); // Reuse for retailer ID
|
||||
setCurrentScreen("retailer-storefront");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "retailer-storefront":
|
||||
return (
|
||||
<RetailerStorefrontScreen
|
||||
onBack={() => setCurrentScreen("my-retailers")}
|
||||
retailerId={selectedProduct || "1"}
|
||||
/>
|
||||
);
|
||||
case "quotes":
|
||||
return <QuotesScreen onBack={() => setCurrentScreen("account")} />;
|
||||
case "orders":
|
||||
return <OrdersScreen onBack={() => setCurrentScreen("account")} />;
|
||||
default:
|
||||
return <HomeScreen retailer={customerState.retailer!} onNavigate={handleNavigate} onCollectionClick={(id) => {}} />;
|
||||
}
|
||||
};
|
||||
|
||||
// Screens without bottom nav (add book-appointment, interest-confirm, and notifications)
|
||||
const screensWithoutNav = [...splashScreens, "book-appointment", "interest-confirm", "notifications"];
|
||||
|
||||
if (screensWithoutNav.includes(currentScreen)) {
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
{renderScreen()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Screens with mobile shell
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<MobileShell currentScreen={currentScreen} onNavigate={handleNavigate}>
|
||||
{renderScreen()}
|
||||
</MobileShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
107
src/ManufacturerApp.tsx
Normal file
107
src/ManufacturerApp.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState } from "react";
|
||||
import { ManufacturerShell } from "./components/manufacturer/ManufacturerShell";
|
||||
import { DashboardPage } from "./components/manufacturer/pages/DashboardPage";
|
||||
import { SharingPage } from "./components/manufacturer/pages/SharingPage";
|
||||
import { InquiriesPage } from "./components/manufacturer/pages/InquiriesPage";
|
||||
import { CustomOrdersPage } from "./components/manufacturer/pages/CustomOrdersPage";
|
||||
import { AnalyticsPage } from "./components/manufacturer/pages/AnalyticsPage";
|
||||
import { CompliancePage } from "./components/manufacturer/pages/CompliancePage";
|
||||
import { SettingsPage } from "./components/manufacturer/pages/SettingsPage";
|
||||
import { KYCPage } from "./components/manufacturer/pages/KYCPage";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
|
||||
export type ManufacturerPage =
|
||||
| "dashboard"
|
||||
| "sharing"
|
||||
| "inquiries"
|
||||
| "custom-orders"
|
||||
| "analytics"
|
||||
| "compliance"
|
||||
| "settings"
|
||||
| "kyc";
|
||||
|
||||
interface ManufacturerState {
|
||||
company: {
|
||||
name: string;
|
||||
legalName: string;
|
||||
logo: string | null;
|
||||
} | null;
|
||||
kycStatus: "pending" | "verified" | "rejected";
|
||||
isSetupComplete: boolean;
|
||||
}
|
||||
|
||||
export default function ManufacturerApp() {
|
||||
const [currentPage, setCurrentPage] = useState<ManufacturerPage>("dashboard");
|
||||
const [manufacturerState, setManufacturerState] = useState<ManufacturerState>({
|
||||
company: {
|
||||
name: "Auric Foundry",
|
||||
legalName: "Auric Foundry Pvt Ltd",
|
||||
logo: null,
|
||||
},
|
||||
kycStatus: "verified",
|
||||
isSetupComplete: true,
|
||||
});
|
||||
|
||||
const handleNavigate = (page: ManufacturerPage) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleKYCComplete = (status: "verified" | "pending") => {
|
||||
setManufacturerState({
|
||||
...manufacturerState,
|
||||
kycStatus: status,
|
||||
isSetupComplete: true,
|
||||
});
|
||||
setCurrentPage("dashboard");
|
||||
};
|
||||
|
||||
// If KYC not complete, show KYC wizard
|
||||
if (!manufacturerState.isSetupComplete || manufacturerState.kycStatus === "pending") {
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<KYCPage
|
||||
onComplete={handleKYCComplete}
|
||||
currentStatus={manufacturerState.kycStatus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case "dashboard":
|
||||
return <DashboardPage onNavigate={handleNavigate} kycStatus={manufacturerState.kycStatus} />;
|
||||
case "sharing":
|
||||
return <SharingPage kycStatus={manufacturerState.kycStatus} />;
|
||||
case "inquiries":
|
||||
return <InquiriesPage />;
|
||||
case "custom-orders":
|
||||
return <CustomOrdersPage />;
|
||||
case "analytics":
|
||||
return <AnalyticsPage />;
|
||||
case "compliance":
|
||||
return <CompliancePage />;
|
||||
case "settings":
|
||||
return <SettingsPage />;
|
||||
case "kyc":
|
||||
return <KYCPage onComplete={handleKYCComplete} currentStatus={manufacturerState.kycStatus} />;
|
||||
default:
|
||||
return <DashboardPage onNavigate={handleNavigate} kycStatus={manufacturerState.kycStatus} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<ManufacturerShell
|
||||
currentPage={currentPage}
|
||||
onNavigate={handleNavigate}
|
||||
companyName={manufacturerState.company?.name || "Manufacturer"}
|
||||
kycStatus={manufacturerState.kycStatus}
|
||||
>
|
||||
{renderPage()}
|
||||
</ManufacturerShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
155
src/RetailerApp.tsx
Normal file
155
src/RetailerApp.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { RetailerShell } from "./components/retailer/RetailerShell";
|
||||
import { DashboardPage } from "./components/retailer/pages/DashboardPage";
|
||||
import { InventoryPage } from "./components/retailer/pages/InventoryPage";
|
||||
import { CurationPage } from "./components/retailer/pages/CurationPage";
|
||||
import { CustomersPage } from "./components/retailer/pages/CustomersPage";
|
||||
import { ChatPage } from "./components/retailer/pages/ChatPage";
|
||||
import { AppointmentsPage } from "./components/retailer/pages/AppointmentsPage";
|
||||
import { QuotesOrdersPage } from "./components/retailer/pages/QuotesOrdersPage";
|
||||
import { StorefrontPage } from "./components/retailer/pages/StorefrontPage";
|
||||
import { AnalyticsPage } from "./components/retailer/pages/AnalyticsPage";
|
||||
import { TeamPage } from "./components/retailer/pages/TeamPage";
|
||||
import { SetupPage } from "./components/retailer/pages/SetupPage";
|
||||
import { GoldRateModal, GoldRates } from "./components/retailer/GoldRateModal";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
export type RetailerPage =
|
||||
| "dashboard"
|
||||
| "inventory"
|
||||
| "curation"
|
||||
| "customers"
|
||||
| "chat"
|
||||
| "appointments"
|
||||
| "quotes-orders"
|
||||
| "storefront"
|
||||
| "analytics"
|
||||
| "team"
|
||||
| "setup";
|
||||
|
||||
interface RetailerState {
|
||||
brand: {
|
||||
name: string;
|
||||
logo: string | null;
|
||||
subdomain: string;
|
||||
} | null;
|
||||
stores: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
location: string;
|
||||
}>;
|
||||
isSetupComplete: boolean;
|
||||
}
|
||||
|
||||
export default function RetailerApp() {
|
||||
const [currentPage, setCurrentPage] = useState<RetailerPage>("dashboard");
|
||||
const [goldRateModalOpen, setGoldRateModalOpen] = useState(false);
|
||||
const [goldRatesSet, setGoldRatesSet] = useState(false);
|
||||
const [retailerState, setRetailerState] = useState<RetailerState>({
|
||||
brand: {
|
||||
name: "Nova Jewels",
|
||||
logo: null,
|
||||
subdomain: "nova-jewels",
|
||||
},
|
||||
stores: [
|
||||
{ id: "1", name: "Mumbai Showroom", location: "Mumbai, Maharashtra" },
|
||||
],
|
||||
isSetupComplete: true,
|
||||
});
|
||||
|
||||
// Check if gold rates need to be set today
|
||||
useEffect(() => {
|
||||
const lastRateDate = localStorage.getItem('lastGoldRateDate');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
if (lastRateDate !== today && retailerState.isSetupComplete) {
|
||||
// Show modal after a short delay
|
||||
setTimeout(() => {
|
||||
setGoldRateModalOpen(true);
|
||||
}, 1000);
|
||||
} else if (lastRateDate === today) {
|
||||
setGoldRatesSet(true);
|
||||
}
|
||||
}, [retailerState.isSetupComplete]);
|
||||
|
||||
const handleNavigate = (page: RetailerPage) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const handleSetupComplete = (brandData: any) => {
|
||||
setRetailerState({
|
||||
...retailerState,
|
||||
brand: brandData,
|
||||
isSetupComplete: true,
|
||||
});
|
||||
setCurrentPage("dashboard");
|
||||
};
|
||||
|
||||
const handleGoldRatesSubmit = (rates: GoldRates) => {
|
||||
// Save rates to localStorage
|
||||
localStorage.setItem('lastGoldRateDate', rates.date);
|
||||
localStorage.setItem('goldRates', JSON.stringify(rates));
|
||||
setGoldRatesSet(true);
|
||||
toast.success(`Gold rates updated successfully! Shop is now open.`, {
|
||||
description: `22K: ₹${rates.gold22k}/g | 24K: ₹${rates.gold24k}/g | Silver: ₹${rates.silver}/g`,
|
||||
});
|
||||
};
|
||||
|
||||
// If setup not complete, show setup wizard
|
||||
if (!retailerState.isSetupComplete) {
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<SetupPage onComplete={handleSetupComplete} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const renderPage = () => {
|
||||
switch (currentPage) {
|
||||
case "dashboard":
|
||||
return <DashboardPage onNavigate={handleNavigate} />;
|
||||
case "inventory":
|
||||
return <InventoryPage />;
|
||||
case "curation":
|
||||
return <CurationPage />;
|
||||
case "customers":
|
||||
return <CustomersPage />;
|
||||
case "chat":
|
||||
return <ChatPage />;
|
||||
case "appointments":
|
||||
return <AppointmentsPage />;
|
||||
case "quotes-orders":
|
||||
return <QuotesOrdersPage />;
|
||||
case "storefront":
|
||||
return <StorefrontPage />;
|
||||
case "analytics":
|
||||
return <AnalyticsPage />;
|
||||
case "team":
|
||||
return <TeamPage />;
|
||||
case "setup":
|
||||
return <SetupPage onComplete={handleSetupComplete} />;
|
||||
default:
|
||||
return <DashboardPage onNavigate={handleNavigate} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toaster />
|
||||
<GoldRateModal
|
||||
open={goldRateModalOpen}
|
||||
onClose={() => setGoldRateModalOpen(false)}
|
||||
onSubmit={handleGoldRatesSubmit}
|
||||
/>
|
||||
<RetailerShell
|
||||
currentPage={currentPage}
|
||||
onNavigate={handleNavigate}
|
||||
brandName={retailerState.brand?.name || "Retailer"}
|
||||
>
|
||||
{renderPage()}
|
||||
</RetailerShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
363
src/components/AIAssistant.tsx
Normal file
363
src/components/AIAssistant.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Sparkles, Send, X, Minimize2, Maximize2, TrendingUp, Package, Users, DollarSign, FileText } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Card } from "./ui/card";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
|
||||
interface AIAssistantProps {
|
||||
portalType: "admin" | "retailer" | "manufacturer";
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
type: "user" | "assistant";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
suggestions?: string[];
|
||||
data?: any;
|
||||
}
|
||||
|
||||
const QUICK_ACTIONS = {
|
||||
admin: [
|
||||
{ icon: Users, label: "Show total users", query: "How many total users do we have?" },
|
||||
{ icon: TrendingUp, label: "Platform analytics", query: "Show me platform analytics summary" },
|
||||
{ icon: Package, label: "Active tenants", query: "How many active tenants?" },
|
||||
{ icon: FileText, label: "Recent activity", query: "Show recent platform activity" },
|
||||
],
|
||||
retailer: [
|
||||
{ icon: DollarSign, label: "Gold rate today", query: "What is the gold rate today?" },
|
||||
{ icon: Package, label: "Top products", query: "Which products are generating most interest?" },
|
||||
{ icon: FileText, label: "Pending quotes", query: "Show my pending quotations" },
|
||||
{ icon: TrendingUp, label: "Sales summary", query: "Show me this month's sales summary" },
|
||||
],
|
||||
manufacturer: [
|
||||
{ icon: Package, label: "Inventory status", query: "What's my current inventory status?" },
|
||||
{ icon: Users, label: "Connected retailers", query: "How many retailers am I connected to?" },
|
||||
{ icon: FileText, label: "Custom orders", query: "Show pending custom orders" },
|
||||
{ icon: TrendingUp, label: "Production analytics", query: "Show production analytics" },
|
||||
],
|
||||
};
|
||||
|
||||
const AI_RESPONSES = {
|
||||
admin: {
|
||||
"users": {
|
||||
content: "📊 **Platform User Statistics**\n\n• Total Users: **2,847**\n• Active This Month: **2,156** (76%)\n• New This Week: **142**\n\nBreakdown by role:\n• Customers: 2,245\n• Retailers: 458\n• Manufacturers: 124\n• Admins: 20",
|
||||
data: { total: 2847, active: 2156, new: 142 },
|
||||
},
|
||||
"analytics": {
|
||||
content: "📈 **Platform Analytics Summary**\n\n**This Month:**\n• Total Revenue: ₹24.5 Cr\n• Growth: +18.3% MoM\n• Active Transactions: 1,847\n• Platform Uptime: 99.97%\n\n**Top Performing Regions:**\n1. Mumbai - ₹8.2 Cr\n2. Delhi - ₹6.4 Cr\n3. Bangalore - ₹4.8 Cr",
|
||||
},
|
||||
"tenants": {
|
||||
content: "🏢 **Active Tenants Overview**\n\n• Total Tenants: **124**\n• Active: **118** (95%)\n• Trial Period: **6**\n\n**By Plan:**\n• Enterprise: 42\n• Professional: 58\n• Starter: 18\n• Trial: 6",
|
||||
},
|
||||
},
|
||||
retailer: {
|
||||
"gold rate": {
|
||||
content: "💰 **Today's Gold Rates** (Updated 2 mins ago)\n\n**22K Gold:**\n• Rate: ₹6,245/gram\n• Change: +₹25 (+0.4%)\n\n**18K Gold:**\n• Rate: ₹5,120/gram\n• Change: +₹20 (+0.4%)\n\n**24K Gold:**\n• Rate: ₹6,820/gram\n• Change: +₹28 (+0.4%)\n\n📊 7-day avg: ₹6,218/gram",
|
||||
suggestions: ["Show gold rate trend", "Update my pricing"],
|
||||
},
|
||||
"products": {
|
||||
content: "🔥 **Top Performing Products** (Last 30 days)\n\n1. **Heritage Bridal Ring**\n • Views: 842\n • Wishlist adds: 142\n • Inquiries: 28\n • Interest Score: 94/100\n\n2. **Classic Gold Necklace**\n • Views: 624\n • Wishlist adds: 98\n • Inquiries: 18\n • Interest Score: 87/100\n\n3. **Diamond Stud Earrings**\n • Views: 512\n • Wishlist adds: 89\n • Inquiries: 15\n • Interest Score: 82/100",
|
||||
suggestions: ["Show full inventory analytics", "Update featured products"],
|
||||
},
|
||||
"quotes": {
|
||||
content: "📋 **Pending Quotations**\n\n**High Priority (3):**\n• Quote #Q-2847 - Aditi Sharma\n Bridal Set | ₹4.2L | Expires in 2 days\n\n• Quote #Q-2845 - Rohan Mehta \n Diamond Ring | ₹2.8L | Expires in 3 days\n\n• Quote #Q-2842 - Priya Patel\n Gold Bangles | ₹1.5L | Expires today\n\n**Total Pending:** 8 quotes worth ₹18.6L",
|
||||
suggestions: ["Send follow-up reminders", "View all quotes"],
|
||||
},
|
||||
"sales": {
|
||||
content: "💼 **Sales Summary - January 2025**\n\n**Revenue:** ₹86 Lakhs\n• Growth: +22.1% vs Dec\n• Orders: 34 completed\n• Avg Order Value: ₹2.53L\n\n**Top Categories:**\n1. Bridal: ₹42L (49%)\n2. Daily Wear: ₹24L (28%)\n3. Diamond: ₹20L (23%)\n\n**Conversion Rate:** 38.2%",
|
||||
suggestions: ["View detailed analytics", "Export report"],
|
||||
},
|
||||
},
|
||||
manufacturer: {
|
||||
"inventory": {
|
||||
content: "📦 **Inventory Status Overview**\n\n**Total SKUs:** 842\n• Available: 687 (82%)\n• Custom Order: 124 (15%)\n• Out of Stock: 31 (3%)\n\n**By Category:**\n• Rings: 245 SKUs\n• Necklaces: 198 SKUs\n• Bangles: 167 SKUs\n• Earrings: 142 SKUs\n• Others: 90 SKUs\n\n⚠️ Low stock alert: 12 items",
|
||||
suggestions: ["View low stock items", "Sync with retailers"],
|
||||
},
|
||||
"retailers": {
|
||||
content: "🤝 **Connected Retailers**\n\n**Total Connections:** 48 retailers\n• Active Orders: 12\n• Pending Approvals: 3\n\n**Top Partners:**\n1. Nova Jewels (Mumbai)\n • Orders: 124 | Revenue: ₹42L\n2. Sparkle Gems (Delhi)\n • Orders: 89 | Revenue: ₹28L\n3. Golden Heritage (Jaipur)\n • Orders: 67 | Revenue: ₹24L\n\n**This Month:** 18 new inquiries",
|
||||
suggestions: ["View all retailers", "Pending approvals"],
|
||||
},
|
||||
"orders": {
|
||||
content: "🔧 **Pending Custom Orders**\n\n**Urgent (2):**\n• Order #CO-2847\n Nova Jewels | Custom Ring Design\n Due: 3 days | Value: ₹2.8L\n\n• Order #CO-2842\n Sparkle Gems | Bangle Set\n Due: 5 days | Value: ₹1.5L\n\n**Total Pending:** 8 orders\n**Total Value:** ₹14.2L\n\n📅 Next week deliveries: 5 orders",
|
||||
suggestions: ["View production schedule", "Update order status"],
|
||||
},
|
||||
"analytics": {
|
||||
content: "📊 **Production Analytics**\n\n**This Month:**\n• Units Produced: 1,247\n• Quality Rate: 98.2%\n• On-time Delivery: 94%\n• Revenue: ₹68L\n\n**Efficiency Metrics:**\n• Avg Production Time: 6.2 days\n• Capacity Utilization: 84%\n• Active Workforce: 42\n\n📈 Growth vs last month: +12%",
|
||||
suggestions: ["View detailed breakdown", "Export report"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function AIAssistant({ portalType }: AIAssistantProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: "welcome",
|
||||
type: "assistant",
|
||||
content: `👋 Hello! I'm your AI assistant. I can help you with:\n\n${
|
||||
portalType === "admin"
|
||||
? "• User and tenant analytics\n• Platform metrics\n• Activity monitoring\n• Compliance reports"
|
||||
: portalType === "retailer"
|
||||
? "• Gold rate updates\n• Inventory insights\n• Sales analytics\n• Customer quotes"
|
||||
: "• Inventory management\n• Retailer connections\n• Custom orders\n• Production analytics"
|
||||
}\n\nHow can I assist you today?`,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const quickActions = QUICK_ACTIONS[portalType];
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = (query: string = inputValue) => {
|
||||
if (!query.trim()) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
type: "user",
|
||||
content: query,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInputValue("");
|
||||
setIsTyping(true);
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
const response = getAIResponse(query.toLowerCase(), portalType);
|
||||
const assistantMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
type: "assistant",
|
||||
content: response.content,
|
||||
timestamp: new Date(),
|
||||
suggestions: response.suggestions,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, assistantMessage]);
|
||||
setIsTyping(false);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const getAIResponse = (query: string, portal: string) => {
|
||||
const responses = AI_RESPONSES[portal as keyof typeof AI_RESPONSES];
|
||||
|
||||
// Match query to response category
|
||||
if (query.includes("user") || query.includes("total")) {
|
||||
return responses["users" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("analytic") || query.includes("summary") || query.includes("sales")) {
|
||||
return responses["analytics" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("tenant") || query.includes("active")) {
|
||||
return responses["tenants" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("gold") || query.includes("rate") || query.includes("price")) {
|
||||
return responses["gold rate" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("product") || query.includes("interest") || query.includes("performing")) {
|
||||
return responses["products" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("quote") || query.includes("quotation") || query.includes("pending")) {
|
||||
return responses["quotes" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("inventory") || query.includes("stock")) {
|
||||
return responses["inventory" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("retailer") || query.includes("connection")) {
|
||||
return responses["retailers" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
} else if (query.includes("order") || query.includes("custom")) {
|
||||
return responses["orders" as keyof typeof responses] || getDefaultResponse(portal);
|
||||
}
|
||||
|
||||
return getDefaultResponse(portal);
|
||||
};
|
||||
|
||||
const getDefaultResponse = (portal: string) => {
|
||||
return {
|
||||
content: `I understand you're asking about that. Let me help you find the information you need.\n\nTry asking me about:\n${
|
||||
portal === "admin"
|
||||
? "• User statistics\n• Platform analytics\n• Tenant information"
|
||||
: portal === "retailer"
|
||||
? "• Gold rates\n• Inventory performance\n• Sales data\n• Customer quotes"
|
||||
: "• Inventory status\n• Retailer connections\n• Custom orders\n• Production metrics"
|
||||
}`,
|
||||
suggestions: quickActions.map((a) => a.query).slice(0, 2),
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Button */}
|
||||
{!isOpen && (
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="fixed bottom-6 right-6 z-50"
|
||||
>
|
||||
<Button
|
||||
size="lg"
|
||||
className="h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-shadow"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Sparkles className="w-6 h-6" />
|
||||
</Button>
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-accent-positive rounded-full animate-pulse" />
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Chat Window */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
className={`fixed z-50 shadow-2xl rounded-2xl bg-background border border-border overflow-hidden flex flex-col ${
|
||||
isMinimized
|
||||
? "bottom-6 right-6 w-80 h-16"
|
||||
: "bottom-6 right-6 w-[420px] h-[600px]"
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-4 bg-gradient-to-r from-primary to-secondary flex items-center justify-between text-white">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-white/20 flex items-center justify-center">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px] font-medium">AI Assistant</p>
|
||||
{!isMinimized && <p className="text-[11px] opacity-90">Always here to help</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 text-white hover:bg-white/20"
|
||||
onClick={() => setIsMinimized(!isMinimized)}
|
||||
>
|
||||
{isMinimized ? <Maximize2 className="w-4 h-4" /> : <Minimize2 className="w-4 h-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 text-white hover:bg-white/20"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isMinimized && (
|
||||
<>
|
||||
{/* Messages */}
|
||||
<ScrollArea className="flex-1 p-4" ref={scrollRef}>
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex ${message.type === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] rounded-2xl px-4 py-3 ${
|
||||
message.type === "user"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-accent/10 border border-border"
|
||||
}`}
|
||||
>
|
||||
<p className="text-[13px] whitespace-pre-line">{message.content}</p>
|
||||
{message.suggestions && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{message.suggestions.map((suggestion, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSendMessage(suggestion)}
|
||||
className="text-[11px] px-3 py-1.5 rounded-full bg-background border border-border hover:border-primary transition-colors"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[10px] opacity-60 mt-2">
|
||||
{message.timestamp.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isTyping && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-accent/10 border border-border rounded-2xl px-4 py-3">
|
||||
<div className="flex gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" />
|
||||
<div className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce delay-100" />
|
||||
<div className="w-2 h-2 rounded-full bg-muted-foreground animate-bounce delay-200" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{messages.length === 1 && (
|
||||
<div className="px-4 pb-3">
|
||||
<p className="text-[11px] text-muted-foreground mb-2">Quick actions:</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{quickActions.map((action, i) => {
|
||||
const Icon = action.icon;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleSendMessage(action.query)}
|
||||
className="p-3 rounded-lg border border-border hover:border-primary hover:bg-accent/5 transition-all text-left"
|
||||
>
|
||||
<Icon className="w-4 h-4 text-primary mb-1" />
|
||||
<p className="text-[11px] font-medium">{action.label}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-border">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleSendMessage()}
|
||||
placeholder="Ask me anything..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={() => handleSendMessage()}
|
||||
disabled={!inputValue.trim() || isTyping}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
212
src/components/AuthModal.tsx
Normal file
212
src/components/AuthModal.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
import { Input } from "./ui/input";
|
||||
import { Button } from "./ui/button";
|
||||
import { Label } from "./ui/label";
|
||||
import { Checkbox } from "./ui/checkbox";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
|
||||
import { Mail } from "lucide-react";
|
||||
|
||||
interface AuthModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onAuthenticated: (email: string) => void;
|
||||
}
|
||||
|
||||
export function AuthModal({ open, onClose, onAuthenticated }: AuthModalProps) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [showOTP, setShowOTP] = useState(false);
|
||||
const [otp, setOtp] = useState("");
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [resendTimer, setResendTimer] = useState(0);
|
||||
|
||||
const handleSendOTP = () => {
|
||||
if (email && consent) {
|
||||
setShowOTP(true);
|
||||
setResendTimer(30);
|
||||
const interval = setInterval(() => {
|
||||
setResendTimer((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyOTP = () => {
|
||||
if (otp.length === 6) {
|
||||
onAuthenticated(email);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleSignIn = () => {
|
||||
onAuthenticated("user@example.com");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[440px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[28px] leading-[36px]">Sign In</DialogTitle>
|
||||
<DialogDescription>
|
||||
Sign in with your email using OTP or use Google authentication
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="email" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="email">Email / OTP</TabsTrigger>
|
||||
<TabsTrigger value="google">Google</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="email" className="space-y-4 mt-6">
|
||||
{!showOTP ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2">
|
||||
<Checkbox
|
||||
id="consent"
|
||||
checked={consent}
|
||||
onCheckedChange={(checked) => setConsent(checked as boolean)}
|
||||
/>
|
||||
<label
|
||||
htmlFor="consent"
|
||||
className="text-[14px] leading-[20px] text-muted-foreground cursor-pointer"
|
||||
>
|
||||
I agree to the{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Terms of Service
|
||||
</a>
|
||||
,{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Privacy Policy
|
||||
</a>
|
||||
, and{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Refund Policy
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSendOTP}
|
||||
disabled={!email || !consent}
|
||||
className="w-full"
|
||||
>
|
||||
Send OTP
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-primary/10">
|
||||
<Mail className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
We sent a code to <span className="text-foreground">{email}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Enter OTP</Label>
|
||||
<div className="flex justify-center">
|
||||
<InputOTP maxLength={6} value={otp} onChange={setOtp}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleVerifyOTP}
|
||||
disabled={otp.length !== 6}
|
||||
className="w-full"
|
||||
>
|
||||
Verify & Continue
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => setShowOTP(false)}
|
||||
className="text-[14px] text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Change email
|
||||
</button>
|
||||
{resendTimer > 0 ? (
|
||||
<p className="text-[14px] text-muted-foreground mt-2">
|
||||
Resend OTP in {resendTimer}s
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSendOTP}
|
||||
className="text-[14px] text-primary hover:underline mt-2 block mx-auto"
|
||||
>
|
||||
Resend OTP
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="google" className="space-y-4 mt-6">
|
||||
<div className="text-center space-y-4">
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
Sign in with your Google account for quick access
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleGoogleSignIn}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
297
src/components/BulkUploadDialog.tsx
Normal file
297
src/components/BulkUploadDialog.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useState } from "react";
|
||||
import { Upload, Download, FileSpreadsheet, CheckCircle, AlertCircle, X, File } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { Progress } from "./ui/progress";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
interface BulkUploadDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onUpload: (file: File) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
templateType: "inventory" | "customers" | "catalogue";
|
||||
}
|
||||
|
||||
const templates = {
|
||||
inventory: {
|
||||
name: "Inventory_Template.xlsx",
|
||||
fields: ["SKU", "Title", "Category", "Metal", "Purity", "Weight", "Making Charges", "Margin", "Status", "Visibility"],
|
||||
},
|
||||
customers: {
|
||||
name: "Customers_Template.xlsx",
|
||||
fields: ["Name", "Email", "Phone", "City", "State", "Tags", "Notes"],
|
||||
},
|
||||
catalogue: {
|
||||
name: "Catalogue_Template.xlsx",
|
||||
fields: ["SKU", "Title", "Category", "Metal", "Purity", "Weight", "Wholesale Price", "Min Order Qty", "Lead Time", "Status"],
|
||||
},
|
||||
};
|
||||
|
||||
export function BulkUploadDialog({ open, onClose, onUpload, title, description, templateType }: BulkUploadDialogProps) {
|
||||
const [uploadStep, setUploadStep] = useState<"select" | "uploading" | "processing" | "complete">("select");
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [results, setResults] = useState<{
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
} | null>(null);
|
||||
|
||||
const template = templates[templateType];
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls') || file.name.endsWith('.csv')) {
|
||||
setSelectedFile(file);
|
||||
} else {
|
||||
toast.error("Please select a valid Excel or CSV file");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!selectedFile) return;
|
||||
|
||||
setUploadStep("uploading");
|
||||
setUploadProgress(0);
|
||||
|
||||
// Simulate upload progress
|
||||
const uploadInterval = setInterval(() => {
|
||||
setUploadProgress((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(uploadInterval);
|
||||
setUploadStep("processing");
|
||||
setTimeout(() => {
|
||||
// Simulate processing
|
||||
setUploadStep("complete");
|
||||
setResults({
|
||||
total: 150,
|
||||
success: 142,
|
||||
failed: 8,
|
||||
errors: [
|
||||
"Row 23: Invalid metal type 'Copper'",
|
||||
"Row 45: Missing required field 'Weight'",
|
||||
"Row 67: Duplicate SKU 'AU-22K-BR-0192'",
|
||||
"Row 89: Invalid purity value",
|
||||
"Row 102: Missing category",
|
||||
],
|
||||
});
|
||||
onUpload(selectedFile);
|
||||
}, 2000);
|
||||
return 100;
|
||||
}
|
||||
return prev + 10;
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
toast.success(`Downloading ${template.name}...`);
|
||||
// In a real implementation, this would trigger an actual file download
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setUploadStep("select");
|
||||
setSelectedFile(null);
|
||||
setUploadProgress(0);
|
||||
setResults(null);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
handleReset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Step 1: Template Download */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-[12px]">
|
||||
1
|
||||
</div>
|
||||
<h4 className="text-[14px] font-medium">Download Template</h4>
|
||||
</div>
|
||||
<div className="ml-8 space-y-3">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Download our Excel template with pre-defined columns and formatting rules.
|
||||
</p>
|
||||
<div className="p-3 rounded-lg border border-border bg-accent/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileSpreadsheet className="w-5 h-5 text-accent-positive" />
|
||||
<div>
|
||||
<p className="text-[13px] font-medium">{template.name}</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Includes: {template.fields.slice(0, 3).join(", ")}
|
||||
{template.fields.length > 3 && ` +${template.fields.length - 3} more`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={handleDownloadTemplate}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Step 2: Fill & Upload */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded-full flex items-center justify-center text-[12px] ${
|
||||
uploadStep !== "select" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
2
|
||||
</div>
|
||||
<h4 className="text-[14px] font-medium">Fill Data & Upload</h4>
|
||||
</div>
|
||||
<div className="ml-8 space-y-3">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Fill in your data following the template format and upload the file.
|
||||
</p>
|
||||
|
||||
{uploadStep === "select" && (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
id="bulk-upload-input"
|
||||
/>
|
||||
<label htmlFor="bulk-upload-input">
|
||||
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center hover:border-primary hover:bg-accent/5 cursor-pointer transition-colors">
|
||||
<Upload className="w-10 h-10 mx-auto mb-3 text-muted-foreground" />
|
||||
<p className="text-[14px] font-medium mb-1">
|
||||
{selectedFile ? selectedFile.name : "Click to select file"}
|
||||
</p>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Supports: .xlsx, .xls, .csv (Max 10MB)
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadStep === "uploading" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3 p-3 rounded-lg border border-border bg-accent/5">
|
||||
<File className="w-5 h-5 text-primary" />
|
||||
<div className="flex-1">
|
||||
<p className="text-[13px] font-medium">{selectedFile?.name}</p>
|
||||
<p className="text-[11px] text-muted-foreground">Uploading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={uploadProgress} className="h-2" />
|
||||
<p className="text-[12px] text-center text-muted-foreground">{uploadProgress}% complete</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadStep === "processing" && (
|
||||
<div className="text-center py-6">
|
||||
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-3" />
|
||||
<p className="text-[14px] font-medium">Processing file...</p>
|
||||
<p className="text-[12px] text-muted-foreground">Validating and importing data</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uploadStep === "complete" && results && (
|
||||
<div className="space-y-3">
|
||||
<Alert className="bg-accent-positive/5 border-accent-positive/20">
|
||||
<CheckCircle className="w-4 h-4 text-accent-positive" />
|
||||
<AlertDescription className="text-[13px]">
|
||||
Successfully imported {results.success} out of {results.total} records
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{results.failed > 0 && (
|
||||
<Alert className="bg-accent-warn/5 border-accent-warn/20">
|
||||
<AlertCircle className="w-4 h-4 text-accent-warn" />
|
||||
<AlertDescription>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[13px] font-medium">{results.failed} records failed</p>
|
||||
<div className="space-y-1 text-[12px] text-muted-foreground max-h-32 overflow-y-auto">
|
||||
{results.errors.map((error, i) => (
|
||||
<p key={i}>• {error}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 text-center">
|
||||
<div className="p-3 rounded-lg border border-border">
|
||||
<p className="text-[20px] font-medium">{results.total}</p>
|
||||
<p className="text-[11px] text-muted-foreground">Total Rows</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border bg-accent-positive/5">
|
||||
<p className="text-[20px] font-medium text-accent-positive">{results.success}</p>
|
||||
<p className="text-[11px] text-muted-foreground">Imported</p>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border bg-accent-warn/5">
|
||||
<p className="text-[20px] font-medium text-accent-warn">{results.failed}</p>
|
||||
<p className="text-[11px] text-muted-foreground">Failed</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{uploadStep === "select" ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleUpload} disabled={!selectedFile}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload File
|
||||
</Button>
|
||||
</>
|
||||
) : uploadStep === "complete" ? (
|
||||
<>
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
Upload Another
|
||||
</Button>
|
||||
<Button onClick={handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
38
src/components/Footer.tsx
Normal file
38
src/components/Footer.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
interface FooterProps {
|
||||
theme?: "light" | "dark";
|
||||
onThemeToggle?: () => void;
|
||||
language?: string;
|
||||
onLanguageChange?: (lang: string) => void;
|
||||
}
|
||||
|
||||
export function Footer({
|
||||
theme = "light",
|
||||
onThemeToggle,
|
||||
language = "en",
|
||||
onLanguageChange,
|
||||
}: FooterProps) {
|
||||
return (
|
||||
<footer className="border-t border-border bg-bg-surface">
|
||||
<div className="container mx-auto px-4 lg:px-8 py-8">
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
{/* Brand & Copyright */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-[10px] bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground">H</span>
|
||||
</div>
|
||||
<span>Hello Jewellers</span>
|
||||
</div>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Your Jewellery Ecosystem, One Hub
|
||||
</p>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
© 2025 Hello Jewellers. v0.1 Prototype
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
246
src/components/HelpOverlay.tsx
Normal file
246
src/components/HelpOverlay.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion";
|
||||
import { Card } from "./ui/card";
|
||||
import { Shield, ShoppingBag, Store, Factory, CheckCircle } from "lucide-react";
|
||||
|
||||
interface HelpOverlayProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function HelpOverlay({ open, onClose }: HelpOverlayProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[800px] max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[28px] leading-[36px]">Help & Documentation</DialogTitle>
|
||||
<DialogDescription>
|
||||
Learn about the platform, portals, and how to get started
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="overview" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">Platform Overview</TabsTrigger>
|
||||
<TabsTrigger value="portals">Portal Guide</TabsTrigger>
|
||||
<TabsTrigger value="faqs">FAQs</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4 mt-6 max-h-[50vh] overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">Welcome to Hello Jewellers</h3>
|
||||
<p className="text-[14px] leading-[20px] text-muted-foreground">
|
||||
A unified platform connecting the entire jewellery ecosystem - from manufacturers to customers, all in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Card className="p-4 border-l-4 border-l-primary">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-[16px] leading-[24px] mb-1">For Customers</h4>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
Browse curated jewellery collections, create wishlists, chat with retailers, and book appointments - all from your mobile device.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 border-l-4 border-l-secondary">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-secondary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-[16px] leading-[24px] mb-1">For Retailers</h4>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
Build your virtual store, manage customer relationships, set daily gold rates, and customize your storefront with AI-powered tools.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 border-l-4 border-l-accent-positive">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-accent-positive mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-[16px] leading-[24px] mb-1">For Manufacturers</h4>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
Sync your inventory, share catalogues with retail partners, handle custom orders, and track production schedules.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 border-l-4 border-l-accent-warn">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-accent-warn mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="text-[16px] leading-[24px] mb-1">For Admins</h4>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
Manage users, monitor platform analytics, moderate content, and ensure DPDP compliance across the ecosystem.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-[16px] bg-accent/5 border border-accent/20">
|
||||
<h4 className="text-[16px] leading-[24px] mb-2">Getting Started</h4>
|
||||
<ol className="space-y-2 text-[14px] text-muted-foreground list-decimal list-inside">
|
||||
<li>Choose the portal that matches your role</li>
|
||||
<li>Sign in or create your account</li>
|
||||
<li>Complete your profile setup</li>
|
||||
<li>Start exploring the platform features</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="portals" className="mt-6 max-h-[50vh] overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<Card className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<ShoppingBag className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">Customer App</h3>
|
||||
<p className="text-[14px] text-muted-foreground mb-3">
|
||||
Mobile-first experience for browsing and purchasing jewellery
|
||||
</p>
|
||||
<ul className="space-y-1 text-[13px] text-muted-foreground">
|
||||
<li>• Browse retailer collections</li>
|
||||
<li>• Create and manage wishlists</li>
|
||||
<li>• Chat with retailers via text, voice, or video</li>
|
||||
<li>• Book showroom appointments</li>
|
||||
<li>• Track orders and quotes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-secondary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Store className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">Retailers' Shop</h3>
|
||||
<p className="text-[14px] text-muted-foreground mb-3">
|
||||
Complete toolkit for managing your jewellery retail business
|
||||
</p>
|
||||
<ul className="space-y-1 text-[13px] text-muted-foreground">
|
||||
<li>• Set daily gold and silver rates</li>
|
||||
<li>• Customize virtual store with AI themes</li>
|
||||
<li>• Manage customer relationships and chat</li>
|
||||
<li>• Handle appointments and bookings</li>
|
||||
<li>• Track inventory and process orders</li>
|
||||
<li>• Mobile app view preview</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-accent-positive/10 flex items-center justify-center flex-shrink-0">
|
||||
<Factory className="w-6 h-6 text-accent-positive" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">Manufacturers' Shop</h3>
|
||||
<p className="text-[14px] text-muted-foreground mb-3">
|
||||
Production and distribution management for manufacturers
|
||||
</p>
|
||||
<ul className="space-y-1 text-[13px] text-muted-foreground">
|
||||
<li>• Sync and manage inventory</li>
|
||||
<li>• Share catalogues with retailers</li>
|
||||
<li>• Accept custom order requests</li>
|
||||
<li>• Track production schedules</li>
|
||||
<li>• API integrations for automation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-accent-warn/10 flex items-center justify-center flex-shrink-0">
|
||||
<Shield className="w-6 h-6 text-accent-warn" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">Admin Console</h3>
|
||||
<p className="text-[14px] text-muted-foreground mb-3">
|
||||
Super admin control panel for platform management
|
||||
</p>
|
||||
<ul className="space-y-1 text-[13px] text-muted-foreground">
|
||||
<li>• User and role management</li>
|
||||
<li>• Platform-wide analytics</li>
|
||||
<li>• Content moderation</li>
|
||||
<li>• DPDP compliance monitoring</li>
|
||||
<li>• System configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="faqs" className="mt-6 max-h-[50vh] overflow-y-auto">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>How do I connect with a retailer as a customer?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
In the Customer App, you can either enter an invite code provided by your retailer or skip this step to browse available jewellery. You can always connect with a retailer later from your account settings.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>What are daily gold rates and why are they required?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
Retailers must set daily gold and silver rates before opening their shop. These rates are displayed on the storefront and used for accurate pricing calculations. You can update rates anytime from the settings.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>Can retailers customize their storefront?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
Yes! Retailers have access to AI-powered customization tools including theme selection, banner management, content generation for product descriptions, and a live mobile preview to see how their store appears to customers.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger>How do voice and video calls work?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
Retailers can initiate voice calls with customers. Video calls can only be initiated by customers, and retailers can accept or decline incoming video calls. All communication is logged for quality and compliance purposes.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger>What languages are supported?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
The platform currently supports English, Hindi (हिन्दी), Marathi (मराठी), and Gujarati (ગુજરાતી). You can change the language from the top navigation bar, and your preference will be saved automatically.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-6">
|
||||
<AccordionTrigger>Is my data secure and DPDP compliant?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
Yes, the platform is built with DPDP (Digital Personal Data Protection) compliance at its core. All personal data is encrypted, access is logged, and users have full rights to view, modify, and delete their data. The Admin Console provides compliance monitoring tools.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="item-7">
|
||||
<AccordionTrigger>Can I use the platform on mobile devices?</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground">
|
||||
The Customer App is specifically designed for mobile devices with a native app-like experience. All other portals (Retailer, Manufacturer, Admin) are responsive and work on mobile, tablet, and desktop devices.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
107
src/components/InviteCodeModal.tsx
Normal file
107
src/components/InviteCodeModal.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import { Button } from "./ui/button";
|
||||
import { Label } from "./ui/label";
|
||||
import { CheckCircle2, AlertCircle } from "lucide-react";
|
||||
|
||||
interface InviteCodeModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onValidateCode: (code: string) => { valid: boolean; portal?: string };
|
||||
}
|
||||
|
||||
export function InviteCodeModal({ open, onClose, onValidateCode }: InviteCodeModalProps) {
|
||||
const [code, setCode] = useState("");
|
||||
const [validated, setValidated] = useState<{ valid: boolean; portal?: string } | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleValidate = () => {
|
||||
if (code.length < 6) {
|
||||
setError("Invite code must be at least 6 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = onValidateCode(code);
|
||||
setValidated(result);
|
||||
|
||||
if (!result.valid) {
|
||||
setError("Invalid invite code. Please check and try again.");
|
||||
} else {
|
||||
setError("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setCode("");
|
||||
setValidated(null);
|
||||
setError("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[440px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[28px] leading-[36px]">Enter Invite Code</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter your invitation code to join a tenant organization
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{!validated ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="inviteCode">Invite Code</Label>
|
||||
<Input
|
||||
id="inviteCode"
|
||||
placeholder="e.g., RJ7K3A"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||
maxLength={8}
|
||||
className="uppercase tracking-wider text-center"
|
||||
/>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Enter the 6-8 character code you received
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-[10px] bg-destructive/10 text-destructive">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<p className="text-[14px]">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={code.length < 6}
|
||||
className="w-full"
|
||||
>
|
||||
Validate Code
|
||||
</Button>
|
||||
</>
|
||||
) : validated.valid ? (
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="flex flex-col items-center text-center space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-8 h-8 text-accent-positive" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[22px] leading-[30px] mb-2">Code Validated!</h3>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
This invite is for <span className="text-foreground">{validated.portal}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Continue to {validated.portal}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
108
src/components/PortalOverlay.tsx
Normal file
108
src/components/PortalOverlay.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { X, LucideIcon } from "lucide-react";
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden@1.1.1";
|
||||
|
||||
interface PortalOverlayProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
keyActions: string[];
|
||||
requiresLogin: boolean;
|
||||
tenantScope: string;
|
||||
onContinue: () => void;
|
||||
}
|
||||
|
||||
export function PortalOverlay({
|
||||
open,
|
||||
onClose,
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
keyActions,
|
||||
requiresLogin,
|
||||
tenantScope,
|
||||
onContinue,
|
||||
}: PortalOverlayProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[700px] p-0">
|
||||
<VisuallyHidden>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
</VisuallyHidden>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 rounded-[10px] p-2 hover:bg-accent/10 transition-colors z-10"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 p-8">
|
||||
<div className="space-y-6">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-[20px] bg-primary/10">
|
||||
<Icon className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-[36px] leading-[44px] mb-3">{title}</h2>
|
||||
<p className="text-[16px] leading-[24px] text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-[18px] leading-[28px] mb-3">Key Actions</h4>
|
||||
<ul className="space-y-2">
|
||||
{keyActions.map((action, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<span className="text-primary mt-1">•</span>
|
||||
<span className="text-[16px] leading-[24px]">{action}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 rounded-[20px] border border-border bg-card space-y-4">
|
||||
<div>
|
||||
<h4 className="text-[16px] leading-[24px] mb-2">User Type</h4>
|
||||
<div className="space-y-2 text-[14px]">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-muted-foreground">Best For:</span>
|
||||
<span className="text-foreground">{tenantScope}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-3">
|
||||
<Button onClick={onContinue} className="w-full">
|
||||
{title === "Admin Console"
|
||||
? "Enter Admin Console"
|
||||
: title === "Customer App"
|
||||
? "Launch Customer App"
|
||||
: title === "Retailers' Shop"
|
||||
? "Open Retailer Portal"
|
||||
: title === "Manufacturers' Shop"
|
||||
? "Open Manufacturer Portal"
|
||||
: "Request Access"}
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="text-[14px] text-primary hover:underline w-full text-center">
|
||||
View Documentation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
67
src/components/PortalTile.tsx
Normal file
67
src/components/PortalTile.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ArrowRight, LucideIcon } from "lucide-react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
|
||||
interface PortalTileProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
requiresLogin: boolean;
|
||||
ctaText: string;
|
||||
onCtaClick: () => void;
|
||||
onLearnMore: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function PortalTile({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
features,
|
||||
requiresLogin,
|
||||
ctaText,
|
||||
onCtaClick,
|
||||
onLearnMore,
|
||||
disabled = false,
|
||||
}: PortalTileProps) {
|
||||
return (
|
||||
<div
|
||||
className="group relative bg-card border border-border rounded-[20px] p-6 transition-all duration-150 hover:scale-[1.01] hover:shadow-[0_8px_24px_rgba(0,0,0,0.12)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.4)]"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="p-3 bg-primary/10 rounded-[16px]">
|
||||
<Icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[22px] leading-[30px]">{title}</h3>
|
||||
<p className="text-[14px] leading-[20px] text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start gap-2 text-[14px] leading-[20px]">
|
||||
<span className="text-primary mt-0.5">•</span>
|
||||
<span className="text-text-secondary">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<Button
|
||||
onClick={onCtaClick}
|
||||
disabled={disabled}
|
||||
className="w-full justify-between group/btn"
|
||||
variant="outline"
|
||||
>
|
||||
<span>{ctaText}</span>
|
||||
<ArrowRight className="w-4 h-4 transition-transform group-hover/btn:translate-x-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
src/components/QRScanModal.tsx
Normal file
99
src/components/QRScanModal.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { QrCode, Upload, Camera } from "lucide-react";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "./ui/sheet";
|
||||
|
||||
interface QRScanModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export function QRScanModal({ open, onClose, isMobile = false }: QRScanModalProps) {
|
||||
const [permissionGranted, setPermissionGranted] = useState(false);
|
||||
|
||||
const handleRequestPermission = () => {
|
||||
// Simulate camera permission
|
||||
setPermissionGranted(true);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div className="space-y-4">
|
||||
{!permissionGranted ? (
|
||||
<div className="text-center space-y-4 py-8">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-primary/10">
|
||||
<Camera className="w-10 h-10 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-[18px] leading-[28px] mb-2">Camera Access Required</h4>
|
||||
<p className="text-[14px] text-muted-foreground max-w-sm mx-auto">
|
||||
We need access to your camera to scan QR codes
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleRequestPermission} className="w-full max-w-xs mx-auto">
|
||||
Grant Camera Access
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="aspect-square bg-muted rounded-[16px] flex items-center justify-center relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/20" />
|
||||
<div className="w-48 h-48 border-4 border-primary rounded-[16px] relative">
|
||||
<div className="absolute top-0 left-0 w-6 h-6 border-t-4 border-l-4 border-primary" />
|
||||
<div className="absolute top-0 right-0 w-6 h-6 border-t-4 border-r-4 border-primary" />
|
||||
<div className="absolute bottom-0 left-0 w-6 h-6 border-b-4 border-l-4 border-primary" />
|
||||
<div className="absolute bottom-0 right-0 w-6 h-6 border-b-4 border-r-4 border-primary" />
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
|
||||
<div className="bg-black/60 backdrop-blur-sm text-white px-4 py-2 rounded-full text-[12px]">
|
||||
Position QR code within frame
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-[12px] uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">Or</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="w-full">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload QR Image
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onClose}>
|
||||
<SheetContent side="bottom" className="rounded-t-[28px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-[28px] leading-[36px]">Scan QR Code</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-6">{content}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[480px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[28px] leading-[36px]">Scan QR Code</DialogTitle>
|
||||
<DialogDescription>
|
||||
Scan or upload a QR code to quickly access products and stores
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{content}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
396
src/components/SmartSearch.tsx
Normal file
396
src/components/SmartSearch.tsx
Normal file
@@ -0,0 +1,396 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Search, Sparkles, TrendingUp, Clock, ArrowRight, Package, Users, FileText, Store } from "lucide-react";
|
||||
import { Input } from "./ui/input";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
|
||||
interface SmartSearchProps {
|
||||
portalType: "admin" | "retailer" | "manufacturer";
|
||||
placeholder?: string;
|
||||
onNavigate?: (page: string, data?: any) => void;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
type: "product" | "customer" | "order" | "quote" | "page" | "user" | "tenant";
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
badge?: string;
|
||||
icon: any;
|
||||
action?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
const SEARCH_DATA = {
|
||||
admin: [
|
||||
{
|
||||
id: "1",
|
||||
type: "user",
|
||||
title: "Priya Sharma",
|
||||
subtitle: "priya.sharma@novajewels.com",
|
||||
badge: "Retailer Admin",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "tenant",
|
||||
title: "Nova Jewels",
|
||||
subtitle: "Mumbai • Active • 42 users",
|
||||
badge: "Enterprise",
|
||||
icon: Store,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "user",
|
||||
title: "Amit Kumar",
|
||||
subtitle: "amit@sparklegems.com",
|
||||
badge: "Retailer Admin",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "page",
|
||||
title: "Analytics Dashboard",
|
||||
subtitle: "View platform metrics and reports",
|
||||
icon: TrendingUp,
|
||||
action: "analytics",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "page",
|
||||
title: "User Management",
|
||||
subtitle: "Manage users and permissions",
|
||||
icon: Users,
|
||||
action: "users",
|
||||
},
|
||||
],
|
||||
retailer: [
|
||||
{
|
||||
id: "1",
|
||||
type: "product",
|
||||
title: "Heritage Bridal Ring",
|
||||
subtitle: "SKU: AU-22K-BR-0192 • 22K Gold",
|
||||
badge: "₹2.8L",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "product",
|
||||
title: "Classic Gold Necklace",
|
||||
subtitle: "SKU: AU-22K-NK-0551 • 22K Gold",
|
||||
badge: "₹1.8L",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "customer",
|
||||
title: "Aditi Sharma",
|
||||
subtitle: "aditi@email.com • Mumbai",
|
||||
badge: "VIP",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "quote",
|
||||
title: "Quote #Q-2847",
|
||||
subtitle: "Aditi Sharma • Bridal Set • ₹4.2L",
|
||||
badge: "Pending",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "order",
|
||||
title: "Order #ORD-2847",
|
||||
subtitle: "Rohan Mehta • Completed",
|
||||
badge: "₹2.8L",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
type: "page",
|
||||
title: "Inventory Management",
|
||||
subtitle: "Manage your product catalog",
|
||||
icon: Package,
|
||||
action: "inventory",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
type: "page",
|
||||
title: "Customer Management",
|
||||
subtitle: "View and manage customers",
|
||||
icon: Users,
|
||||
action: "customers",
|
||||
},
|
||||
],
|
||||
manufacturer: [
|
||||
{
|
||||
id: "1",
|
||||
type: "product",
|
||||
title: "Temple Design Necklace",
|
||||
subtitle: "SKU: MFG-22K-NK-0551 • 22K Gold",
|
||||
badge: "Available",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "product",
|
||||
title: "Classic Bangle Set",
|
||||
subtitle: "SKU: MFG-22K-BG-1023 • 22K Gold",
|
||||
badge: "In Stock",
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "user",
|
||||
title: "Nova Jewels",
|
||||
subtitle: "Connected Retailer • Mumbai",
|
||||
badge: "124 Orders",
|
||||
icon: Store,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "order",
|
||||
title: "Custom Order #CO-2847",
|
||||
subtitle: "Nova Jewels • Ring Design",
|
||||
badge: "Urgent",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "page",
|
||||
title: "Sharing & Connections",
|
||||
subtitle: "Manage retailer partnerships",
|
||||
icon: Users,
|
||||
action: "sharing",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const RECENT_SEARCHES = {
|
||||
admin: ["User analytics", "Active tenants", "Platform metrics"],
|
||||
retailer: ["Gold rate today", "Bridal collection", "Pending quotes"],
|
||||
manufacturer: ["Inventory status", "Connected retailers", "Custom orders"],
|
||||
};
|
||||
|
||||
const TRENDING_SEARCHES = {
|
||||
admin: ["Compliance reports", "New registrations", "Revenue analytics"],
|
||||
retailer: ["Diamond rings", "Customer preferences", "Sales report"],
|
||||
manufacturer: ["Production schedule", "Quality metrics", "Order fulfillment"],
|
||||
};
|
||||
|
||||
export function SmartSearch({ portalType, placeholder, onNavigate }: SmartSearchProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const searchData = SEARCH_DATA[portalType];
|
||||
const recentSearches = RECENT_SEARCHES[portalType];
|
||||
const trendingSearches = TRENDING_SEARCHES[portalType];
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery.trim()) {
|
||||
setIsSearching(true);
|
||||
const timer = setTimeout(() => {
|
||||
const filtered = searchData.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.subtitle?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setResults(filtered);
|
||||
setIsSearching(false);
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
setResults([]);
|
||||
}
|
||||
}, [searchQuery, portalType]);
|
||||
|
||||
const handleResultClick = (result: SearchResult) => {
|
||||
if (result.action && onNavigate) {
|
||||
onNavigate(result.action, result.data);
|
||||
}
|
||||
setIsOpen(false);
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
const handleQuickSearch = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-md" ref={containerRef}>
|
||||
{/* Search Input */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
placeholder={placeholder || "Search anything... (⌘K)"}
|
||||
className="pl-10 pr-12"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 gap-1 text-[10px] px-2 py-0.5"
|
||||
>
|
||||
<Sparkles className="w-3 h-3" />
|
||||
AI
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
className="absolute top-full left-0 right-0 mt-2 bg-background border border-border rounded-lg shadow-lg overflow-hidden z-50"
|
||||
>
|
||||
<ScrollArea className="max-h-[500px]">
|
||||
{/* Search Results */}
|
||||
{searchQuery && (
|
||||
<div className="p-3">
|
||||
{isSearching ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[12px] text-muted-foreground">AI is searching...</p>
|
||||
</div>
|
||||
</div>
|
||||
) : results.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<Sparkles className="w-4 h-4 text-primary" />
|
||||
<p className="text-[12px] font-medium">AI-powered results</p>
|
||||
</div>
|
||||
{results.map((result) => {
|
||||
const Icon = result.icon;
|
||||
return (
|
||||
<button
|
||||
key={result.id}
|
||||
onClick={() => handleResultClick(result)}
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-accent/5 transition-colors text-left group"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[13px] font-medium truncate">{result.title}</p>
|
||||
{result.subtitle && (
|
||||
<p className="text-[11px] text-muted-foreground truncate">
|
||||
{result.subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{result.badge && (
|
||||
<Badge variant="outline" className="text-[10px] px-2 py-0.5">
|
||||
{result.badge}
|
||||
</Badge>
|
||||
)}
|
||||
<ArrowRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<Search className="w-12 h-12 text-muted-foreground mb-2" />
|
||||
<p className="text-[13px] font-medium mb-1">No results found</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Try a different search term
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Default State */}
|
||||
{!searchQuery && (
|
||||
<div className="p-3 space-y-4">
|
||||
{/* Recent Searches */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 px-3 py-2 mb-2">
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
<p className="text-[12px] font-medium text-muted-foreground">Recent</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{recentSearches.map((search, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleQuickSearch(search)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg hover:bg-accent/5 transition-colors text-left"
|
||||
>
|
||||
<span className="text-[13px]">{search}</span>
|
||||
<ArrowRight className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Trending Searches */}
|
||||
<div>
|
||||
<div className="flex items-center gap-2 px-3 py-2 mb-2">
|
||||
<TrendingUp className="w-4 h-4 text-primary" />
|
||||
<p className="text-[12px] font-medium">Trending</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 px-3">
|
||||
{trendingSearches.map((search, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleQuickSearch(search)}
|
||||
className="px-3 py-1.5 rounded-full bg-accent/10 hover:bg-accent/20 text-[11px] transition-colors"
|
||||
>
|
||||
{search}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* AI Tips */}
|
||||
<div className="px-3 py-2 rounded-lg bg-primary/5 border border-primary/20">
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles className="w-4 h-4 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-[11px] font-medium mb-1">AI Search Tips</p>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Try natural language like "Show me pending orders" or "Find customers in
|
||||
Mumbai"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/TenantSelectModal.tsx
Normal file
91
src/components/TenantSelectModal.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
|
||||
import { Input } from "./ui/input";
|
||||
import { Button } from "./ui/button";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Search, Building2 } from "lucide-react";
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
interface TenantSelectModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
tenants: Tenant[];
|
||||
onSelectTenant: (tenant: Tenant) => void;
|
||||
}
|
||||
|
||||
export function TenantSelectModal({
|
||||
open,
|
||||
onClose,
|
||||
tenants,
|
||||
onSelectTenant,
|
||||
}: TenantSelectModalProps) {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredTenants = tenants.filter((tenant) =>
|
||||
tenant.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[28px] leading-[36px]">Select Tenant</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose which organization you want to access
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search tenants..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
||||
{filteredTenants.map((tenant) => (
|
||||
<button
|
||||
key={tenant.id}
|
||||
onClick={() => {
|
||||
onSelectTenant(tenant);
|
||||
onClose();
|
||||
}}
|
||||
className="w-full flex items-center gap-4 p-4 rounded-[16px] border border-border bg-card hover:bg-accent/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-[12px] bg-primary/10">
|
||||
{tenant.logo ? (
|
||||
<img src={tenant.logo} alt={tenant.name} className="w-8 h-8" />
|
||||
) : (
|
||||
<Building2 className="w-6 h-6 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="text-[16px] leading-[24px]">{tenant.name}</h4>
|
||||
<Badge variant="secondary" className="mt-1 text-[11px]">
|
||||
{tenant.role}
|
||||
</Badge>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredTenants.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p>No tenants found matching "{search}"</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
155
src/components/TopNavBar.tsx
Normal file
155
src/components/TopNavBar.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from "react";
|
||||
import { Search, Bell, ChevronDown, Moon, Sun } from "lucide-react";
|
||||
import { Input } from "./ui/input";
|
||||
import { Avatar, AvatarFallback } from "./ui/avatar";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu";
|
||||
import { Badge } from "./ui/badge";
|
||||
|
||||
interface TopNavBarProps {
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
currentTenant: string;
|
||||
tenants: { id: string; name: string; role: string }[];
|
||||
onTenantSwitch: (tenantId: string) => void;
|
||||
notificationCount?: number;
|
||||
theme: "light" | "dark";
|
||||
onThemeToggle: () => void;
|
||||
}
|
||||
|
||||
export function TopNavBar({
|
||||
userName,
|
||||
userEmail,
|
||||
currentTenant,
|
||||
tenants,
|
||||
onTenantSwitch,
|
||||
notificationCount = 0,
|
||||
theme,
|
||||
onThemeToggle,
|
||||
}: TopNavBarProps) {
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
const initials = userName
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4 lg:px-8">
|
||||
<div className="flex h-16 items-center justify-between gap-4">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-[10px] bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground">H</span>
|
||||
</div>
|
||||
<span className="hidden sm:inline">Hello Jewellers</span>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex-1 max-w-md hidden md:block">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Global search..."
|
||||
className="pl-10"
|
||||
onFocus={() => setSearchOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Tenant Switcher */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="hidden lg:flex items-center gap-2">
|
||||
<span className="text-[14px] max-w-[150px] truncate">{currentTenant}</span>
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[280px]">
|
||||
<DropdownMenuLabel>Switch Tenant</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{tenants.map((tenant) => (
|
||||
<DropdownMenuItem
|
||||
key={tenant.id}
|
||||
onClick={() => onTenantSwitch(tenant.id)}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[14px]">{tenant.name}</span>
|
||||
<span className="text-[12px] text-muted-foreground">{tenant.role}</span>
|
||||
</div>
|
||||
{tenant.name === currentTenant && (
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onThemeToggle}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Moon className="w-5 h-5" />
|
||||
) : (
|
||||
<Sun className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<Button variant="ghost" size="icon" className="relative">
|
||||
<Bell className="w-5 h-5" />
|
||||
{notificationCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground rounded-full flex items-center justify-center text-[10px]">
|
||||
{notificationCount > 9 ? "9+" : notificationCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* User Menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="flex items-center gap-2">
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground hidden sm:block" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[240px]">
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span>{userName}</span>
|
||||
<span className="text-[12px] text-muted-foreground">{userEmail}</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Preferences</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive">Sign Out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
476
src/components/admin/AdminShell.tsx
Normal file
476
src/components/admin/AdminShell.tsx
Normal file
@@ -0,0 +1,476 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
FileText,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Search,
|
||||
ChevronDown,
|
||||
Menu,
|
||||
X,
|
||||
Bell,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { AIAssistant } from "../AIAssistant";
|
||||
import { SmartSearch } from "../SmartSearch";
|
||||
import { Avatar, AvatarFallback } from "../ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "../ui/sheet";
|
||||
import { Card } from "../ui/card";
|
||||
|
||||
interface AdminShellProps {
|
||||
children: React.ReactNode;
|
||||
currentPage: string;
|
||||
onNavigate: (page: string) => void;
|
||||
}
|
||||
|
||||
const navigationItems = [
|
||||
{ id: "overview", label: "Overview", icon: LayoutDashboard, section: null },
|
||||
{
|
||||
id: "users",
|
||||
label: "Users",
|
||||
icon: Users,
|
||||
section: "Management",
|
||||
children: [
|
||||
{ id: "users-retailers", label: "Retailers" },
|
||||
{ id: "users-manufacturers", label: "Manufacturers" },
|
||||
{ id: "users-customers", label: "Customers" },
|
||||
]
|
||||
},
|
||||
{ id: "content", label: "Content Management", icon: FileText, section: "Operations" },
|
||||
{ id: "analytics", label: "Analytics", icon: BarChart3, section: "Analytics" },
|
||||
{ id: "settings", label: "Settings", icon: Settings, section: null },
|
||||
];
|
||||
|
||||
export function AdminShell({ children, currentPage, onNavigate }: AdminShellProps) {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [notificationsOpen, setNotificationsOpen] = useState(false);
|
||||
const [profileOpen, setProfileOpen] = useState(false);
|
||||
const [usersExpanded, setUsersExpanded] = useState(
|
||||
currentPage.startsWith("users-")
|
||||
);
|
||||
|
||||
const groupedNav = navigationItems.reduce((acc, item) => {
|
||||
const section = item.section || "main";
|
||||
if (!acc[section]) acc[section] = [];
|
||||
acc[section].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof navigationItems>);
|
||||
|
||||
const notifications = [
|
||||
{ id: 1, title: "New Retailer Signup", message: "Nova Jewels has joined the platform", time: "5 min ago", unread: true },
|
||||
{ id: 2, title: "System Update", message: "Platform maintenance scheduled for tonight", time: "1 hour ago", unread: true },
|
||||
{ id: 3, title: "Analytics Report", message: "Weekly report is ready to view", time: "2 hours ago", unread: true },
|
||||
{ id: 4, title: "Content Published", message: "New policy page has been published", time: "1 day ago", unread: false },
|
||||
];
|
||||
|
||||
const handleNotificationClick = () => {
|
||||
setNotificationsOpen(true);
|
||||
};
|
||||
|
||||
const handleProfileClick = () => {
|
||||
setProfileOpen(true);
|
||||
};
|
||||
|
||||
const renderNavItem = (item: typeof navigationItems[0]) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentPage === item.id || currentPage.startsWith(item.id + "-");
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
|
||||
if (hasChildren) {
|
||||
return (
|
||||
<div key={item.id} className="space-y-1">
|
||||
<Button
|
||||
variant={isActive ? "secondary" : "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
if (item.id === "users") {
|
||||
setUsersExpanded(!usersExpanded);
|
||||
} else {
|
||||
onNavigate(item.id);
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon className="w-4 h-4 mr-3" />
|
||||
{item.label}
|
||||
{item.id === "users" && (
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 ml-auto transition-transform ${usersExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
{usersExpanded && item.children && (
|
||||
<div className="ml-6 space-y-1">
|
||||
{item.children.map((child) => (
|
||||
<Button
|
||||
key={child.id}
|
||||
variant={currentPage === child.id ? "secondary" : "ghost"}
|
||||
className="w-full justify-start text-[14px]"
|
||||
onClick={() => {
|
||||
onNavigate(child.id);
|
||||
setMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
{child.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={item.id}
|
||||
variant={isActive ? "secondary" : "ghost"}
|
||||
className="w-full justify-start"
|
||||
onClick={() => {
|
||||
onNavigate(item.id);
|
||||
setMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Icon className="w-4 h-4 mr-3" />
|
||||
{item.label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page">
|
||||
{/* Top Bar */}
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-16 items-center px-4 lg:px-6">
|
||||
<div className="flex items-center gap-3 lg:gap-4 flex-1 min-w-0">
|
||||
{/* Mobile Menu Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="lg:hidden shrink-0"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
|
||||
</Button>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="w-8 h-8 rounded-[10px] bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground text-[14px]">H</span>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="text-[14px]">Hello Jewellers</div>
|
||||
<div className="text-[11px] text-muted-foreground uppercase tracking-wider">Super Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Smart AI Search - centered with max width */}
|
||||
<div className="flex-1 max-w-[500px] hidden md:block mx-auto">
|
||||
<SmartSearch
|
||||
portalType="admin"
|
||||
placeholder="Search users, tenants, analytics..."
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Actions */}
|
||||
<div className="flex items-center gap-2 shrink-0 ml-auto">
|
||||
{/* Notifications */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="relative"
|
||||
onClick={handleNotificationClick}
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="absolute -top-1 -right-1 w-5 h-5 bg-destructive text-destructive-foreground rounded-full flex items-center justify-center text-[10px]">
|
||||
3
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* User Menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarFallback>SA</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[240px]">
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex flex-col">
|
||||
<span>Super Admin</span>
|
||||
<span className="text-[12px] text-muted-foreground">admin@hellojewellers.com</span>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleProfileClick}>
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Profile Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onNavigate("settings")}>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Preferences
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive">Sign Out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-[calc(100vh-4rem)]">
|
||||
{/* Side Navigation */}
|
||||
<aside
|
||||
className={`${
|
||||
mobileMenuOpen ? "block" : "hidden"
|
||||
} lg:block w-64 border-r border-border bg-bg-surface fixed top-16 left-0 h-[calc(100vh-4rem)] z-40 lg:sticky`}
|
||||
>
|
||||
<ScrollArea className="h-full py-6 px-3">
|
||||
<nav className="space-y-6">
|
||||
{groupedNav.main && (
|
||||
<div className="space-y-1">
|
||||
{groupedNav.main.map((item) => renderNavItem(item))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{["Management", "Operations", "Analytics"].map((section) => {
|
||||
if (!groupedNav[section]) return null;
|
||||
return (
|
||||
<div key={section}>
|
||||
<Separator />
|
||||
<div className="px-3 py-2">
|
||||
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">
|
||||
{section}
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{groupedNav[section].map((item) => renderNavItem(item))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 w-full lg:w-[calc(100%-16rem)]">
|
||||
<div className="w-full p-6 lg:p-8 max-w-[1400px] mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* AI Assistant */}
|
||||
<AIAssistant portalType="admin" />
|
||||
|
||||
{/* Notifications Sheet */}
|
||||
<Sheet open={notificationsOpen} onOpenChange={setNotificationsOpen}>
|
||||
<SheetContent side="right" className="w-[400px] sm:w-[540px] p-0">
|
||||
<div className="p-6 border-b border-border">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Notifications</SheetTitle>
|
||||
<SheetDescription>View and manage your notifications</SheetDescription>
|
||||
</SheetHeader>
|
||||
</div>
|
||||
<ScrollArea className="h-[calc(100vh-12rem)] px-6 py-4">
|
||||
<div className="space-y-3">
|
||||
{notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`p-4 rounded-lg border ${
|
||||
notification.unread ? "bg-accent/5 border-accent" : "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="text-[14px] font-medium">{notification.title}</h4>
|
||||
{notification.unread && (
|
||||
<div className="w-2 h-2 rounded-full bg-primary shrink-0 ml-2" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[13px] text-muted-foreground mb-2">
|
||||
{notification.message}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground">{notification.time}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className="p-6 border-t border-border">
|
||||
<Button variant="outline" className="w-full">
|
||||
Mark All as Read
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* Profile Dialog - Full Screen */}
|
||||
{profileOpen && (
|
||||
<div className="fixed inset-0 z-50 bg-background">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur">
|
||||
<div className="flex h-16 items-center justify-between px-6">
|
||||
<div>
|
||||
<h2 className="text-[22px] leading-[30px]">Admin Profile</h2>
|
||||
<p className="text-[13px] text-muted-foreground">Manage your profile settings and security</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={() => setProfileOpen(false)}>
|
||||
<X className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<ScrollArea className="h-[calc(100vh-4rem)]">
|
||||
<div className="max-w-4xl mx-auto p-6 lg:p-8 space-y-8">
|
||||
{/* Profile Picture Section */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Profile Picture</h3>
|
||||
<div className="flex items-center gap-6">
|
||||
<Avatar className="w-24 h-24">
|
||||
<AvatarFallback className="text-[32px]">SA</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">Change Photo</Button>
|
||||
<Button variant="outline">Remove</Button>
|
||||
</div>
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
JPG, PNG or GIF. Max size 2MB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Personal Information */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Personal Information</h3>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[14px] font-medium">First Name</label>
|
||||
<Input defaultValue="Super" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[14px] font-medium">Last Name</label>
|
||||
<Input defaultValue="Admin" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[14px] font-medium">Email Address</label>
|
||||
<Input defaultValue="admin@hellojewellers.com" type="email" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[14px] font-medium">Phone Number</label>
|
||||
<Input defaultValue="+91 98765 43210" type="tel" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[14px] font-medium">Role</label>
|
||||
<Input defaultValue="Super Administrator" disabled />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[14px] font-medium">Department</label>
|
||||
<Input defaultValue="Platform Management" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Security Settings */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Security</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Password</div>
|
||||
<div className="text-[13px] text-muted-foreground">Last changed 45 days ago</div>
|
||||
</div>
|
||||
<Button variant="outline">Change Password</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Two-Factor Authentication</div>
|
||||
<div className="text-[13px] text-muted-foreground">Add an extra layer of security</div>
|
||||
</div>
|
||||
<Button variant="outline">Enable 2FA</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Active Sessions</div>
|
||||
<div className="text-[13px] text-muted-foreground">3 devices currently logged in</div>
|
||||
</div>
|
||||
<Button variant="outline">Manage Sessions</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Login History</div>
|
||||
<div className="text-[13px] text-muted-foreground">View recent login activity</div>
|
||||
</div>
|
||||
<Button variant="outline">View History</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Preferences */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Preferences</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Language</div>
|
||||
<div className="text-[13px] text-muted-foreground">Choose your preferred language</div>
|
||||
</div>
|
||||
<select className="px-3 py-2 border border-border rounded-lg bg-background">
|
||||
<option>English</option>
|
||||
<option>हिन्दी (Hindi)</option>
|
||||
<option>मराठी (Marathi)</option>
|
||||
</select>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Time Zone</div>
|
||||
<div className="text-[13px] text-muted-foreground">Set your local time zone</div>
|
||||
</div>
|
||||
<select className="px-3 py-2 border border-border rounded-lg bg-background">
|
||||
<option>IST (UTC+5:30)</option>
|
||||
<option>PST (UTC-8:00)</option>
|
||||
<option>EST (UTC-5:00)</option>
|
||||
</select>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Email Notifications</div>
|
||||
<div className="text-[13px] text-muted-foreground">Receive email updates</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Configure</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="outline" onClick={() => setProfileOpen(false)}>Cancel</Button>
|
||||
<Button>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/components/admin/RoleBadge.tsx
Normal file
25
src/components/admin/RoleBadge.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
type RoleType = "admin" | "retailer-admin" | "retail-associate" | "manufacturer-admin" | "customer" | "auditor";
|
||||
|
||||
interface RoleBadgeProps {
|
||||
role: RoleType;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const roleStyles: Record<RoleType, string> = {
|
||||
admin: "bg-role-admin/10 text-role-admin border-role-admin/20",
|
||||
"retailer-admin": "bg-role-retailer-admin/10 text-role-retailer-admin border-role-retailer-admin/20",
|
||||
"retail-associate": "bg-role-retail-associate/10 text-role-retail-associate border-role-retail-associate/20",
|
||||
"manufacturer-admin": "bg-role-manufacturer-admin/10 text-role-manufacturer-admin border-role-manufacturer-admin/20",
|
||||
customer: "bg-role-customer/10 text-role-customer border-role-customer/20",
|
||||
auditor: "bg-role-auditor/10 text-role-auditor border-role-auditor/20",
|
||||
};
|
||||
|
||||
export function RoleBadge({ role, children }: RoleBadgeProps) {
|
||||
return (
|
||||
<Badge variant="outline" className={roleStyles[role]}>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
37
src/components/admin/StatCard.tsx
Normal file
37
src/components/admin/StatCard.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Card } from "../ui/card";
|
||||
import { LucideIcon, TrendingUp, TrendingDown } from "lucide-react";
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon: LucideIcon;
|
||||
trend?: {
|
||||
value: string;
|
||||
positive: boolean;
|
||||
};
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export function StatCard({ title, value, icon: Icon, trend, iconColor = "text-primary" }: StatCardProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<p className="text-[14px] text-muted-foreground">{title}</p>
|
||||
<Icon className={`w-5 h-5 ${iconColor}`} />
|
||||
</div>
|
||||
<p className="text-[28px] leading-[36px] mb-2">{value}</p>
|
||||
{trend && (
|
||||
<div className="flex items-center gap-1 text-[12px]">
|
||||
{trend.positive ? (
|
||||
<TrendingUp className="w-3 h-3 text-accent-positive" />
|
||||
) : (
|
||||
<TrendingDown className="w-3 h-3 text-accent-error" />
|
||||
)}
|
||||
<span className={trend.positive ? "text-accent-positive" : "text-accent-error"}>
|
||||
{trend.value}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
27
src/components/admin/StatusBadge.tsx
Normal file
27
src/components/admin/StatusBadge.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
type StatusType = "active" | "suspended" | "trial" | "over-limit" | "flagged" | "pending" | "compliant" | "action-needed";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: StatusType;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const statusStyles: Record<StatusType, string> = {
|
||||
active: "bg-status-active/10 text-status-active border-status-active/20",
|
||||
suspended: "bg-status-suspended/10 text-status-suspended border-status-suspended/20",
|
||||
trial: "bg-status-trial/10 text-status-trial border-status-trial/20",
|
||||
"over-limit": "bg-status-over-limit/10 text-status-over-limit border-status-over-limit/20",
|
||||
flagged: "bg-status-flagged/10 text-status-flagged border-status-flagged/20",
|
||||
pending: "bg-status-pending/10 text-status-pending border-status-pending/20",
|
||||
compliant: "bg-status-compliant/10 text-status-compliant border-status-compliant/20",
|
||||
"action-needed": "bg-status-action-needed/10 text-status-action-needed border-status-action-needed/20",
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, children }: StatusBadgeProps) {
|
||||
return (
|
||||
<Badge variant="outline" className={statusStyles[status]}>
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
241
src/components/admin/pages/AnalyticsPage.tsx
Normal file
241
src/components/admin/pages/AnalyticsPage.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
import { TrendingUp, Users, Package, ShoppingBag, BarChart3 } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { StatCard } from "../StatCard";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
const topRetailers = [
|
||||
{ name: "Sparkle Jewels Pvt Ltd", views: 24850, inquiries: 342, conversion: "14.2%", responseTime: "2.4h" },
|
||||
{ name: "Auric Gold Co.", views: 18420, inquiries: 256, conversion: "12.8%", responseTime: "3.1h" },
|
||||
{ name: "Nova Jewels", views: 15680, inquiries: 198, conversion: "11.4%", responseTime: "2.8h" },
|
||||
{ name: "Zephyr Gems LLP", views: 12340, inquiries: 165, conversion: "10.2%", responseTime: "4.2h" },
|
||||
];
|
||||
|
||||
const categoryDemand = [
|
||||
{ category: "Bridal", inquiries: 842, orders: 124, avgValue: "₹2.8L" },
|
||||
{ category: "Daily Wear", inquiries: 1240, orders: 340, avgValue: "₹45K" },
|
||||
{ category: "Temple Jewellery", inquiries: 580, orders: 86, avgValue: "₹1.2L" },
|
||||
{ category: "Contemporary", inquiries: 420, orders: 92, avgValue: "₹68K" },
|
||||
];
|
||||
|
||||
export function AnalyticsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Platform Analytics</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Track usage, growth, and performance across the platform
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Platform Overview</TabsTrigger>
|
||||
<TabsTrigger value="growth">Tenant Growth</TabsTrigger>
|
||||
<TabsTrigger value="retailers">Retailer Performance</TabsTrigger>
|
||||
<TabsTrigger value="demand">Demand Insights</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
{/* Platform KPIs */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Monthly Active Users"
|
||||
value="3,284"
|
||||
icon={Users}
|
||||
trend={{ value: "+18% vs last month", positive: true }}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active Tenants"
|
||||
value="47"
|
||||
icon={BarChart3}
|
||||
trend={{ value: "+8% vs last month", positive: true }}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Live SKUs"
|
||||
value="58,240"
|
||||
icon={Package}
|
||||
trend={{ value: "+24% vs last month", positive: true }}
|
||||
iconColor="text-secondary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Messages Sent (30d)"
|
||||
value="12.4K"
|
||||
icon={ShoppingBag}
|
||||
trend={{ value: "+32% vs last month", positive: true }}
|
||||
iconColor="text-accent-warn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Usage Trends Chart */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[22px] leading-[30px] mb-4">Usage Trends (30 Days)</h3>
|
||||
<div className="h-[300px] bg-muted/30 rounded-[16px] flex items-center justify-center">
|
||||
<p className="text-muted-foreground">Chart: Daily Active Users & Engagement</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Engagement Metrics */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Customer Funnel</h3>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-[14px]">Wishlist Adds</span>
|
||||
<span className="text-[14px]">8,420</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-primary" style={{ width: "100%" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-[14px]">Inquiries</span>
|
||||
<span className="text-[14px]">2,840</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent-positive" style={{ width: "34%" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-[14px]">Appointments</span>
|
||||
<span className="text-[14px]">840</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-secondary" style={{ width: "10%" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="text-[14px]">Orders</span>
|
||||
<span className="text-[14px]">420</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent-warn" style={{ width: "5%" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Notification Impact</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px]">OTP Messages</p>
|
||||
<p className="text-[12px] text-muted-foreground">Delivery: 99.8%</p>
|
||||
</div>
|
||||
<span className="text-[16px]">8,240</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px]">Quote Ready</p>
|
||||
<p className="text-[12px] text-muted-foreground">Open Rate: 84%</p>
|
||||
</div>
|
||||
<span className="text-[16px]">2,180</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px]">Order Updates</p>
|
||||
<p className="text-[12px] text-muted-foreground">Reply Rate: 42%</p>
|
||||
</div>
|
||||
<span className="text-[16px]">1,840</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="growth" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[22px] leading-[30px] mb-4">Tenant Activation Funnel</h3>
|
||||
<div className="h-[300px] bg-muted/30 rounded-[16px] flex items-center justify-center">
|
||||
<p className="text-muted-foreground">Chart: Invite → Setup → First Publish → First Order</p>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="retailers" className="space-y-6">
|
||||
<Card>
|
||||
<div className="p-6 border-b border-border">
|
||||
<h3 className="text-[22px] leading-[30px]">Top Performing Retailers</h3>
|
||||
<p className="text-[14px] text-muted-foreground mt-1">Last 30 days</p>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Retailer</TableHead>
|
||||
<TableHead className="text-right">Views</TableHead>
|
||||
<TableHead className="text-right">Inquiries</TableHead>
|
||||
<TableHead className="text-right">Conversion</TableHead>
|
||||
<TableHead className="text-right">Avg Response Time</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{topRetailers.map((retailer, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{retailer.name}</TableCell>
|
||||
<TableCell className="text-right">{retailer.views.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-right">{retailer.inquiries}</TableCell>
|
||||
<TableCell className="text-right">{retailer.conversion}</TableCell>
|
||||
<TableCell className="text-right">{retailer.responseTime}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="demand" className="space-y-6">
|
||||
<Card>
|
||||
<div className="p-6 border-b border-border">
|
||||
<h3 className="text-[22px] leading-[30px]">Category Demand Insights</h3>
|
||||
<p className="text-[14px] text-muted-foreground mt-1">Last 30 days</p>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead className="text-right">Inquiries</TableHead>
|
||||
<TableHead className="text-right">Orders</TableHead>
|
||||
<TableHead className="text-right">Avg Order Value</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{categoryDemand.map((category, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{category.category}</TableCell>
|
||||
<TableCell className="text-right">{category.inquiries}</TableCell>
|
||||
<TableCell className="text-right">{category.orders}</TableCell>
|
||||
<TableCell className="text-right">{category.avgValue}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[22px] leading-[30px] mb-4">Demand Heatmap (Next 4 Weeks - Forecast)</h3>
|
||||
<div className="h-[300px] bg-muted/30 rounded-[16px] flex items-center justify-center">
|
||||
<p className="text-muted-foreground">Visual: Category × Occasion demand prediction</p>
|
||||
</div>
|
||||
<p className="text-[12px] text-muted-foreground mt-4">
|
||||
* Forecasting based on historical trends and upcoming occasions
|
||||
</p>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
314
src/components/admin/pages/CompliancePage.tsx
Normal file
314
src/components/admin/pages/CompliancePage.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
import { Shield, CheckCircle, AlertTriangle, FileText, Clock } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { StatCard } from "../StatCard";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
const dsrQueue = [
|
||||
{
|
||||
id: "DSR-2025-0091",
|
||||
type: "Access",
|
||||
subject: "Priya Sharma",
|
||||
email: "priya@example.com",
|
||||
submitted: "2 days ago",
|
||||
sla: "5 days remaining",
|
||||
status: "pending" as const,
|
||||
},
|
||||
{
|
||||
id: "DSR-2025-0104",
|
||||
type: "Deletion",
|
||||
subject: "Amit Patel",
|
||||
email: "amit@example.com",
|
||||
submitted: "1 day ago",
|
||||
sla: "6 days remaining",
|
||||
status: "pending" as const,
|
||||
},
|
||||
{
|
||||
id: "DSR-2025-0088",
|
||||
type: "Correction",
|
||||
subject: "Sarah Johnson",
|
||||
email: "sarah@example.com",
|
||||
submitted: "4 days ago",
|
||||
sla: "3 days remaining",
|
||||
status: "pending" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const consentRecords = [
|
||||
{
|
||||
subject: "Rajesh Kumar",
|
||||
channel: "WhatsApp",
|
||||
purpose: "Order Updates",
|
||||
timestamp: "2025-01-08 14:23",
|
||||
status: "granted" as const,
|
||||
},
|
||||
{
|
||||
subject: "Priya Sharma",
|
||||
channel: "Email",
|
||||
purpose: "Marketing",
|
||||
timestamp: "2025-01-10 09:15",
|
||||
status: "granted" as const,
|
||||
},
|
||||
{
|
||||
subject: "Dev Mehta",
|
||||
channel: "SMS",
|
||||
purpose: "OTP",
|
||||
timestamp: "2025-01-11 16:40",
|
||||
status: "granted" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export function CompliancePage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">DPDP Compliance</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage data privacy, consent, and subject rights compliance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Compliance Scorecard */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Consent Coverage"
|
||||
value="98.4%"
|
||||
icon={CheckCircle}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Open DSRs"
|
||||
value={dsrQueue.length}
|
||||
icon={FileText}
|
||||
iconColor="text-accent-warn"
|
||||
/>
|
||||
<StatCard
|
||||
title="Breach Incidents"
|
||||
value="0"
|
||||
icon={Shield}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Last Audit"
|
||||
value="Dec 2024"
|
||||
icon={Clock}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Compliance Posture */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[22px] leading-[30px] mb-6">Compliance Posture</h3>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px]">Data Minimization</span>
|
||||
<StatusBadge status="compliant">Compliant</StatusBadge>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent-positive" style={{ width: "95%" }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px]">Consent Management</span>
|
||||
<StatusBadge status="compliant">Compliant</StatusBadge>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent-positive" style={{ width: "98%" }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px]">Retention Hygiene</span>
|
||||
<StatusBadge status="action-needed">Needs Review</StatusBadge>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent-warn" style={{ width: "72%" }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px]">Breach Readiness</span>
|
||||
<StatusBadge status="compliant">Compliant</StatusBadge>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent-positive" style={{ width: "88%" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-accent-warn/5 border border-accent-warn/20 rounded-[16px] p-6">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle className="w-5 h-5 text-accent-warn mt-1" />
|
||||
<div>
|
||||
<h4 className="text-[16px] leading-[24px] mb-1">Action Required</h4>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
2 retention policies need configuration for new data types
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">Review Retention Policies</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Tabs defaultValue="dsr" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="dsr">Data Subject Requests</TabsTrigger>
|
||||
<TabsTrigger value="consent">Consent Registry</TabsTrigger>
|
||||
<TabsTrigger value="retention">Retention Policies</TabsTrigger>
|
||||
<TabsTrigger value="security">Security Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="dsr" className="space-y-4">
|
||||
<Card>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-[22px] leading-[30px]">DSR Queue</h3>
|
||||
<p className="text-[14px] text-muted-foreground mt-1">
|
||||
{dsrQueue.length} pending requests
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline">Export Log</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Request ID</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Subject</TableHead>
|
||||
<TableHead>Submitted</TableHead>
|
||||
<TableHead>SLA</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dsrQueue.map((dsr) => (
|
||||
<TableRow key={dsr.id}>
|
||||
<TableCell className="text-[14px]">{dsr.id}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status="pending">{dsr.type}</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-[14px]">{dsr.subject}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{dsr.email}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{dsr.submitted}</TableCell>
|
||||
<TableCell className="text-[14px]">{dsr.sla}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={dsr.status}>Pending</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline">Process</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="consent" className="space-y-4">
|
||||
<Card>
|
||||
<div className="p-6 border-b border-border">
|
||||
<h3 className="text-[22px] leading-[30px]">Consent Records</h3>
|
||||
<p className="text-[14px] text-muted-foreground mt-1">
|
||||
Recent consent grants and revocations
|
||||
</p>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Subject</TableHead>
|
||||
<TableHead>Channel</TableHead>
|
||||
<TableHead>Purpose</TableHead>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{consentRecords.map((record, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="text-[14px]">{record.subject}</TableCell>
|
||||
<TableCell>{record.channel}</TableCell>
|
||||
<TableCell>{record.purpose}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{record.timestamp}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status="compliant">Granted</StatusBadge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="retention" className="space-y-4">
|
||||
<Card className="p-12 text-center">
|
||||
<FileText className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-[22px] leading-[30px] mb-2">Retention Policy Configuration</h3>
|
||||
<p className="text-muted-foreground max-w-md mx-auto">
|
||||
Configure auto-delete schedules for different data types
|
||||
</p>
|
||||
<Button className="mt-6">Configure Policies</Button>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="security" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[22px] leading-[30px] mb-6">Security Controls</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[16px] leading-[24px]">MFA Enforcement</p>
|
||||
<p className="text-[14px] text-muted-foreground">Required for all admin users</p>
|
||||
</div>
|
||||
<StatusBadge status="compliant">Enabled</StatusBadge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[16px] leading-[24px]">Session Timeout</p>
|
||||
<p className="text-[14px] text-muted-foreground">Auto logout after 30 minutes idle</p>
|
||||
</div>
|
||||
<StatusBadge status="compliant">Active</StatusBadge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[16px] leading-[24px]">Webhook Signing</p>
|
||||
<p className="text-[14px] text-muted-foreground">HMAC-SHA256 verification</p>
|
||||
</div>
|
||||
<StatusBadge status="compliant">Enabled</StatusBadge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[16px] leading-[24px]">IP Allow List</p>
|
||||
<p className="text-[14px] text-muted-foreground">Restrict admin access by IP</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Configure</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
src/components/admin/pages/ConnectionsPage.tsx
Normal file
61
src/components/admin/pages/ConnectionsPage.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Link2, AlertCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
|
||||
export function ConnectionsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Connections Governance</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage retailer-manufacturer sharing policies and compliance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-12 text-center">
|
||||
<Link2 className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
|
||||
<h3 className="text-[22px] leading-[30px] mb-2">Connections Management</h3>
|
||||
<p className="text-muted-foreground max-w-md mx-auto mb-6">
|
||||
View and manage catalogue sharing connections between manufacturers and retailers
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button>View Connection Graph</Button>
|
||||
<Button variant="outline">Review Violations</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-accent-warn mt-1" />
|
||||
<div>
|
||||
<h4 className="text-[18px] leading-[28px] mb-1">2 Policy Violations</h4>
|
||||
<p className="text-[14px] text-muted-foreground mb-4">
|
||||
Retailers publishing hidden SKUs or pricing below MRP band
|
||||
</p>
|
||||
<Button size="sm" variant="outline">Review & Remediate</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h4 className="text-[18px] leading-[28px] mb-4">Active Connections</h4>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">Total Connections</span>
|
||||
<span className="text-[16px]">124</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">Active Shares</span>
|
||||
<span className="text-[16px]">8,420 SKUs</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">Pending Approvals</span>
|
||||
<span className="text-[16px]">12</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
398
src/components/admin/pages/ContentPage.tsx
Normal file
398
src/components/admin/pages/ContentPage.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import { FileText, Globe, Users, ShoppingBag, Factory } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
|
||||
const platformPages = [
|
||||
{ name: "Terms of Service", platform: "All Platforms", status: "active" as const, lastUpdated: "2024-12-15", version: "v2.1" },
|
||||
{ name: "Privacy Policy", platform: "All Platforms", status: "active" as const, lastUpdated: "2024-12-15", version: "v2.1" },
|
||||
{ name: "DPDP Compliance Notice", platform: "All Platforms", status: "active" as const, lastUpdated: "2025-01-05", version: "v1.3" },
|
||||
{ name: "Refund & Return Policy", platform: "Customer App", status: "active" as const, lastUpdated: "2024-11-20", version: "v1.8" },
|
||||
{ name: "Shipping Policy", platform: "Customer App", status: "active" as const, lastUpdated: "2024-10-12", version: "v1.5" },
|
||||
];
|
||||
|
||||
const helpContent = [
|
||||
{ category: "Getting Started", platform: "Customer App", articles: 12, status: "active" as const },
|
||||
{ category: "Account Management", platform: "All Platforms", articles: 8, status: "active" as const },
|
||||
{ category: "Inventory Setup", platform: "Retailer Portal", articles: 15, status: "active" as const },
|
||||
{ category: "Catalogue Management", platform: "Manufacturer Portal", articles: 18, status: "active" as const },
|
||||
{ category: "Order Processing", platform: "Retailer Portal", articles: 10, status: "active" as const },
|
||||
{ category: "Custom Orders", platform: "Manufacturer Portal", articles: 7, status: "active" as const },
|
||||
{ category: "Analytics & Reports", platform: "All Platforms", articles: 9, status: "active" as const },
|
||||
];
|
||||
|
||||
const emailTemplates = [
|
||||
{ name: "Welcome Email", platform: "All Platforms", status: "active" as const, lastUpdated: "2024-12-01" },
|
||||
{ name: "Order Confirmation", platform: "Customer App", status: "active" as const, lastUpdated: "2024-11-15" },
|
||||
{ name: "Appointment Reminder", platform: "Customer App", status: "active" as const, lastUpdated: "2024-11-28" },
|
||||
{ name: "Quote Request Received", platform: "Retailer Portal", status: "active" as const, lastUpdated: "2024-10-20" },
|
||||
{ name: "Inventory Alert", platform: "Retailer Portal", status: "active" as const, lastUpdated: "2024-12-10" },
|
||||
{ name: "PO Notification", platform: "Manufacturer Portal", status: "active" as const, lastUpdated: "2024-11-05" },
|
||||
];
|
||||
|
||||
const bannerContent = [
|
||||
{ name: "Holiday Season Sale", platform: "Customer App", status: "active" as const, startDate: "2025-01-10", endDate: "2025-01-20" },
|
||||
{ name: "New Collection Launch", platform: "Retailer Portal", status: "active" as const, startDate: "2025-01-08", endDate: "2025-01-15" },
|
||||
{ name: "System Maintenance Notice", platform: "All Platforms", status: "pending" as const, startDate: "2025-01-14", endDate: "2025-01-14" },
|
||||
];
|
||||
|
||||
export function ContentPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Content Management</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage platform content, policies, help documentation, and communications across all portals
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="pages" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="pages">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Legal & Policy Pages
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="help">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Help & Documentation
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="templates">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Email Templates
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="banners">
|
||||
<ShoppingBag className="w-4 h-4 mr-2" />
|
||||
Announcements
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Legal & Policy Pages */}
|
||||
<TabsContent value="pages" className="space-y-6">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Page Title</TableHead>
|
||||
<TableHead>Platform</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Last Updated</TableHead>
|
||||
<TableHead>Version</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{platformPages.map((page, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">{page.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{page.platform === "All Platforms" && <Globe className="w-3 h-3 text-muted-foreground" />}
|
||||
{page.platform === "Customer App" && <ShoppingBag className="w-3 h-3 text-muted-foreground" />}
|
||||
{page.platform === "Retailer Portal" && <Users className="w-3 h-3 text-muted-foreground" />}
|
||||
{page.platform === "Manufacturer Portal" && <Factory className="w-3 h-3 text-muted-foreground" />}
|
||||
<span className="text-[13px] text-muted-foreground">{page.platform}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={page.status}>Published</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{page.lastUpdated}</TableCell>
|
||||
<TableCell className="text-[13px]">{page.version}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">Edit</Button>
|
||||
<Button size="sm" variant="outline">View</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Localization Coverage</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">English (Primary)</span>
|
||||
<span className="text-[14px] text-accent-positive">100%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">हिन्दी (Hindi)</span>
|
||||
<span className="text-[14px] text-accent-warn">85%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">मराठी (Marathi)</span>
|
||||
<span className="text-[14px] text-accent-warn">72%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">ગુજરાતી (Gujarati)</span>
|
||||
<span className="text-[14px] text-accent-error">45%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Content Compliance</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">DPDP Compliant</span>
|
||||
<StatusBadge status="compliant">Yes</StatusBadge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">Last Audit</span>
|
||||
<span className="text-[13px] text-muted-foreground">2025-01-05</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[14px]">Next Review</span>
|
||||
<span className="text-[13px] text-muted-foreground">2025-04-05</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Help & Documentation */}
|
||||
<TabsContent value="help" className="space-y-6">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Platform</TableHead>
|
||||
<TableHead>Articles</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{helpContent.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">{item.category}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.platform === "All Platforms" && <Globe className="w-3 h-3 text-muted-foreground" />}
|
||||
{item.platform === "Customer App" && <ShoppingBag className="w-3 h-3 text-muted-foreground" />}
|
||||
{item.platform === "Retailer Portal" && <Users className="w-3 h-3 text-muted-foreground" />}
|
||||
{item.platform === "Manufacturer Portal" && <Factory className="w-3 h-3 text-muted-foreground" />}
|
||||
<span className="text-[13px] text-muted-foreground">{item.platform}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px]">{item.articles} articles</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status}>Active</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline">Manage</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px]">Documentation Stats</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-[24px] text-primary">79</div>
|
||||
<div className="text-[13px] text-muted-foreground">Total Articles</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[24px] text-accent-positive">95%</div>
|
||||
<div className="text-[13px] text-muted-foreground">Completion</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[24px] text-secondary">3.2k</div>
|
||||
<div className="text-[13px] text-muted-foreground">Monthly Views</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[24px] text-accent-warn">12</div>
|
||||
<div className="text-[13px] text-muted-foreground">Updates This Month</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Email Templates */}
|
||||
<TabsContent value="templates" className="space-y-6">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Template Name</TableHead>
|
||||
<TableHead>Platform</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Last Updated</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{emailTemplates.map((template, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">{template.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{template.platform === "All Platforms" && <Globe className="w-3 h-3 text-muted-foreground" />}
|
||||
{template.platform === "Customer App" && <ShoppingBag className="w-3 h-3 text-muted-foreground" />}
|
||||
{template.platform === "Retailer Portal" && <Users className="w-3 h-3 text-muted-foreground" />}
|
||||
{template.platform === "Manufacturer Portal" && <Factory className="w-3 h-3 text-muted-foreground" />}
|
||||
<span className="text-[13px] text-muted-foreground">{template.platform}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={template.status}>Active</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{template.lastUpdated}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">Edit</Button>
|
||||
<Button size="sm" variant="outline">Preview</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Email Delivery Stats</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-[24px] text-primary">12.4k</div>
|
||||
<div className="text-[13px] text-muted-foreground">Sent This Month</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[24px] text-accent-positive">98.2%</div>
|
||||
<div className="text-[13px] text-muted-foreground">Delivery Rate</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[24px] text-secondary">42%</div>
|
||||
<div className="text-[13px] text-muted-foreground">Open Rate</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[24px] text-accent-warn">8%</div>
|
||||
<div className="text-[13px] text-muted-foreground">Click Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Announcements & Banners */}
|
||||
<TabsContent value="banners" className="space-y-6">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Announcement</TableHead>
|
||||
<TableHead>Platform</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Start Date</TableHead>
|
||||
<TableHead>End Date</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bannerContent.map((banner, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">{banner.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{banner.platform === "All Platforms" && <Globe className="w-3 h-3 text-muted-foreground" />}
|
||||
{banner.platform === "Customer App" && <ShoppingBag className="w-3 h-3 text-muted-foreground" />}
|
||||
{banner.platform === "Retailer Portal" && <Users className="w-3 h-3 text-muted-foreground" />}
|
||||
<span className="text-[13px] text-muted-foreground">{banner.platform}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={banner.status}>
|
||||
{banner.status === "active" ? "Active" : "Scheduled"}
|
||||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{banner.startDate}</TableCell>
|
||||
<TableCell className="text-[13px] text-muted-foreground">{banner.endDate}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">Edit</Button>
|
||||
{banner.status === "active" && (
|
||||
<Button size="sm" variant="outline">Deactivate</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Active Announcements by Platform</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">Customer App</span>
|
||||
</div>
|
||||
<span className="text-[14px]">1 active</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">Retailer Portal</span>
|
||||
</div>
|
||||
<span className="text-[14px]">1 active</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Factory className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">Manufacturer Portal</span>
|
||||
</div>
|
||||
<span className="text-[14px]">0 active</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">All Platforms</span>
|
||||
</div>
|
||||
<span className="text-[14px]">0 active, 1 scheduled</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
250
src/components/admin/pages/ModerationPage.tsx
Normal file
250
src/components/admin/pages/ModerationPage.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useState } from "react";
|
||||
import { ShieldCheck, CheckCircle, XCircle, Clock, AlertTriangle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { StatCard } from "../StatCard";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
const moderationQueue = [
|
||||
{
|
||||
id: "SKU-2025-0847",
|
||||
title: "22K Gold Temple Necklace Set",
|
||||
tenant: "Sparkle Jewels Pvt Ltd",
|
||||
type: "Manufacturer",
|
||||
flags: ["Missing Hallmark", "Price Anomaly"],
|
||||
submitted: "18 hours ago",
|
||||
sla: "30h remaining",
|
||||
slaStatus: "ok" as const,
|
||||
},
|
||||
{
|
||||
id: "SKU-2025-0912",
|
||||
title: "Diamond Stud Earrings 0.5ct",
|
||||
tenant: "Auric Gold Co.",
|
||||
type: "Retailer",
|
||||
flags: ["Bad Image Quality"],
|
||||
submitted: "52 hours ago",
|
||||
sla: "Overdue 4h",
|
||||
slaStatus: "overdue" as const,
|
||||
},
|
||||
{
|
||||
id: "SKU-2025-0723",
|
||||
title: "Silver Oxidized Bangles Set",
|
||||
tenant: "Zephyr Gems LLP",
|
||||
type: "Manufacturer",
|
||||
flags: ["Duplicate SKU"],
|
||||
submitted: "6 hours ago",
|
||||
sla: "42h remaining",
|
||||
slaStatus: "ok" as const,
|
||||
},
|
||||
{
|
||||
id: "SKU-2025-0834",
|
||||
title: "Bridal Polki Choker",
|
||||
tenant: "Nova Jewels",
|
||||
type: "Retailer",
|
||||
flags: ["Missing Fields", "Price Anomaly"],
|
||||
submitted: "48 hours ago",
|
||||
sla: "Critical",
|
||||
slaStatus: "critical" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export function ModerationPage() {
|
||||
const [selectedItem, setSelectedItem] = useState<string | null>(null);
|
||||
|
||||
const pendingCount = moderationQueue.length;
|
||||
const overdueCount = moderationQueue.filter((item) => item.slaStatus !== "ok").length;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Catalogue Oversight</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Review and moderate catalogue submissions from retailers and manufacturers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Pending Review"
|
||||
value={pendingCount}
|
||||
icon={Clock}
|
||||
iconColor="text-accent-warn"
|
||||
/>
|
||||
<StatCard
|
||||
title="Overdue (>48h)"
|
||||
value={overdueCount}
|
||||
icon={AlertTriangle}
|
||||
iconColor="text-accent-error"
|
||||
/>
|
||||
<StatCard
|
||||
title="Avg Resolution Time"
|
||||
value="18h"
|
||||
icon={ShieldCheck}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Approved Today"
|
||||
value="24"
|
||||
icon={CheckCircle}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button variant="outline">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Bulk Approve Selected
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
Bulk Reject Selected
|
||||
</Button>
|
||||
<Button variant="outline">View Auto-Rules</Button>
|
||||
<Button variant="outline">SLA Dashboard</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Moderation Queue */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<input type="checkbox" className="rounded" />
|
||||
</TableHead>
|
||||
<TableHead>SKU / Title</TableHead>
|
||||
<TableHead>Tenant</TableHead>
|
||||
<TableHead>Flags</TableHead>
|
||||
<TableHead>Submitted</TableHead>
|
||||
<TableHead>SLA</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{moderationQueue.map((item) => (
|
||||
<TableRow key={item.id} className={selectedItem === item.id ? "bg-accent/5" : ""}>
|
||||
<TableCell>
|
||||
<input type="checkbox" className="rounded" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-[14px]">{item.id}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{item.title}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p className="text-[14px]">{item.tenant}</p>
|
||||
<Badge variant="outline" className="mt-1 text-[11px]">{item.type}</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.flags.map((flag, index) => (
|
||||
<Badge key={index} variant="outline" className="bg-accent-warn/10 text-accent-warn border-accent-warn/20 text-[11px]">
|
||||
{flag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-[14px]">
|
||||
{item.submitted}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.slaStatus === "ok" ? "active" : item.slaStatus === "overdue" ? "action-needed" : "flagged"}>
|
||||
{item.sla}
|
||||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setSelectedItem(item.id)}
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
<Button size="sm" className="bg-accent-positive hover:bg-accent-positive/90">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="destructive">
|
||||
<XCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{selectedItem && (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[22px] leading-[30px]">Item Details: {selectedItem}</h3>
|
||||
<Button variant="ghost" onClick={() => setSelectedItem(null)}>Close</Button>
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<div className="aspect-square bg-muted rounded-[16px] flex items-center justify-center mb-4">
|
||||
<p className="text-muted-foreground">Product Image Preview</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Button variant="outline" className="w-full">View 360° Gallery</Button>
|
||||
<Button variant="outline" className="w-full">View Certificates</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-[14px] text-muted-foreground mb-1">Specifications</h4>
|
||||
<div className="space-y-2 text-[14px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Metal:</span>
|
||||
<span>Gold 22K</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Weight:</span>
|
||||
<span>45.2g</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">MRP:</span>
|
||||
<span>₹3,42,000</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Making Charges:</span>
|
||||
<span>18%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4 space-y-3">
|
||||
<Button className="w-full bg-accent-positive hover:bg-accent-positive/90">
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Approve & Publish
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Request Changes
|
||||
</Button>
|
||||
<Button variant="destructive" className="w-full">
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
Reject
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
157
src/components/admin/pages/NotificationsPage.tsx
Normal file
157
src/components/admin/pages/NotificationsPage.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Bell, Plus, Mail, MessageSquare, Smartphone } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { StatCard } from "../StatCard";
|
||||
|
||||
const templates = [
|
||||
{ name: "OTP", channel: "SMS", linkedEvents: 1, lastEdited: "2024-12-10", status: "active" as const },
|
||||
{ name: "Quote Ready", channel: "WhatsApp", linkedEvents: 1, lastEdited: "2024-12-28", status: "active" as const },
|
||||
{ name: "Order: Under Making", channel: "Email", linkedEvents: 1, lastEdited: "2025-01-05", status: "active" as const },
|
||||
{ name: "Appointment Confirmed", channel: "Email", linkedEvents: 1, lastEdited: "2024-11-15", status: "active" as const },
|
||||
{ name: "Moderation Result", channel: "Email", linkedEvents: 2, lastEdited: "2024-12-20", status: "active" as const },
|
||||
{ name: "Plan Limit 80%", channel: "Email", linkedEvents: 1, lastEdited: "2024-12-01", status: "active" as const },
|
||||
];
|
||||
|
||||
const deliveryLogs = [
|
||||
{ template: "OTP", channel: "SMS", tenant: "Sparkle Jewels", status: "delivered", timestamp: "2 mins ago" },
|
||||
{ template: "Quote Ready", channel: "WhatsApp", tenant: "Auric Gold", status: "delivered", timestamp: "15 mins ago" },
|
||||
{ template: "Order Update", channel: "Email", tenant: "Nova Jewels", status: "delivered", timestamp: "1 hour ago" },
|
||||
];
|
||||
|
||||
export function NotificationsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Notifications Orchestrator</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage templates, routing rules, and delivery monitoring
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Template
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Active Templates"
|
||||
value={templates.length}
|
||||
icon={Bell}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Sent (24h)"
|
||||
value="2,840"
|
||||
icon={Mail}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Delivery Rate"
|
||||
value="99.2%"
|
||||
icon={MessageSquare}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Avg Response Time"
|
||||
value="1.2s"
|
||||
icon={Smartphone}
|
||||
iconColor="text-secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div className="p-6 border-b border-border">
|
||||
<h3 className="text-[22px] leading-[30px]">Templates</h3>
|
||||
<p className="text-[14px] text-muted-foreground mt-1">
|
||||
Email, SMS, WhatsApp, and Push notification templates
|
||||
</p>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Template Name</TableHead>
|
||||
<TableHead>Channel</TableHead>
|
||||
<TableHead>Linked Events</TableHead>
|
||||
<TableHead>Last Edited</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{templates.map((template, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="text-[14px]">{template.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{template.channel === "SMS" && <MessageSquare className="w-4 h-4 text-muted-foreground" />}
|
||||
{template.channel === "WhatsApp" && <MessageSquare className="w-4 h-4 text-accent-positive" />}
|
||||
{template.channel === "Email" && <Mail className="w-4 h-4 text-muted-foreground" />}
|
||||
<span className="text-[14px]">{template.channel}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{template.linkedEvents} event{template.linkedEvents > 1 ? "s" : ""}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{template.lastEdited}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status="active">Active</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline">Edit</Button>
|
||||
<Button size="sm" variant="outline">Test</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="p-6 border-b border-border">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-[22px] leading-[30px]">Recent Delivery Logs</h3>
|
||||
<p className="text-[14px] text-muted-foreground mt-1">Last 100 notifications</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">View All Logs</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Template</TableHead>
|
||||
<TableHead>Channel</TableHead>
|
||||
<TableHead>Tenant</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Timestamp</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{deliveryLogs.map((log, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="text-[14px]">{log.template}</TableCell>
|
||||
<TableCell>{log.channel}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{log.tenant}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status="compliant">Delivered</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{log.timestamp}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
src/components/admin/pages/OverviewPage.tsx
Normal file
154
src/components/admin/pages/OverviewPage.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { Building2, Users, Package, TrendingUp, ShieldAlert, Clock } from "lucide-react";
|
||||
import { StatCard } from "../StatCard";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
|
||||
interface OverviewPageProps {
|
||||
onNavigate: (page: string) => void;
|
||||
}
|
||||
|
||||
const quickActions = [
|
||||
{ id: "users-retailers", label: "Manage Retailers", description: "View and manage retailer accounts" },
|
||||
{ id: "users-manufacturers", label: "Manage Manufacturers", description: "View and manage manufacturer accounts" },
|
||||
{ id: "users-customers", label: "Manage Customers", description: "View and manage customer accounts" },
|
||||
{ id: "content", label: "Content Management", description: "Manage static pages and policies" },
|
||||
{ id: "analytics", label: "Platform Analytics", description: "View usage and growth metrics" },
|
||||
{ id: "settings", label: "System Settings", description: "Configure platform settings" },
|
||||
];
|
||||
|
||||
const recentActivity = [
|
||||
{ event: "New retailer registered", tenant: "Zephyr Gems LLP", time: "2 hours ago", status: "active" as const },
|
||||
{ event: "Manufacturer verified", tenant: "Auric Foundry", time: "5 hours ago", status: "active" as const },
|
||||
{ event: "Customer account created", tenant: "John Doe", time: "1 day ago", status: "active" as const },
|
||||
{ event: "Content page updated", tenant: "Privacy Policy", time: "2 days ago", status: "compliant" as const },
|
||||
];
|
||||
|
||||
const systemAlerts = [
|
||||
{ message: "5 new retailer registrations pending review", severity: "warning", action: "Review Users" },
|
||||
{ message: "Platform content updates available", severity: "warning", action: "View Content" },
|
||||
{ message: "Weekly analytics report ready", severity: "info", action: "View Report" },
|
||||
];
|
||||
|
||||
export function OverviewPage({ onNavigate }: OverviewPageProps) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Admin Console</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Welcome back! Here's what's happening across your platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Total Retailers"
|
||||
value="142"
|
||||
icon={Building2}
|
||||
trend={{ value: "+8% this month", positive: true }}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Manufacturers"
|
||||
value="68"
|
||||
icon={Package}
|
||||
trend={{ value: "+12% this month", positive: true }}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Customers"
|
||||
value="1,284"
|
||||
icon={Users}
|
||||
trend={{ value: "+18% this month", positive: true }}
|
||||
iconColor="text-secondary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Platform Growth"
|
||||
value="+15%"
|
||||
icon={TrendingUp}
|
||||
trend={{ value: "vs last month", positive: true }}
|
||||
iconColor="text-accent-warn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* System Alerts */}
|
||||
{systemAlerts.length > 0 && (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[22px] leading-[30px]">System Alerts</h2>
|
||||
<StatusBadge status="action-needed">{systemAlerts.length} Active</StatusBadge>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{systemAlerts.map((alert, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center justify-between p-4 rounded-[16px] border ${
|
||||
alert.severity === "error"
|
||||
? "border-accent-error/20 bg-accent-error/5"
|
||||
: "border-accent-warn/20 bg-accent-warn/5"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<ShieldAlert
|
||||
className={`w-5 h-5 ${
|
||||
alert.severity === "error" ? "text-accent-error" : "text-accent-warn"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-[14px]">{alert.message}</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
{alert.action}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Quick Actions Grid */}
|
||||
<div>
|
||||
<h2 className="text-[22px] leading-[30px] mb-4">Quick Actions</h2>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{quickActions.map((action) => (
|
||||
<Card
|
||||
key={action.id}
|
||||
className="p-6 hover:shadow-lg transition-shadow cursor-pointer"
|
||||
onClick={() => onNavigate(action.id)}
|
||||
>
|
||||
<h3 className="text-[18px] leading-[28px] mb-1">{action.label}</h3>
|
||||
<p className="text-[14px] text-muted-foreground">{action.description}</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[22px] leading-[30px]">Recent Activity</h2>
|
||||
<Button variant="ghost" size="sm">View All</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{recentActivity.map((activity, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-4 rounded-[16px] border border-border">
|
||||
<div className="flex items-center gap-4">
|
||||
<Clock className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-[14px]">{activity.event}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{activity.tenant}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusBadge status={activity.status}>
|
||||
{activity.status.charAt(0).toUpperCase() + activity.status.slice(1)}
|
||||
</StatusBadge>
|
||||
<span className="text-[12px] text-muted-foreground">{activity.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
676
src/components/admin/pages/SettingsPage.tsx
Normal file
676
src/components/admin/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,676 @@
|
||||
import {
|
||||
Settings,
|
||||
Globe,
|
||||
Shield,
|
||||
Bell,
|
||||
Palette,
|
||||
Database,
|
||||
Mail,
|
||||
Server,
|
||||
Key,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Moon,
|
||||
Sun
|
||||
} from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Separator } from "../../ui/separator";
|
||||
import { Switch } from "../../ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Textarea } from "../../ui/textarea";
|
||||
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Settings</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Configure platform settings, security, and system preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="general" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="general">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
General
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="appearance">
|
||||
<Palette className="w-4 h-4 mr-2" />
|
||||
Appearance
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="security">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
Security
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notifications">
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
Notifications
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="integrations">
|
||||
<Server className="w-4 h-4 mr-2" />
|
||||
Integrations
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="advanced">
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
Advanced
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* General Settings */}
|
||||
<TabsContent value="general" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Platform Information</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="platform-name">Platform Name</Label>
|
||||
<Input id="platform-name" defaultValue="Hello Jewellers" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="support-email">Support Email</Label>
|
||||
<Input id="support-email" defaultValue="support@hellojewellers.com" type="email" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="platform-description">Platform Description</Label>
|
||||
<Textarea
|
||||
id="platform-description"
|
||||
defaultValue="A unified jewellery ecosystem connecting manufacturers, retailers, and customers."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Regional Settings</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="default-language">Default Language</Label>
|
||||
<Select defaultValue="en">
|
||||
<SelectTrigger id="default-language">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="hi">हिन्दी (Hindi)</SelectItem>
|
||||
<SelectItem value="mr">मराठी (Marathi)</SelectItem>
|
||||
<SelectItem value="gu">ગુજરાતી (Gujarati)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="timezone">Default Timezone</Label>
|
||||
<Select defaultValue="ist">
|
||||
<SelectTrigger id="timezone">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ist">IST (UTC+5:30)</SelectItem>
|
||||
<SelectItem value="pst">PST (UTC-8:00)</SelectItem>
|
||||
<SelectItem value="est">EST (UTC-5:00)</SelectItem>
|
||||
<SelectItem value="gmt">GMT (UTC+0:00)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currency">Default Currency</Label>
|
||||
<Select defaultValue="inr">
|
||||
<SelectTrigger id="currency">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="inr">INR (₹)</SelectItem>
|
||||
<SelectItem value="usd">USD ($)</SelectItem>
|
||||
<SelectItem value="eur">EUR (€)</SelectItem>
|
||||
<SelectItem value="gbp">GBP (£)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="date-format">Date Format</Label>
|
||||
<Select defaultValue="dd-mm-yyyy">
|
||||
<SelectTrigger id="date-format">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="dd-mm-yyyy">DD/MM/YYYY</SelectItem>
|
||||
<SelectItem value="mm-dd-yyyy">MM/DD/YYYY</SelectItem>
|
||||
<SelectItem value="yyyy-mm-dd">YYYY-MM-DD</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Business Hours</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Platform Support Hours</div>
|
||||
<div className="text-[13px] text-muted-foreground">Monday - Saturday: 9:00 AM - 6:00 PM IST</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Edit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save General Settings</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Appearance Settings */}
|
||||
<TabsContent value="appearance" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Theme</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Color Scheme</div>
|
||||
<div className="text-[13px] text-muted-foreground">Choose between light and dark mode</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Sun className="w-4 h-4" />
|
||||
Light
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Moon className="w-4 h-4" />
|
||||
Dark
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<Label>Brand Colors</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="primary-color" className="text-[13px]">Primary</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input id="primary-color" defaultValue="#5B8DEF" className="flex-1" />
|
||||
<div className="w-10 h-10 rounded-lg border border-border" style={{ backgroundColor: "#5B8DEF" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="secondary-color" className="text-[13px]">Secondary</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input id="secondary-color" defaultValue="#F4B400" className="flex-1" />
|
||||
<div className="w-10 h-10 rounded-lg border border-border" style={{ backgroundColor: "#F4B400" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="success-color" className="text-[13px]">Success</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input id="success-color" defaultValue="#10B981" className="flex-1" />
|
||||
<div className="w-10 h-10 rounded-lg border border-border" style={{ backgroundColor: "#10B981" }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="error-color" className="text-[13px]">Error</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input id="error-color" defaultValue="#EF4444" className="flex-1" />
|
||||
<div className="w-10 h-10 rounded-lg border border-border" style={{ backgroundColor: "#EF4444" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Logo & Branding</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-lg bg-primary flex items-center justify-center">
|
||||
<span className="text-primary-foreground text-[24px]">H</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Platform Logo</div>
|
||||
<div className="text-[13px] text-muted-foreground">SVG, PNG recommended (max 2MB)</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Upload New</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Display Settings</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Compact Mode</div>
|
||||
<div className="text-[13px] text-muted-foreground">Reduce spacing for more content on screen</div>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Show Animations</div>
|
||||
<div className="text-[13px] text-muted-foreground">Enable smooth transitions and animations</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Appearance Settings</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Security Settings */}
|
||||
<TabsContent value="security" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Authentication</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Require Two-Factor Authentication</div>
|
||||
<div className="text-[13px] text-muted-foreground">Enforce 2FA for all admin users</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Session Timeout</div>
|
||||
<div className="text-[13px] text-muted-foreground">Auto-logout after inactivity</div>
|
||||
</div>
|
||||
<Select defaultValue="30">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="15">15 minutes</SelectItem>
|
||||
<SelectItem value="30">30 minutes</SelectItem>
|
||||
<SelectItem value="60">1 hour</SelectItem>
|
||||
<SelectItem value="120">2 hours</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Password Policy</div>
|
||||
<div className="text-[13px] text-muted-foreground">Minimum 8 characters, 1 uppercase, 1 number</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Configure</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">API Security</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Key className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">API Keys</div>
|
||||
<div className="text-[13px] text-muted-foreground">Manage platform API access keys</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Manage Keys</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Rate Limiting</div>
|
||||
<div className="text-[13px] text-muted-foreground">Limit API requests per minute</div>
|
||||
</div>
|
||||
<Input type="number" defaultValue="100" className="w-[120px]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Data Protection</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">DPDP Compliance Mode</div>
|
||||
<div className="text-[13px] text-muted-foreground">Enable enhanced data protection features</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-accent-positive" />
|
||||
<span className="text-[13px] text-accent-positive">Enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Data Retention Period</div>
|
||||
<div className="text-[13px] text-muted-foreground">How long to keep inactive user data</div>
|
||||
</div>
|
||||
<Select defaultValue="365">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="90">90 days</SelectItem>
|
||||
<SelectItem value="180">180 days</SelectItem>
|
||||
<SelectItem value="365">1 year</SelectItem>
|
||||
<SelectItem value="730">2 years</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Automatic Backups</div>
|
||||
<div className="text-[13px] text-muted-foreground">Schedule daily encrypted backups</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Security Settings</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Notifications Settings */}
|
||||
<TabsContent value="notifications" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Email Notifications</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">New User Registrations</div>
|
||||
<div className="text-[13px] text-muted-foreground">Notify when a new user signs up</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">System Alerts</div>
|
||||
<div className="text-[13px] text-muted-foreground">Critical platform errors and warnings</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Weekly Reports</div>
|
||||
<div className="text-[13px] text-muted-foreground">Analytics and activity summaries</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Security Alerts</div>
|
||||
<div className="text-[13px] text-muted-foreground">Failed login attempts and suspicious activity</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Email Configuration</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtp-host">SMTP Host</Label>
|
||||
<Input id="smtp-host" defaultValue="smtp.hellojewellers.com" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtp-port">SMTP Port</Label>
|
||||
<Input id="smtp-port" defaultValue="587" type="number" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="from-email">From Email</Label>
|
||||
<Input id="from-email" defaultValue="noreply@hellojewellers.com" type="email" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="from-name">From Name</Label>
|
||||
<Input id="from-name" defaultValue="Hello Jewellers" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">Test Configuration</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Push Notifications</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Browser Notifications</div>
|
||||
<div className="text-[13px] text-muted-foreground">Show desktop notifications</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Mobile Push</div>
|
||||
<div className="text-[13px] text-muted-foreground">Send push to mobile apps</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Notification Settings</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Integrations */}
|
||||
<TabsContent value="integrations" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Third-Party Services</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<Mail className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Email Service (SendGrid)</div>
|
||||
<div className="text-[13px] text-muted-foreground flex items-center gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-accent-positive" />
|
||||
Connected
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Configure</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center">
|
||||
<Server className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">SMS Gateway (Twilio)</div>
|
||||
<div className="text-[13px] text-muted-foreground flex items-center gap-2">
|
||||
<CheckCircle2 className="w-3 h-3 text-accent-positive" />
|
||||
Connected
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Configure</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent-warn/10 flex items-center justify-center">
|
||||
<Database className="w-5 h-5 text-accent-warn" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Analytics (Google Analytics)</div>
|
||||
<div className="text-[13px] text-muted-foreground flex items-center gap-2">
|
||||
<AlertCircle className="w-3 h-3 text-muted-foreground" />
|
||||
Not Connected
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Connect</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Webhooks</h3>
|
||||
<div className="space-y-4">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Configure webhooks to receive real-time notifications about platform events
|
||||
</p>
|
||||
<Button variant="outline">
|
||||
Add Webhook
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Payment Gateways</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Razorpay</div>
|
||||
<div className="text-[13px] text-muted-foreground">Primary payment gateway</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-accent-positive" />
|
||||
<span className="text-[13px]">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Integration Settings</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Advanced Settings */}
|
||||
<TabsContent value="advanced" className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">System Maintenance</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-5 h-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Maintenance Mode</div>
|
||||
<div className="text-[13px] text-muted-foreground">Temporarily disable platform access</div>
|
||||
</div>
|
||||
</div>
|
||||
<Switch />
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Clear Cache</div>
|
||||
<div className="text-[13px] text-muted-foreground">Clear application cache and temporary files</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Clear Now</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Database</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Database Backup</div>
|
||||
<div className="text-[13px] text-muted-foreground">Last backup: 2 hours ago</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Backup Now</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-border rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Database Size</div>
|
||||
<div className="text-[13px] text-muted-foreground">Current: 2.4 GB / Limit: 10 GB</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">Optimize</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-6">Audit Logs</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Enable Audit Logging</div>
|
||||
<div className="text-[13px] text-muted-foreground">Track all admin actions</div>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Log Retention</div>
|
||||
<div className="text-[13px] text-muted-foreground">How long to keep audit logs</div>
|
||||
</div>
|
||||
<Select defaultValue="90">
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="30">30 days</SelectItem>
|
||||
<SelectItem value="90">90 days</SelectItem>
|
||||
<SelectItem value="180">180 days</SelectItem>
|
||||
<SelectItem value="365">1 year</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<Button variant="outline">View Audit Logs</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-destructive/50 bg-destructive/5">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4 text-destructive flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
Danger Zone
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border border-destructive/20 rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Reset Platform Settings</div>
|
||||
<div className="text-[13px] text-muted-foreground">Restore all settings to default values</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="border-destructive/50 text-destructive hover:bg-destructive/10">
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-4 border border-destructive/20 rounded-lg">
|
||||
<div>
|
||||
<div className="text-[14px] font-medium mb-1">Export Platform Data</div>
|
||||
<div className="text-[13px] text-muted-foreground">Download complete platform data archive</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button>Save Advanced Settings</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
239
src/components/admin/pages/TenantsPage.tsx
Normal file
239
src/components/admin/pages/TenantsPage.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useState } from "react";
|
||||
import { Building2, Plus, MoreVertical, Users, Package, TrendingUp } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "../../ui/dropdown-menu";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { StatCard } from "../StatCard";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../ui/select";
|
||||
|
||||
const tenantsData = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Sparkle Jewels Pvt Ltd",
|
||||
status: "active" as const,
|
||||
plan: "Enterprise",
|
||||
seats: { used: 24, total: 50 },
|
||||
retailers: 42,
|
||||
manufacturers: 8,
|
||||
skus: 12840,
|
||||
created: "2024-01-15",
|
||||
lastActivity: "2 hours ago",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Auric Gold Co.",
|
||||
status: "active" as const,
|
||||
plan: "Pro",
|
||||
seats: { used: 12, total: 20 },
|
||||
retailers: 18,
|
||||
manufacturers: 5,
|
||||
skus: 8420,
|
||||
created: "2024-03-22",
|
||||
lastActivity: "1 day ago",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Zephyr Gems LLP",
|
||||
status: "trial" as const,
|
||||
plan: "Trial",
|
||||
seats: { used: 3, total: 5 },
|
||||
retailers: 5,
|
||||
manufacturers: 2,
|
||||
skus: 1240,
|
||||
created: "2025-01-10",
|
||||
lastActivity: "5 hours ago",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Nova Jewels",
|
||||
status: "active" as const,
|
||||
plan: "Starter",
|
||||
seats: { used: 6, total: 10 },
|
||||
retailers: 12,
|
||||
manufacturers: 3,
|
||||
skus: 4280,
|
||||
created: "2024-06-18",
|
||||
lastActivity: "3 hours ago",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Radiant Designs",
|
||||
status: "over-limit" as const,
|
||||
plan: "Pro",
|
||||
seats: { used: 22, total: 20 },
|
||||
retailers: 28,
|
||||
manufacturers: 6,
|
||||
skus: 9840,
|
||||
created: "2024-02-10",
|
||||
lastActivity: "6 hours ago",
|
||||
},
|
||||
];
|
||||
|
||||
export function TenantsPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
|
||||
const filteredTenants = tenantsData.filter((tenant) => {
|
||||
const matchesSearch = tenant.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesStatus = statusFilter === "all" || tenant.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Tenants & Plans</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage tenant organizations, plans, and billing
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Tenant
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Total Tenants"
|
||||
value={tenantsData.length}
|
||||
icon={Building2}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Active Plans"
|
||||
value={tenantsData.filter((t) => t.status === "active").length}
|
||||
icon={TrendingUp}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Retailers"
|
||||
value={tenantsData.reduce((sum, t) => sum + t.retailers, 0)}
|
||||
icon={Users}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Total SKUs"
|
||||
value={tenantsData.reduce((sum, t) => sum + t.skus, 0).toLocaleString()}
|
||||
icon={Package}
|
||||
iconColor="text-secondary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Search tenants..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="trial">Trial</SelectItem>
|
||||
<SelectItem value="suspended">Suspended</SelectItem>
|
||||
<SelectItem value="over-limit">Over Limit</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tenants Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Tenant</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead>Seats</TableHead>
|
||||
<TableHead>Retailers</TableHead>
|
||||
<TableHead>SKUs</TableHead>
|
||||
<TableHead>Last Activity</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredTenants.map((tenant) => (
|
||||
<TableRow key={tenant.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-[10px] bg-primary/10 flex items-center justify-center">
|
||||
<Building2 className="w-4 h-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">{tenant.name}</p>
|
||||
<p className="text-[12px] text-muted-foreground">Created {tenant.created}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={tenant.status}>
|
||||
{tenant.status === "over-limit" ? "Over Limit" : tenant.status.charAt(0).toUpperCase() + tenant.status.slice(1)}
|
||||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell>{tenant.plan}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={tenant.seats.used > tenant.seats.total ? "text-accent-error" : ""}>
|
||||
{tenant.seats.used}/{tenant.seats.total}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{tenant.retailers}</TableCell>
|
||||
<TableCell>{tenant.skus.toLocaleString()}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{tenant.lastActivity}</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>View Details</DropdownMenuItem>
|
||||
<DropdownMenuItem>Edit Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Change Plan</DropdownMenuItem>
|
||||
<DropdownMenuItem>View Activity Log</DropdownMenuItem>
|
||||
<DropdownMenuItem className="text-destructive">Suspend Tenant</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
339
src/components/admin/pages/UsersManufacturersPage.tsx
Normal file
339
src/components/admin/pages/UsersManufacturersPage.tsx
Normal file
@@ -0,0 +1,339 @@
|
||||
import { useState } from "react";
|
||||
import { Factory, Search, Filter, Plus, Eye, Mail, Phone, MapPin, Package } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../../ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import { Separator } from "../../ui/separator";
|
||||
|
||||
const manufacturers = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Auric Foundry Pvt Ltd",
|
||||
email: "contact@auricfoundry.com",
|
||||
phone: "+91-98XXXXXXXX",
|
||||
location: "Mumbai, Maharashtra",
|
||||
kycStatus: "Verified",
|
||||
status: "Active",
|
||||
skus: 248,
|
||||
collections: 12,
|
||||
retailers: 18,
|
||||
joined: "Dec 10, 2024",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Zephyr Casting Co.",
|
||||
email: "info@zephyrcasting.com",
|
||||
phone: "+91-98XXXXXXXX",
|
||||
location: "Surat, Gujarat",
|
||||
kycStatus: "Verified",
|
||||
status: "Active",
|
||||
skus: 186,
|
||||
collections: 8,
|
||||
retailers: 12,
|
||||
joined: "Jan 5, 2025",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "BlueRay Gems",
|
||||
email: "hello@blueraygems.com",
|
||||
phone: "+91-98XXXXXXXX",
|
||||
location: "Jaipur, Rajasthan",
|
||||
kycStatus: "Pending",
|
||||
status: "Pending",
|
||||
skus: 64,
|
||||
collections: 4,
|
||||
retailers: 0,
|
||||
joined: "Feb 18, 2025",
|
||||
},
|
||||
];
|
||||
|
||||
export function UsersManufacturersPage() {
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState<typeof manufacturers[0] | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
|
||||
const handleViewDetail = (manufacturer: typeof manufacturers[0]) => {
|
||||
setSelectedManufacturer(manufacturer);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Manufacturers</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage manufacturer accounts and monitor their catalog
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Manufacturer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Total Manufacturers</p>
|
||||
<p className="text-[24px] leading-[32px]">18</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Verified</p>
|
||||
<p className="text-[24px] leading-[32px] text-status-active">14</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Pending KYC</p>
|
||||
<p className="text-[24px] leading-[32px] text-status-pending">4</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Total SKUs</p>
|
||||
<p className="text-[24px] leading-[32px]">3,248</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||
<Input placeholder="Search manufacturers..." className="pl-10" />
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Manufacturer</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead>KYC Status</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>SKUs</TableHead>
|
||||
<TableHead>Collections</TableHead>
|
||||
<TableHead>Retailers</TableHead>
|
||||
<TableHead>Joined</TableHead>
|
||||
<TableHead className="w-24"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{manufacturers.map((manufacturer) => (
|
||||
<TableRow key={manufacturer.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[10px] bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Factory className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">{manufacturer.name}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{manufacturer.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{manufacturer.location}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={manufacturer.kycStatus === "Verified" ? "default" : "outline"}
|
||||
className={
|
||||
manufacturer.kycStatus === "Verified"
|
||||
? "bg-status-active"
|
||||
: "bg-status-pending"
|
||||
}
|
||||
>
|
||||
{manufacturer.kycStatus}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
manufacturer.status === "Active"
|
||||
? "bg-status-active"
|
||||
: "bg-status-pending"
|
||||
}
|
||||
>
|
||||
{manufacturer.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{manufacturer.skus}</TableCell>
|
||||
<TableCell>{manufacturer.collections}</TableCell>
|
||||
<TableCell>{manufacturer.retailers}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{manufacturer.joined}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleViewDetail(manufacturer)}>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* Detail Dialog */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedManufacturer?.name}</DialogTitle>
|
||||
<DialogDescription>Manufacturer account details and catalog</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedManufacturer && (
|
||||
<Tabs defaultValue="overview" className="mt-4">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="catalog">Catalog</TabsTrigger>
|
||||
<TabsTrigger value="connections">Connections</TabsTrigger>
|
||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Mail className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Email</span>
|
||||
</div>
|
||||
<p className="text-[14px]">{selectedManufacturer.email}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Phone className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Phone</span>
|
||||
</div>
|
||||
<p className="text-[14px]">{selectedManufacturer.phone}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<MapPin className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Location</span>
|
||||
</div>
|
||||
<p className="text-[14px]">{selectedManufacturer.location}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Package className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">KYC Status</span>
|
||||
</div>
|
||||
<Badge
|
||||
className={
|
||||
selectedManufacturer.kycStatus === "Verified"
|
||||
? "bg-status-active"
|
||||
: "bg-status-pending"
|
||||
}
|
||||
>
|
||||
{selectedManufacturer.kycStatus}
|
||||
</Badge>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground mb-1">SKUs</p>
|
||||
<p className="text-[24px] leading-[32px]">{selectedManufacturer.skus}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Collections</p>
|
||||
<p className="text-[24px] leading-[32px]">{selectedManufacturer.collections}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Retailers</p>
|
||||
<p className="text-[24px] leading-[32px]">{selectedManufacturer.retailers}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1">Send Email</Button>
|
||||
<Button variant="outline" className="flex-1">View Catalog</Button>
|
||||
<Button variant="outline" className="flex-1">Manage KYC</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="catalog">
|
||||
<div className="space-y-3">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Bridal '25 Collection</p>
|
||||
<p className="text-[12px] text-muted-foreground">128 SKUs</p>
|
||||
</div>
|
||||
<Badge>Published</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Daily Edit Collection</p>
|
||||
<p className="text-[12px] text-muted-foreground">76 SKUs</p>
|
||||
</div>
|
||||
<Badge>Published</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="connections">
|
||||
<div className="space-y-3">
|
||||
<Card className="p-4">
|
||||
<p className="text-[14px] mb-1">Nova Jewels</p>
|
||||
<p className="text-[12px] text-muted-foreground">3 collections shared</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[14px] mb-1">Zephyr Gems</p>
|
||||
<p className="text-[12px] text-muted-foreground">2 collections shared</p>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="activity">
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 border-l-2 border-primary">
|
||||
<p className="text-[14px] mb-1">Shared collection with Nova Jewels</p>
|
||||
<p className="text-[12px] text-muted-foreground">3 hours ago</p>
|
||||
</div>
|
||||
<div className="p-3 border-l-2 border-border">
|
||||
<p className="text-[14px] mb-1">Added 48 new SKUs</p>
|
||||
<p className="text-[12px] text-muted-foreground">1 day ago</p>
|
||||
</div>
|
||||
<div className="p-3 border-l-2 border-border">
|
||||
<p className="text-[14px] mb-1">Updated pricing rules</p>
|
||||
<p className="text-[12px] text-muted-foreground">2 days ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
297
src/components/admin/pages/UsersPage.tsx
Normal file
297
src/components/admin/pages/UsersPage.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useState } from "react";
|
||||
import { Users, Plus, Search, Shield } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import { RoleBadge } from "../RoleBadge";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
import { StatCard } from "../StatCard";
|
||||
import { Avatar, AvatarFallback } from "../../ui/avatar";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
|
||||
const usersData = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Rajesh Kumar",
|
||||
email: "rajesh@sparklejewels.com",
|
||||
role: "admin" as const,
|
||||
tenant: "Sparkle Jewels Pvt Ltd",
|
||||
status: "active" as const,
|
||||
lastLogin: "2 hours ago",
|
||||
mfa: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Priya Sharma",
|
||||
email: "priya@auricgold.com",
|
||||
role: "retailer-admin" as const,
|
||||
tenant: "Auric Gold Co.",
|
||||
status: "active" as const,
|
||||
lastLogin: "1 day ago",
|
||||
mfa: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Amit Patel",
|
||||
email: "amit@zephyrgems.com",
|
||||
role: "manufacturer-admin" as const,
|
||||
tenant: "Zephyr Gems LLP",
|
||||
status: "active" as const,
|
||||
lastLogin: "5 hours ago",
|
||||
mfa: false,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Sarah Johnson",
|
||||
email: "sarah@example.com",
|
||||
role: "customer" as const,
|
||||
tenant: "—",
|
||||
status: "active" as const,
|
||||
lastLogin: "3 hours ago",
|
||||
mfa: false,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Dev Mehta",
|
||||
email: "dev@novajewels.com",
|
||||
role: "retail-associate" as const,
|
||||
tenant: "Nova Jewels",
|
||||
status: "active" as const,
|
||||
lastLogin: "6 hours ago",
|
||||
mfa: true,
|
||||
},
|
||||
];
|
||||
|
||||
const rolesMatrix = [
|
||||
{
|
||||
capability: "Manage Tenants",
|
||||
admin: true,
|
||||
retailerAdmin: false,
|
||||
manufacturerAdmin: false,
|
||||
retailAssociate: false,
|
||||
auditor: false,
|
||||
},
|
||||
{
|
||||
capability: "Invite Users",
|
||||
admin: true,
|
||||
retailerAdmin: true,
|
||||
manufacturerAdmin: true,
|
||||
retailAssociate: false,
|
||||
auditor: false,
|
||||
},
|
||||
{
|
||||
capability: "Catalogue Moderation",
|
||||
admin: true,
|
||||
retailerAdmin: false,
|
||||
manufacturerAdmin: false,
|
||||
retailAssociate: false,
|
||||
auditor: false,
|
||||
},
|
||||
{
|
||||
capability: "View Analytics",
|
||||
admin: true,
|
||||
retailerAdmin: true,
|
||||
manufacturerAdmin: true,
|
||||
retailAssociate: false,
|
||||
auditor: true,
|
||||
},
|
||||
{
|
||||
capability: "Manage Compliance",
|
||||
admin: true,
|
||||
retailerAdmin: false,
|
||||
manufacturerAdmin: false,
|
||||
retailAssociate: false,
|
||||
auditor: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function UsersPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredUsers = usersData.filter((user) =>
|
||||
user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[36px] leading-[44px] mb-2">Users & Roles</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage users, roles, and permissions across all tenants
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Invite Users
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="Total Users"
|
||||
value={usersData.length}
|
||||
icon={Users}
|
||||
iconColor="text-primary"
|
||||
/>
|
||||
<StatCard
|
||||
title="Admins"
|
||||
value={usersData.filter((u) => u.role === "admin").length}
|
||||
icon={Shield}
|
||||
iconColor="text-role-admin"
|
||||
/>
|
||||
<StatCard
|
||||
title="MFA Enabled"
|
||||
value={`${usersData.filter((u) => u.mfa).length}/${usersData.length}`}
|
||||
icon={Shield}
|
||||
iconColor="text-accent-positive"
|
||||
/>
|
||||
<StatCard
|
||||
title="Pending Invites"
|
||||
value="4"
|
||||
icon={Users}
|
||||
iconColor="text-accent-warn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="users" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="users">User Directory</TabsTrigger>
|
||||
<TabsTrigger value="roles">Roles Matrix</TabsTrigger>
|
||||
<TabsTrigger value="invites">Pending Invites</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users" className="space-y-4">
|
||||
{/* Search */}
|
||||
<Card className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search users by name or email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Users Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Tenant</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Last Login</TableHead>
|
||||
<TableHead>MFA</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarFallback>
|
||||
{user.name.split(" ").map((n) => n[0]).join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="text-[14px]">{user.name}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<RoleBadge role={user.role}>
|
||||
{user.role === "retailer-admin" ? "Retailer Admin" :
|
||||
user.role === "retail-associate" ? "Retail Associate" :
|
||||
user.role === "manufacturer-admin" ? "Manufacturer Admin" :
|
||||
user.role.charAt(0).toUpperCase() + user.role.slice(1)}
|
||||
</RoleBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{user.tenant}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={user.status}>Active</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{user.lastLogin}</TableCell>
|
||||
<TableCell>
|
||||
{user.mfa ? (
|
||||
<StatusBadge status="compliant">Enabled</StatusBadge>
|
||||
) : (
|
||||
<StatusBadge status="action-needed">Disabled</StatusBadge>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="roles" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[22px] leading-[30px] mb-6">Permissions Matrix</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[250px]">Capability</TableHead>
|
||||
<TableHead className="text-center">Admin</TableHead>
|
||||
<TableHead className="text-center">Retailer Admin</TableHead>
|
||||
<TableHead className="text-center">Manufacturer Admin</TableHead>
|
||||
<TableHead className="text-center">Retail Associate</TableHead>
|
||||
<TableHead className="text-center">Auditor</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rolesMatrix.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{row.capability}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{row.admin ? "✓" : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{row.retailerAdmin ? "✓" : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{row.manufacturerAdmin ? "✓" : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{row.retailAssociate ? "✓" : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{row.auditor ? "✓" : "—"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="invites" className="space-y-4">
|
||||
<Card className="p-12 text-center">
|
||||
<p className="text-muted-foreground">4 pending invites</p>
|
||||
<p className="text-[14px] text-muted-foreground mt-2">
|
||||
Invite management coming soon
|
||||
</p>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
324
src/components/admin/pages/UsersRetailersPage.tsx
Normal file
324
src/components/admin/pages/UsersRetailersPage.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import { useState } from "react";
|
||||
import { Store, Search, Filter, Plus, Eye, Mail, Phone, MapPin } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../../ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import { Separator } from "../../ui/separator";
|
||||
|
||||
const retailers = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Nova Jewels",
|
||||
email: "contact@novajewels.com",
|
||||
phone: "+91-98XXXXXXXX",
|
||||
location: "Mumbai, Maharashtra",
|
||||
plan: "Premium",
|
||||
status: "Active",
|
||||
stores: 2,
|
||||
users: 8,
|
||||
collections: 12,
|
||||
joined: "Jan 15, 2025",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Zephyr Gems Boutique",
|
||||
email: "info@zephyrgems.com",
|
||||
phone: "+91-98XXXXXXXX",
|
||||
location: "Delhi, NCR",
|
||||
plan: "Professional",
|
||||
status: "Active",
|
||||
stores: 1,
|
||||
users: 5,
|
||||
collections: 8,
|
||||
joined: "Feb 3, 2025",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "BlueLeaf Jewels",
|
||||
email: "hello@blueleaf.com",
|
||||
phone: "+91-98XXXXXXXX",
|
||||
location: "Bengaluru, Karnataka",
|
||||
plan: "Starter",
|
||||
status: "Trial",
|
||||
stores: 1,
|
||||
users: 3,
|
||||
collections: 4,
|
||||
joined: "Feb 20, 2025",
|
||||
},
|
||||
];
|
||||
|
||||
export function UsersRetailersPage() {
|
||||
const [selectedRetailer, setSelectedRetailer] = useState<typeof retailers[0] | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
|
||||
const handleViewDetail = (retailer: typeof retailers[0]) => {
|
||||
setSelectedRetailer(retailer);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Retailers</h1>
|
||||
<p className="text-[16px] text-muted-foreground">
|
||||
Manage retailer accounts and monitor their activity
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Retailer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Total Retailers</p>
|
||||
<p className="text-[24px] leading-[32px]">24</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Active</p>
|
||||
<p className="text-[24px] leading-[32px] text-status-active">18</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Trial</p>
|
||||
<p className="text-[24px] leading-[32px] text-status-trial">4</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Total Stores</p>
|
||||
<p className="text-[24px] leading-[32px]">32</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||
<Input placeholder="Search retailers..." className="pl-10" />
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Retailer</TableHead>
|
||||
<TableHead>Location</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Stores</TableHead>
|
||||
<TableHead>Users</TableHead>
|
||||
<TableHead>Collections</TableHead>
|
||||
<TableHead>Joined</TableHead>
|
||||
<TableHead className="w-24"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{retailers.map((retailer) => (
|
||||
<TableRow key={retailer.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[10px] bg-primary/10 flex items-center justify-center">
|
||||
<Store className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">{retailer.name}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{retailer.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{retailer.location}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{retailer.plan}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
className={
|
||||
retailer.status === "Active"
|
||||
? "bg-status-active"
|
||||
: retailer.status === "Trial"
|
||||
? "bg-status-trial"
|
||||
: "bg-status-suspended"
|
||||
}
|
||||
>
|
||||
{retailer.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{retailer.stores}</TableCell>
|
||||
<TableCell>{retailer.users}</TableCell>
|
||||
<TableCell>{retailer.collections}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{retailer.joined}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleViewDetail(retailer)}>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* Detail Dialog */}
|
||||
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{selectedRetailer?.name}</DialogTitle>
|
||||
<DialogDescription>Retailer account details and activity</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedRetailer && (
|
||||
<Tabs defaultValue="overview" className="mt-4">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="stores">Stores</TabsTrigger>
|
||||
<TabsTrigger value="users">Users</TabsTrigger>
|
||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Mail className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Email</span>
|
||||
</div>
|
||||
<p className="text-[14px]">{selectedRetailer.email}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Phone className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Phone</span>
|
||||
</div>
|
||||
<p className="text-[14px]">{selectedRetailer.phone}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<MapPin className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Location</span>
|
||||
</div>
|
||||
<p className="text-[14px]">{selectedRetailer.location}</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Store className="w-5 h-5 text-muted-foreground" />
|
||||
<span className="text-[12px] text-muted-foreground">Plan</span>
|
||||
</div>
|
||||
<Badge variant="outline">{selectedRetailer.plan}</Badge>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Stores</p>
|
||||
<p className="text-[24px] leading-[32px]">{selectedRetailer.stores}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Users</p>
|
||||
<p className="text-[24px] leading-[32px]">{selectedRetailer.users}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground mb-1">Collections</p>
|
||||
<p className="text-[24px] leading-[32px]">{selectedRetailer.collections}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1">Send Email</Button>
|
||||
<Button variant="outline" className="flex-1">View Activity</Button>
|
||||
<Button variant="outline" className="flex-1">Manage Plan</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stores">
|
||||
<div className="space-y-3">
|
||||
<Card className="p-4">
|
||||
<p className="text-[14px] mb-1">Mumbai Showroom</p>
|
||||
<p className="text-[12px] text-muted-foreground">Andheri West, Mumbai - 400053</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[14px] mb-1">Bandra Branch</p>
|
||||
<p className="text-[12px] text-muted-foreground">Bandra West, Mumbai - 400050</p>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="users">
|
||||
<div className="space-y-3">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Aditi Rao - Admin</p>
|
||||
<p className="text-[12px] text-muted-foreground">aditi@novajewels.com</p>
|
||||
</div>
|
||||
<Badge className="bg-status-active">Active</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Rohan Kumar - Associate</p>
|
||||
<p className="text-[12px] text-muted-foreground">rohan@novajewels.com</p>
|
||||
</div>
|
||||
<Badge className="bg-status-active">Active</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="activity">
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 border-l-2 border-primary">
|
||||
<p className="text-[14px] mb-1">Published new collection</p>
|
||||
<p className="text-[12px] text-muted-foreground">2 hours ago</p>
|
||||
</div>
|
||||
<div className="p-3 border-l-2 border-border">
|
||||
<p className="text-[14px] mb-1">Synced inventory from Auric Foundry</p>
|
||||
<p className="text-[12px] text-muted-foreground">1 day ago</p>
|
||||
</div>
|
||||
<div className="p-3 border-l-2 border-border">
|
||||
<p className="text-[14px] mb-1">Added team member</p>
|
||||
<p className="text-[12px] text-muted-foreground">3 days ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
src/components/customer/MobileShell.tsx
Normal file
68
src/components/customer/MobileShell.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Home, Heart, MessageCircle, Calendar, User } from "lucide-react";
|
||||
import { CustomerScreen } from "../../CustomerApp";
|
||||
|
||||
interface MobileShellProps {
|
||||
children: React.ReactNode;
|
||||
currentScreen: CustomerScreen;
|
||||
onNavigate: (screen: CustomerScreen) => void;
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: "home" as CustomerScreen, icon: Home, label: "Home" },
|
||||
{ id: "wishlist" as CustomerScreen, icon: Heart, label: "Wishlist" },
|
||||
{ id: "chat" as CustomerScreen, icon: MessageCircle, label: "Chat", badge: 2 },
|
||||
{ id: "appointments" as CustomerScreen, icon: Calendar, label: "Visits" },
|
||||
{ id: "account" as CustomerScreen, icon: User, label: "Account" },
|
||||
];
|
||||
|
||||
export function MobileShell({ children, currentScreen, onNavigate }: MobileShellProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto relative">
|
||||
{/* Main Content Area - with safe area padding */}
|
||||
<main className="flex-1 pb-[calc(64px+34px)] overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Bottom Navigation - Fixed with safe area */}
|
||||
<nav className="fixed bottom-0 left-0 right-0 max-w-[390px] mx-auto bg-background/95 backdrop-blur-lg border-t border-border pb-[34px] shadow-lg">
|
||||
<div className="flex items-center justify-around h-16 px-2">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentScreen === item.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className={`flex flex-col items-center justify-center gap-1 px-4 py-2 rounded-[12px] min-w-[64px] transition-all ${
|
||||
isActive ? "bg-primary/10" : "hover:bg-accent/5"
|
||||
}`}
|
||||
aria-label={item.label}
|
||||
>
|
||||
<div className="relative">
|
||||
<Icon
|
||||
className={`w-6 h-6 transition-colors ${
|
||||
isActive ? "text-primary" : "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
{item.badge && (
|
||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-destructive text-destructive-foreground rounded-full flex items-center justify-center text-[10px] font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`text-[11px] font-medium transition-colors ${
|
||||
isActive ? "text-primary" : "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
119
src/components/customer/screens/AccountScreen.tsx
Normal file
119
src/components/customer/screens/AccountScreen.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { User, MapPin, Ruler, Tag, Bell, Store, FileText, LogOut, ChevronRight } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { CustomerScreen } from "../../../CustomerApp";
|
||||
|
||||
interface AccountScreenProps {
|
||||
retailer: { name: string; location: string };
|
||||
preferences: any;
|
||||
onNavigate: (screen: CustomerScreen) => void;
|
||||
}
|
||||
|
||||
export function AccountScreen({ retailer, preferences, onNavigate }: AccountScreenProps) {
|
||||
return (
|
||||
<div className="pt-[44px] pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border px-4 py-3">
|
||||
<h1 className="text-[18px] leading-[28px]">Account</h1>
|
||||
</div>
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="p-4">
|
||||
<Card className="p-4 mb-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<User className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[18px] leading-[28px]">Sarah Johnson</p>
|
||||
<p className="text-[14px] text-text-secondary">sarah@example.com</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Edit Profile
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* My Retailers Section */}
|
||||
<Card className="p-4 mb-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-[16px] leading-[24px] mb-1">My Retailers</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Store className="w-4 h-4 text-text-secondary" />
|
||||
<span className="text-[14px]">{retailer.name}</span>
|
||||
</div>
|
||||
<p className="text-[12px] text-text-secondary ml-6">{retailer.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => onNavigate("my-retailers")}
|
||||
>
|
||||
<Store className="w-4 h-4 mr-2" />
|
||||
Manage Connected Retailers
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Settings Sections */}
|
||||
<div className="space-y-2">
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[16px] border border-border hover:bg-accent/5 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<MapPin className="w-5 h-5 text-text-secondary" />
|
||||
<span className="text-[16px]">Addresses</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[16px] border border-border hover:bg-accent/5 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<Ruler className="w-5 h-5 text-text-secondary" />
|
||||
<span className="text-[16px]">Sizes & Measurements</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[16px] border border-border hover:bg-accent/5 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<Tag className="w-5 h-5 text-text-secondary" />
|
||||
<span className="text-[16px]">Preferences</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[16px] border border-border hover:bg-accent/5 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bell className="w-5 h-5 text-text-secondary" />
|
||||
<span className="text-[16px]">Notifications</span>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Legal Links */}
|
||||
<div className="mt-6 space-y-2">
|
||||
<button className="w-full flex items-center gap-3 p-3 text-[14px] text-text-secondary hover:text-text-primary">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Terms of Service</span>
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-3 p-3 text-[14px] text-text-secondary hover:text-text-primary">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Privacy Policy</span>
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-3 p-3 text-[14px] text-text-secondary hover:text-text-primary">
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Refund Policy</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<Button variant="destructive" className="w-full mt-6">
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
220
src/components/customer/screens/AppointmentsScreen.tsx
Normal file
220
src/components/customer/screens/AppointmentsScreen.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Calendar, Plus, MapPin, Video, Clock, Phone, MessageCircle, AlertCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
interface AppointmentsScreenProps {
|
||||
retailer: { name: string; location: string };
|
||||
onBookNew: () => void;
|
||||
}
|
||||
|
||||
const appointments = [
|
||||
{
|
||||
id: 1,
|
||||
date: "Jan 25, 2025",
|
||||
fullDate: "Saturday, Jan 25",
|
||||
time: "2:00 PM",
|
||||
mode: "in-store",
|
||||
location: "Mumbai Showroom",
|
||||
address: "Shop 12, Zaveri Bazaar",
|
||||
associate: "Aditi Rao",
|
||||
status: "confirmed",
|
||||
notes: "Looking for bridal collection",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: "Jan 28, 2025",
|
||||
fullDate: "Tuesday, Jan 28",
|
||||
time: "11:00 AM",
|
||||
mode: "virtual",
|
||||
location: "Video Call",
|
||||
address: "WhatsApp Video",
|
||||
associate: "Priya Sharma",
|
||||
status: "confirmed",
|
||||
notes: "Anniversary gift discussion",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: "Jan 20, 2025",
|
||||
fullDate: "Monday, Jan 20",
|
||||
time: "4:30 PM",
|
||||
mode: "in-store",
|
||||
location: "Mumbai Showroom",
|
||||
address: "Shop 12, Zaveri Bazaar",
|
||||
associate: "Aditi Rao",
|
||||
status: "completed",
|
||||
notes: "",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: "Jan 15, 2025",
|
||||
fullDate: "Wednesday, Jan 15",
|
||||
time: "3:00 PM",
|
||||
mode: "virtual",
|
||||
location: "Video Call",
|
||||
address: "Zoom Meeting",
|
||||
associate: "Rajesh Kumar",
|
||||
status: "completed",
|
||||
notes: "",
|
||||
},
|
||||
];
|
||||
|
||||
export function AppointmentsScreen({ retailer, onBookNew }: AppointmentsScreenProps) {
|
||||
const upcomingAppointments = appointments.filter(a => a.status === "confirmed");
|
||||
const pastAppointments = appointments.filter(a => a.status === "completed");
|
||||
|
||||
return (
|
||||
<div className="pt-[44px] pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 border-b border-border px-4 py-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div>
|
||||
<h1 className="text-[20px] leading-[28px]">My Appointments</h1>
|
||||
<p className="text-[13px] text-text-secondary">{retailer.name}</p>
|
||||
</div>
|
||||
<Button size="sm" onClick={onBookNew}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Book New
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Appointments List */}
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Upcoming Appointments */}
|
||||
{upcomingAppointments.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-[16px] leading-[24px] mb-3 px-1">
|
||||
Upcoming ({upcomingAppointments.length})
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{upcomingAppointments.map((apt) => (
|
||||
<Card key={apt.id} className="overflow-hidden border-l-4 border-l-primary">
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar className="w-4 h-4 text-primary" />
|
||||
<span className="text-[15px] font-medium">{apt.fullDate}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-text-secondary mb-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span className="text-[14px]">{apt.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-accent-positive text-primary-foreground">
|
||||
{apt.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 text-[14px] text-text-secondary p-3 rounded-[8px] bg-bg-surface">
|
||||
{apt.mode === "in-store" ? (
|
||||
<MapPin className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<Video className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-foreground mb-0.5">{apt.location}</p>
|
||||
<p className="text-[13px]">{apt.address}</p>
|
||||
<p className="text-[13px] mt-1">with {apt.associate}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{apt.notes && (
|
||||
<div className="flex items-start gap-2 p-2 rounded-[8px] bg-accent/5 border border-accent/10">
|
||||
<AlertCircle className="w-3 h-3 text-accent mt-0.5 flex-shrink-0" />
|
||||
<p className="text-[12px] text-text-secondary">{apt.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2 border-t border-border">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Phone className="w-3 h-3 mr-1" />
|
||||
Call Store
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<MessageCircle className="w-3 h-3 mr-1" />
|
||||
Chat
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" className="flex-1 text-primary">
|
||||
Reschedule
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="flex-1 text-destructive">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Past Appointments */}
|
||||
{pastAppointments.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-[16px] leading-[24px] mb-3 px-1">
|
||||
Past Appointments
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{pastAppointments.map((apt) => (
|
||||
<Card key={apt.id} className="opacity-75">
|
||||
<div className="p-4 space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Calendar className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px]">{apt.fullDate}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-text-secondary">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span className="text-[13px]">{apt.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{apt.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-[13px] text-text-secondary">
|
||||
{apt.mode === "in-store" ? (
|
||||
<MapPin className="w-3 h-3" />
|
||||
) : (
|
||||
<Video className="w-3 h-3" />
|
||||
)}
|
||||
<span>{apt.location}</span>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={onBookNew}>
|
||||
<Plus className="w-3 h-3 mr-1" />
|
||||
Book Again
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{appointments.length === 0 && (
|
||||
<Card className="p-8 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4">
|
||||
<Calendar className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">No Appointments Yet</h3>
|
||||
<p className="text-[14px] text-text-secondary mb-4 max-w-[280px] mx-auto">
|
||||
Book your first appointment to explore our collection
|
||||
</p>
|
||||
<Button onClick={onBookNew}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Book Appointment
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
297
src/components/customer/screens/BookAppointmentScreen.tsx
Normal file
297
src/components/customer/screens/BookAppointmentScreen.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useState } from "react";
|
||||
import { ArrowLeft, MapPin, Video, Calendar, Clock, Check, User } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Calendar as CalendarComponent } from "../../ui/calendar";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Textarea } from "../../ui/textarea";
|
||||
import { RadioGroup, RadioGroupItem } from "../../ui/radio-group";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
interface BookAppointmentScreenProps {
|
||||
onBack: () => void;
|
||||
onSuccess: () => void;
|
||||
retailer: { name: string; location: string };
|
||||
}
|
||||
|
||||
const timeSlots = [
|
||||
"10:00 AM", "10:30 AM", "11:00 AM", "11:30 AM",
|
||||
"12:00 PM", "12:30 PM", "2:00 PM", "2:30 PM",
|
||||
"3:00 PM", "3:30 PM", "4:00 PM", "4:30 PM",
|
||||
"5:00 PM", "5:30 PM", "6:00 PM", "6:30 PM"
|
||||
];
|
||||
|
||||
export function BookAppointmentScreen({ onBack, onSuccess, retailer }: BookAppointmentScreenProps) {
|
||||
const [step, setStep] = useState<"mode" | "datetime" | "details">("mode");
|
||||
const [appointmentMode, setAppointmentMode] = useState<"in-store" | "virtual">("in-store");
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date());
|
||||
const [selectedTime, setSelectedTime] = useState<string>("");
|
||||
const [notes, setNotes] = useState("");
|
||||
|
||||
const handleContinue = () => {
|
||||
if (step === "mode") {
|
||||
setStep("datetime");
|
||||
} else if (step === "datetime") {
|
||||
if (!selectedDate || !selectedTime) {
|
||||
toast.error("Please select both date and time");
|
||||
return;
|
||||
}
|
||||
setStep("details");
|
||||
} else {
|
||||
toast.success("Appointment booked successfully!");
|
||||
onSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (step === "datetime") {
|
||||
setStep("mode");
|
||||
} else if (step === "details") {
|
||||
setStep("datetime");
|
||||
} else {
|
||||
onBack();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-bg-page">
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border pt-[44px] flex-shrink-0">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<button onClick={handleBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="ml-2">
|
||||
<h1 className="text-[18px] leading-[24px]">Book Appointment</h1>
|
||||
<p className="text-[12px] text-text-secondary">{retailer.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator */}
|
||||
<div className="bg-background border-b border-border px-4 py-3 flex-shrink-0">
|
||||
<div className="flex items-center justify-between max-w-md mx-auto">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
step === "mode" ? "bg-primary text-primary-foreground" : "bg-accent-positive text-primary-foreground"
|
||||
}`}>
|
||||
{step === "mode" ? "1" : <Check className="w-5 h-5" />}
|
||||
</div>
|
||||
<span className="text-[13px]">Mode</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-border mx-2" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
step === "datetime" ? "bg-primary text-primary-foreground" :
|
||||
step === "details" ? "bg-accent-positive text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
{step === "details" ? <Check className="w-5 h-5" /> : "2"}
|
||||
</div>
|
||||
<span className="text-[13px]">Date & Time</span>
|
||||
</div>
|
||||
<div className="flex-1 h-px bg-border mx-2" />
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
step === "details" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
3
|
||||
</div>
|
||||
<span className="text-[13px]">Details</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 pb-[calc(80px+34px)]">
|
||||
{step === "mode" && (
|
||||
<div className="space-y-4 max-w-md mx-auto">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-[20px] leading-[28px] mb-2">Choose Visit Type</h2>
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Select how you'd like to meet with our experts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
className={`p-5 cursor-pointer transition-all ${
|
||||
appointmentMode === "in-store" ? "border-2 border-primary bg-primary/5" : "hover:border-primary/50"
|
||||
}`}
|
||||
onClick={() => setAppointmentMode("in-store")}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[14px] bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<MapPin className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[16px] leading-[24px] mb-1">In-Store Visit</h3>
|
||||
<p className="text-[13px] text-text-secondary mb-2">
|
||||
Visit our showroom and experience our collection in person
|
||||
</p>
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
📍 {retailer.location}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
|
||||
appointmentMode === "in-store" ? "border-primary bg-primary" : "border-border"
|
||||
}`}>
|
||||
{appointmentMode === "in-store" && <Check className="w-3 h-3 text-primary-foreground" />}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className={`p-5 cursor-pointer transition-all ${
|
||||
appointmentMode === "virtual" ? "border-2 border-primary bg-primary/5" : "hover:border-primary/50"
|
||||
}`}
|
||||
onClick={() => setAppointmentMode("virtual")}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[14px] bg-secondary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Video className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[16px] leading-[24px] mb-1">Virtual Consultation</h3>
|
||||
<p className="text-[13px] text-text-secondary mb-2">
|
||||
Connect via video call from the comfort of your home
|
||||
</p>
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
💻 Video Call via WhatsApp/Zoom
|
||||
</p>
|
||||
</div>
|
||||
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
|
||||
appointmentMode === "virtual" ? "border-primary bg-primary" : "border-border"
|
||||
}`}>
|
||||
{appointmentMode === "virtual" && <Check className="w-3 h-3 text-primary-foreground" />}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "datetime" && (
|
||||
<div className="space-y-6 max-w-md mx-auto">
|
||||
<div className="text-center mb-4">
|
||||
<h2 className="text-[20px] leading-[28px] mb-2">Select Date & Time</h2>
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Choose your preferred appointment slot
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-4">
|
||||
<Label className="mb-3 block">Select Date</Label>
|
||||
<CalendarComponent
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={setSelectedDate}
|
||||
disabled={(date) => date < new Date() || date > new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)}
|
||||
className="rounded-[12px] border-0"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<Label className="mb-3 block">Select Time</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{timeSlots.map((time) => (
|
||||
<button
|
||||
key={time}
|
||||
onClick={() => setSelectedTime(time)}
|
||||
className={`px-3 py-2 rounded-[8px] text-[13px] transition-all ${
|
||||
selectedTime === time
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-bg-surface border border-border hover:border-primary"
|
||||
}`}
|
||||
>
|
||||
{time}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "details" && (
|
||||
<div className="space-y-4 max-w-md mx-auto">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-[20px] leading-[28px] mb-2">Appointment Details</h2>
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Review and add any special requests
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 bg-primary/5 border-primary/20">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{appointmentMode === "in-store" ? (
|
||||
<MapPin className="w-5 h-5 text-primary" />
|
||||
) : (
|
||||
<Video className="w-5 h-5 text-primary" />
|
||||
)}
|
||||
<div>
|
||||
<p className="text-[12px] text-text-secondary">Visit Type</p>
|
||||
<p className="text-[15px]">
|
||||
{appointmentMode === "in-store" ? "In-Store Visit" : "Virtual Consultation"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Calendar className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<p className="text-[12px] text-text-secondary">Date & Time</p>
|
||||
<p className="text-[15px]">
|
||||
{selectedDate?.toLocaleDateString("en-IN", {
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
})} at {selectedTime}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<User className="w-5 h-5 text-primary" />
|
||||
<div>
|
||||
<p className="text-[12px] text-text-secondary">Store</p>
|
||||
<p className="text-[15px]">{retailer.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<Label htmlFor="notes" className="mb-2 block">
|
||||
Special Requests (Optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="E.g., Looking for bridal jewellery, specific budget range, particular designs..."
|
||||
className="min-h-[100px] text-[14px]"
|
||||
/>
|
||||
<p className="text-[11px] text-text-secondary mt-2">
|
||||
Help us prepare for your visit by sharing what you're looking for
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4 bg-accent/5 border-accent/20">
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
💡 <strong>Tip:</strong> You'll receive a confirmation via SMS and email. Our associate will call you 30 minutes before the appointment.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<div className="border-t border-border bg-background p-4 pb-[calc(34px+16px)] flex-shrink-0">
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={step === "datetime" && (!selectedDate || !selectedTime)}
|
||||
>
|
||||
{step === "details" ? "Confirm Appointment" : "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
144
src/components/customer/screens/ChatScreen.tsx
Normal file
144
src/components/customer/screens/ChatScreen.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ArrowLeft, Send, Paperclip, Calendar, CheckCheck } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
interface ChatScreenProps {
|
||||
retailer: { name: string; associate: string };
|
||||
attachedProduct?: string | null;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
const initialMessages = [
|
||||
{ id: 1, sender: "associate", text: "Hello! I'm Aditi from Nova Jewels. How can I help you today?", time: "10:30 AM", status: "read" },
|
||||
{ id: 2, sender: "customer", text: "Hi! I'm looking for bridal jewellery sets", time: "10:32 AM", status: "read" },
|
||||
{ id: 3, sender: "associate", text: "Great! We have some beautiful bridal collections. Let me share a few pieces with you.", time: "10:33 AM", status: "read" },
|
||||
];
|
||||
|
||||
export function ChatScreen({ retailer, attachedProduct, onBack }: ChatScreenProps) {
|
||||
const [message, setMessage] = useState("");
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (message.trim()) {
|
||||
const newMessage = {
|
||||
id: messages.length + 1,
|
||||
sender: "customer" as const,
|
||||
text: message,
|
||||
time: new Date().toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" }),
|
||||
status: "sent" as const,
|
||||
};
|
||||
setMessages([...messages, newMessage]);
|
||||
setMessage("");
|
||||
toast.success("Message sent! Our associate will respond shortly.");
|
||||
|
||||
// Simulate associate response
|
||||
setTimeout(() => {
|
||||
const response = {
|
||||
id: messages.length + 2,
|
||||
sender: "associate" as const,
|
||||
text: "Thank you for your interest! I'll prepare some options for you. Would you like to schedule a visit to see these pieces in person?",
|
||||
time: new Date().toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit" }),
|
||||
status: "read" as const,
|
||||
};
|
||||
setMessages(prev => [...prev, response]);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen max-h-screen">
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border pt-[44px] flex-shrink-0">
|
||||
<div className="flex items-center gap-3 h-14 px-4">
|
||||
{onBack && (
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="text-[14px]">AR</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[16px] leading-[20px]">{retailer.associate}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 rounded-full bg-accent-positive" />
|
||||
<p className="text-[11px] text-text-secondary">Online • {retailer.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-[calc(56px+34px+80px)]">
|
||||
{attachedProduct && (
|
||||
<Card className="p-3 bg-primary/5 border-primary/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-secondary/30 to-primary/30 rounded-[8px] flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Inquiry about:</p>
|
||||
<p className="text-[14px]">Heritage Bridal Ring</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.sender === "customer" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[75%] rounded-[16px] p-3 ${
|
||||
msg.sender === "customer"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card border border-border"
|
||||
}`}
|
||||
>
|
||||
<p className="text-[14px] leading-[20px] mb-1">{msg.text}</p>
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<p className={`text-[11px] ${msg.sender === "customer" ? "text-primary-foreground/70" : "text-text-secondary"}`}>
|
||||
{msg.time}
|
||||
</p>
|
||||
{msg.sender === "customer" && (
|
||||
<CheckCheck className={`w-3 h-3 ${msg.status === "read" ? "text-accent-positive" : "text-primary-foreground/70"}`} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-border bg-background p-4 pb-[calc(56px+34px+16px)] flex-shrink-0">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Button variant="outline" size="sm">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
Book Appointment
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Paperclip className="w-4 h-4 mr-1" />
|
||||
Attach Photo
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSendMessage()}
|
||||
placeholder="Type your message..."
|
||||
className="flex-1 text-[16px]"
|
||||
/>
|
||||
<Button size="icon" disabled={!message} onClick={handleSendMessage}>
|
||||
<Send className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/components/customer/screens/CollectionsScreen.tsx
Normal file
48
src/components/customer/screens/CollectionsScreen.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
|
||||
interface CollectionsScreenProps {
|
||||
onCollectionClick: (id: string) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const collections = [
|
||||
{ id: "bridal", name: "Bridal Collection", items: 24, updated: "2 days ago" },
|
||||
{ id: "daily", name: "Daily Wear", items: 42, updated: "1 week ago" },
|
||||
{ id: "festive", name: "Festive Collection", items: 18, updated: "3 days ago" },
|
||||
{ id: "new", name: "New Arrivals", items: 12, updated: "Today" },
|
||||
];
|
||||
|
||||
export function CollectionsScreen({ onCollectionClick, onBack }: CollectionsScreenProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border sticky top-0 z-10 pt-[44px]">
|
||||
<div className="flex items-center gap-3 h-14 px-4">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<h1 className="text-[18px] leading-[28px]">Collections</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collections List */}
|
||||
<div className="p-4 space-y-3">
|
||||
{collections.map((collection) => (
|
||||
<Card
|
||||
key={collection.id}
|
||||
className="p-4 cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={() => onCollectionClick(collection.id)}
|
||||
>
|
||||
<h3 className="text-[16px] leading-[24px] mb-1">{collection.name}</h3>
|
||||
<div className="flex items-center gap-2 text-[14px] text-text-secondary">
|
||||
<span>{collection.items} items</span>
|
||||
<span>•</span>
|
||||
<span>Updated {collection.updated}</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
src/components/customer/screens/ConfirmRetailerScreen.tsx
Normal file
111
src/components/customer/screens/ConfirmRetailerScreen.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { CheckCircle, Info, ArrowLeft } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Checkbox } from "../../ui/checkbox";
|
||||
import { useState } from "react";
|
||||
|
||||
interface ConfirmRetailerScreenProps {
|
||||
onConfirm: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmRetailerScreen({ onConfirm, onSkip }: ConfirmRetailerScreenProps) {
|
||||
const [understood, setUnderstood] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between h-14 px-4 pt-[44px] border-b border-border bg-background">
|
||||
<h1 className="text-[18px] leading-[28px]">Confirm Retailer</h1>
|
||||
<Button variant="ghost" size="sm" onClick={onSkip}>
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-6 space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-4 py-4">
|
||||
<div className="w-20 h-20 rounded-[20px] bg-primary/10 flex items-center justify-center">
|
||||
<span className="text-[32px]">N</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-[16px] text-text-secondary mb-2">
|
||||
You are choosing
|
||||
</p>
|
||||
<h2 className="text-[28px] leading-[36px]">Nova Jewels</h2>
|
||||
<p className="text-[16px] text-text-secondary">as your retailer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-6 space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h3 className="text-[16px] leading-[24px] mb-2">What this means</h3>
|
||||
<ul className="space-y-2 text-[14px] text-text-secondary">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-accent-positive mt-0.5 flex-shrink-0" />
|
||||
<span>You'll see curated collections from Nova Jewels</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-accent-positive mt-0.5 flex-shrink-0" />
|
||||
<span>Direct communication with your personal associate</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-accent-positive mt-0.5 flex-shrink-0" />
|
||||
<span>Personalized recommendations based on your preferences</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-accent-positive mt-0.5 flex-shrink-0" />
|
||||
<span>Exclusive access to their inventory and offers</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-start space-x-3 p-4 rounded-[16px] border border-border bg-card">
|
||||
<Checkbox
|
||||
id="understood"
|
||||
checked={understood}
|
||||
onCheckedChange={(checked) => setUnderstood(checked as boolean)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<label
|
||||
htmlFor="understood"
|
||||
className="text-[14px] leading-[20px]"
|
||||
>
|
||||
I understand I'll see their curated catalogue and can contact them for all my jewellery needs
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 bg-muted/50">
|
||||
<p className="text-[13px] text-text-secondary text-center">
|
||||
You can change or disconnect from this retailer anytime from your account settings
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Bottom CTAs */}
|
||||
<div className="p-6 pb-[calc(34px+24px)] space-y-3">
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
disabled={!understood}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Confirm & Continue
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onSkip}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Skip for Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
312
src/components/customer/screens/HomeScreen.tsx
Normal file
312
src/components/customer/screens/HomeScreen.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import { Search, Bell, Calendar, MessageCircle, Package, Heart, Sparkles } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { CustomerScreen } from "../../../CustomerApp";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
interface HomeScreenProps {
|
||||
retailer: { name: string; location: string };
|
||||
onNavigate: (screen: CustomerScreen) => void;
|
||||
onCollectionClick: (id: string) => void;
|
||||
}
|
||||
|
||||
const collections = [
|
||||
{
|
||||
id: "bridal",
|
||||
name: "Bridal Collection",
|
||||
items: 24,
|
||||
image: "bg-gradient-to-br from-primary/20 to-secondary/20",
|
||||
badge: "Popular"
|
||||
},
|
||||
{
|
||||
id: "daily",
|
||||
name: "Daily Wear",
|
||||
items: 42,
|
||||
image: "bg-gradient-to-br from-accent-positive/20 to-primary/20",
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
id: "festive",
|
||||
name: "Festive Collection",
|
||||
items: 18,
|
||||
image: "bg-gradient-to-br from-secondary/20 to-accent-warn/20",
|
||||
badge: "New"
|
||||
},
|
||||
];
|
||||
|
||||
const recentlyViewed = [
|
||||
{ id: "1", name: "Gold Necklace", price: "₹45,000", image: "bg-gradient-to-br from-secondary/30 to-secondary/10" },
|
||||
{ id: "2", name: "Diamond Ring", price: "₹85,000", image: "bg-gradient-to-br from-primary/30 to-primary/10" },
|
||||
{ id: "3", name: "Pearl Earrings", price: "₹12,500", image: "bg-gradient-to-br from-accent-positive/30 to-accent-positive/10" },
|
||||
];
|
||||
|
||||
export function HomeScreen({ retailer, onNavigate, onCollectionClick }: HomeScreenProps) {
|
||||
const isGenericRetailer = retailer.name === "Hello Jewellers";
|
||||
|
||||
return (
|
||||
<div className="pb-6">
|
||||
{/* Header with safe area */}
|
||||
<div className="bg-background border-b border-border sticky top-0 z-10 pt-[44px]">
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-gradient-to-br from-primary to-primary/80 flex items-center justify-center shadow-sm">
|
||||
<span className="text-[16px] text-primary-foreground">
|
||||
{isGenericRetailer ? "H" : "N"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[15px] font-medium">{retailer.name}</p>
|
||||
<p className="text-[12px] text-text-secondary">{retailer.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="p-2 hover:bg-accent/10 rounded-[12px] relative transition-colors"
|
||||
onClick={() => onNavigate("notifications")}
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-destructive rounded-full" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<button className="w-full flex items-center gap-3 px-4 py-3 rounded-[14px] border border-border bg-bg-surface text-text-secondary hover:border-primary transition-colors">
|
||||
<Search className="w-5 h-5" />
|
||||
<span className="text-[15px]">Search jewellery...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Banner */}
|
||||
<div className="px-4 py-6">
|
||||
<Card className="overflow-hidden bg-gradient-to-br from-primary via-primary/90 to-primary/80 border-0 shadow-lg">
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-primary-foreground" />
|
||||
<Badge variant="outline" className="bg-primary-foreground/20 text-primary-foreground border-primary-foreground/30">
|
||||
Trending
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[24px] leading-[32px] text-primary-foreground">
|
||||
Discover Your Perfect Piece
|
||||
</h2>
|
||||
<p className="text-[14px] text-primary-foreground/90">
|
||||
Browse curated collections handpicked for you
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="bg-primary-foreground text-primary hover:bg-primary-foreground/90"
|
||||
>
|
||||
Explore Now
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="px-4 mb-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4 font-medium">Quick Actions</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
className="flex flex-col items-center gap-2 p-4 rounded-[16px] bg-card border border-border hover:border-primary hover:shadow-sm transition-all"
|
||||
onClick={() => onNavigate("appointments")}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-[14px] bg-primary/10 flex items-center justify-center">
|
||||
<Calendar className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<span className="text-[13px] text-center">Book Visit</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="flex flex-col items-center gap-2 p-4 rounded-[16px] bg-card border border-border hover:border-primary hover:shadow-sm transition-all"
|
||||
onClick={() => onNavigate("chat")}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-[14px] bg-secondary/10 flex items-center justify-center">
|
||||
<MessageCircle className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
<span className="text-[13px] text-center">Chat</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="flex flex-col items-center gap-2 p-4 rounded-[16px] bg-card border border-border hover:border-primary hover:shadow-sm transition-all"
|
||||
onClick={() => onNavigate("wishlist")}
|
||||
>
|
||||
<div className="w-12 h-12 rounded-[14px] bg-accent-error/10 flex items-center justify-center">
|
||||
<Heart className="w-6 h-6 text-accent-error" />
|
||||
</div>
|
||||
<span className="text-[13px] text-center">Wishlist</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* My Retailers Section */}
|
||||
<div className="px-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px] font-medium">My Retailers</h3>
|
||||
<button
|
||||
className="text-[14px] text-primary hover:underline"
|
||||
onClick={() => onNavigate("my-retailers")}
|
||||
>
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Nova Jewels Card */}
|
||||
<Card
|
||||
className="overflow-hidden cursor-pointer hover:shadow-md transition-all"
|
||||
onClick={() => onNavigate("my-retailers")}
|
||||
>
|
||||
<div className="flex items-center gap-3 p-4">
|
||||
<div className="w-14 h-14 rounded-[12px] bg-gradient-to-br from-primary/20 to-secondary/20 flex items-center justify-center flex-shrink-0">
|
||||
<Package className="w-7 h-7 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<h4 className="text-[15px] font-medium">Nova Jewels</h4>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 bg-accent-positive/10 text-accent-positive border-accent-positive/20">
|
||||
Active
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[12px] text-text-secondary">Mumbai • 842 Products</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<Button size="sm" variant="outline" className="h-8">
|
||||
Visit Store
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Add Retailer Card */}
|
||||
<Card
|
||||
className="overflow-hidden cursor-pointer hover:shadow-md transition-all border-dashed border-2"
|
||||
onClick={() => onNavigate("my-retailers")}
|
||||
>
|
||||
<div className="flex items-center gap-3 p-4">
|
||||
<div className="w-14 h-14 rounded-[12px] bg-accent/5 flex items-center justify-center flex-shrink-0">
|
||||
<Sparkles className="w-7 h-7 text-accent" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-[15px] font-medium mb-0.5">Connect More Retailers</h4>
|
||||
<p className="text-[12px] text-text-secondary">Enter invite code: NOVA2025</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Featured Collections */}
|
||||
<div className="px-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px] font-medium">Featured Collections</h3>
|
||||
<button className="text-[14px] text-primary hover:underline">
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{collections.map((collection) => (
|
||||
<Card
|
||||
key={collection.id}
|
||||
className="overflow-hidden cursor-pointer hover:shadow-md transition-all border-border"
|
||||
onClick={() => onCollectionClick(collection.id)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-28 h-28 ${collection.image} flex-shrink-0 flex items-center justify-center`}>
|
||||
<Package className="w-10 h-10 text-text-secondary/40" />
|
||||
</div>
|
||||
<div className="flex-1 py-4 pr-4">
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<h4 className="text-[16px] leading-[24px] font-medium">{collection.name}</h4>
|
||||
{collection.badge && (
|
||||
<Badge variant="outline" className="text-[11px] px-2 py-0">
|
||||
{collection.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[14px] text-text-secondary">{collection.items} items</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recently Viewed */}
|
||||
<div className="px-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px] font-medium">Recently Viewed</h3>
|
||||
<button
|
||||
className="text-[14px] text-primary hover:underline"
|
||||
onClick={() => onCollectionClick("recent")}
|
||||
>
|
||||
See All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide">
|
||||
{recentlyViewed.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="flex-shrink-0 w-[140px] overflow-hidden cursor-pointer hover:shadow-md transition-all"
|
||||
onClick={() => {
|
||||
// In real app, this would set product and navigate
|
||||
onCollectionClick(item.id);
|
||||
}}
|
||||
>
|
||||
<div className={`w-full h-[140px] ${item.image} flex items-center justify-center`}>
|
||||
<Sparkles className="w-8 h-8 text-text-secondary/40" />
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<p className="text-[13px] font-medium line-clamp-1 mb-1">{item.name}</p>
|
||||
<p className="text-[14px] text-primary font-medium">{item.price}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Special Offer Banner */}
|
||||
<div className="px-4 mb-6">
|
||||
<Card className="overflow-hidden bg-gradient-to-r from-secondary/20 via-secondary/10 to-transparent border-secondary/30">
|
||||
<div className="p-5 flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[12px] text-secondary font-medium uppercase tracking-wide">Limited Time</p>
|
||||
<h4 className="text-[16px] leading-[24px] font-medium">Get 15% Off on Gold</h4>
|
||||
<p className="text-[13px] text-text-secondary">Valid till end of month</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" className="border-secondary text-secondary hover:bg-secondary/10">
|
||||
View Offer
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Info Card - Only show if generic retailer */}
|
||||
{isGenericRetailer && (
|
||||
<div className="px-4 mb-6">
|
||||
<Card className="p-5 bg-accent/5 border-accent/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-accent/10 flex items-center justify-center flex-shrink-0">
|
||||
<Sparkles className="w-5 h-5 text-accent" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<h4 className="text-[15px] font-medium">Connect with a Retailer</h4>
|
||||
<p className="text-[13px] text-text-secondary">
|
||||
Get personalized recommendations and exclusive access to retailer collections
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={() => onNavigate("account")}>
|
||||
Connect Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
91
src/components/customer/screens/InterestConfirmScreen.tsx
Normal file
91
src/components/customer/screens/InterestConfirmScreen.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { CheckCircle, Home, MessageCircle, Calendar } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Card } from "../../ui/card";
|
||||
|
||||
interface InterestConfirmScreenProps {
|
||||
productName: string;
|
||||
onGoHome: () => void;
|
||||
onContinueChat: () => void;
|
||||
onBookAppointment: () => void;
|
||||
}
|
||||
|
||||
export function InterestConfirmScreen({
|
||||
productName,
|
||||
onGoHome,
|
||||
onContinueChat,
|
||||
onBookAppointment
|
||||
}: InterestConfirmScreenProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto">
|
||||
{/* Success Content */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-accent-positive/10 flex items-center justify-center mb-6 animate-in zoom-in duration-300">
|
||||
<CheckCircle className="w-12 h-12 text-accent-positive" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-[28px] leading-[36px] mb-3">
|
||||
Interest Registered!
|
||||
</h1>
|
||||
|
||||
<p className="text-[16px] text-text-secondary mb-6 max-w-[320px]">
|
||||
Thank you for showing interest in <strong>{productName}</strong>
|
||||
</p>
|
||||
|
||||
<Card className="w-full p-5 bg-primary/5 border-primary/20 mb-8">
|
||||
<div className="space-y-3 text-left">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-primary">1</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">
|
||||
Our associate will reach out to you via chat or call within 24 hours
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-primary">2</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">
|
||||
You can book an appointment to see the piece in person
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="text-primary">3</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">
|
||||
We'll provide personalized quotes and customization options
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="w-full space-y-3">
|
||||
<Button onClick={onContinueChat} className="w-full" size="lg">
|
||||
<MessageCircle className="w-5 h-5 mr-2" />
|
||||
Continue Chat
|
||||
</Button>
|
||||
|
||||
<Button onClick={onBookAppointment} variant="outline" className="w-full" size="lg">
|
||||
<Calendar className="w-5 h-5 mr-2" />
|
||||
Book Appointment
|
||||
</Button>
|
||||
|
||||
<Button onClick={onGoHome} variant="ghost" className="w-full">
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Back to Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom safe area */}
|
||||
<div className="h-[34px]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
205
src/components/customer/screens/InviteScreen.tsx
Normal file
205
src/components/customer/screens/InviteScreen.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { useState } from "react";
|
||||
import { QrCode, KeyRound, CheckCircle, ArrowRight, Copy, Sparkles } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
import { copyWithToast } from "../../utils/clipboard";
|
||||
|
||||
interface InviteScreenProps {
|
||||
onSuccess: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
export function InviteScreen({ onSuccess, onSkip }: InviteScreenProps) {
|
||||
const [code, setCode] = useState("");
|
||||
const [validated, setValidated] = useState(false);
|
||||
|
||||
const handleValidate = () => {
|
||||
// Simulate validation - Accept demo code NOVA2025 as well
|
||||
if (code.toUpperCase() === "RJ7K3A" || code.toUpperCase() === "ZQ4M9T" || code.toUpperCase() === "NOVA2025") {
|
||||
setValidated(true);
|
||||
toast.success("Invite code validated!");
|
||||
} else {
|
||||
toast.error("Invalid invite code. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyDemoCode = async () => {
|
||||
await copyWithToast("NOVA2025", "Demo code copied!");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between h-14 px-4 pt-[44px] border-b border-border bg-background">
|
||||
<h1 className="text-[18px] leading-[28px]">Connect with Retailer</h1>
|
||||
<Button variant="ghost" size="sm" onClick={onSkip}>
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-6 space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h2 className="text-[22px] leading-[30px]">Connect with Your Jeweller</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Connect with a retailer to view their exclusive collections, or skip to browse available jewellery
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Demo Code Banner */}
|
||||
<Card className="p-4 bg-gradient-to-br from-primary/10 to-secondary/10 border-primary/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-primary/20 flex items-center justify-center flex-shrink-0">
|
||||
<Sparkles className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-[14px] font-medium">Try Our Demo Store</p>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 bg-secondary/20 text-secondary border-secondary/30">
|
||||
Free
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-[12px] text-muted-foreground mb-3">
|
||||
Use this code to connect to Nova Jewels demo store
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 px-3 py-2 rounded-lg bg-background border border-border">
|
||||
<p className="text-[16px] font-mono tracking-wider text-center">NOVA2025</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={handleCopyDemoCode}>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{!validated ? (
|
||||
<>
|
||||
{/* Action Cards */}
|
||||
<div className="space-y-4">
|
||||
<Card className="p-6 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<QrCode className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-1">Scan QR Code</h3>
|
||||
<p className="text-[14px] text-text-secondary mb-4">
|
||||
Use your camera to scan the QR code from your retailer
|
||||
</p>
|
||||
<Button variant="outline" className="w-full">
|
||||
<QrCode className="w-4 h-4 mr-2" />
|
||||
Open Camera
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-[14px] text-text-secondary">OR</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
<Card className="p-6 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-secondary/10 flex items-center justify-center flex-shrink-0">
|
||||
<KeyRound className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-1">Enter Invite Code</h3>
|
||||
<p className="text-[14px] text-text-secondary mb-4">
|
||||
Type the 6-8 character code provided by your retailer
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="code">Invite Code</Label>
|
||||
<Input
|
||||
id="code"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||
placeholder="RJ7K3A"
|
||||
maxLength={8}
|
||||
className="text-[16px] uppercase tracking-wider text-center"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleValidate}
|
||||
disabled={code.length < 6}
|
||||
className="w-full"
|
||||
>
|
||||
Validate Code
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Skip Option Card */}
|
||||
<Card className="p-6 bg-accent/5 border-accent/20">
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-[16px] leading-[24px]">Don't have a code?</h3>
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
You can still explore available jewellery and connect with retailers later from your account settings.
|
||||
</p>
|
||||
<Button variant="outline" className="w-full" onClick={onSkip}>
|
||||
Continue Without Retailer
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<Card className="p-6 space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-4">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-accent-positive" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-[22px] leading-[30px] mb-2">Code Validated!</h3>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
You're connecting with
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="w-full p-4 border-2 border-primary/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-[12px] bg-primary/10 flex items-center justify-center">
|
||||
<span className="text-[18px]">N</span>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-[16px]">Nova Jewels</p>
|
||||
<p className="text-[14px] text-text-secondary">Mumbai</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
By continuing, you agree to the{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Terms of Service
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
{validated && (
|
||||
<div className="p-6 pb-[calc(34px+24px)]">
|
||||
<Button onClick={onSuccess} className="w-full" size="lg">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
178
src/components/customer/screens/LoginScreen.tsx
Normal file
178
src/components/customer/screens/LoginScreen.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { useState } from "react";
|
||||
import { Mail, ArrowLeft } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../../ui/input-otp";
|
||||
import { Checkbox } from "../../ui/checkbox";
|
||||
|
||||
interface LoginScreenProps {
|
||||
onLogin: () => void;
|
||||
}
|
||||
|
||||
export function LoginScreen({ onLogin }: LoginScreenProps) {
|
||||
const [step, setStep] = useState<"email" | "otp">("email");
|
||||
const [email, setEmail] = useState("");
|
||||
const [otp, setOtp] = useState("");
|
||||
const [consent, setConsent] = useState(false);
|
||||
const [resendTimer, setResendTimer] = useState(0);
|
||||
|
||||
const handleSendOTP = () => {
|
||||
if (email && consent) {
|
||||
setStep("otp");
|
||||
setResendTimer(30);
|
||||
const interval = setInterval(() => {
|
||||
setResendTimer((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyOTP = () => {
|
||||
if (otp.length === 6) {
|
||||
onLogin();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center h-14 px-4 pt-[44px] border-b border-border bg-background pr-[16px] pb-[17px] pl-[16px] m-[0px]">
|
||||
{step === "otp" && (
|
||||
<button
|
||||
onClick={() => setStep("email")}
|
||||
className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px] transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
)}
|
||||
<h1 className="text-[18px] leading-[28px] ml-2">Sign In</h1>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-6 space-y-6">
|
||||
{step === "email" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Enter your email to receive a one-time password
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="text-[16px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="consent"
|
||||
checked={consent}
|
||||
onCheckedChange={(checked) => setConsent(checked as boolean)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<label
|
||||
htmlFor="consent"
|
||||
className="text-[14px] leading-[20px] text-text-secondary"
|
||||
>
|
||||
I agree to the{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Terms of Service
|
||||
</a>
|
||||
,{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Privacy Policy
|
||||
</a>
|
||||
, and{" "}
|
||||
<a href="#" className="text-primary hover:underline">
|
||||
Refund Policy
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col items-center space-y-4 py-8">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Mail className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[16px] text-text-secondary mb-1">
|
||||
We sent a code to
|
||||
</p>
|
||||
<p className="text-[16px]">{email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Enter 6-digit code</Label>
|
||||
<div className="flex justify-center">
|
||||
<InputOTP maxLength={6} value={otp} onChange={setOtp}>
|
||||
<InputOTPGroup>
|
||||
<InputOTPSlot index={0} />
|
||||
<InputOTPSlot index={1} />
|
||||
<InputOTPSlot index={2} />
|
||||
<InputOTPSlot index={3} />
|
||||
<InputOTPSlot index={4} />
|
||||
<InputOTPSlot index={5} />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
{resendTimer > 0 ? (
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Resend code in {resendTimer}s
|
||||
</p>
|
||||
) : (
|
||||
<button className="text-[14px] text-primary hover:underline">
|
||||
Resend code
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<div className="p-6 pb-[calc(34px+24px)]">
|
||||
{step === "email" ? (
|
||||
<Button
|
||||
onClick={handleSendOTP}
|
||||
disabled={!email || !consent}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Send OTP
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleVerifyOTP}
|
||||
disabled={otp.length !== 6}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Verify & Continue
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
251
src/components/customer/screens/MyRetailersScreen.tsx
Normal file
251
src/components/customer/screens/MyRetailersScreen.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { Store, Plus, MapPin, Phone, Mail, ExternalLink, Share2, Copy, ChevronRight, Sparkles } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Separator } from "../../ui/separator";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../ui/dialog";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
import { copyWithToast } from "../../utils/clipboard";
|
||||
|
||||
interface MyRetailersScreenProps {
|
||||
onBack: () => void;
|
||||
onViewStore: (retailerId: string) => void;
|
||||
}
|
||||
|
||||
const connectedRetailers = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Nova Jewels",
|
||||
location: "Mumbai, Maharashtra",
|
||||
phone: "+91 98765 43210",
|
||||
email: "hello@novajewels.com",
|
||||
associate: "Priya Sharma",
|
||||
theme: "elegant",
|
||||
connectedDate: "Jan 2025",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Sparkle Gems",
|
||||
location: "Delhi, NCR",
|
||||
phone: "+91 98111 22333",
|
||||
email: "contact@sparklegems.com",
|
||||
associate: "Amit Kumar",
|
||||
theme: "modern",
|
||||
connectedDate: "Dec 2024",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Golden Heritage",
|
||||
location: "Jaipur, Rajasthan",
|
||||
phone: "+91 98444 55666",
|
||||
email: "info@goldenheritage.com",
|
||||
associate: "Rajesh Verma",
|
||||
theme: "royal",
|
||||
connectedDate: "Nov 2024",
|
||||
status: "active",
|
||||
},
|
||||
];
|
||||
|
||||
const DEMO_CODE = "NOVA2025";
|
||||
|
||||
export function MyRetailersScreen({ onBack, onViewStore }: MyRetailersScreenProps) {
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showDemoCodeDialog, setShowDemoCodeDialog] = useState(false);
|
||||
const [inviteCode, setInviteCode] = useState("");
|
||||
|
||||
const handleCopyCode = async () => {
|
||||
await copyWithToast(DEMO_CODE, "Demo code copied!");
|
||||
};
|
||||
|
||||
const handleAddRetailer = () => {
|
||||
if (inviteCode.trim()) {
|
||||
toast.success(`Connecting to retailer with code: ${inviteCode}`);
|
||||
setShowAddDialog(false);
|
||||
setInviteCode("");
|
||||
} else {
|
||||
toast.error("Please enter an invite code");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bg-page flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 border-b border-border px-4 py-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<button onClick={onBack} className="text-[14px] text-primary">
|
||||
← Back
|
||||
</button>
|
||||
<Button size="sm" className="bg-secondary text-secondary-foreground hover:bg-secondary/90" onClick={() => setShowDemoCodeDialog(true)}>
|
||||
<Sparkles className="w-4 h-4 mr-1" />
|
||||
Demo Code
|
||||
</Button>
|
||||
</div>
|
||||
<h1 className="text-[24px] leading-[32px]">My Retailers</h1>
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
{connectedRetailers.length} store{connectedRetailers.length !== 1 ? 's' : ''} connected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Add New Retailer Card */}
|
||||
<button
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
className="w-full p-4 rounded-lg border-2 border-dashed border-border hover:border-primary hover:bg-accent/5 transition-all"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Plus className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<p className="text-[14px] font-medium">Connect New Retailer</p>
|
||||
<p className="text-[12px] text-muted-foreground">Enter invite code to connect</p>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Connected Retailers */}
|
||||
{connectedRetailers.map((retailer) => (
|
||||
<Card key={retailer.id} className="p-4">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-primary/20 to-secondary/20 flex items-center justify-center flex-shrink-0">
|
||||
<Store className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 className="text-[16px] font-medium">{retailer.name}</h3>
|
||||
<Badge variant="outline" className="text-[10px] bg-accent-positive/10 text-accent-positive border-accent-positive/20">
|
||||
Active
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[12px] text-muted-foreground mb-1">
|
||||
<MapPin className="w-3 h-3" />
|
||||
<span>{retailer.location}</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
Your contact: {retailer.associate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-3" />
|
||||
|
||||
<div className="space-y-2 text-[12px]">
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Phone className="w-3.5 h-3.5" />
|
||||
<span>{retailer.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Mail className="w-3.5 h-3.5" />
|
||||
<span>{retailer.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-3" />
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => onViewStore(retailer.id)}
|
||||
>
|
||||
<Store className="w-4 h-4 mr-1" />
|
||||
View Store
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
<Phone className="w-4 h-4 mr-1" />
|
||||
Contact
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground text-center mt-2">
|
||||
Connected since {retailer.connectedDate}
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Retailer Dialog */}
|
||||
<Dialog open={showAddDialog} onOpenChange={setShowAddDialog}>
|
||||
<DialogContent className="max-w-[340px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect to Retailer</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter the invite code provided by your jeweller
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invite-code">Invite Code</Label>
|
||||
<Input
|
||||
id="invite-code"
|
||||
placeholder="e.g., NOVA2025"
|
||||
value={inviteCode}
|
||||
onChange={(e) => setInviteCode(e.target.value.toUpperCase())}
|
||||
className="text-center tracking-wider"
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
💡 Ask your jeweller for their unique invite code
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setShowAddDialog(false)} className="flex-1">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAddRetailer} className="flex-1">
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Demo Code Dialog */}
|
||||
<Dialog open={showDemoCodeDialog} onOpenChange={setShowDemoCodeDialog}>
|
||||
<DialogContent className="max-w-[340px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-secondary" />
|
||||
Demo Retailer Code
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use this code to connect to Nova Jewels demo store
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-5 rounded-lg bg-gradient-to-br from-primary/10 to-secondary/10 border-2 border-primary/20 text-center">
|
||||
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mb-2">INVITE CODE</p>
|
||||
<p className="text-[32px] font-mono tracking-widest font-medium text-primary">{DEMO_CODE}</p>
|
||||
</div>
|
||||
|
||||
<Button className="w-full" onClick={handleCopyCode}>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
Copy Code
|
||||
</Button>
|
||||
|
||||
<div className="p-4 rounded-lg bg-secondary/5 border border-secondary/20">
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles className="w-4 h-4 text-secondary mt-0.5 flex-shrink-0" />
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground mb-1">How to use:</p>
|
||||
<p>Tap "Connect New Retailer" above and paste this code to instantly connect to our demo store with sample jewellery collections.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
320
src/components/customer/screens/NotificationsScreen.tsx
Normal file
320
src/components/customer/screens/NotificationsScreen.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import { ArrowLeft, Bell, Check, CheckCheck, Package, Calendar, MessageCircle, Tag, Sparkles, TrendingUp, Info } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Separator } from "../../ui/separator";
|
||||
|
||||
interface NotificationsScreenProps {
|
||||
onBack: () => void;
|
||||
onNavigate: (screen: string) => void;
|
||||
}
|
||||
|
||||
interface Notification {
|
||||
id: string;
|
||||
type: "order" | "appointment" | "message" | "offer" | "system";
|
||||
title: string;
|
||||
message: string;
|
||||
time: string;
|
||||
read: boolean;
|
||||
actionable?: boolean;
|
||||
category: "today" | "earlier" | "week";
|
||||
}
|
||||
|
||||
const notificationIcons = {
|
||||
order: Package,
|
||||
appointment: Calendar,
|
||||
message: MessageCircle,
|
||||
offer: Tag,
|
||||
system: Info,
|
||||
};
|
||||
|
||||
const notificationColors = {
|
||||
order: "text-accent-positive",
|
||||
appointment: "text-primary",
|
||||
message: "text-secondary",
|
||||
offer: "text-accent-warn",
|
||||
system: "text-muted-foreground",
|
||||
};
|
||||
|
||||
const notificationBgColors = {
|
||||
order: "bg-accent-positive/10",
|
||||
appointment: "bg-primary/10",
|
||||
message: "bg-secondary/10",
|
||||
offer: "bg-accent-warn/10",
|
||||
system: "bg-muted/10",
|
||||
};
|
||||
|
||||
const initialNotifications: Notification[] = [
|
||||
{
|
||||
id: "1",
|
||||
type: "message",
|
||||
title: "New message from Nova Jewels",
|
||||
message: "Aditi replied: \"We have the perfect piece for your wedding! Let me show you...\"",
|
||||
time: "5 min ago",
|
||||
read: false,
|
||||
actionable: true,
|
||||
category: "today",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "appointment",
|
||||
title: "Appointment confirmed",
|
||||
message: "Your visit to Nova Jewels is confirmed for Jan 25, 2:00 PM",
|
||||
time: "2 hours ago",
|
||||
read: false,
|
||||
actionable: true,
|
||||
category: "today",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "offer",
|
||||
title: "Exclusive Bridal Collection Sale",
|
||||
message: "Get 15% off on our new bridal collection. Valid until Jan 31st!",
|
||||
time: "5 hours ago",
|
||||
read: false,
|
||||
actionable: true,
|
||||
category: "today",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "system",
|
||||
title: "Wishlist price drop",
|
||||
message: "Heritage Bridal Ring is now ₹2.6L (was ₹2.8L)",
|
||||
time: "Yesterday",
|
||||
read: true,
|
||||
actionable: true,
|
||||
category: "earlier",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "message",
|
||||
title: "Quote received",
|
||||
message: "Nova Jewels sent you a quote for Diamond Stud Earrings",
|
||||
time: "Yesterday",
|
||||
read: true,
|
||||
actionable: true,
|
||||
category: "earlier",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
type: "appointment",
|
||||
title: "Appointment reminder",
|
||||
message: "Your appointment with Nova Jewels is tomorrow at 2:00 PM",
|
||||
time: "2 days ago",
|
||||
read: true,
|
||||
actionable: false,
|
||||
category: "earlier",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
type: "offer",
|
||||
title: "New collection launched",
|
||||
message: "Explore our stunning Festive 2025 collection now available",
|
||||
time: "3 days ago",
|
||||
read: true,
|
||||
actionable: true,
|
||||
category: "week",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
type: "system",
|
||||
title: "Profile updated",
|
||||
message: "Your preferences have been saved successfully",
|
||||
time: "4 days ago",
|
||||
read: true,
|
||||
actionable: false,
|
||||
category: "week",
|
||||
},
|
||||
];
|
||||
|
||||
export function NotificationsScreen({ onBack, onNavigate }: NotificationsScreenProps) {
|
||||
const [notifications, setNotifications] = useState<Notification[]>(initialNotifications);
|
||||
|
||||
const unreadCount = notifications.filter((n) => !n.read).length;
|
||||
|
||||
const handleMarkAllRead = () => {
|
||||
setNotifications(notifications.map((n) => ({ ...n, read: true })));
|
||||
};
|
||||
|
||||
const handleNotificationClick = (notification: Notification) => {
|
||||
// Mark as read
|
||||
setNotifications(
|
||||
notifications.map((n) =>
|
||||
n.id === notification.id ? { ...n, read: true } : n
|
||||
)
|
||||
);
|
||||
|
||||
// Navigate based on type
|
||||
if (notification.actionable) {
|
||||
switch (notification.type) {
|
||||
case "message":
|
||||
onNavigate("chat");
|
||||
break;
|
||||
case "appointment":
|
||||
onNavigate("appointments");
|
||||
break;
|
||||
case "order":
|
||||
onNavigate("orders");
|
||||
break;
|
||||
case "offer":
|
||||
onNavigate("collections");
|
||||
break;
|
||||
case "system":
|
||||
if (notification.title.includes("Wishlist")) {
|
||||
onNavigate("wishlist");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const todayNotifications = notifications.filter((n) => n.category === "today");
|
||||
const earlierNotifications = notifications.filter((n) => n.category === "earlier");
|
||||
const weekNotifications = notifications.filter((n) => n.category === "week");
|
||||
|
||||
const renderNotification = (notification: Notification) => {
|
||||
const Icon = notificationIcons[notification.type];
|
||||
const iconColor = notificationColors[notification.type];
|
||||
const bgColor = notificationBgColors[notification.type];
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={notification.id}
|
||||
className={`overflow-hidden transition-all active:scale-[0.98] ${
|
||||
notification.read ? "opacity-75" : ""
|
||||
} ${notification.actionable ? "cursor-pointer" : ""}`}
|
||||
onClick={() => notification.actionable && handleNotificationClick(notification)}
|
||||
>
|
||||
<div className="flex gap-3 p-3">
|
||||
<div className={`w-10 h-10 rounded-full ${bgColor} flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon className={`w-5 h-5 ${iconColor}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 className={`text-[14px] leading-[20px] ${!notification.read ? "font-medium" : ""}`}>
|
||||
{notification.title}
|
||||
</h3>
|
||||
{!notification.read && (
|
||||
<div className="w-2 h-2 rounded-full bg-primary flex-shrink-0 mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[13px] text-text-secondary line-clamp-2 mb-1.5">
|
||||
{notification.message}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[11px] text-text-secondary">{notification.time}</span>
|
||||
{notification.actionable && (
|
||||
<>
|
||||
<span className="text-[11px] text-text-secondary">•</span>
|
||||
<span className="text-[11px] text-primary">Tap to view</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
if (notifications.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page max-w-[390px] mx-auto pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border pt-[44px]">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<h1 className="ml-2 text-[18px] leading-[24px]">Notifications</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
<div className="flex flex-col items-center justify-center p-6 text-center min-h-[calc(100vh-200px)]">
|
||||
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-6">
|
||||
<Bell className="w-10 h-10 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-[22px] leading-[30px] mb-2">All Caught Up!</h2>
|
||||
<p className="text-[14px] text-text-secondary max-w-[280px]">
|
||||
You don't have any notifications right now. We'll notify you when something new happens.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page max-w-[390px] mx-auto pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 border-b border-border pt-[44px]">
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-[18px] leading-[24px]">Notifications</h1>
|
||||
{unreadCount > 0 && (
|
||||
<Badge className="bg-primary text-primary-foreground text-[11px] px-1.5 py-0 h-5">
|
||||
{unreadCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={handleMarkAllRead}
|
||||
className="text-[13px] text-primary hover:underline"
|
||||
>
|
||||
Mark all read
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications List */}
|
||||
<div className="p-4 space-y-6">
|
||||
{/* Today */}
|
||||
{todayNotifications.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-[13px] text-text-secondary mb-3 px-1 uppercase tracking-wider">Today</h2>
|
||||
<div className="space-y-2">
|
||||
{todayNotifications.map(renderNotification)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Earlier */}
|
||||
{earlierNotifications.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-[13px] text-text-secondary mb-3 px-1 uppercase tracking-wider">Earlier</h2>
|
||||
<div className="space-y-2">
|
||||
{earlierNotifications.map(renderNotification)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* This Week */}
|
||||
{weekNotifications.length > 0 && (
|
||||
<div>
|
||||
<h2 className="text-[13px] text-text-secondary mb-3 px-1 uppercase tracking-wider">This Week</h2>
|
||||
<div className="space-y-2">
|
||||
{weekNotifications.map(renderNotification)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Card */}
|
||||
<Card className="p-3 bg-accent/5 border-accent/20">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<Sparkles className="w-4 h-4 text-accent mt-0.5 flex-shrink-0" />
|
||||
<p className="text-[11px] text-text-secondary leading-relaxed">
|
||||
💡 <strong>Tip:</strong> Tap on notifications to view details and take actions. Manage preferences in Settings.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
99
src/components/customer/screens/OnboardingScreen.tsx
Normal file
99
src/components/customer/screens/OnboardingScreen.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useState } from "react";
|
||||
import { Sparkles, MessageCircle, Calendar, ChevronRight } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
|
||||
interface OnboardingScreenProps {
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
const slides = [
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: "Discover Curated Pieces",
|
||||
description: "Browse exclusive collections handpicked by your trusted jeweller",
|
||||
color: "text-primary",
|
||||
bgColor: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
icon: MessageCircle,
|
||||
title: "Save & Chat",
|
||||
description: "Build your wishlist and chat directly with your personal associate",
|
||||
color: "text-accent-positive",
|
||||
bgColor: "bg-accent-positive/10",
|
||||
},
|
||||
{
|
||||
icon: Calendar,
|
||||
title: "Book Try-Ons",
|
||||
description: "Schedule in-store appointments or virtual consultations at your convenience",
|
||||
color: "text-secondary",
|
||||
bgColor: "bg-secondary/10",
|
||||
},
|
||||
];
|
||||
|
||||
export function OnboardingScreen({ onComplete }: OnboardingScreenProps) {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentSlide < slides.length - 1) {
|
||||
setCurrentSlide(currentSlide + 1);
|
||||
} else {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
onComplete();
|
||||
};
|
||||
|
||||
const slide = slides[currentSlide];
|
||||
const Icon = slide.icon;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto">
|
||||
{/* Skip button */}
|
||||
<div className="flex justify-end p-4 pt-[44px]">
|
||||
<Button variant="ghost" onClick={handleSkip}>
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6 pb-24">
|
||||
<div className={`w-32 h-32 rounded-[32px] ${slide.bgColor} flex items-center justify-center mb-12`}>
|
||||
<Icon className={`w-16 h-16 ${slide.color}`} />
|
||||
</div>
|
||||
|
||||
<h2 className="text-[28px] leading-[36px] text-center mb-4">
|
||||
{slide.title}
|
||||
</h2>
|
||||
|
||||
<p className="text-[16px] leading-[24px] text-text-secondary text-center max-w-[300px]">
|
||||
{slide.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bottom section */}
|
||||
<div className="p-6 pb-[calc(34px+24px)] space-y-6">
|
||||
{/* Pagination dots */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{slides.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
index === currentSlide
|
||||
? "w-8 bg-primary"
|
||||
: "w-2 bg-border"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Next button */}
|
||||
<Button onClick={handleNext} className="w-full" size="lg">
|
||||
{currentSlide === slides.length - 1 ? "Get Started" : "Next"}
|
||||
<ChevronRight className="w-5 h-5 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
101
src/components/customer/screens/OrdersScreen.tsx
Normal file
101
src/components/customer/screens/OrdersScreen.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { ArrowLeft, CheckCircle, Package, Truck } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
interface OrdersScreenProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const orders = [
|
||||
{
|
||||
id: "O-2025-0048",
|
||||
date: "Jan 12, 2025",
|
||||
items: 2,
|
||||
total: "₹4.2L",
|
||||
status: "Under Making",
|
||||
milestone: 2,
|
||||
expectedDate: "Feb 5, 2025",
|
||||
},
|
||||
{
|
||||
id: "O-2025-0039",
|
||||
date: "Dec 28, 2024",
|
||||
items: 1,
|
||||
total: "₹2.8L",
|
||||
status: "Ready",
|
||||
milestone: 3,
|
||||
expectedDate: "Jan 18, 2025",
|
||||
},
|
||||
];
|
||||
|
||||
const milestones = ["Confirmed", "Under Making", "Ready"];
|
||||
|
||||
export function OrdersScreen({ onBack }: OrdersScreenProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border sticky top-0 z-10 pt-[44px]">
|
||||
<div className="flex items-center gap-3 h-14 px-4">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<h1 className="text-[18px] leading-[28px]">Orders</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Orders List */}
|
||||
<div className="p-4 space-y-3">
|
||||
{orders.map((order) => (
|
||||
<Card key={order.id} className="p-4 space-y-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-[16px] leading-[24px] mb-1">{order.id}</p>
|
||||
<p className="text-[14px] text-text-secondary">{order.date}</p>
|
||||
</div>
|
||||
<Badge className="bg-primary">{order.status}</Badge>
|
||||
</div>
|
||||
|
||||
{/* Progress Tracker */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{milestones.map((milestone, index) => (
|
||||
<div key={index} className="flex flex-col items-center gap-2 flex-1">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
index < order.milestone
|
||||
? "bg-accent-positive text-white"
|
||||
: index === order.milestone
|
||||
? "bg-primary text-white"
|
||||
: "bg-muted text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{index < order.milestone ? (
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
) : index === order.milestone ? (
|
||||
<Package className="w-5 h-5" />
|
||||
) : (
|
||||
<span className="text-[14px]">{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] text-center text-text-secondary">
|
||||
{milestone}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-[14px] text-text-secondary">
|
||||
<Truck className="w-4 h-4" />
|
||||
<span>Expected by {order.expectedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-border">
|
||||
<span className="text-[14px] text-text-secondary">{order.items} items</span>
|
||||
<span className="text-[18px]">{order.total}</span>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
245
src/components/customer/screens/ProductDetailScreen.tsx
Normal file
245
src/components/customer/screens/ProductDetailScreen.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { ArrowLeft, Heart, Share2, MessageCircle, Play, Calendar, Info, Package, Shield, TrendingUp, Star } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Separator } from "../../ui/separator";
|
||||
import { copyWithToast } from "../../utils/clipboard";
|
||||
|
||||
interface ProductDetailScreenProps {
|
||||
productId: string | null;
|
||||
isWishlisted: boolean;
|
||||
onAddToWishlist: () => void;
|
||||
onRemoveFromWishlist: () => void;
|
||||
onInquire: () => void;
|
||||
onBookAppointment: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const productDetails: Record<string, any> = {
|
||||
"1": {
|
||||
name: "Heritage Bridal Ring",
|
||||
price: "₹2.8L",
|
||||
sku: "AU-22K-BR-0192",
|
||||
metal: "Gold 22K, Rose",
|
||||
weight: "6.8g",
|
||||
stone: "Diamonds 0.24ct",
|
||||
delivery: "Ready to ship",
|
||||
making: "₹12,000",
|
||||
description: "Exquisite handcrafted bridal ring featuring intricate traditional motifs with modern elegance. Perfect for your special day.",
|
||||
purity: "916 Hallmark",
|
||||
rating: 4.8,
|
||||
reviews: 124,
|
||||
inStock: true,
|
||||
},
|
||||
"2": {
|
||||
name: "Aurora Diamond Pendant",
|
||||
price: "₹45K",
|
||||
sku: "AU-18K-PE-0245",
|
||||
metal: "18K White Gold",
|
||||
weight: "3.2g",
|
||||
stone: "Diamonds 0.15ct",
|
||||
delivery: "Ready to ship",
|
||||
making: "₹4,500",
|
||||
description: "Elegant diamond pendant with a minimalist design, perfect for everyday wear or special occasions.",
|
||||
purity: "750 Hallmark",
|
||||
rating: 4.9,
|
||||
reviews: 89,
|
||||
inStock: true,
|
||||
},
|
||||
// Add more as needed
|
||||
};
|
||||
|
||||
export function ProductDetailScreen({
|
||||
productId,
|
||||
isWishlisted,
|
||||
onAddToWishlist,
|
||||
onRemoveFromWishlist,
|
||||
onInquire,
|
||||
onBookAppointment,
|
||||
onBack,
|
||||
}: ProductDetailScreenProps) {
|
||||
const product = productDetails[productId || "1"] || productDetails["1"];
|
||||
|
||||
const handleShare = async () => {
|
||||
await copyWithToast(`Check out ${product.name} at Nova Jewels`, "Product link copied!");
|
||||
};
|
||||
|
||||
const toggleWishlist = () => {
|
||||
if (isWishlisted) {
|
||||
onRemoveFromWishlist();
|
||||
} else {
|
||||
onAddToWishlist();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-background/95 backdrop-blur border-b border-border sticky top-0 z-10 pt-[44px]">
|
||||
<div className="flex items-center justify-between h-14 px-4">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleWishlist}
|
||||
className="p-2 hover:bg-accent/10 rounded-[10px]"
|
||||
>
|
||||
<Heart className={`w-6 h-6 ${isWishlisted ? "fill-destructive text-destructive" : ""}`} />
|
||||
</button>
|
||||
<button onClick={handleShare} className="p-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<Share2 className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Media Gallery */}
|
||||
<div className="relative">
|
||||
<div className="aspect-square bg-gradient-to-br from-secondary/20 to-primary/20 flex items-center justify-center">
|
||||
<Play className="w-16 h-16 text-primary/50" />
|
||||
</div>
|
||||
<div className="absolute top-4 left-4 flex flex-col gap-2">
|
||||
<Badge className="bg-primary">360° View</Badge>
|
||||
{!product.inStock && (
|
||||
<Badge className="bg-accent-warn text-accent-warn-foreground">Made to Order</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="p-4 space-y-4 pb-[calc(160px)]">
|
||||
<div>
|
||||
<h1 className="text-[24px] leading-[32px] mb-2">{product.name}</h1>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="w-4 h-4 fill-secondary text-secondary" />
|
||||
<span className="text-[14px] font-medium">{product.rating}</span>
|
||||
</div>
|
||||
<span className="text-[14px] text-text-secondary">({product.reviews} reviews)</span>
|
||||
<span>•</span>
|
||||
<span className="text-[13px] text-text-secondary">SKU: {product.sku}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2 mb-4">
|
||||
<p className="text-[28px] leading-[36px] font-medium text-primary">{product.price}</p>
|
||||
<Badge variant="outline" className={product.inStock ? "text-accent-positive border-accent-positive/30 bg-accent-positive/5" : "text-accent-warn border-accent-warn/30 bg-accent-warn/5"}>
|
||||
{product.inStock ? "In Stock" : "Made to Order"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-[14px] text-text-secondary leading-[22px]">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Info Cards */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Card className="p-3 text-center">
|
||||
<Shield className="w-5 h-5 text-accent-positive mx-auto mb-1" />
|
||||
<p className="text-[11px] text-text-secondary">BIS Hallmark</p>
|
||||
<p className="text-[13px] font-medium">{product.purity}</p>
|
||||
</Card>
|
||||
<Card className="p-3 text-center">
|
||||
<Package className="w-5 h-5 text-primary mx-auto mb-1" />
|
||||
<p className="text-[11px] text-text-secondary">Delivery</p>
|
||||
<p className="text-[13px] font-medium">{product.delivery}</p>
|
||||
</Card>
|
||||
<Card className="p-3 text-center">
|
||||
<TrendingUp className="w-5 h-5 text-secondary mx-auto mb-1" />
|
||||
<p className="text-[11px] text-text-secondary">Making</p>
|
||||
<p className="text-[13px] font-medium">{product.making}</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Specs */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-[17px] leading-[24px] mb-3 flex items-center gap-2">
|
||||
<Info className="w-4 h-4 text-primary" />
|
||||
Specifications
|
||||
</h3>
|
||||
<div className="space-y-2.5 text-[14px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Metal</span>
|
||||
<span className="font-medium">{product.metal}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Weight</span>
|
||||
<span className="font-medium">{product.weight}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Stone</span>
|
||||
<span className="font-medium">{product.stone}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Purity</span>
|
||||
<span className="font-medium">{product.purity}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Why Buy Section */}
|
||||
<Card className="p-4 bg-primary/5 border-primary/20">
|
||||
<h3 className="text-[15px] leading-[24px] mb-3 font-medium">Why Choose This Piece?</h3>
|
||||
<ul className="space-y-2 text-[13px]">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-accent-positive mt-0.5">✓</span>
|
||||
<span>Certified by BIS with hallmark guarantee</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-accent-positive mt-0.5">✓</span>
|
||||
<span>Free lifetime maintenance and cleaning</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-accent-positive mt-0.5">✓</span>
|
||||
<span>7-day return policy with full refund</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-accent-positive mt-0.5">✓</span>
|
||||
<span>Customization available on request</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
{/* Customer Reviews Teaser */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-[17px] leading-[24px]">Customer Reviews</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="w-4 h-4 fill-secondary text-secondary" />
|
||||
<span className="text-[15px] font-medium">{product.rating}</span>
|
||||
<span className="text-[13px] text-text-secondary">/ 5</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[13px] text-text-secondary mb-3">
|
||||
Based on {product.reviews} verified purchases
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
View All Reviews
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="fixed bottom-[calc(56px+34px)] left-0 right-0 max-w-[390px] mx-auto bg-background border-t border-border shadow-lg">
|
||||
<div className="p-4 space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onInquire} className="flex-1" size="lg">
|
||||
<MessageCircle className="w-5 h-5 mr-2" />
|
||||
Chat to Inquire
|
||||
</Button>
|
||||
<Button onClick={onBookAppointment} variant="outline" size="lg">
|
||||
<Calendar className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[11px] text-center text-text-secondary">
|
||||
Get personalized assistance • Book a visit to see in person
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
214
src/components/customer/screens/ProductGridScreen.tsx
Normal file
214
src/components/customer/screens/ProductGridScreen.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { ArrowLeft, Filter, SlidersHorizontal, Heart, Sparkles, Star, TrendingUp } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
interface ProductGridScreenProps {
|
||||
collectionId: string | null;
|
||||
onProductClick: (id: string) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Heritage Bridal Ring",
|
||||
price: "₹2.8L",
|
||||
metal: "22K Gold",
|
||||
weight: "6.8g",
|
||||
inStock: true,
|
||||
rating: 4.8,
|
||||
trending: true,
|
||||
image: "from-secondary/30 to-secondary/10"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Aurora Diamond Pendant",
|
||||
price: "₹45K",
|
||||
metal: "18K White Gold",
|
||||
weight: "3.2g",
|
||||
inStock: true,
|
||||
rating: 4.9,
|
||||
trending: false,
|
||||
image: "from-primary/30 to-primary/10"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Classic Bangle Set",
|
||||
price: "₹1.2L",
|
||||
metal: "22K Gold",
|
||||
weight: "42g",
|
||||
inStock: true,
|
||||
rating: 4.7,
|
||||
trending: false,
|
||||
image: "from-accent-warn/30 to-accent-warn/10"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Diamond Stud Earrings",
|
||||
price: "₹68K",
|
||||
metal: "18K Gold",
|
||||
weight: "4.5g",
|
||||
inStock: true,
|
||||
rating: 4.6,
|
||||
trending: true,
|
||||
image: "from-primary/25 to-accent-positive/20"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Temple Necklace",
|
||||
price: "₹3.4L",
|
||||
metal: "22K Gold",
|
||||
weight: "65g",
|
||||
inStock: false,
|
||||
rating: 4.9,
|
||||
trending: false,
|
||||
image: "from-secondary/25 to-accent-positive/15"
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Daily Wear Chain",
|
||||
price: "₹28K",
|
||||
metal: "18K Gold",
|
||||
weight: "8.2g",
|
||||
inStock: true,
|
||||
rating: 4.5,
|
||||
trending: false,
|
||||
image: "from-muted/40 to-primary/20"
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "Pearl Drop Earrings",
|
||||
price: "₹38K",
|
||||
metal: "18K Gold",
|
||||
weight: "5.5g",
|
||||
inStock: true,
|
||||
rating: 4.8,
|
||||
trending: false,
|
||||
image: "from-accent-positive/20 to-primary/15"
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
name: "Emerald Cocktail Ring",
|
||||
price: "₹95K",
|
||||
metal: "18K Gold",
|
||||
weight: "8.2g",
|
||||
inStock: true,
|
||||
rating: 4.7,
|
||||
trending: true,
|
||||
image: "from-accent-positive/30 to-primary/15"
|
||||
},
|
||||
];
|
||||
|
||||
export function ProductGridScreen({ collectionId, onProductClick, onBack }: ProductGridScreenProps) {
|
||||
return (
|
||||
<div className="pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border sticky top-0 z-10 pt-[44px]">
|
||||
<div className="flex items-center justify-between h-14 px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-[18px] leading-[24px]">Bridal Collection</h1>
|
||||
<p className="text-[11px] text-text-secondary">{products.length} pieces</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="p-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort/Filter Bar */}
|
||||
<div className="bg-bg-surface border-b border-border px-4 py-3 flex items-center gap-2 overflow-x-auto">
|
||||
<Button variant="outline" size="sm" className="flex-shrink-0">
|
||||
<SlidersHorizontal className="w-3 h-3 mr-1" />
|
||||
All Filters
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-shrink-0">
|
||||
Price
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-shrink-0">
|
||||
Metal Type
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-shrink-0">
|
||||
In Stock
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Product Grid */}
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{products.map((product) => (
|
||||
<Card
|
||||
key={product.id}
|
||||
className="overflow-hidden cursor-pointer hover:shadow-lg transition-all active:scale-[0.98]"
|
||||
onClick={() => onProductClick(product.id)}
|
||||
>
|
||||
<div className="relative">
|
||||
<div className={`aspect-square bg-gradient-to-br ${product.image} flex items-center justify-center`}>
|
||||
<Sparkles className="w-10 h-10 text-text-secondary/30" />
|
||||
</div>
|
||||
{product.trending && (
|
||||
<div className="absolute top-2 left-2">
|
||||
<Badge className="bg-secondary text-secondary-foreground text-[10px] px-1.5 py-0 flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
Trending
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{!product.inStock && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<Badge variant="outline" className="bg-background/90 text-[10px] px-1.5 py-0">
|
||||
MTO
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="absolute top-2 right-2 w-8 h-8 rounded-full bg-background/90 backdrop-blur flex items-center justify-center hover:bg-background transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Toggle wishlist
|
||||
}}
|
||||
>
|
||||
<Heart className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-3 space-y-1.5">
|
||||
<h3 className="text-[14px] leading-[18px] line-clamp-2 min-h-[36px]">{product.name}</h3>
|
||||
<div className="flex items-center gap-1 text-[11px] text-text-secondary">
|
||||
<span>{product.metal}</span>
|
||||
<span>•</span>
|
||||
<span>{product.weight}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<Star className="w-3 h-3 fill-secondary text-secondary" />
|
||||
<span className="text-[12px] font-medium">{product.rating}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<p className="text-[16px] font-medium text-primary">{product.price}</p>
|
||||
{product.inStock && (
|
||||
<Badge variant="outline" className="text-[9px] px-1.5 py-0 bg-accent-positive/10 text-accent-positive border-accent-positive/20">
|
||||
In Stock
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Info */}
|
||||
<div className="px-4 pb-4">
|
||||
<Card className="p-3 text-center bg-accent/5 border-accent/20">
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
Showing {products.length} pieces • Scroll up for filters
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
221
src/components/customer/screens/ProfileSetupScreen.tsx
Normal file
221
src/components/customer/screens/ProfileSetupScreen.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { useState } from "react";
|
||||
import { User, Tag, DollarSign, ArrowRight, CheckCircle } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Card } from "../../ui/card";
|
||||
|
||||
interface ProfileSetupScreenProps {
|
||||
onComplete: (preferences: any) => void;
|
||||
}
|
||||
|
||||
const occasions = ["Bridal", "Daily Wear", "Festive", "Gifting"];
|
||||
const categories = ["Rings", "Bangles", "Necklaces", "Earrings", "Sets"];
|
||||
const budgetRanges = ["₹25k-₹50k", "₹50k-₹1L", "₹1L-₹2L", "₹2L+"];
|
||||
|
||||
export function ProfileSetupScreen({ onComplete }: ProfileSetupScreenProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [name, setName] = useState("");
|
||||
const [selectedOccasions, setSelectedOccasions] = useState<string[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [selectedBudget, setSelectedBudget] = useState<string>("");
|
||||
|
||||
const toggleSelection = (item: string, list: string[], setList: (list: string[]) => void) => {
|
||||
if (list.includes(item)) {
|
||||
setList(list.filter((i) => i !== item));
|
||||
} else {
|
||||
setList([...list, item]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (step < 3) {
|
||||
setStep(step + 1);
|
||||
} else {
|
||||
onComplete({
|
||||
name,
|
||||
occasions: selectedOccasions,
|
||||
categories: selectedCategories,
|
||||
budgetRange: selectedBudget,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const canProceed = () => {
|
||||
if (step === 1) return name.length > 0;
|
||||
if (step === 2) return selectedOccasions.length > 0 && selectedCategories.length > 0;
|
||||
if (step === 3) return selectedBudget.length > 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex flex-col max-w-[390px] mx-auto">
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-[44px] pb-4 border-b border-border bg-background">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-[18px] leading-[28px]">Profile Setup</h1>
|
||||
<span className="text-[14px] text-text-secondary">Step {step} of 3</span>
|
||||
</div>
|
||||
{/* Progress bar */}
|
||||
<div className="h-1 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300"
|
||||
style={{ width: `${(step / 3) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 p-6 space-y-6">
|
||||
{step === 1 && (
|
||||
<>
|
||||
<div className="flex flex-col items-center text-center space-y-3 py-4">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<User className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[22px] leading-[30px] mb-2">Welcome!</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Let's personalize your experience
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Your Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter your name"
|
||||
className="text-[16px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 bg-accent-positive/5 border-accent-positive/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-accent-positive mt-0.5 flex-shrink-0" />
|
||||
<div className="text-[14px]">
|
||||
<p className="mb-1">You're connected with</p>
|
||||
<p>Associate: <strong>Aditi Rao</strong> (Nova Jewels)</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-primary/10 flex items-center justify-center">
|
||||
<Tag className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[22px] leading-[30px]">Your Preferences</h2>
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Help us curate the perfect collection
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Shopping Occasions</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{occasions.map((occasion) => (
|
||||
<button
|
||||
key={occasion}
|
||||
onClick={() => toggleSelection(occasion, selectedOccasions, setSelectedOccasions)}
|
||||
className={`px-4 py-2 rounded-full text-[14px] border transition-colors ${
|
||||
selectedOccasions.includes(occasion)
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background border-border hover:border-primary"
|
||||
}`}
|
||||
>
|
||||
{occasion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Interested Categories</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => toggleSelection(category, selectedCategories, setSelectedCategories)}
|
||||
className={`px-4 py-2 rounded-full text-[14px] border transition-colors ${
|
||||
selectedCategories.includes(category)
|
||||
? "bg-primary text-primary-foreground border-primary"
|
||||
: "bg-background border-border hover:border-primary"
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-[16px] bg-secondary/10 flex items-center justify-center">
|
||||
<DollarSign className="w-6 h-6 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[22px] leading-[30px]">Budget Range</h2>
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
We'll show you relevant pieces
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{budgetRanges.map((range) => (
|
||||
<Card
|
||||
key={range}
|
||||
className={`p-4 cursor-pointer transition-all border-2 ${
|
||||
selectedBudget === range
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
}`}
|
||||
onClick={() => setSelectedBudget(range)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[16px]">{range}</span>
|
||||
{selectedBudget === range && (
|
||||
<CheckCircle className="w-5 h-5 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom CTA */}
|
||||
<div className="p-6 pb-[calc(34px+24px)]">
|
||||
<Button
|
||||
onClick={handleNext}
|
||||
disabled={!canProceed()}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{step === 3 ? "Complete Setup" : "Continue"}
|
||||
<ArrowRight className="w-5 h-5 ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
95
src/components/customer/screens/QuotesScreen.tsx
Normal file
95
src/components/customer/screens/QuotesScreen.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ArrowLeft, Clock, CheckCircle, FileText } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
interface QuotesScreenProps {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const quotes = [
|
||||
{
|
||||
id: "Q-2025-0912",
|
||||
date: "Jan 15, 2025",
|
||||
items: 2,
|
||||
total: "₹4.2L",
|
||||
status: "pending",
|
||||
expiresIn: "3 days",
|
||||
},
|
||||
{
|
||||
id: "Q-2025-0913",
|
||||
date: "Jan 10, 2025",
|
||||
items: 1,
|
||||
total: "₹2.8L",
|
||||
status: "approved",
|
||||
},
|
||||
];
|
||||
|
||||
export function QuotesScreen({ onBack }: QuotesScreenProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="bg-background border-b border-border sticky top-0 z-10 pt-[44px]">
|
||||
<div className="flex items-center gap-3 h-14 px-4">
|
||||
<button onClick={onBack} className="p-2 -ml-2 hover:bg-accent/10 rounded-[10px]">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<h1 className="text-[18px] leading-[28px]">Quotations</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quotes List */}
|
||||
<div className="p-4 space-y-3">
|
||||
{quotes.map((quote) => (
|
||||
<Card key={quote.id} className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-[16px] leading-[24px] mb-1">{quote.id}</p>
|
||||
<p className="text-[14px] text-text-secondary">{quote.date}</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant={quote.status === "approved" ? "default" : "outline"}
|
||||
className={quote.status === "approved" ? "bg-accent-positive" : ""}
|
||||
>
|
||||
{quote.status === "pending" ? (
|
||||
<>
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
Pending
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Approved
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-[14px]">
|
||||
<span className="text-text-secondary">{quote.items} items</span>
|
||||
<span className="text-[18px]">{quote.total}</span>
|
||||
</div>
|
||||
|
||||
{quote.status === "pending" && quote.expiresIn && (
|
||||
<div className="text-[12px] text-accent-warn">
|
||||
Expires in {quote.expiresIn}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-2 border-t border-border">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<FileText className="w-4 h-4 mr-1" />
|
||||
View Details
|
||||
</Button>
|
||||
{quote.status === "pending" && (
|
||||
<Button size="sm" className="flex-1 bg-accent-positive hover:bg-accent-positive/90">
|
||||
Approve
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
264
src/components/customer/screens/RetailerStorefrontScreen.tsx
Normal file
264
src/components/customer/screens/RetailerStorefrontScreen.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { Store, Search, Heart, Phone, MapPin, Clock, Star, ChevronRight, Package, MessageCircle } from "lucide-react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import { Separator } from "../../ui/separator";
|
||||
|
||||
interface RetailerStorefrontScreenProps {
|
||||
onBack: () => void;
|
||||
retailerId: string;
|
||||
}
|
||||
|
||||
const retailerData: Record<string, any> = {
|
||||
"1": {
|
||||
name: "Nova Jewels",
|
||||
location: "Linking Road, Bandra West, Mumbai - 400050",
|
||||
phone: "+91 98765 43210",
|
||||
rating: 4.8,
|
||||
reviews: 284,
|
||||
theme: { primary: "#D4AF37", secondary: "#0B0D10" },
|
||||
timing: "Mon-Sat: 11AM - 8PM, Sun: 12PM - 6PM",
|
||||
description: "Experience the timeless elegance of Nova Jewels, where every piece is meticulously crafted to perfection. Our collection features stunning designs that blend traditional artistry with contemporary aesthetics.",
|
||||
},
|
||||
"2": {
|
||||
name: "Sparkle Gems",
|
||||
location: "Connaught Place, New Delhi - 110001",
|
||||
phone: "+91 98111 22333",
|
||||
rating: 4.6,
|
||||
reviews: 192,
|
||||
theme: { primary: "#5B8DEF", secondary: "#F4B400" },
|
||||
timing: "Mon-Sun: 10:30AM - 9PM",
|
||||
description: "Modern jewellery boutique offering contemporary designs with traditional craftsmanship. Specializing in diamond and platinum collections.",
|
||||
},
|
||||
"3": {
|
||||
name: "Golden Heritage",
|
||||
location: "MI Road, Jaipur - 302001",
|
||||
phone: "+91 98444 55666",
|
||||
rating: 4.9,
|
||||
reviews: 346,
|
||||
theme: { primary: "#7C3AED", secondary: "#EC4899" },
|
||||
timing: "Mon-Sat: 10AM - 8PM, Sun: Closed",
|
||||
description: "Royal Rajasthani jewellery with traditional Kundan and Meenakari work. Three generations of heritage craftsmanship.",
|
||||
},
|
||||
};
|
||||
|
||||
const collections = [
|
||||
{ id: "1", name: "Bridal 2025", items: 48, image: null },
|
||||
{ id: "2", name: "Daily Wear", items: 124, image: null },
|
||||
{ id: "3", name: "Festive Special", items: 67, image: null },
|
||||
{ id: "4", name: "Diamond Collection", items: 89, image: null },
|
||||
];
|
||||
|
||||
const products = [
|
||||
{ id: "1", name: "Heritage Bridal Ring", price: "₹2.8L", category: "Rings" },
|
||||
{ id: "2", name: "Classic Gold Necklace", price: "₹1.8L", category: "Necklaces" },
|
||||
{ id: "3", name: "Diamond Studs", price: "₹68K", category: "Earrings" },
|
||||
{ id: "4", name: "Gold Bangle Set", price: "₹1.2L", category: "Bangles" },
|
||||
{ id: "5", name: "Pearl Pendant", price: "₹45K", category: "Pendants" },
|
||||
{ id: "6", name: "Designer Chain", price: "₹95K", category: "Chains" },
|
||||
];
|
||||
|
||||
export function RetailerStorefrontScreen({ onBack, retailerId }: RetailerStorefrontScreenProps) {
|
||||
const retailer = retailerData[retailerId] || retailerData["1"];
|
||||
const theme = retailer.theme;
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bg-page flex flex-col">
|
||||
{/* Header with Theme Colors */}
|
||||
<div
|
||||
className="text-white px-4 py-6"
|
||||
style={{ background: `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary} 100%)` }}
|
||||
>
|
||||
<button onClick={onBack} className="text-[14px] mb-3 opacity-90 hover:opacity-100">
|
||||
← Back
|
||||
</button>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-16 h-16 rounded-lg bg-white/20 backdrop-blur-sm flex items-center justify-center flex-shrink-0">
|
||||
<Store className="w-8 h-8" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-[24px] leading-[32px] mb-1">{retailer.name}</h1>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Star className="w-4 h-4 fill-current" />
|
||||
<span className="text-[13px]">{retailer.rating}</span>
|
||||
</div>
|
||||
<span className="text-[12px] opacity-75">({retailer.reviews} reviews)</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="secondary" className="text-[10px] bg-white/20 border-white/30">
|
||||
Open Now
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-[10px] bg-white/20 border-white/30">
|
||||
Verified
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* Quick Actions */}
|
||||
<div className="p-4 bg-background border-b border-border">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<button className="p-3 rounded-lg border border-border hover:border-primary transition-colors text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Phone className="w-5 h-5" style={{ color: theme.primary }} />
|
||||
<span className="text-[11px]">Call</span>
|
||||
</div>
|
||||
</button>
|
||||
<button className="p-3 rounded-lg border border-border hover:border-primary transition-colors text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<MapPin className="w-5 h-5" style={{ color: theme.primary }} />
|
||||
<span className="text-[11px]">Directions</span>
|
||||
</div>
|
||||
</button>
|
||||
<button className="p-3 rounded-lg border border-border hover:border-primary transition-colors text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<MessageCircle className="w-5 h-5" style={{ color: theme.primary }} />
|
||||
<span className="text-[11px]">Chat</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search jewellery..."
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="collections" className="px-4">
|
||||
<TabsList className="w-full grid grid-cols-3">
|
||||
<TabsTrigger value="collections">Collections</TabsTrigger>
|
||||
<TabsTrigger value="products">Products</TabsTrigger>
|
||||
<TabsTrigger value="about">About</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="collections" className="space-y-3 mt-4">
|
||||
{collections.map((collection) => (
|
||||
<Card key={collection.id} className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-16 h-16 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ backgroundColor: `${theme.primary}20` }}
|
||||
>
|
||||
<Package className="w-8 h-8" style={{ color: theme.primary }} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[15px] font-medium mb-0.5">{collection.name}</h3>
|
||||
<p className="text-[12px] text-muted-foreground">{collection.items} items</p>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="products" className="space-y-3 mt-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{products.map((product) => (
|
||||
<Card key={product.id} className="overflow-hidden">
|
||||
<div
|
||||
className="aspect-square flex items-center justify-center"
|
||||
style={{ backgroundColor: `${theme.primary}10` }}
|
||||
>
|
||||
<Package className="w-12 h-12 text-muted-foreground/50" />
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<p className="text-[13px] font-medium line-clamp-2 mb-1">{product.name}</p>
|
||||
<p className="text-[13px]" style={{ color: theme.primary }}>
|
||||
{product.price}
|
||||
</p>
|
||||
<Badge variant="outline" className="text-[10px] mt-1">
|
||||
{product.category}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="about" className="space-y-4 mt-4">
|
||||
<Card className="p-4">
|
||||
<h3 className="text-[15px] font-medium mb-2">About Us</h3>
|
||||
<p className="text-[13px] text-muted-foreground leading-relaxed">
|
||||
{retailer.description}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="text-[15px] font-medium mb-3">Store Information</h3>
|
||||
<div className="space-y-3 text-[13px]">
|
||||
<div className="flex items-start gap-2">
|
||||
<MapPin className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{retailer.location}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Phone className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{retailer.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<Clock className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{retailer.timing}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<h3 className="text-[15px] font-medium mb-2">Services</h3>
|
||||
<div className="space-y-2 text-[13px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: theme.primary }} />
|
||||
<span>Custom Design Consultation</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: theme.primary }} />
|
||||
<span>Gold Exchange</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: theme.primary }} />
|
||||
<span>Free Cleaning & Polish</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: theme.primary }} />
|
||||
<span>Lifetime Warranty</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 rounded-full" style={{ backgroundColor: theme.primary }} />
|
||||
<span>Video Call Consultation</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Bottom padding for fixed button */}
|
||||
<div className="h-24" />
|
||||
</div>
|
||||
|
||||
{/* Fixed Bottom Actions */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 bg-background border-t border-border">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
<Heart className="w-4 h-4 mr-1" />
|
||||
Save Store
|
||||
</Button>
|
||||
<Button size="sm" className="flex-1" style={{ backgroundColor: theme.primary }}>
|
||||
<Phone className="w-4 h-4 mr-1" />
|
||||
Book Appointment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/customer/screens/SplashScreen.tsx
Normal file
18
src/components/customer/screens/SplashScreen.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function SplashScreen() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary/10 to-secondary/10 flex flex-col items-center justify-center max-w-[390px] mx-auto p-6">
|
||||
<div className="w-24 h-24 rounded-[24px] bg-primary flex items-center justify-center mb-6 shadow-lg">
|
||||
<span className="text-primary-foreground text-[48px]">H</span>
|
||||
</div>
|
||||
<h1 className="text-[32px] leading-[40px] text-center mb-3">
|
||||
Hello Jewellers
|
||||
</h1>
|
||||
<p className="text-[16px] text-text-secondary text-center max-w-[280px]">
|
||||
Fine jewellery, curated by your retailer
|
||||
</p>
|
||||
<div className="mt-12">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
218
src/components/customer/screens/WishlistScreen.tsx
Normal file
218
src/components/customer/screens/WishlistScreen.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Heart, X, Package, Sparkles, Tag, AlertCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
interface WishlistScreenProps {
|
||||
wishlistIds: string[];
|
||||
onProductClick: (id: string) => void;
|
||||
onRemoveFromWishlist: (id: string) => void;
|
||||
}
|
||||
|
||||
const wishlistItems = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Heritage Bridal Ring",
|
||||
price: "₹2.8L",
|
||||
metal: "22K Gold",
|
||||
weight: "6.8g",
|
||||
note: "Perfect for wedding ceremony",
|
||||
inStock: true,
|
||||
image: "from-secondary/30 to-secondary/10"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Aurora Diamond Pendant",
|
||||
price: "₹45K",
|
||||
metal: "18K White Gold",
|
||||
weight: "3.2g",
|
||||
note: "Anniversary gift idea",
|
||||
inStock: true,
|
||||
image: "from-primary/30 to-primary/10"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Classic Bangle Set (6pcs)",
|
||||
price: "₹1.2L",
|
||||
metal: "22K Gold",
|
||||
weight: "42g",
|
||||
note: "Gift for mom's birthday",
|
||||
inStock: true,
|
||||
image: "from-accent-warn/30 to-accent-warn/10"
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Temple Jewellery Necklace",
|
||||
price: "₹3.4L",
|
||||
metal: "22K Gold",
|
||||
weight: "65g",
|
||||
note: "",
|
||||
inStock: false,
|
||||
image: "from-secondary/25 to-accent-positive/15"
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
name: "Diamond Stud Earrings",
|
||||
price: "₹68K",
|
||||
metal: "18K Gold",
|
||||
weight: "4.5g",
|
||||
note: "Daily wear collection",
|
||||
inStock: true,
|
||||
image: "from-primary/25 to-accent-positive/20"
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
name: "Emerald Cocktail Ring",
|
||||
price: "₹95K",
|
||||
metal: "18K Gold",
|
||||
weight: "8.2g",
|
||||
note: "",
|
||||
inStock: true,
|
||||
image: "from-accent-positive/30 to-primary/15"
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
name: "Pearl Drop Earrings",
|
||||
price: "₹28K",
|
||||
metal: "18K Gold",
|
||||
weight: "5.5g",
|
||||
note: "Office wear - elegant",
|
||||
inStock: true,
|
||||
image: "from-muted/40 to-primary/20"
|
||||
},
|
||||
];
|
||||
|
||||
export function WishlistScreen({ wishlistIds, onProductClick, onRemoveFromWishlist }: WishlistScreenProps) {
|
||||
const items = wishlistItems.filter((item) => wishlistIds.includes(item.id));
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-200px)] p-6 text-center">
|
||||
<div className="w-24 h-24 rounded-full bg-accent/10 flex items-center justify-center mb-6">
|
||||
<Heart className="w-12 h-12 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-[22px] leading-[30px] mb-2">Your Wishlist is Empty</h2>
|
||||
<p className="text-[16px] text-text-secondary mb-6 max-w-[280px]">
|
||||
Browse collections and save your favorite pieces here
|
||||
</p>
|
||||
<Button>Browse Collections</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalValue = items.reduce((sum, item) => {
|
||||
const price = parseFloat(item.price.replace(/[₹,LK]/g, ""));
|
||||
const multiplier = item.price.includes("L") ? 100000 : 1000;
|
||||
return sum + (price * multiplier);
|
||||
}, 0);
|
||||
|
||||
const formatPrice = (value: number) => {
|
||||
if (value >= 100000) {
|
||||
return `₹${(value / 100000).toFixed(1)}L`;
|
||||
}
|
||||
return `₹${(value / 1000).toFixed(0)}K`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-[44px] pb-6">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-br from-accent-error/5 to-primary/5 border-b border-border px-4 py-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Heart className="w-5 h-5 text-accent-error" />
|
||||
<h1 className="text-[20px] leading-[28px]">My Wishlist</h1>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[14px] text-text-secondary">{items.length} items saved</p>
|
||||
{items.length > 0 && (
|
||||
<div className="text-right">
|
||||
<p className="text-[12px] text-text-secondary">Total Value</p>
|
||||
<p className="text-[16px] font-medium text-primary">{formatPrice(totalValue)}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Wishlist Items */}
|
||||
<div className="p-4 space-y-3">
|
||||
{items.map((item) => (
|
||||
<Card key={item.id} className="overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div
|
||||
className="flex gap-3 cursor-pointer p-3"
|
||||
onClick={() => onProductClick(item.id)}
|
||||
>
|
||||
<div className={`w-28 h-28 bg-gradient-to-br ${item.image} flex-shrink-0 rounded-[12px] flex items-center justify-center`}>
|
||||
<Sparkles className="w-8 h-8 text-text-secondary/30" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-between py-1">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 className="text-[15px] leading-[20px] line-clamp-2">{item.name}</h3>
|
||||
{!item.inStock && (
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0 bg-accent-warn/10 text-accent-warn border-accent-warn/20 flex-shrink-0">
|
||||
MTO
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[12px] text-text-secondary mb-1">
|
||||
<span>{item.metal}</span>
|
||||
<span>•</span>
|
||||
<span>{item.weight}</span>
|
||||
</div>
|
||||
<p className="text-[17px] font-medium text-primary">{item.price}</p>
|
||||
</div>
|
||||
{item.note && (
|
||||
<div className="flex items-start gap-1.5 mt-2 p-2 rounded-[8px] bg-accent/5 border border-accent/10">
|
||||
<Tag className="w-3 h-3 text-accent mt-0.5 flex-shrink-0" />
|
||||
<p className="text-[11px] text-text-secondary line-clamp-1">
|
||||
{item.note}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-border px-3 py-2 flex justify-between items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-accent-error hover:bg-accent-error/10 hover:text-accent-error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveFromWishlist(item.id);
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onProductClick(item.id);
|
||||
}}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty State Info */}
|
||||
{items.length > 0 && (
|
||||
<div className="px-4 pb-4">
|
||||
<Card className="p-4 bg-primary/5 border-primary/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-primary flex-shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-[13px] text-text-secondary">
|
||||
Prices are indicative and may vary based on current gold rates and availability. Contact your retailer for accurate quotes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/components/figma/ImageWithFallback.tsx
Normal file
27
src/components/figma/ImageWithFallback.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const ERROR_IMG_SRC =
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
|
||||
|
||||
export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||
const [didError, setDidError] = useState(false)
|
||||
|
||||
const handleError = () => {
|
||||
setDidError(true)
|
||||
}
|
||||
|
||||
const { src, alt, style, className, ...rest } = props
|
||||
|
||||
return didError ? (
|
||||
<div
|
||||
className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
|
||||
style={style}
|
||||
>
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
|
||||
)
|
||||
}
|
||||
332
src/components/manufacturer/AddSkuDialog.tsx
Normal file
332
src/components/manufacturer/AddSkuDialog.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, ImageIcon } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
interface AddSkuDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onAdd: (skuData: any) => void;
|
||||
}
|
||||
|
||||
export function AddSkuDialog({ open, onClose, onAdd }: AddSkuDialogProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
sku: "",
|
||||
category: "",
|
||||
metal: "",
|
||||
purity: "",
|
||||
weight: "",
|
||||
description: "",
|
||||
minOrderQty: "1",
|
||||
leadTime: "",
|
||||
wholesalePrice: "",
|
||||
status: "available",
|
||||
certifications: "",
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.title || !formData.sku || !formData.category) {
|
||||
toast.error("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
onAdd(formData);
|
||||
toast.success("SKU added to catalogue successfully");
|
||||
onClose();
|
||||
setFormData({
|
||||
title: "",
|
||||
sku: "",
|
||||
category: "",
|
||||
metal: "",
|
||||
purity: "",
|
||||
weight: "",
|
||||
description: "",
|
||||
minOrderQty: "1",
|
||||
leadTime: "",
|
||||
wholesalePrice: "",
|
||||
status: "available",
|
||||
certifications: "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleImageUpload = () => {
|
||||
toast.success("Image upload functionality would be implemented here");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New SKU to Catalogue</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new product for sharing with retail partners
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="basic">Basic Info</TabsTrigger>
|
||||
<TabsTrigger value="specs">Specifications</TabsTrigger>
|
||||
<TabsTrigger value="pricing">Wholesale Pricing</TabsTrigger>
|
||||
<TabsTrigger value="media">Media</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Product Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g., Temple Design Necklace"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sku">Manufacturer SKU *</Label>
|
||||
<Input
|
||||
id="sku"
|
||||
placeholder="e.g., MFG-22K-NK-0551"
|
||||
value={formData.sku}
|
||||
onChange={(e) => setFormData({ ...formData, sku: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Product Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Describe the design, craftsmanship, suitable occasions..."
|
||||
rows={4}
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category *</Label>
|
||||
<Select value={formData.category} onValueChange={(v) => setFormData({ ...formData, category: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="rings">Rings</SelectItem>
|
||||
<SelectItem value="necklaces">Necklaces</SelectItem>
|
||||
<SelectItem value="earrings">Earrings</SelectItem>
|
||||
<SelectItem value="bangles">Bangles</SelectItem>
|
||||
<SelectItem value="bracelets">Bracelets</SelectItem>
|
||||
<SelectItem value="pendants">Pendants</SelectItem>
|
||||
<SelectItem value="chains">Chains</SelectItem>
|
||||
<SelectItem value="sets">Sets</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Production Status</Label>
|
||||
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="available">Available</SelectItem>
|
||||
<SelectItem value="custom-only">Custom Order Only</SelectItem>
|
||||
<SelectItem value="discontinued">Discontinued</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="minOrderQty">Minimum Order Quantity</Label>
|
||||
<Input
|
||||
id="minOrderQty"
|
||||
type="number"
|
||||
min="1"
|
||||
value={formData.minOrderQty}
|
||||
onChange={(e) => setFormData({ ...formData, minOrderQty: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="leadTime">Lead Time (days)</Label>
|
||||
<Input
|
||||
id="leadTime"
|
||||
type="number"
|
||||
placeholder="e.g., 7"
|
||||
value={formData.leadTime}
|
||||
onChange={(e) => setFormData({ ...formData, leadTime: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="specs" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="metal">Metal Type</Label>
|
||||
<Select value={formData.metal} onValueChange={(v) => setFormData({ ...formData, metal: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select metal" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gold">Gold</SelectItem>
|
||||
<SelectItem value="silver">Silver</SelectItem>
|
||||
<SelectItem value="platinum">Platinum</SelectItem>
|
||||
<SelectItem value="white-gold">White Gold</SelectItem>
|
||||
<SelectItem value="rose-gold">Rose Gold</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="purity">Purity/Karat</Label>
|
||||
<Select value={formData.purity} onValueChange={(v) => setFormData({ ...formData, purity: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select purity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="24k">24K Gold</SelectItem>
|
||||
<SelectItem value="22k">22K Gold</SelectItem>
|
||||
<SelectItem value="18k">18K Gold</SelectItem>
|
||||
<SelectItem value="14k">14K Gold</SelectItem>
|
||||
<SelectItem value="925">925 Sterling Silver</SelectItem>
|
||||
<SelectItem value="950">950 Platinum</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="weight">Net Weight (in grams)</Label>
|
||||
<Input
|
||||
id="weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="e.g., 24.20"
|
||||
value={formData.weight}
|
||||
onChange={(e) => setFormData({ ...formData, weight: e.target.value })}
|
||||
/>
|
||||
<p className="text-[12px] text-muted-foreground">Enter the approximate weight (can vary ±5%)</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="certifications">Certifications & Hallmarks</Label>
|
||||
<Input
|
||||
id="certifications"
|
||||
placeholder="e.g., BIS Hallmarked, IGI Certified"
|
||||
value={formData.certifications}
|
||||
onChange={(e) => setFormData({ ...formData, certifications: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pricing" className="space-y-4 mt-4">
|
||||
<div className="p-4 rounded-lg bg-accent/5 border border-accent/20">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
Set your wholesale pricing. Retailers will see this as their base cost and add their margins.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="wholesalePrice">Wholesale Price (₹)</Label>
|
||||
<Input
|
||||
id="wholesalePrice"
|
||||
type="number"
|
||||
placeholder="e.g., 125000"
|
||||
value={formData.wholesalePrice}
|
||||
onChange={(e) => setFormData({ ...formData, wholesalePrice: e.target.value })}
|
||||
/>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Price per unit excluding GST
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg border border-border space-y-2">
|
||||
<h4 className="text-[14px] font-medium">Pricing Breakdown</h4>
|
||||
<div className="space-y-1 text-[13px]">
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Base Price</span>
|
||||
<span>₹{formData.wholesalePrice || "0"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>GST (3%)</span>
|
||||
<span>₹{formData.wholesalePrice ? (parseFloat(formData.wholesalePrice) * 0.03).toFixed(2) : "0"}</span>
|
||||
</div>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex justify-between font-medium">
|
||||
<span>Total to Retailer</span>
|
||||
<span>₹{formData.wholesalePrice ? (parseFloat(formData.wholesalePrice) * 1.03).toFixed(2) : "0"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-primary/5 border border-primary/20">
|
||||
<p className="text-[13px]">
|
||||
💡 <strong>Tip:</strong> Set competitive wholesale prices to attract more retailers. Consider volume discounts for bulk orders.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="media" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Product Images</Label>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={handleImageUpload}
|
||||
className="aspect-square rounded-lg border-2 border-dashed border-border hover:border-primary hover:bg-accent/5 flex flex-col items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<ImageIcon className="w-8 h-8 text-muted-foreground" />
|
||||
<span className="text-[11px] text-muted-foreground">Upload {i}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Upload high-quality images from multiple angles. First image will be the primary display.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-4">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add to Catalogue
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
259
src/components/manufacturer/ManufacturerShell.tsx
Normal file
259
src/components/manufacturer/ManufacturerShell.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
Share2,
|
||||
MessageCircle,
|
||||
FileText,
|
||||
BarChart3,
|
||||
Shield,
|
||||
Code,
|
||||
Settings,
|
||||
Search,
|
||||
Bell,
|
||||
ChevronDown,
|
||||
Factory,
|
||||
AlertCircle,
|
||||
Globe,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ManufacturerPage } from "../../ManufacturerApp";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { AIAssistant } from "../AIAssistant";
|
||||
import { SmartSearch } from "../SmartSearch";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Alert, AlertDescription } from "../ui/alert";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { NotificationsPanel } from "./NotificationsPanel";
|
||||
|
||||
interface ManufacturerShellProps {
|
||||
children: React.ReactNode;
|
||||
currentPage: ManufacturerPage;
|
||||
onNavigate: (page: ManufacturerPage) => void;
|
||||
companyName: string;
|
||||
kycStatus: "pending" | "verified" | "rejected";
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: "dashboard" as ManufacturerPage, icon: LayoutDashboard, label: "Dashboard" },
|
||||
{ id: "sharing" as ManufacturerPage, icon: Share2, label: "Sharing & Connections" },
|
||||
{ id: "inquiries" as ManufacturerPage, icon: MessageCircle, label: "Inquiries", badge: 5 },
|
||||
{ id: "custom-orders" as ManufacturerPage, icon: FileText, label: "Custom Orders" },
|
||||
{ id: "analytics" as ManufacturerPage, icon: BarChart3, label: "Analytics" },
|
||||
{ id: "compliance" as ManufacturerPage, icon: Shield, label: "Compliance" },
|
||||
];
|
||||
|
||||
export function ManufacturerShell({
|
||||
children,
|
||||
currentPage,
|
||||
onNavigate,
|
||||
companyName,
|
||||
kycStatus
|
||||
}: ManufacturerShellProps) {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
const [language, setLanguage] = useState("en");
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-background border-r border-border flex flex-col">
|
||||
{/* Brand Header */}
|
||||
<div className="h-16 px-4 flex items-center gap-3 border-b border-border">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-[#A855F7]/10 flex items-center justify-center flex-shrink-0">
|
||||
<Factory className="w-6 h-6 text-[#A855F7]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[14px] truncate">{companyName}</p>
|
||||
<p className="text-[12px] text-text-secondary">Manufacturer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-1 overflow-y-auto">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentPage === item.id;
|
||||
const isLocked = kycStatus !== "verified" && item.id === "sharing";
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => !isLocked && onNavigate(item.id)}
|
||||
disabled={isLocked}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-[10px] transition-colors ${
|
||||
isActive
|
||||
? "bg-[#A855F7] text-white"
|
||||
: isLocked
|
||||
? "text-text-secondary opacity-50 cursor-not-allowed"
|
||||
: "text-text-secondary hover:bg-accent/10 hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-[14px] flex-1 text-left">{item.label}</span>
|
||||
{item.badge && !isLocked && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-destructive text-destructive-foreground text-[11px]">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Settings at bottom */}
|
||||
<div className="p-3 border-t border-border">
|
||||
<button
|
||||
onClick={() => onNavigate("settings")}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-[10px] transition-colors ${
|
||||
currentPage === "settings"
|
||||
? "bg-[#A855F7] text-white"
|
||||
: "text-text-secondary hover:bg-accent/10 hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
<span className="text-[14px]">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Top Bar */}
|
||||
<header className="h-16 bg-background border-b border-border px-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
{/* Smart AI Search */}
|
||||
<div className="w-96">
|
||||
<SmartSearch
|
||||
portalType="manufacturer"
|
||||
placeholder="Search SKUs, retailers, inquiries... (⌘K)"
|
||||
onNavigate={(page) => onNavigate(page as ManufacturerPage)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* KYC Status Badge */}
|
||||
{kycStatus === "pending" && (
|
||||
<Badge variant="outline" className="bg-accent-warn/10 text-accent-warn border-accent-warn/20">
|
||||
<AlertCircle className="w-3 h-3 mr-1" />
|
||||
KYC Pending
|
||||
</Badge>
|
||||
)}
|
||||
{kycStatus === "verified" && (
|
||||
<Badge variant="outline" className="bg-accent-positive/10 text-accent-positive border-accent-positive/20">
|
||||
Verified
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger className="w-[120px] h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="hi">हिन्दी</SelectItem>
|
||||
<SelectItem value="mr">मराठी</SelectItem>
|
||||
<SelectItem value="gu">ગુજરાતી</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Moon className="w-5 h-5" />
|
||||
) : (
|
||||
<Sun className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<NotificationsPanel unreadCount={3} />
|
||||
|
||||
{/* Profile */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<span className="text-[14px]">A</span>
|
||||
</div>
|
||||
<span className="text-[14px]">Admin</span>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onNavigate("settings")}>
|
||||
Company Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onNavigate("kyc")}>
|
||||
KYC & Verification
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Sign out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* KYC Warning Banner */}
|
||||
{kycStatus === "pending" && (
|
||||
<div className="px-6 py-3 border-b border-border">
|
||||
<Alert className="bg-accent-warn/5 border-accent-warn/20">
|
||||
<AlertCircle className="w-4 h-4 text-accent-warn" />
|
||||
<AlertDescription className="text-[14px]">
|
||||
Your KYC verification is pending. Sharing and API features are locked until verification is complete.
|
||||
<Button
|
||||
variant="link"
|
||||
className="ml-2 p-0 h-auto text-accent-warn"
|
||||
onClick={() => onNavigate("kyc")}
|
||||
>
|
||||
Complete KYC
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
|
||||
{/* AI Assistant */}
|
||||
<AIAssistant portalType="manufacturer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
143
src/components/manufacturer/NotificationsPanel.tsx
Normal file
143
src/components/manufacturer/NotificationsPanel.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Bell, Package, MessageCircle, FileText, AlertCircle, X } from "lucide-react";
|
||||
import { Card } from "../ui/card";
|
||||
import { Button } from "../ui/button";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "../ui/popover";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
interface NotificationsPanelProps {
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
id: "1",
|
||||
type: "inquiry",
|
||||
icon: MessageCircle,
|
||||
title: "New inquiry from Nova Jewels",
|
||||
description: "Interested in 22K Gold Bangles collection",
|
||||
time: "15 minutes ago",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "order",
|
||||
icon: Package,
|
||||
title: "Custom order request",
|
||||
description: "Retailer requested custom design modification",
|
||||
time: "1 hour ago",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "sync",
|
||||
icon: AlertCircle,
|
||||
title: "Inventory sync completed",
|
||||
description: "248 SKUs synced successfully",
|
||||
time: "3 hours ago",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "approval",
|
||||
icon: FileText,
|
||||
title: "Connection request pending",
|
||||
description: "Sparkle Gems wants to connect",
|
||||
time: "Yesterday",
|
||||
unread: false,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "kyc",
|
||||
icon: AlertCircle,
|
||||
title: "KYC update required",
|
||||
description: "Please update your business documents",
|
||||
time: "2 days ago",
|
||||
unread: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function NotificationsPanel({ unreadCount = 3 }: NotificationsPanelProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="relative">
|
||||
<Bell className="w-5 h-5" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-destructive rounded-full" />
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="end">
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<div>
|
||||
<h3 className="text-[16px] font-medium">Notifications</h3>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
You have {unreadCount} unread messages
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
Mark all read
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="p-2">
|
||||
{notifications.map((notification) => {
|
||||
const Icon = notification.icon;
|
||||
return (
|
||||
<button
|
||||
key={notification.id}
|
||||
className={`w-full text-left p-3 rounded-lg hover:bg-accent/5 transition-colors ${
|
||||
notification.unread ? "bg-primary/5" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
notification.type === "inquiry" ? "bg-primary/10" :
|
||||
notification.type === "order" ? "bg-accent-positive/10" :
|
||||
notification.type === "sync" ? "bg-secondary/10" :
|
||||
"bg-accent-warn/10"
|
||||
}`}>
|
||||
<Icon className={`w-5 h-5 ${
|
||||
notification.type === "inquiry" ? "text-primary" :
|
||||
notification.type === "order" ? "text-accent-positive" :
|
||||
notification.type === "sync" ? "text-secondary" :
|
||||
"text-accent-warn"
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<p className="text-[14px] font-medium line-clamp-1">{notification.title}</p>
|
||||
{notification.unread && (
|
||||
<div className="w-2 h-2 rounded-full bg-primary flex-shrink-0 mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[13px] text-muted-foreground line-clamp-2 mb-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground">{notification.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="p-3">
|
||||
<Button variant="outline" className="w-full" size="sm">
|
||||
View all notifications
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
167
src/components/manufacturer/pages/APIsPage.tsx
Normal file
167
src/components/manufacturer/pages/APIsPage.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Code, Key, Webhook, Copy, Plus, RotateCw } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Alert, AlertDescription } from "../../ui/alert";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
import { copyWithToast } from "../../utils/clipboard";
|
||||
|
||||
const apiKeys = [
|
||||
{ id: "1", name: "Production API", key: "pk_live_••••••••••••1234", role: "Read/Write", created: "Jan 1, 2025", lastUsed: "2 hours ago" },
|
||||
{ id: "2", name: "Webhook Service", key: "pk_live_••••••••••••5678", role: "Webhook-only", created: "Dec 15, 2024", lastUsed: "1 day ago" },
|
||||
];
|
||||
|
||||
const webhooks = [
|
||||
{ id: "1", event: "SKU.created", endpoint: "https://api.example.com/webhooks/sku", status: "Active" },
|
||||
{ id: "2", event: "Quote.accepted", endpoint: "https://api.example.com/webhooks/quote", status: "Active" },
|
||||
{ id: "3", event: "PO.updated", endpoint: "https://api.example.com/webhooks/po", status: "Inactive" },
|
||||
];
|
||||
|
||||
export function APIsPage() {
|
||||
const handleCopyKey = async (key: string) => {
|
||||
await copyWithToast(key, "API key copied!");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">APIs & Integrations</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage API keys, webhooks, and integrations
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create API Key
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* API Endpoint Info */}
|
||||
<Alert>
|
||||
<Code className="w-4 h-4" />
|
||||
<AlertDescription>
|
||||
<p className="text-[14px] mb-2">Base URL: <code className="px-2 py-1 bg-muted rounded text-[12px]">https://api.hellojewellers.com/v1</code></p>
|
||||
<Button variant="link" className="p-0 h-auto text-primary">
|
||||
View API Documentation
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">API Keys</p>
|
||||
<p className="text-[24px] leading-[32px]">2</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Active Webhooks</p>
|
||||
<p className="text-[24px] leading-[32px]">2</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Requests (24h)</p>
|
||||
<p className="text-[24px] leading-[32px]">1.2K</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Success Rate</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">99.8%</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* API Keys */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">API Keys</h2>
|
||||
<div className="space-y-3">
|
||||
{apiKeys.map((key) => (
|
||||
<div key={key.id} className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Key className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">{key.name}</p>
|
||||
<p className="text-[12px] text-text-secondary font-mono mb-1">{key.key}</p>
|
||||
<div className="flex items-center gap-3 text-[12px] text-text-secondary">
|
||||
<span>Role: {key.role}</span>
|
||||
<span>•</span>
|
||||
<span>Last used: {key.lastUsed}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={() => handleCopyKey(key.key)}>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost">
|
||||
<RotateCw className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Webhooks */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[18px] leading-[28px]">Webhooks</h2>
|
||||
<Button variant="outline">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Webhook
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{webhooks.map((webhook) => (
|
||||
<div key={webhook.id} className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center">
|
||||
<Webhook className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">{webhook.event}</p>
|
||||
<p className="text-[12px] text-text-secondary font-mono">{webhook.endpoint}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={webhook.status === "Active" ? "default" : "secondary"}>
|
||||
{webhook.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Rate Limits */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Rate Limits</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Requests per minute</p>
|
||||
<p className="text-[12px] text-text-secondary">Current usage</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[14px]">45 / 100</p>
|
||||
<div className="w-32 h-2 bg-border rounded-full overflow-hidden mt-1">
|
||||
<div className="h-full bg-[#A855F7]" style={{ width: "45%" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Daily quota</p>
|
||||
<p className="text-[12px] text-text-secondary">Resets in 6 hours</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[14px]">1,248 / 10,000</p>
|
||||
<div className="w-32 h-2 bg-border rounded-full overflow-hidden mt-1">
|
||||
<div className="h-full bg-accent-positive" style={{ width: "12%" }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
191
src/components/manufacturer/pages/AnalyticsPage.tsx
Normal file
191
src/components/manufacturer/pages/AnalyticsPage.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { BarChart3, TrendingUp, Eye, Users, Download } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../ui/select";
|
||||
|
||||
export function AnalyticsPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Analytics</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Track performance, signals, and engagement metrics
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select defaultValue="30">
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7">Last 7 days</SelectItem>
|
||||
<SelectItem value="30">Last 30 days</SelectItem>
|
||||
<SelectItem value="90">Last 90 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Eye className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+24%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">8,427</p>
|
||||
<p className="text-[12px] text-text-secondary">SKU Views</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-secondary/10 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+18%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">142</p>
|
||||
<p className="text-[12px] text-text-secondary">Wishlist Adds</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-accent-positive/10 flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+12%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">48</p>
|
||||
<p className="text-[12px] text-text-secondary">Inquiries</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-accent-warn/10 flex items-center justify-center">
|
||||
<BarChart3 className="w-5 h-5 text-accent-warn" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+28%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">34</p>
|
||||
<p className="text-[12px] text-text-secondary">POs Won</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Views & Engagement</h3>
|
||||
<div className="h-64 flex items-center justify-center bg-gradient-to-br from-[#A855F7]/5 to-secondary/5 rounded-[12px]">
|
||||
<BarChart3 className="w-12 h-12 text-text-secondary" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Conversion Funnel</h3>
|
||||
<div className="h-64 flex items-center justify-center bg-gradient-to-br from-accent-positive/5 to-[#A855F7]/5 rounded-[12px]">
|
||||
<TrendingUp className="w-12 h-12 text-text-secondary" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* SKU Performance */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Top Performing SKUs</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1 font-mono">AU-22K-BR-0192 • Heritage Bridal Ring</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>1,248 views</span>
|
||||
<span>89 wishlist adds</span>
|
||||
<span>12 inquiries</span>
|
||||
<span>3 orders</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">+32%</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1 font-mono">AU-18K-NK-0441 • Aurora Pendant</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>842 views</span>
|
||||
<span>64 wishlist adds</span>
|
||||
<span>8 inquiries</span>
|
||||
<span>2 orders</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">+24%</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1 font-mono">AU-22K-BG-1023 • Classic Bangle 2-6</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>624 views</span>
|
||||
<span>42 wishlist adds</span>
|
||||
<span>5 inquiries</span>
|
||||
<span>1 order</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">+18%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Retailer Performance */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Retailer Engagement</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Nova Jewels • Mumbai</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>3 collections</span>
|
||||
<span>2.4K views</span>
|
||||
<span>18 inquiries</span>
|
||||
<span>Avg response: 1.2h</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">Gold Tier</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Zephyr Gems Boutique • Delhi</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>2 collections</span>
|
||||
<span>1.8K views</span>
|
||||
<span>12 inquiries</span>
|
||||
<span>Avg response: 2.1h</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-text-secondary">Silver Tier</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
415
src/components/manufacturer/pages/CataloguePage.tsx
Normal file
415
src/components/manufacturer/pages/CataloguePage.tsx
Normal file
@@ -0,0 +1,415 @@
|
||||
import { useState } from "react";
|
||||
import { Package, Upload, Download, Filter, Plus, Search, Eye, Edit, Trash2, RefreshCw, Grid } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import { Checkbox } from "../../ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "../../ui/sheet";
|
||||
import { Label } from "../../ui/label";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
const catalogue = [
|
||||
{
|
||||
id: "1",
|
||||
sku: "AU-22K-BR-0192",
|
||||
title: "Heritage Bridal Ring",
|
||||
collection: "Bridal '25",
|
||||
category: "Rings",
|
||||
metal: "22K Gold",
|
||||
weight: "6.8g",
|
||||
priceBand: "₹2.4L - ₹2.8L",
|
||||
leadTime: "7-10 days",
|
||||
status: "Published",
|
||||
updated: "2 days ago",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
sku: "AU-18K-NK-0441",
|
||||
title: "Aurora Pendant",
|
||||
collection: "Daily Edit",
|
||||
category: "Necklaces",
|
||||
metal: "18K Gold",
|
||||
weight: "3.2g",
|
||||
priceBand: "₹42K - ₹48K",
|
||||
leadTime: "5-7 days",
|
||||
status: "Published",
|
||||
updated: "1 week ago",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
sku: "AU-22K-BG-1023",
|
||||
title: "Classic Bangle 2-6",
|
||||
collection: "Festive Picks",
|
||||
category: "Bangles",
|
||||
metal: "22K Gold",
|
||||
weight: "18.4g",
|
||||
priceBand: "₹1.1L - ₹1.3L",
|
||||
leadTime: "10-14 days",
|
||||
status: "Draft",
|
||||
updated: "3 days ago",
|
||||
},
|
||||
];
|
||||
|
||||
const collections = [
|
||||
{ id: "1", name: "Bridal '25", items: 128, status: "Published", sharedWith: 12 },
|
||||
{ id: "2", name: "Daily Edit", items: 76, status: "Published", sharedWith: 18 },
|
||||
{ id: "3", name: "Festive Picks", items: 42, status: "Draft", sharedWith: 0 },
|
||||
];
|
||||
|
||||
export function CataloguePage() {
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [selectedSku, setSelectedSku] = useState<typeof catalogue[0] | null>(null);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedItems.length === catalogue.length) {
|
||||
setSelectedItems([]);
|
||||
} else {
|
||||
setSelectedItems(catalogue.map((item) => item.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectItem = (id: string) => {
|
||||
if (selectedItems.includes(id)) {
|
||||
setSelectedItems(selectedItems.filter((item) => item !== id));
|
||||
} else {
|
||||
setSelectedItems([...selectedItems, id]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (item: typeof catalogue[0]) => {
|
||||
setSelectedSku(item);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const handleBulkAction = (action: string) => {
|
||||
toast.success(`${action} applied to ${selectedItems.length} items`);
|
||||
setSelectedItems([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Catalogue</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage your product catalogue and collections
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Bulk Import
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
API Sync
|
||||
</Button>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add SKU
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total SKUs</p>
|
||||
<p className="text-[24px] leading-[32px]">248</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Published</p>
|
||||
<p className="text-[24px] leading-[32px]">204</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Draft</p>
|
||||
<p className="text-[24px] leading-[32px]">44</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Collections</p>
|
||||
<p className="text-[24px] leading-[32px]">12</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="skus" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="skus">SKUs</TabsTrigger>
|
||||
<TabsTrigger value="collections">Collections</TabsTrigger>
|
||||
<TabsTrigger value="price-bands">Price Bands</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="skus" className="space-y-4">
|
||||
{/* Filters & Search */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
||||
<Input placeholder="Search by SKU, title, or collection..." className="pl-10" />
|
||||
</div>
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="published">Published</SelectItem>
|
||||
<SelectItem value="draft">Draft</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select defaultValue="all">
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="rings">Rings</SelectItem>
|
||||
<SelectItem value="necklaces">Necklaces</SelectItem>
|
||||
<SelectItem value="bangles">Bangles</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
More
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Bulk Actions */}
|
||||
{selectedItems.length > 0 && (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[14px]">
|
||||
{selectedItems.length} item{selectedItems.length > 1 ? "s" : ""} selected
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Publish")}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Publish
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Add to Collection")}>
|
||||
<Grid className="w-4 h-4 mr-2" />
|
||||
Add to Collection
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Delete")}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* SKUs Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<Checkbox checked={selectedItems.length === catalogue.length} onCheckedChange={handleSelectAll} />
|
||||
</TableHead>
|
||||
<TableHead>SKU</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Collection</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Metal</TableHead>
|
||||
<TableHead>Weight</TableHead>
|
||||
<TableHead>Price Band</TableHead>
|
||||
<TableHead>Lead Time</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="w-24"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{catalogue.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onCheckedChange={() => handleSelectItem(item.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-[12px]">{item.sku}</TableCell>
|
||||
<TableCell>{item.title}</TableCell>
|
||||
<TableCell><Badge variant="outline">{item.collection}</Badge></TableCell>
|
||||
<TableCell>{item.category}</TableCell>
|
||||
<TableCell>{item.metal}</TableCell>
|
||||
<TableCell>{item.weight}</TableCell>
|
||||
<TableCell>{item.priceBand}</TableCell>
|
||||
<TableCell>{item.leadTime}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={item.status === "Published" ? "default" : "secondary"}>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleViewDetail(item)}>
|
||||
View
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="collections" className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Collection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{collections.map((collection) => (
|
||||
<Card key={collection.id} className="overflow-hidden">
|
||||
<div className="aspect-video bg-gradient-to-br from-[#A855F7]/20 to-secondary/20" />
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-1">{collection.name}</h3>
|
||||
<p className="text-[14px] text-text-secondary">{collection.items} items</p>
|
||||
</div>
|
||||
<Badge variant={collection.status === "Published" ? "default" : "secondary"}>
|
||||
{collection.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-[12px] text-text-secondary">
|
||||
Shared with {collection.sharedWith} retailers
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border">
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="price-bands" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Retailer Tier Price Bands</h3>
|
||||
<div className="space-y-4">
|
||||
{["Bronze", "Silver", "Gold", "Key Account"].map((tier) => (
|
||||
<div key={tier} className="p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="text-[16px]">{tier} Tier</h4>
|
||||
<Button size="sm" variant="ghost">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4 text-[14px]">
|
||||
<div>
|
||||
<p className="text-text-secondary mb-1">Min Margin</p>
|
||||
<p>12%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-secondary mb-1">Making/g</p>
|
||||
<p>₹500</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-text-secondary mb-1">Floor Price</p>
|
||||
<p>Cost + 8%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* SKU Detail Sheet */}
|
||||
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<SheetContent className="w-[600px] sm:max-w-[600px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{selectedSku?.title}</SheetTitle>
|
||||
<SheetDescription>SKU: {selectedSku?.sku}</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{selectedSku && (
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="aspect-square bg-gradient-to-br from-[#A855F7]/20 to-secondary/20 rounded-[12px]" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Collection</span>
|
||||
<Badge variant="outline">{selectedSku.collection}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Category</span>
|
||||
<span>{selectedSku.category}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Metal</span>
|
||||
<span>{selectedSku.metal}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Weight</span>
|
||||
<span>{selectedSku.weight}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Price Band</span>
|
||||
<span>{selectedSku.priceBand}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Lead Time</span>
|
||||
<span>{selectedSku.leadTime}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Status</span>
|
||||
<Badge>{selectedSku.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-2">
|
||||
<Button className="w-full">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Edit SKU
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Share to Retailers
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
src/components/manufacturer/pages/CompliancePage.tsx
Normal file
142
src/components/manufacturer/pages/CompliancePage.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Shield, FileText, Upload, CheckCircle, AlertCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
const certificates = [
|
||||
{ id: "1", type: "Hallmark Certificate", number: "HM-2024-1234", expiry: "Dec 31, 2025", status: "Valid", skus: 128 },
|
||||
{ id: "2", type: "Assay Certificate", number: "AS-2024-5678", expiry: "Jun 30, 2025", status: "Valid", skus: 76 },
|
||||
{ id: "3", type: "ISO 9001", number: "ISO-2023-9876", expiry: "Mar 15, 2024", status: "Expiring Soon", skus: 0 },
|
||||
];
|
||||
|
||||
export function CompliancePage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Compliance & Certificates</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage certificates, quality standards, and compliance
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload Certificate
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Overview */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total Certificates</p>
|
||||
<p className="text-[24px] leading-[32px]">8</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Valid</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">6</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Expiring Soon</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-warn">2</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Certificates */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Certificates</h2>
|
||||
<div className="space-y-3">
|
||||
{certificates.map((cert) => (
|
||||
<div key={cert.id} className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
cert.status === "Valid"
|
||||
? "bg-accent-positive/10"
|
||||
: "bg-accent-warn/10"
|
||||
}`}>
|
||||
{cert.status === "Valid" ? (
|
||||
<CheckCircle className="w-5 h-5 text-accent-positive" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-accent-warn" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">{cert.type}</p>
|
||||
<p className="text-[12px] text-text-secondary font-mono">{cert.number}</p>
|
||||
<div className="flex items-center gap-3 text-[12px] text-text-secondary mt-1">
|
||||
<span>Expires: {cert.expiry}</span>
|
||||
{cert.skus > 0 && <span>• {cert.skus} SKUs</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={cert.status === "Valid" ? "default" : "outline"} className={
|
||||
cert.status === "Valid"
|
||||
? "bg-accent-positive"
|
||||
: "bg-accent-warn text-accent-warn"
|
||||
}>
|
||||
{cert.status}
|
||||
</Badge>
|
||||
<Button size="sm" variant="ghost">
|
||||
<FileText className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* DPDP Compliance */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Data Protection & Privacy</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Consent Management</p>
|
||||
<p className="text-[12px] text-text-secondary">Retailer data handling consent</p>
|
||||
</div>
|
||||
<Badge className="bg-accent-positive">Compliant</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Data Retention</p>
|
||||
<p className="text-[12px] text-text-secondary">Automated cleanup policies</p>
|
||||
</div>
|
||||
<Badge className="bg-accent-positive">Compliant</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Access Controls</p>
|
||||
<p className="text-[12px] text-text-secondary">Role-based permissions</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Review</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Quality Standards */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Quality Standards</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Internal QA Checklist</p>
|
||||
<p className="text-[12px] text-text-secondary">Pre-shipment quality checks</p>
|
||||
</div>
|
||||
<Badge className="bg-accent-positive">Enabled</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Hallmark Participation</p>
|
||||
<p className="text-[12px] text-text-secondary">BIS hallmarking compliance</p>
|
||||
</div>
|
||||
<Badge className="bg-accent-positive">Active</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
152
src/components/manufacturer/pages/CustomOrdersPage.tsx
Normal file
152
src/components/manufacturer/pages/CustomOrdersPage.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { FileText, Clock, CheckCircle, Package } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
const customOrders = [
|
||||
{
|
||||
id: "BRF-2025-011",
|
||||
retailer: "Nova Jewels",
|
||||
customer: "A.S.",
|
||||
occasion: "Bridal",
|
||||
budget: "₹3-4L",
|
||||
dueDate: "Feb 15, 2025",
|
||||
status: "New",
|
||||
stage: "Brief Received",
|
||||
},
|
||||
{
|
||||
id: "BRF-2025-012",
|
||||
retailer: "Zephyr Gems",
|
||||
customer: "R.M.",
|
||||
occasion: "Anniversary",
|
||||
budget: "₹1.5-2L",
|
||||
dueDate: "Feb 10, 2025",
|
||||
status: "Under Review",
|
||||
stage: "Feasibility Check",
|
||||
},
|
||||
{
|
||||
id: "PO-AF-2025-017",
|
||||
retailer: "Nova Jewels",
|
||||
customer: "S.K.",
|
||||
occasion: "Bridal",
|
||||
budget: "₹4.2L",
|
||||
dueDate: "Feb 5, 2025",
|
||||
status: "Accepted",
|
||||
stage: "Under Making",
|
||||
},
|
||||
];
|
||||
|
||||
export function CustomOrdersPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Custom Orders</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage custom order briefs, feasibility checks, and production
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Active Briefs</p>
|
||||
<p className="text-[24px] leading-[32px]">8</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">In Production</p>
|
||||
<p className="text-[24px] leading-[32px]">3</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Delivered (30d)</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">12</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">On-time %</p>
|
||||
<p className="text-[24px] leading-[32px]">94%</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Pipeline */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Production Pipeline</h2>
|
||||
<div className="space-y-3">
|
||||
{customOrders.map((order) => (
|
||||
<Card key={order.id} className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<p className="font-mono text-[14px]">{order.id}</p>
|
||||
<Badge variant={
|
||||
order.status === "New"
|
||||
? "outline"
|
||||
: order.status === "Accepted"
|
||||
? "default"
|
||||
: "secondary"
|
||||
}>
|
||||
{order.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-[14px] text-text-secondary">
|
||||
<span>{order.retailer}</span>
|
||||
<span>•</span>
|
||||
<span>{order.occasion}</span>
|
||||
<span>•</span>
|
||||
<span>{order.budget}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Due Date</p>
|
||||
<p className="text-[14px]">{order.dueDate}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex-1 h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#A855F7]"
|
||||
style={{
|
||||
width: order.stage === "Brief Received"
|
||||
? "25%"
|
||||
: order.stage === "Feasibility Check"
|
||||
? "50%"
|
||||
: "75%"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[12px] text-text-secondary whitespace-nowrap">{order.stage}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{order.status === "New" && (
|
||||
<>
|
||||
<Button size="sm" className="flex-1">Review Brief</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Feasibility
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{order.status === "Under Review" && (
|
||||
<>
|
||||
<Button size="sm" className="flex-1">Send Quote</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1">Request Info</Button>
|
||||
</>
|
||||
)}
|
||||
{order.status === "Accepted" && (
|
||||
<>
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
<Package className="w-4 h-4 mr-2" />
|
||||
Update Status
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1">View PO</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
198
src/components/manufacturer/pages/DashboardPage.tsx
Normal file
198
src/components/manufacturer/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Package, Users, MessageCircle, FileText, Upload, Share2, Eye, TrendingUp } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ManufacturerPage } from "../../../ManufacturerApp";
|
||||
|
||||
interface DashboardPageProps {
|
||||
onNavigate: (page: ManufacturerPage) => void;
|
||||
kycStatus: "pending" | "verified" | "rejected";
|
||||
}
|
||||
|
||||
const kpis = [
|
||||
{ label: "Live SKUs", value: "248", change: "+12", trend: "up", icon: Package },
|
||||
{ label: "Collections", value: "12", change: "+2", trend: "up", icon: Package },
|
||||
{ label: "Retailers Connected", value: "18", change: "+3", trend: "up", icon: Users },
|
||||
{ label: "Active Inquiries", value: "5", change: "-2", trend: "up", icon: MessageCircle },
|
||||
{ label: "POs in Progress", value: "3", change: "+1", trend: "up", icon: FileText },
|
||||
{ label: "Total Views", value: "2.4K", change: "+18%", trend: "up", icon: Eye },
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ id: "catalogue", label: "Upload Catalogue", description: "Add new SKUs", icon: Upload },
|
||||
{ id: "catalogue", label: "Create Collection", description: "Organize products", icon: Package },
|
||||
{ id: "sharing", label: "Share to Retailers", description: "Manage connections", icon: Share2 },
|
||||
{ id: "inquiries", label: "Review Inquiries", description: "5 pending", icon: MessageCircle },
|
||||
{ id: "custom-orders", label: "Custom Briefs", description: "2 new requests", icon: FileText },
|
||||
{ id: "analytics", label: "View Analytics", description: "Performance data", icon: TrendingUp },
|
||||
];
|
||||
|
||||
const recentActivity = [
|
||||
{ id: 1, text: "Nova Jewels viewed Bridal '25 collection (128 views)", time: "2 hours ago", type: "view" },
|
||||
{ id: 2, text: "PO PO-AF-2025-017 accepted and in production", time: "4 hours ago", type: "po" },
|
||||
{ id: 3, text: "New inquiry from Zephyr Gems about rose gold variants", time: "1 day ago", type: "inquiry" },
|
||||
{ id: 4, text: "Shared Daily Edit collection with BlueLeaf Jewels", time: "1 day ago", type: "share" },
|
||||
{ id: 5, text: "48 SKUs synced from API", time: "2 days ago", type: "sync" },
|
||||
];
|
||||
|
||||
export function DashboardPage({ onNavigate, kycStatus }: DashboardPageProps) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Dashboard</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Welcome back! Here's an overview of your manufacturing operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KYC Status Card */}
|
||||
{kycStatus === "verified" ? (
|
||||
<Card className="p-4 bg-accent-positive/5 border-accent-positive/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Your account is verified</p>
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
All features including sharing and API access are enabled
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="p-4 bg-accent-warn/5 border-accent-warn/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-warn/10 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-accent-warn" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">KYC verification pending</p>
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
Complete verification to unlock sharing and API features
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("kyc")}>Complete KYC</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||
{kpis.map((kpi) => {
|
||||
const Icon = kpi.icon;
|
||||
return (
|
||||
<Card key={kpi.label} className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
{kpi.change}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">{kpi.value}</p>
|
||||
<p className="text-[12px] text-text-secondary">{kpi.label}</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions & Recent Activity */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Quick Actions</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{quickActions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
const isLocked = kycStatus !== "verified" && action.id === "sharing";
|
||||
return (
|
||||
<button
|
||||
key={action.label}
|
||||
onClick={() => !isLocked && onNavigate(action.id as ManufacturerPage)}
|
||||
disabled={isLocked}
|
||||
className={`p-4 rounded-[12px] border border-border hover:border-[#A855F7] hover:bg-[#A855F7]/5 transition-colors text-left ${
|
||||
isLocked ? "opacity-50 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5 text-[#A855F7] mb-2" />
|
||||
<p className="text-[14px] mb-1">{action.label}</p>
|
||||
<p className="text-[12px] text-text-secondary">{action.description}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[18px] leading-[28px]">Recent Activity</h2>
|
||||
<Button variant="ghost" size="sm">View All</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{recentActivity.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3 p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="w-2 h-2 rounded-full bg-[#A855F7] mt-2 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[14px]">{activity.text}</p>
|
||||
<p className="text-[12px] text-text-secondary">{activity.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Pending Items */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Pending Actions</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-warn/10 flex items-center justify-center">
|
||||
<MessageCircle className="w-5 h-5 text-accent-warn" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">5 unanswered inquiries</p>
|
||||
<p className="text-[12px] text-text-secondary">Retailers awaiting responses</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("inquiries")}>Review</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">2 custom order briefs</p>
|
||||
<p className="text-[12px] text-text-secondary">New requests for feasibility check</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("custom-orders")}>View</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">3 new retailer access requests</p>
|
||||
<p className="text-[12px] text-text-secondary">Approve or deny connection requests</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("sharing")}>Manage</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
169
src/components/manufacturer/pages/InquiriesPage.tsx
Normal file
169
src/components/manufacturer/pages/InquiriesPage.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { MessageCircle, Search, Clock, CheckCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
const inquiries = [
|
||||
{
|
||||
id: "1",
|
||||
retailer: "Nova Jewels",
|
||||
skus: "AU-22K-BR-0192",
|
||||
topic: "Availability for bridal season",
|
||||
priority: "High",
|
||||
status: "New",
|
||||
updated: "2 hours ago",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
retailer: "Zephyr Gems",
|
||||
skus: "AU-18K-NK-0441, AU-18K-ER-0234",
|
||||
topic: "Rose gold variant inquiry",
|
||||
priority: "Medium",
|
||||
status: "Replied",
|
||||
updated: "1 day ago",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
retailer: "BlueLeaf Jewels",
|
||||
skus: "AU-22K-BG-1023",
|
||||
topic: "Custom sizing options",
|
||||
priority: "Low",
|
||||
status: "Awaiting",
|
||||
updated: "2 days ago",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
retailer: "Nova Jewels",
|
||||
skus: "Multiple",
|
||||
topic: "Bulk pricing discussion",
|
||||
priority: "High",
|
||||
status: "New",
|
||||
updated: "3 hours ago",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
retailer: "Royal Gems",
|
||||
skus: "AU-22K-BR-0192",
|
||||
topic: "Lead time clarification",
|
||||
priority: "Medium",
|
||||
status: "Closed",
|
||||
updated: "5 days ago",
|
||||
},
|
||||
];
|
||||
|
||||
export function InquiriesPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Inquiries</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage retailer inquiries and provide responses
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
SLA Monitor
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total Inquiries</p>
|
||||
<p className="text-[24px] leading-[32px]">48</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">New</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-warn">5</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Avg Response Time</p>
|
||||
<p className="text-[24px] leading-[32px]">2.3h</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Closed (30d)</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">42</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<Card className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
||||
<Input placeholder="Search inquiries by retailer, SKU, or topic..." className="pl-10" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Inquiries Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Retailer</TableHead>
|
||||
<TableHead>SKU(s)</TableHead>
|
||||
<TableHead>Topic</TableHead>
|
||||
<TableHead>Priority</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Updated</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{inquiries.map((inquiry) => (
|
||||
<TableRow key={inquiry.id}>
|
||||
<TableCell>{inquiry.retailer}</TableCell>
|
||||
<TableCell className="font-mono text-[12px]">{inquiry.skus}</TableCell>
|
||||
<TableCell>{inquiry.topic}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
inquiry.priority === "High"
|
||||
? "destructive"
|
||||
: inquiry.priority === "Medium"
|
||||
? "outline"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{inquiry.priority}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
inquiry.status === "New"
|
||||
? "default"
|
||||
: inquiry.status === "Closed"
|
||||
? "secondary"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{inquiry.status === "New" && <MessageCircle className="w-3 h-3 mr-1" />}
|
||||
{inquiry.status === "Closed" && <CheckCircle className="w-3 h-3 mr-1" />}
|
||||
{inquiry.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-text-secondary">{inquiry.updated}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline">
|
||||
Reply
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
318
src/components/manufacturer/pages/KYCPage.tsx
Normal file
318
src/components/manufacturer/pages/KYCPage.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import { useState } from "react";
|
||||
import { Building, FileText, CreditCard, CheckCircle, Upload, AlertCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Alert, AlertDescription } from "../../ui/alert";
|
||||
|
||||
interface KYCPageProps {
|
||||
onComplete: (status: "verified" | "pending") => void;
|
||||
currentStatus: "pending" | "verified" | "rejected";
|
||||
}
|
||||
|
||||
export function KYCPage({ onComplete, currentStatus }: KYCPageProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
const handleNext = () => {
|
||||
if (step < 4) {
|
||||
setStep(step + 1);
|
||||
} else {
|
||||
onComplete("pending");
|
||||
}
|
||||
};
|
||||
|
||||
if (currentStatus === "verified") {
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center p-6">
|
||||
<Card className="w-full max-w-2xl p-8 text-center">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-positive/10 flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-8 h-8 text-accent-positive" />
|
||||
</div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">KYC Verified</h2>
|
||||
<p className="text-[16px] text-text-secondary mb-6">
|
||||
Your account is fully verified. All features are enabled.
|
||||
</p>
|
||||
<Button onClick={() => onComplete("verified")}>Go to Dashboard</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentStatus === "rejected") {
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center p-6">
|
||||
<Card className="w-full max-w-2xl p-8">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-error/10 flex items-center justify-center mx-auto mb-4">
|
||||
<AlertCircle className="w-8 h-8 text-accent-error" />
|
||||
</div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2 text-center">KYC Rejected</h2>
|
||||
<p className="text-[16px] text-text-secondary mb-6 text-center">
|
||||
Your KYC submission was rejected. Please review the reasons below and resubmit.
|
||||
</p>
|
||||
|
||||
<Alert className="mb-6 bg-accent-error/5 border-accent-error/20">
|
||||
<AlertDescription>
|
||||
<ul className="list-disc list-inside space-y-1 text-[14px]">
|
||||
<li>GSTIN document is unclear - please upload a clearer copy</li>
|
||||
<li>Bank letter is missing signature</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button onClick={() => onComplete("pending")} className="w-full">
|
||||
Resubmit KYC
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center p-6">
|
||||
<Card className="w-full max-w-2xl p-8">
|
||||
{/* Progress */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-[14px] text-text-secondary">Step {step} of 4</span>
|
||||
<span className="text-[14px] text-text-secondary">{Math.round((step / 4) * 100)}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#A855F7] transition-all duration-300"
|
||||
style={{ width: `${(step / 4) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Building className="w-8 h-8 text-[#A855F7]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">Business Profile</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Provide your company's legal and business information
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="legalName">Legal Name</Label>
|
||||
<Input id="legalName" placeholder="Your Company Pvt Ltd" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="brandName">Brand Name</Label>
|
||||
<Input id="brandName" placeholder="Your Brand" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gstin">GSTIN</Label>
|
||||
<Input id="gstin" placeholder="29XXXXX1234X1ZX" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pan">PAN</Label>
|
||||
<Input id="pan" placeholder="XXXXX1234X" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">Registered Address</Label>
|
||||
<textarea
|
||||
id="address"
|
||||
className="w-full min-h-[100px] p-3 rounded-[12px] border border-border bg-background text-[14px] resize-none"
|
||||
placeholder="Street address, area, city, state, pin code"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Contact Number</Label>
|
||||
<Input id="phone" type="tel" placeholder="+91-XXXXXXXXXX" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="website">Website (optional)</Label>
|
||||
<Input id="website" type="url" placeholder="https://yourcompany.com" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-secondary/10 flex items-center justify-center">
|
||||
<FileText className="w-8 h-8 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">KYC Documents</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Upload required documents for verification
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>GST Certificate</Label>
|
||||
<div className="border-2 border-dashed border-border rounded-[12px] p-6 text-center">
|
||||
<Upload className="w-8 h-8 text-text-secondary mx-auto mb-2" />
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Click to upload or drag and drop
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="mt-2">
|
||||
Choose File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Government ID (Aadhaar/Passport)</Label>
|
||||
<div className="border-2 border-dashed border-border rounded-[12px] p-6 text-center">
|
||||
<Upload className="w-8 h-8 text-text-secondary mx-auto mb-2" />
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Click to upload or drag and drop
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="mt-2">
|
||||
Choose File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Bank Account Proof</Label>
|
||||
<div className="border-2 border-dashed border-border rounded-[12px] p-6 text-center">
|
||||
<Upload className="w-8 h-8 text-text-secondary mx-auto mb-2" />
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
Click to upload or drag and drop
|
||||
</p>
|
||||
<Button variant="outline" size="sm" className="mt-2">
|
||||
Choose File
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<CreditCard className="w-8 h-8 text-accent-positive" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">Bank Details</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Add your bank account for payments
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="accountName">Account Holder Name</Label>
|
||||
<Input id="accountName" placeholder="As per bank records" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="accountNumber">Account Number</Label>
|
||||
<Input id="accountNumber" placeholder="XXXXXXXXXXXX" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ifsc">IFSC Code</Label>
|
||||
<Input id="ifsc" placeholder="XXXX0000000" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bankName">Bank Name</Label>
|
||||
<Input id="bankName" placeholder="State Bank of India" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="branch">Branch</Label>
|
||||
<Input id="branch" placeholder="Main Branch, City" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 4 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-warn/10 flex items-center justify-center">
|
||||
<CheckCircle className="w-8 h-8 text-accent-warn" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">Review & Submit</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Review your information and submit for verification
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 bg-accent/5">
|
||||
<div className="space-y-2 text-[14px]">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Company:</span>
|
||||
<span>Auric Foundry Pvt Ltd</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">GSTIN:</span>
|
||||
<span>29XXXXX1234X1ZX</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Documents:</span>
|
||||
<span>3 uploaded</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Bank Account:</span>
|
||||
<span>Configured</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Alert>
|
||||
<AlertDescription className="text-[14px]">
|
||||
By submitting, you agree to the Terms of Service and Privacy Policy. Your application will be reviewed within 24-48 hours.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 mt-8">
|
||||
{step > 1 && (
|
||||
<Button variant="outline" onClick={() => setStep(step - 1)} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleNext} className="flex-1">
|
||||
{step === 4 ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Submit for Review
|
||||
</>
|
||||
) : (
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
src/components/manufacturer/pages/SettingsPage.tsx
Normal file
183
src/components/manufacturer/pages/SettingsPage.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { Settings, Building, Users, Bell, Shield } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
|
||||
export function SettingsPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Settings</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage company settings, team, and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Company Profile */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Building className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<h2 className="text-[18px] leading-[28px]">Company Profile</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="legalName">Legal Name</Label>
|
||||
<Input id="legalName" defaultValue="Auric Foundry Pvt Ltd" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="brandName">Brand Name</Label>
|
||||
<Input id="brandName" defaultValue="Auric Foundry" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gstin">GSTIN</Label>
|
||||
<Input id="gstin" defaultValue="29XXXXX1234X1ZX" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pan">PAN</Label>
|
||||
<Input id="pan" defaultValue="XXXXX1234X" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">Registered Address</Label>
|
||||
<textarea
|
||||
id="address"
|
||||
className="w-full min-h-[100px] p-3 rounded-[12px] border border-border bg-background text-[14px] resize-none"
|
||||
defaultValue="123 Industrial Area, Phase II, Mumbai, Maharashtra - 400001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Contact Number</Label>
|
||||
<Input id="phone" type="tel" defaultValue="+91-98XXXXXXXX" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" defaultValue="contact@auricfoundry.com" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<Button>Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Team Management */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<h2 className="text-[18px] leading-[28px]">Team & Roles</h2>
|
||||
</div>
|
||||
<Button variant="outline">Manage Team</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Active Members</p>
|
||||
<p className="text-[12px] text-text-secondary">Users with access to manufacturer portal</p>
|
||||
</div>
|
||||
<span className="text-[14px]">8</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Roles Configured</p>
|
||||
<p className="text-[12px] text-text-secondary">Admin, Catalog Manager, Sales, Operations</p>
|
||||
</div>
|
||||
<span className="text-[14px]">4</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Notifications */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<Bell className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<h2 className="text-[18px] leading-[28px]">Notification Preferences</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">New Inquiries</p>
|
||||
<p className="text-[12px] text-text-secondary">Get notified when retailers send inquiries</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Enabled</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Custom Order Requests</p>
|
||||
<p className="text-[12px] text-text-secondary">Alerts for new custom brief submissions</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Enabled</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Access Requests</p>
|
||||
<p className="text-[12px] text-text-secondary">When new retailers request connection</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Enabled</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Security */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-error/10 flex items-center justify-center">
|
||||
<Shield className="w-5 h-5 text-accent-error" />
|
||||
</div>
|
||||
<h2 className="text-[18px] leading-[28px]">Security & Access</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Two-Factor Authentication</p>
|
||||
<p className="text-[12px] text-text-secondary">Extra security for account access</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Enable</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Password</p>
|
||||
<p className="text-[12px] text-text-secondary">Last changed 60 days ago</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Change</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Active Sessions</p>
|
||||
<p className="text-[12px] text-text-secondary">Manage logged-in devices</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">View</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
203
src/components/manufacturer/pages/SharingPage.tsx
Normal file
203
src/components/manufacturer/pages/SharingPage.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import { Share2, Users, AlertCircle, Check, X, Globe, Lock } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Alert, AlertDescription } from "../../ui/alert";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
interface SharingPageProps {
|
||||
kycStatus: "pending" | "verified" | "rejected";
|
||||
}
|
||||
|
||||
const retailers = [
|
||||
{ id: "1", name: "Nova Jewels", city: "Mumbai", tier: "Gold", collections: 3, lastActivity: "2 hours ago", status: "Active" },
|
||||
{ id: "2", name: "Zephyr Gems Boutique", city: "Delhi", tier: "Silver", collections: 2, lastActivity: "1 day ago", status: "Active" },
|
||||
{ id: "3", name: "BlueLeaf Jewels", city: "Bengaluru", tier: "Bronze", collections: 1, lastActivity: "3 days ago", status: "Active" },
|
||||
];
|
||||
|
||||
const accessRequests = [
|
||||
{ id: "1", retailer: "Sunrise Jewellers", city: "Hyderabad", message: "Interested in bridal collections", date: "2 hours ago" },
|
||||
{ id: "2", retailer: "Golden Touch", city: "Chennai", message: "Looking for daily wear products", date: "1 day ago" },
|
||||
{ id: "3", retailer: "Royal Gems", city: "Pune", message: "Want to explore festive collection", date: "2 days ago" },
|
||||
];
|
||||
|
||||
export function SharingPage({ kycStatus }: SharingPageProps) {
|
||||
if (kycStatus !== "verified") {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Sharing & Connections</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage retailer connections and sharing policies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert className="bg-accent-warn/5 border-accent-warn/20">
|
||||
<Lock className="w-4 h-4 text-accent-warn" />
|
||||
<AlertDescription>
|
||||
<p className="text-[14px] mb-2">
|
||||
Sharing features are locked until KYC verification is complete.
|
||||
</p>
|
||||
<Button size="sm" className="mt-2">Complete KYC Verification</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Sharing & Connections</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage retailer connections and sharing policies
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Share2 className="w-4 h-4 mr-2" />
|
||||
Share Policy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Connected Retailers</p>
|
||||
<p className="text-[24px] leading-[32px]">18</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Active</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">16</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Pending Requests</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-warn">3</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Collections Shared</p>
|
||||
<p className="text-[24px] leading-[32px]">8</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Access Requests */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[18px] leading-[28px]">Access Requests</h2>
|
||||
<Badge className="bg-accent-warn">{accessRequests.length} Pending</Badge>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{accessRequests.map((request) => (
|
||||
<div key={request.id} className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#A855F7]/10 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-[#A855F7]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">{request.retailer} • {request.city}</p>
|
||||
<p className="text-[12px] text-text-secondary">{request.message}</p>
|
||||
<p className="text-[11px] text-text-secondary mt-1">{request.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Approve
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Deny
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Connected Retailers */}
|
||||
<Card>
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<h2 className="text-[18px] leading-[28px]">Connected Retailers</h2>
|
||||
<Button variant="outline" size="sm">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
View Map
|
||||
</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Retailer</TableHead>
|
||||
<TableHead>City</TableHead>
|
||||
<TableHead>Tier</TableHead>
|
||||
<TableHead>Collections Shared</TableHead>
|
||||
<TableHead>Last Activity</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{retailers.map((retailer) => (
|
||||
<TableRow key={retailer.id}>
|
||||
<TableCell>{retailer.name}</TableCell>
|
||||
<TableCell>{retailer.city}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={retailer.tier === "Gold" ? "default" : "outline"}>
|
||||
{retailer.tier}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{retailer.collections}</TableCell>
|
||||
<TableCell className="text-text-secondary">{retailer.lastActivity}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-accent-positive">{retailer.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost">
|
||||
Manage
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* Sharing Policy */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Default Sharing Policy</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Default Visibility</p>
|
||||
<p className="text-[12px] text-text-secondary">New SKUs are private by default</p>
|
||||
</div>
|
||||
<Badge variant="secondary">Private</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Price Floor Enforcement</p>
|
||||
<p className="text-[12px] text-text-secondary">Prevent retailers from going below minimum</p>
|
||||
</div>
|
||||
<Badge className="bg-accent-positive">Enabled</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Geofence Restrictions</p>
|
||||
<p className="text-[12px] text-text-secondary">Control where products can be sold</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Configure</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
266
src/components/retailer/AddFromManufacturerDialog.tsx
Normal file
266
src/components/retailer/AddFromManufacturerDialog.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { useState } from "react";
|
||||
import { Factory, Plus, Check, Search } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
interface AddFromManufacturerDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onAdd: (skus: any[]) => void;
|
||||
}
|
||||
|
||||
const connectedManufacturers = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Auric Foundry",
|
||||
location: "Mumbai",
|
||||
category: "Gold Jewellery",
|
||||
skuCount: 842,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Zephyr Casting",
|
||||
location: "Surat",
|
||||
category: "Silver & Gold",
|
||||
skuCount: 624,
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "BlueRay Gems",
|
||||
location: "Jaipur",
|
||||
category: "Diamond Jewellery",
|
||||
skuCount: 512,
|
||||
status: "active",
|
||||
},
|
||||
];
|
||||
|
||||
const manufacturerSkus = {
|
||||
"1": [
|
||||
{ id: "1-1", sku: "AU-22K-BR-0192", title: "Heritage Bridal Ring", category: "Rings", metal: "22K Gold", weight: "6.8g", price: "₹2.8L", alreadyAdded: true },
|
||||
{ id: "1-2", sku: "AU-22K-BG-1023", title: "Classic Bangle Set", category: "Bangles", metal: "22K Gold", weight: "18.4g", price: "₹1.2L", alreadyAdded: true },
|
||||
{ id: "1-3", sku: "AU-22K-NK-0551", title: "Temple Design Necklace", category: "Necklaces", metal: "22K Gold", weight: "24.2g", price: "₹1.8L", alreadyAdded: false },
|
||||
{ id: "1-4", sku: "AU-18K-ER-0234", title: "Diamond Stud Earrings", category: "Earrings", metal: "18K Gold", weight: "2.4g", price: "₹68K", alreadyAdded: false },
|
||||
{ id: "1-5", sku: "AU-22K-BR-0293", title: "Antique Finger Ring", category: "Rings", metal: "22K Gold", weight: "5.2g", price: "₹2.2L", alreadyAdded: false },
|
||||
],
|
||||
"2": [
|
||||
{ id: "2-1", sku: "ZC-SLV-BG-0142", title: "Silver Bangle Pair", category: "Bangles", metal: "Sterling Silver", weight: "32.4g", price: "₹8.5K", alreadyAdded: false },
|
||||
{ id: "2-2", sku: "ZC-18K-NK-0442", title: "Fusion Chain Necklace", category: "Necklaces", metal: "18K Gold", weight: "12.8g", price: "₹95K", alreadyAdded: false },
|
||||
{ id: "2-3", sku: "ZC-22K-ER-0334", title: "Jhumka Earrings", category: "Earrings", metal: "22K Gold", weight: "6.4g", price: "₹42K", alreadyAdded: false },
|
||||
],
|
||||
"3": [
|
||||
{ id: "3-1", sku: "BR-DIA-BR-0524", title: "Solitaire Engagement Ring", category: "Rings", metal: "Platinum", weight: "4.2g", price: "₹4.8L", alreadyAdded: false },
|
||||
{ id: "3-2", sku: "BR-DIA-NK-0624", title: "Diamond Tennis Necklace", category: "Necklaces", metal: "18K White Gold", weight: "18.6g", price: "₹6.2L", alreadyAdded: false },
|
||||
{ id: "3-3", sku: "BR-DIA-ER-0124", title: "Halo Diamond Studs", category: "Earrings", metal: "18K Gold", weight: "3.8g", price: "₹1.8L", alreadyAdded: false },
|
||||
],
|
||||
};
|
||||
|
||||
export function AddFromManufacturerDialog({ open, onClose, onAdd }: AddFromManufacturerDialogProps) {
|
||||
const [selectedManufacturer, setSelectedManufacturer] = useState(connectedManufacturers[0].id);
|
||||
const [selectedSkus, setSelectedSkus] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const currentSkus = manufacturerSkus[selectedManufacturer as keyof typeof manufacturerSkus] || [];
|
||||
const filteredSkus = currentSkus.filter(
|
||||
(sku) =>
|
||||
!sku.alreadyAdded &&
|
||||
(sku.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
sku.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
sku.category.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
const handleToggleSku = (skuId: string) => {
|
||||
setSelectedSkus((prev) =>
|
||||
prev.includes(skuId) ? prev.filter((id) => id !== skuId) : [...prev, skuId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const availableSkuIds = filteredSkus.map((sku) => sku.id);
|
||||
if (selectedSkus.length === availableSkuIds.length) {
|
||||
setSelectedSkus([]);
|
||||
} else {
|
||||
setSelectedSkus(availableSkuIds);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
const skusToAdd = currentSkus.filter((sku) => selectedSkus.includes(sku.id));
|
||||
onAdd(skusToAdd);
|
||||
toast.success(`Added ${skusToAdd.length} SKUs to your inventory`);
|
||||
setSelectedSkus([]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-5xl max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Factory className="w-5 h-5 text-primary" />
|
||||
Add SKUs from Connected Manufacturers
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Browse and select products from your connected manufacturer partners
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
||||
{/* Manufacturer List */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-[14px] font-medium mb-3">Connected Manufacturers</h4>
|
||||
<div className="space-y-2">
|
||||
{connectedManufacturers.map((manufacturer) => (
|
||||
<button
|
||||
key={manufacturer.id}
|
||||
onClick={() => {
|
||||
setSelectedManufacturer(manufacturer.id);
|
||||
setSelectedSkus([]);
|
||||
}}
|
||||
className={`w-full text-left p-3 rounded-lg border transition-all ${
|
||||
selectedManufacturer === manufacturer.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Factory className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[14px] font-medium truncate">{manufacturer.name}</p>
|
||||
<p className="text-[12px] text-muted-foreground">{manufacturer.location}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||
{manufacturer.skuCount} SKUs
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SKU List */}
|
||||
<div className="col-span-2 space-y-4">
|
||||
{/* Search and Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search SKUs..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSelectAll}
|
||||
disabled={filteredSkus.length === 0}
|
||||
>
|
||||
{selectedSkus.length === filteredSkus.length && filteredSkus.length > 0
|
||||
? "Deselect All"
|
||||
: "Select All"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* SKU Cards */}
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<div className="space-y-2">
|
||||
{filteredSkus.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-[300px] text-center">
|
||||
<Package className="w-12 h-12 text-muted-foreground mb-3" />
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
{searchQuery
|
||||
? "No SKUs found matching your search"
|
||||
: "All available SKUs from this manufacturer are already in your inventory"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredSkus.map((sku) => (
|
||||
<div
|
||||
key={sku.id}
|
||||
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||
selectedSkus.includes(sku.id)
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
}`}
|
||||
onClick={() => handleToggleSku(sku.id)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
checked={selectedSkus.includes(sku.id)}
|
||||
onCheckedChange={() => handleToggleSku(sku.id)}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<p className="text-[14px] font-medium">{sku.title}</p>
|
||||
<p className="text-[12px] font-mono text-muted-foreground">{sku.sku}</p>
|
||||
</div>
|
||||
<p className="text-[14px] font-medium text-primary">{sku.price}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[12px] text-muted-foreground">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{sku.category}
|
||||
</Badge>
|
||||
<span>•</span>
|
||||
<span>{sku.metal}</span>
|
||||
<span>•</span>
|
||||
<span>{sku.weight}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
{selectedSkus.length > 0 ? (
|
||||
<>
|
||||
<span className="font-medium text-foreground">{selectedSkus.length}</span> SKU
|
||||
{selectedSkus.length > 1 ? "s" : ""} selected
|
||||
</>
|
||||
) : (
|
||||
"No SKUs selected"
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAdd} disabled={selectedSkus.length === 0}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add {selectedSkus.length > 0 && `(${selectedSkus.length})`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
324
src/components/retailer/AddSkuDialog.tsx
Normal file
324
src/components/retailer/AddSkuDialog.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import { useState } from "react";
|
||||
import { Plus, Upload, ImageIcon, X } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
interface AddSkuDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onAdd: (skuData: any) => void;
|
||||
}
|
||||
|
||||
export function AddSkuDialog({ open, onClose, onAdd }: AddSkuDialogProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
sku: "",
|
||||
category: "",
|
||||
metal: "",
|
||||
purity: "",
|
||||
weight: "",
|
||||
description: "",
|
||||
makingCharges: "",
|
||||
margin: "",
|
||||
status: "in-stock",
|
||||
visibility: "public",
|
||||
});
|
||||
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!formData.title || !formData.sku || !formData.category) {
|
||||
toast.error("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
onAdd(formData);
|
||||
toast.success("SKU added successfully");
|
||||
onClose();
|
||||
setFormData({
|
||||
title: "",
|
||||
sku: "",
|
||||
category: "",
|
||||
metal: "",
|
||||
purity: "",
|
||||
weight: "",
|
||||
description: "",
|
||||
makingCharges: "",
|
||||
margin: "",
|
||||
status: "in-stock",
|
||||
visibility: "public",
|
||||
});
|
||||
setImages([]);
|
||||
};
|
||||
|
||||
const handleImageUpload = () => {
|
||||
// Simulate image upload
|
||||
toast.success("Image upload functionality would be implemented here");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[85vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New SKU</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new product listing for your inventory
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="basic">Basic Info</TabsTrigger>
|
||||
<TabsTrigger value="specs">Specifications</TabsTrigger>
|
||||
<TabsTrigger value="pricing">Pricing</TabsTrigger>
|
||||
<TabsTrigger value="media">Media</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Product Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="e.g., Heritage Bridal Ring"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sku">SKU Code *</Label>
|
||||
<Input
|
||||
id="sku"
|
||||
placeholder="e.g., AU-22K-BR-0192"
|
||||
value={formData.sku}
|
||||
onChange={(e) => setFormData({ ...formData, sku: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
placeholder="Describe the product features, craftsmanship, and design..."
|
||||
rows={4}
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category *</Label>
|
||||
<Select value={formData.category} onValueChange={(v) => setFormData({ ...formData, category: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="rings">Rings</SelectItem>
|
||||
<SelectItem value="necklaces">Necklaces</SelectItem>
|
||||
<SelectItem value="earrings">Earrings</SelectItem>
|
||||
<SelectItem value="bangles">Bangles</SelectItem>
|
||||
<SelectItem value="bracelets">Bracelets</SelectItem>
|
||||
<SelectItem value="pendants">Pendants</SelectItem>
|
||||
<SelectItem value="chains">Chains</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Availability Status</Label>
|
||||
<Select value={formData.status} onValueChange={(v) => setFormData({ ...formData, status: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="in-stock">In Stock</SelectItem>
|
||||
<SelectItem value="made-to-order">Made to Order</SelectItem>
|
||||
<SelectItem value="out-of-stock">Out of Stock</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="specs" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="metal">Metal Type</Label>
|
||||
<Select value={formData.metal} onValueChange={(v) => setFormData({ ...formData, metal: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select metal" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gold">Gold</SelectItem>
|
||||
<SelectItem value="silver">Silver</SelectItem>
|
||||
<SelectItem value="platinum">Platinum</SelectItem>
|
||||
<SelectItem value="white-gold">White Gold</SelectItem>
|
||||
<SelectItem value="rose-gold">Rose Gold</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="purity">Purity/Karat</Label>
|
||||
<Select value={formData.purity} onValueChange={(v) => setFormData({ ...formData, purity: v })}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select purity" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="24k">24K Gold</SelectItem>
|
||||
<SelectItem value="22k">22K Gold</SelectItem>
|
||||
<SelectItem value="18k">18K Gold</SelectItem>
|
||||
<SelectItem value="14k">14K Gold</SelectItem>
|
||||
<SelectItem value="925">925 Sterling Silver</SelectItem>
|
||||
<SelectItem value="950">950 Platinum</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="weight">Weight (in grams)</Label>
|
||||
<Input
|
||||
id="weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="e.g., 6.80"
|
||||
value={formData.weight}
|
||||
onChange={(e) => setFormData({ ...formData, weight: e.target.value })}
|
||||
/>
|
||||
<p className="text-[12px] text-muted-foreground">Enter the total weight including stones</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pricing" className="space-y-4 mt-4">
|
||||
<div className="p-4 rounded-lg bg-accent/5 border border-accent/20">
|
||||
<p className="text-[13px] text-muted-foreground mb-2">
|
||||
Final price will be calculated based on current gold rate, weight, making charges, and margin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="makingCharges">Making Charges (%)</Label>
|
||||
<Input
|
||||
id="makingCharges"
|
||||
type="number"
|
||||
step="0.5"
|
||||
placeholder="e.g., 12.5"
|
||||
value={formData.makingCharges}
|
||||
onChange={(e) => setFormData({ ...formData, makingCharges: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="margin">Margin (%)</Label>
|
||||
<Input
|
||||
id="margin"
|
||||
type="number"
|
||||
step="0.5"
|
||||
placeholder="e.g., 8.0"
|
||||
value={formData.margin}
|
||||
onChange={(e) => setFormData({ ...formData, margin: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg border border-border space-y-2">
|
||||
<h4 className="text-[14px] font-medium">Price Preview</h4>
|
||||
<div className="space-y-1 text-[13px]">
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Base Metal Cost</span>
|
||||
<span>Calculated from daily rate</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Making Charges</span>
|
||||
<span>{formData.makingCharges || "0"}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Margin</span>
|
||||
<span>{formData.margin || "0"}%</span>
|
||||
</div>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex justify-between font-medium">
|
||||
<span>Final MRP</span>
|
||||
<span>Calculated at checkout</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="media" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Product Images</Label>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={handleImageUpload}
|
||||
className="aspect-square rounded-lg border-2 border-dashed border-border hover:border-primary hover:bg-accent/5 flex flex-col items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<ImageIcon className="w-8 h-8 text-muted-foreground" />
|
||||
<span className="text-[11px] text-muted-foreground">Upload {i}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Upload up to 4 images. First image will be the primary display image.
|
||||
</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="visibility">Visibility</Label>
|
||||
<Select value={formData.visibility} onValueChange={(v) => setFormData({ ...formData, visibility: v })}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="public">Public - Visible to all</SelectItem>
|
||||
<SelectItem value="private">Private - Hidden from store</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add SKU
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
220
src/components/retailer/GoldRateModal.tsx
Normal file
220
src/components/retailer/GoldRateModal.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { useState } from "react";
|
||||
import { TrendingUp, IndianRupee, Calendar, Save } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
import { Card } from "../ui/card";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
interface GoldRateModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (rates: GoldRates) => void;
|
||||
}
|
||||
|
||||
export interface GoldRates {
|
||||
gold22k: string;
|
||||
gold24k: string;
|
||||
silver: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export function GoldRateModal({ open, onClose, onSubmit }: GoldRateModalProps) {
|
||||
const [rates, setRates] = useState<GoldRates>({
|
||||
gold22k: "",
|
||||
gold24k: "",
|
||||
silver: "",
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!rates.gold22k || !rates.gold24k || !rates.silver) {
|
||||
return;
|
||||
}
|
||||
onSubmit(rates);
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Previous day's rates for reference
|
||||
const previousRates = {
|
||||
gold22k: "6,245",
|
||||
gold24k: "6,815",
|
||||
silver: "78",
|
||||
};
|
||||
|
||||
const calculateChange = (current: string, previous: string) => {
|
||||
if (!current) return null;
|
||||
const diff = parseFloat(current.replace(/,/g, '')) - parseFloat(previous.replace(/,/g, ''));
|
||||
return diff;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-primary" />
|
||||
Set Today's Gold & Silver Rates
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update daily rates to open your shop. These rates will be visible to all your customers.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Date Display */}
|
||||
<Card className="p-4 bg-accent/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[14px] text-muted-foreground">Rate Date</span>
|
||||
</div>
|
||||
<span className="text-[14px] font-medium">
|
||||
{new Date().toLocaleDateString('en-IN', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Gold Rates */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-[16px] font-medium">Gold Rates (per gram)</h4>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
{/* 22K Gold */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gold22k">22 Karat Gold</Label>
|
||||
<div className="relative">
|
||||
<IndianRupee className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="gold22k"
|
||||
type="text"
|
||||
placeholder="6,245"
|
||||
value={rates.gold22k}
|
||||
onChange={(e) => setRates({ ...rates, gold22k: e.target.value })}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[12px]">
|
||||
<span className="text-muted-foreground">Yesterday: ₹{previousRates.gold22k}</span>
|
||||
{rates.gold22k && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
(calculateChange(rates.gold22k, previousRates.gold22k) || 0) >= 0
|
||||
? "text-accent-positive border-accent-positive/50"
|
||||
: "text-accent-error border-accent-error/50"
|
||||
}
|
||||
>
|
||||
{(calculateChange(rates.gold22k, previousRates.gold22k) || 0) >= 0 ? "+" : ""}
|
||||
{calculateChange(rates.gold22k, previousRates.gold22k)?.toFixed(0)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 24K Gold */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gold24k">24 Karat Gold</Label>
|
||||
<div className="relative">
|
||||
<IndianRupee className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="gold24k"
|
||||
type="text"
|
||||
placeholder="6,815"
|
||||
value={rates.gold24k}
|
||||
onChange={(e) => setRates({ ...rates, gold24k: e.target.value })}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[12px]">
|
||||
<span className="text-muted-foreground">Yesterday: ₹{previousRates.gold24k}</span>
|
||||
{rates.gold24k && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
(calculateChange(rates.gold24k, previousRates.gold24k) || 0) >= 0
|
||||
? "text-accent-positive border-accent-positive/50"
|
||||
: "text-accent-error border-accent-error/50"
|
||||
}
|
||||
>
|
||||
{(calculateChange(rates.gold24k, previousRates.gold24k) || 0) >= 0 ? "+" : ""}
|
||||
{calculateChange(rates.gold24k, previousRates.gold24k)?.toFixed(0)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Silver Rate */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-[16px] font-medium">Silver Rate (per gram)</h4>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="silver">Pure Silver (99.9%)</Label>
|
||||
<div className="relative">
|
||||
<IndianRupee className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="silver"
|
||||
type="text"
|
||||
placeholder="78"
|
||||
value={rates.silver}
|
||||
onChange={(e) => setRates({ ...rates, silver: e.target.value })}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[12px]">
|
||||
<span className="text-muted-foreground">Yesterday: ₹{previousRates.silver}</span>
|
||||
{rates.silver && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
(calculateChange(rates.silver, previousRates.silver) || 0) >= 0
|
||||
? "text-accent-positive border-accent-positive/50"
|
||||
: "text-accent-error border-accent-error/50"
|
||||
}
|
||||
>
|
||||
{(calculateChange(rates.silver, previousRates.silver) || 0) >= 0 ? "+" : ""}
|
||||
{calculateChange(rates.silver, previousRates.silver)?.toFixed(0)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Card */}
|
||||
<Card className="p-4 bg-primary/5 border-primary/20">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
<strong>Note:</strong> These rates will be displayed on your storefront and used for price calculations.
|
||||
You can update rates anytime from Settings → Daily Rates.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Skip for Now
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!rates.gold22k || !rates.gold24k || !rates.silver}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Set Rates & Open Shop
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
234
src/components/retailer/LiveStorefrontPreview.tsx
Normal file
234
src/components/retailer/LiveStorefrontPreview.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { X, Home, Search, Heart, User, Package, ChevronRight } from "lucide-react";
|
||||
import { Dialog, DialogContent } from "../ui/dialog";
|
||||
import { Button } from "../ui/button";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
|
||||
interface LiveStorefrontPreviewProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
storeName: string;
|
||||
storeLocation: string;
|
||||
theme: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function LiveStorefrontPreview({
|
||||
open,
|
||||
onClose,
|
||||
storeName,
|
||||
storeLocation,
|
||||
theme,
|
||||
description,
|
||||
}: LiveStorefrontPreviewProps) {
|
||||
const themeColors = {
|
||||
elegant: { primary: "#D4AF37", secondary: "#0B0D10" },
|
||||
modern: { primary: "#5B8DEF", secondary: "#F4B400" },
|
||||
royal: { primary: "#7C3AED", secondary: "#EC4899" },
|
||||
luxury: { primary: "#0B0D10", secondary: "#FBBF24" },
|
||||
};
|
||||
|
||||
const currentTheme = themeColors[theme as keyof typeof themeColors] || themeColors.modern;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-[440px] p-0 gap-0">
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<div>
|
||||
<h3 className="text-[16px] font-medium">Live Preview</h3>
|
||||
<p className="text-[12px] text-muted-foreground">Mobile App View</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Phone Frame */}
|
||||
<div className="p-6 bg-muted/30">
|
||||
<div className="relative mx-auto" style={{ width: '340px' }}>
|
||||
{/* Phone Frame */}
|
||||
<div className="relative bg-background border-[12px] border-border rounded-[32px] shadow-2xl overflow-hidden">
|
||||
{/* Notch */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-28 h-6 bg-border rounded-b-2xl z-20" />
|
||||
|
||||
{/* Screen Content */}
|
||||
<div className="relative bg-background" style={{ height: '640px' }}>
|
||||
<ScrollArea className="h-full">
|
||||
{/* Status Bar */}
|
||||
<div className="h-12 flex items-center justify-between px-6 text-[11px] sticky top-0 bg-background z-10">
|
||||
<span>9:41</span>
|
||||
<span>●●●●</span>
|
||||
</div>
|
||||
|
||||
{/* Store Header */}
|
||||
<div
|
||||
className="text-white p-6 text-center"
|
||||
style={{ backgroundColor: currentTheme.primary }}
|
||||
>
|
||||
<h2 className="text-[20px] font-medium mb-1">{storeName}</h2>
|
||||
<p className="text-[13px] opacity-90">{storeLocation}</p>
|
||||
<div className="mt-3 flex items-center justify-center gap-2">
|
||||
<Badge variant="secondary" className="text-[10px] px-2 py-0.5">
|
||||
Open Now
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-[10px] px-2 py-0.5">
|
||||
Verified
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="px-4 -mt-4 mb-4">
|
||||
<div className="bg-background rounded-lg border border-border shadow-sm p-3 flex items-center gap-2">
|
||||
<Search className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-[13px] text-muted-foreground">Search jewellery...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Store Description */}
|
||||
{description && (
|
||||
<div className="px-4 mb-4">
|
||||
<p className="text-[12px] text-muted-foreground line-clamp-2">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="px-4 mb-4">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ icon: Package, label: "Collections" },
|
||||
{ icon: Heart, label: "Wishlist" },
|
||||
{ icon: User, label: "Book Visit" },
|
||||
].map((action, i) => {
|
||||
const Icon = action.icon;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
className="flex flex-col items-center gap-1 p-3 rounded-lg border border-border hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center"
|
||||
style={{ backgroundColor: `${currentTheme.primary}20` }}
|
||||
>
|
||||
<Icon className="w-5 h-5" style={{ color: currentTheme.primary }} />
|
||||
</div>
|
||||
<span className="text-[11px]">{action.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Featured Banner */}
|
||||
<div className="px-4 mb-4">
|
||||
<div
|
||||
className="aspect-[16/9] rounded-lg flex items-center justify-center text-white"
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${currentTheme.primary} 0%, ${currentTheme.secondary} 100%)`
|
||||
}}
|
||||
>
|
||||
<div className="text-center p-4">
|
||||
<h3 className="text-[16px] font-medium mb-1">Featured Collection</h3>
|
||||
<p className="text-[12px] opacity-90">Discover Our Latest Designs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Grid */}
|
||||
<div className="px-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-[14px] font-medium">New Arrivals</h3>
|
||||
<button className="text-[12px] flex items-center gap-1" style={{ color: currentTheme.primary }}>
|
||||
View All
|
||||
<ChevronRight className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="rounded-lg border border-border overflow-hidden">
|
||||
<div
|
||||
className="aspect-square flex items-center justify-center"
|
||||
style={{ backgroundColor: `${currentTheme.primary}10` }}
|
||||
>
|
||||
<Package className="w-8 h-8 text-muted-foreground/50" />
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<p className="text-[12px] font-medium line-clamp-1">Gold Ring {i}</p>
|
||||
<p className="text-[11px]" style={{ color: currentTheme.primary }}>
|
||||
₹{(25 + i * 5)}K
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="px-4 mb-4">
|
||||
<h3 className="text-[14px] font-medium mb-3">Shop by Category</h3>
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{['Rings', 'Necklaces', 'Earrings', 'Bangles', 'Bracelets'].map((cat) => (
|
||||
<div
|
||||
key={cat}
|
||||
className="px-4 py-2 rounded-full border text-[12px] whitespace-nowrap"
|
||||
style={{ borderColor: currentTheme.primary }}
|
||||
>
|
||||
{cat}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Offer Banner */}
|
||||
<div className="px-4 mb-20">
|
||||
<div
|
||||
className="p-4 rounded-lg text-white"
|
||||
style={{ backgroundColor: currentTheme.secondary }}
|
||||
>
|
||||
<p className="text-[10px] uppercase tracking-wide opacity-90 mb-1">Limited Time</p>
|
||||
<h4 className="text-[14px] font-medium mb-1">Special Offer</h4>
|
||||
<p className="text-[11px] opacity-90">Get 15% off on gold jewellery</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-16 bg-background border-t border-border">
|
||||
<div className="flex items-center justify-around h-full px-2">
|
||||
{[
|
||||
{ icon: Home, label: "Home", active: true },
|
||||
{ icon: Search, label: "Search" },
|
||||
{ icon: Heart, label: "Wishlist" },
|
||||
{ icon: User, label: "Account" },
|
||||
].map((item, i) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
className="flex flex-col items-center gap-1 px-3 py-2"
|
||||
>
|
||||
<Icon
|
||||
className="w-5 h-5"
|
||||
style={{ color: item.active ? currentTheme.primary : 'currentColor' }}
|
||||
/>
|
||||
<span
|
||||
className="text-[10px]"
|
||||
style={{ color: item.active ? currentTheme.primary : 'currentColor' }}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
143
src/components/retailer/NotificationsPanel.tsx
Normal file
143
src/components/retailer/NotificationsPanel.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { Bell, Package, MessageCircle, Calendar, TrendingUp, X } from "lucide-react";
|
||||
import { Card } from "../ui/card";
|
||||
import { Button } from "../ui/button";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "../ui/popover";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
interface NotificationsPanelProps {
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
id: "1",
|
||||
type: "message",
|
||||
icon: MessageCircle,
|
||||
title: "New message from Aditi Sharma",
|
||||
description: "Interested in the Heritage Bridal Ring",
|
||||
time: "5 minutes ago",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
type: "order",
|
||||
icon: Package,
|
||||
title: "New order placed",
|
||||
description: "Order #ORD-2847 for ₹2.8L",
|
||||
time: "1 hour ago",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
type: "appointment",
|
||||
icon: Calendar,
|
||||
title: "Appointment confirmed",
|
||||
description: "Rohan Mehta - Tomorrow at 2:30 PM",
|
||||
time: "2 hours ago",
|
||||
unread: true,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
type: "inventory",
|
||||
icon: TrendingUp,
|
||||
title: "Gold rate update reminder",
|
||||
description: "Update today's rates to open your shop",
|
||||
time: "3 hours ago",
|
||||
unread: false,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
type: "message",
|
||||
icon: MessageCircle,
|
||||
title: "Quote request received",
|
||||
description: "Sana Khan requested quote for Festive Collection",
|
||||
time: "Yesterday",
|
||||
unread: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function NotificationsPanel({ unreadCount = 3 }: NotificationsPanelProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="relative">
|
||||
<Bell className="w-5 h-5" />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-destructive rounded-full" />
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0" align="end">
|
||||
<div className="flex items-center justify-between p-4 border-b border-border">
|
||||
<div>
|
||||
<h3 className="text-[16px] font-medium">Notifications</h3>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
You have {unreadCount} unread messages
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
Mark all read
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-[400px]">
|
||||
<div className="p-2">
|
||||
{notifications.map((notification) => {
|
||||
const Icon = notification.icon;
|
||||
return (
|
||||
<button
|
||||
key={notification.id}
|
||||
className={`w-full text-left p-3 rounded-lg hover:bg-accent/5 transition-colors ${
|
||||
notification.unread ? "bg-primary/5" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
notification.type === "message" ? "bg-primary/10" :
|
||||
notification.type === "order" ? "bg-accent-positive/10" :
|
||||
notification.type === "appointment" ? "bg-secondary/10" :
|
||||
"bg-accent-warn/10"
|
||||
}`}>
|
||||
<Icon className={`w-5 h-5 ${
|
||||
notification.type === "message" ? "text-primary" :
|
||||
notification.type === "order" ? "text-accent-positive" :
|
||||
notification.type === "appointment" ? "text-secondary" :
|
||||
"text-accent-warn"
|
||||
}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<p className="text-[14px] font-medium line-clamp-1">{notification.title}</p>
|
||||
{notification.unread && (
|
||||
<div className="w-2 h-2 rounded-full bg-primary flex-shrink-0 mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[13px] text-muted-foreground line-clamp-2 mb-1">
|
||||
{notification.description}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground">{notification.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="p-3">
|
||||
<Button variant="outline" className="w-full" size="sm">
|
||||
View all notifications
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
216
src/components/retailer/RetailerShell.tsx
Normal file
216
src/components/retailer/RetailerShell.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
Grid,
|
||||
Users,
|
||||
MessageCircle,
|
||||
Calendar,
|
||||
FileText,
|
||||
Store,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Search,
|
||||
Bell,
|
||||
ChevronDown,
|
||||
Globe,
|
||||
Moon,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { RetailerPage } from "../../RetailerApp";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { AIAssistant } from "../AIAssistant";
|
||||
import { SmartSearch } from "../SmartSearch";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/dropdown-menu";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { NotificationsPanel } from "./NotificationsPanel";
|
||||
|
||||
interface RetailerShellProps {
|
||||
children: React.ReactNode;
|
||||
currentPage: RetailerPage;
|
||||
onNavigate: (page: RetailerPage) => void;
|
||||
brandName: string;
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: "dashboard" as RetailerPage, icon: LayoutDashboard, label: "Dashboard" },
|
||||
{ id: "inventory" as RetailerPage, icon: Package, label: "Inventory" },
|
||||
{ id: "curation" as RetailerPage, icon: Grid, label: "Curation" },
|
||||
{ id: "customers" as RetailerPage, icon: Users, label: "Customers" },
|
||||
{ id: "chat" as RetailerPage, icon: MessageCircle, label: "Chat", badge: 3 },
|
||||
{ id: "appointments" as RetailerPage, icon: Calendar, label: "Appointments" },
|
||||
{ id: "quotes-orders" as RetailerPage, icon: FileText, label: "Quotes & Orders" },
|
||||
{ id: "storefront" as RetailerPage, icon: Store, label: "Storefront" },
|
||||
{ id: "analytics" as RetailerPage, icon: BarChart3, label: "Analytics" },
|
||||
];
|
||||
|
||||
export function RetailerShell({ children, currentPage, onNavigate, brandName }: RetailerShellProps) {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
const [language, setLanguage] = useState("en");
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === "dark") {
|
||||
root.classList.add("dark");
|
||||
} else {
|
||||
root.classList.remove("dark");
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-background border-r border-border flex flex-col">
|
||||
{/* Brand Header */}
|
||||
<div className="h-16 px-4 flex items-center gap-3 border-b border-border">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Store className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[14px] truncate">{brandName}</p>
|
||||
<p className="text-[12px] text-text-secondary">Retailer Portal</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-3 space-y-1 overflow-y-auto">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = currentPage === item.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-[10px] transition-colors ${
|
||||
isActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-text-secondary hover:bg-accent/10 hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-[14px] flex-1 text-left">{item.label}</span>
|
||||
{item.badge && (
|
||||
<span className="px-2 py-0.5 rounded-full bg-destructive text-destructive-foreground text-[11px]">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Settings at bottom */}
|
||||
<div className="p-3 border-t border-border">
|
||||
<button
|
||||
onClick={() => onNavigate("team")}
|
||||
className={`w-full flex items-center gap-3 px-3 py-2 rounded-[10px] transition-colors ${
|
||||
currentPage === "team"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-text-secondary hover:bg-accent/10 hover:text-text-primary"
|
||||
}`}
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
<span className="text-[14px]">Team & Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Top Bar */}
|
||||
<header className="h-16 bg-background border-b border-border px-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
{/* Smart AI Search */}
|
||||
<div className="w-96">
|
||||
<SmartSearch
|
||||
portalType="retailer"
|
||||
placeholder="Search SKUs, collections, customers... (⌘K)"
|
||||
onNavigate={(page) => onNavigate(page as RetailerPage)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Language Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||
<Select value={language} onValueChange={setLanguage}>
|
||||
<SelectTrigger className="w-[120px] h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="hi">हिन्दी</SelectItem>
|
||||
<SelectItem value="mr">मराठी</SelectItem>
|
||||
<SelectItem value="gu">ગુજરાતી</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<Moon className="w-5 h-5" />
|
||||
) : (
|
||||
<Sun className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Notifications */}
|
||||
<NotificationsPanel unreadCount={3} />
|
||||
|
||||
{/* Profile */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="text-[14px]">A</span>
|
||||
</div>
|
||||
<span className="text-[14px]">Admin</span>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onNavigate("setup")}>
|
||||
Brand Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onNavigate("team")}>
|
||||
Team & Roles
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Sign out</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-auto">{children}</main>
|
||||
</div>
|
||||
|
||||
{/* AI Assistant */}
|
||||
<AIAssistant portalType="retailer" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
322
src/components/retailer/pages/AnalyticsPage.tsx
Normal file
322
src/components/retailer/pages/AnalyticsPage.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
import { BarChart3, TrendingUp, Users, Package, Download } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../ui/select";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "../../ui/chart";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Area,
|
||||
AreaChart,
|
||||
ResponsiveContainer,
|
||||
} from "recharts@2.15.2";
|
||||
|
||||
const viewsData = [
|
||||
{ date: "Jan 1", views: 420, inquiries: 28 },
|
||||
{ date: "Jan 8", views: 580, inquiries: 42 },
|
||||
{ date: "Jan 15", views: 720, inquiries: 58 },
|
||||
{ date: "Jan 22", views: 680, inquiries: 51 },
|
||||
{ date: "Jan 29", views: 890, inquiries: 73 },
|
||||
{ date: "Feb 5", views: 1240, inquiries: 98 },
|
||||
{ date: "Feb 12", views: 1580, inquiries: 124 },
|
||||
];
|
||||
|
||||
const conversionData = [
|
||||
{ stage: "Visits", count: 2847 },
|
||||
{ stage: "Wishlist", count: 1423 },
|
||||
{ stage: "Inquiry", count: 542 },
|
||||
{ stage: "Quote", count: 186 },
|
||||
{ stage: "Order", count: 72 },
|
||||
];
|
||||
|
||||
const revenueData = [
|
||||
{ month: "Aug", revenue: 45 },
|
||||
{ month: "Sep", revenue: 52 },
|
||||
{ month: "Oct", revenue: 61 },
|
||||
{ month: "Nov", revenue: 58 },
|
||||
{ month: "Dec", revenue: 73 },
|
||||
{ month: "Jan", revenue: 86 },
|
||||
];
|
||||
|
||||
export function AnalyticsPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Analytics</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Track performance and insights across your store
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Select defaultValue="30">
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="7">Last 7 days</SelectItem>
|
||||
<SelectItem value="30">Last 30 days</SelectItem>
|
||||
<SelectItem value="90">Last 90 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-primary/10 flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+12.5%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">2,847</p>
|
||||
<p className="text-[12px] text-text-secondary">Store Views</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-secondary/10 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+22.1%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">34</p>
|
||||
<p className="text-[12px] text-text-secondary">Orders</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-accent-positive/10 flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+5.1%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">38.2%</p>
|
||||
<p className="text-[12px] text-text-secondary">Conversion</p>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-accent-warn/10 flex items-center justify-center">
|
||||
<BarChart3 className="w-5 h-5 text-accent-warn" />
|
||||
</div>
|
||||
<span className="text-[12px] px-2 py-0.5 rounded-full bg-accent-positive/10 text-accent-positive">
|
||||
+18.3%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">₹86L</p>
|
||||
<p className="text-[12px] text-text-secondary">Revenue</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Views & Inquiries</h3>
|
||||
<ChartContainer
|
||||
config={{
|
||||
views: {
|
||||
label: "Views",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
inquiries: {
|
||||
label: "Inquiries",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
}}
|
||||
className="h-64"
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={viewsData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="views"
|
||||
stroke="hsl(var(--chart-1))"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="inquiries"
|
||||
stroke="hsl(var(--chart-2))"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Conversion Funnel</h3>
|
||||
<ChartContainer
|
||||
config={{
|
||||
count: {
|
||||
label: "Count",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
}}
|
||||
className="h-64"
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={conversionData} layout="horizontal">
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||
<XAxis
|
||||
type="number"
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="stage"
|
||||
type="category"
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
width={80}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="count" fill="hsl(var(--chart-3))" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Revenue Chart */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Revenue Trend (Lakhs)</h3>
|
||||
<ChartContainer
|
||||
config={{
|
||||
revenue: {
|
||||
label: "Revenue",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
}}
|
||||
className="h-64"
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={revenueData}>
|
||||
<defs>
|
||||
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="hsl(var(--chart-4))" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="hsl(var(--chart-4))" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
stroke="hsl(var(--chart-4))"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorRevenue)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
|
||||
{/* Collection Performance */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Top Performing Collections</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Bridal 2025</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>842 views</span>
|
||||
<span>142 wishlist adds</span>
|
||||
<span>28 inquiries</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">+24%</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Daily Edit</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>624 views</span>
|
||||
<span>98 wishlist adds</span>
|
||||
<span>18 inquiries</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">+18%</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Festive Picks</p>
|
||||
<div className="flex items-center gap-4 text-[12px] text-text-secondary">
|
||||
<span>412 views</span>
|
||||
<span>76 wishlist adds</span>
|
||||
<span>12 inquiries</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[14px] text-accent-positive">+12%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
146
src/components/retailer/pages/AppointmentsPage.tsx
Normal file
146
src/components/retailer/pages/AppointmentsPage.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Calendar as CalendarIcon, Plus, MapPin, Video, Clock, Check, X } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
const appointments = [
|
||||
{
|
||||
id: "1",
|
||||
customer: "Aditi Sharma",
|
||||
date: "Jan 25, 2025",
|
||||
time: "2:00 PM",
|
||||
duration: "60 min",
|
||||
mode: "in-store",
|
||||
store: "Mumbai Showroom",
|
||||
status: "confirmed",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
customer: "Rohan Mehta",
|
||||
date: "Jan 25, 2025",
|
||||
time: "4:00 PM",
|
||||
duration: "45 min",
|
||||
mode: "virtual",
|
||||
store: "Video Call",
|
||||
status: "pending",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
customer: "Sana Khan",
|
||||
date: "Jan 26, 2025",
|
||||
time: "11:00 AM",
|
||||
duration: "90 min",
|
||||
mode: "in-store",
|
||||
store: "Mumbai Showroom",
|
||||
status: "confirmed",
|
||||
},
|
||||
];
|
||||
|
||||
export function AppointmentsPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Appointments</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage customer appointments and availability
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Slot
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">This Week</p>
|
||||
<p className="text-[24px] leading-[32px]">12</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Pending</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-warn">2</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Confirmed</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">10</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Completion Rate</p>
|
||||
<p className="text-[24px] leading-[32px]">94%</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Calendar View */}
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
{/* Calendar Widget */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Calendar</h3>
|
||||
<div className="aspect-square bg-gradient-to-br from-primary/5 to-secondary/5 rounded-[12px] flex items-center justify-center">
|
||||
<CalendarIcon className="w-12 h-12 text-text-secondary" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Appointments List */}
|
||||
<Card className="lg:col-span-2 p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Upcoming Appointments</h3>
|
||||
<div className="space-y-3">
|
||||
{appointments.map((apt) => (
|
||||
<Card key={apt.id} className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<p className="text-[16px] mb-1">{apt.customer}</p>
|
||||
<div className="flex items-center gap-3 text-[14px] text-text-secondary">
|
||||
<div className="flex items-center gap-1">
|
||||
<CalendarIcon className="w-4 h-4" />
|
||||
<span>{apt.date}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>{apt.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={apt.status === "confirmed" ? "default" : "secondary"}>
|
||||
{apt.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-[14px] text-text-secondary mb-3">
|
||||
{apt.mode === "in-store" ? (
|
||||
<>
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span>{apt.store}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Video className="w-4 h-4" />
|
||||
<span>{apt.store}</span>
|
||||
</>
|
||||
)}
|
||||
<span>•</span>
|
||||
<span>{apt.duration}</span>
|
||||
</div>
|
||||
|
||||
{apt.status === "pending" && (
|
||||
<div className="flex items-center gap-2 pt-3 border-t border-border">
|
||||
<Button size="sm" className="flex-1">
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
Confirm
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Decline
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
336
src/components/retailer/pages/ChatPage.tsx
Normal file
336
src/components/retailer/pages/ChatPage.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
import { Search, Paperclip, Send, MoreVertical, Phone, Video, PhoneOff, Mic, MicOff, VideoOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
|
||||
const conversations = [
|
||||
{ id: "1", customer: "Aditi Sharma", lastMessage: "Thank you for the details!", time: "2h ago", unread: 0, online: true },
|
||||
{ id: "2", customer: "Rohan Mehta", lastMessage: "Can I see more bridal sets?", time: "4h ago", unread: 2, online: true },
|
||||
{ id: "3", customer: "Sana Khan", lastMessage: "What's the lead time?", time: "1d ago", unread: 1, online: false },
|
||||
];
|
||||
|
||||
const messages = [
|
||||
{ id: 1, sender: "customer", text: "Hi! I'm interested in the Heritage Bridal Ring", time: "10:30 AM" },
|
||||
{ id: 2, sender: "associate", text: "Hello Aditi! Great choice. Let me share the details with you.", time: "10:32 AM" },
|
||||
{ id: 3, sender: "customer", text: "Can I see it in person?", time: "10:35 AM" },
|
||||
{ id: 4, sender: "associate", text: "Absolutely! Would you like to book an appointment?", time: "10:36 AM" },
|
||||
];
|
||||
|
||||
type CallState = {
|
||||
active: boolean;
|
||||
type: 'voice' | 'video' | null;
|
||||
initiatedBy: 'customer' | 'retailer' | null;
|
||||
muted: boolean;
|
||||
videoOff: boolean;
|
||||
};
|
||||
|
||||
export function ChatPage() {
|
||||
const [message, setMessage] = useState("");
|
||||
const [selectedConversation, setSelectedConversation] = useState(conversations[0]);
|
||||
const [callState, setCallState] = useState<CallState>({
|
||||
active: false,
|
||||
type: null,
|
||||
initiatedBy: null,
|
||||
muted: false,
|
||||
videoOff: false,
|
||||
});
|
||||
|
||||
const handleVoiceCall = () => {
|
||||
setCallState({
|
||||
active: true,
|
||||
type: 'voice',
|
||||
initiatedBy: 'retailer',
|
||||
muted: false,
|
||||
videoOff: false,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEndCall = () => {
|
||||
setCallState({
|
||||
active: false,
|
||||
type: null,
|
||||
initiatedBy: null,
|
||||
muted: false,
|
||||
videoOff: false,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
setCallState({ ...callState, muted: !callState.muted });
|
||||
};
|
||||
|
||||
const toggleVideo = () => {
|
||||
setCallState({ ...callState, videoOff: !callState.videoOff });
|
||||
};
|
||||
|
||||
// Simulate incoming video call from customer
|
||||
const [incomingCall, setIncomingCall] = useState(false);
|
||||
|
||||
const handleAcceptCall = () => {
|
||||
setCallState({
|
||||
active: true,
|
||||
type: 'video',
|
||||
initiatedBy: 'customer',
|
||||
muted: false,
|
||||
videoOff: false,
|
||||
});
|
||||
setIncomingCall(false);
|
||||
};
|
||||
|
||||
const handleRejectCall = () => {
|
||||
setIncomingCall(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-4rem)] flex">
|
||||
{/* Conversations List */}
|
||||
<div className="w-80 border-r border-border flex flex-col">
|
||||
<div className="p-4 border-b border-border">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
||||
<Input placeholder="Search conversations..." className="pl-10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{conversations.map((conversation) => (
|
||||
<button
|
||||
key={conversation.id}
|
||||
onClick={() => setSelectedConversation(conversation)}
|
||||
className={`w-full p-4 text-left border-b border-border hover:bg-accent/5 transition-colors ${
|
||||
selectedConversation.id === conversation.id ? "bg-accent/10" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-[14px]">{conversation.customer}</p>
|
||||
{conversation.online && (
|
||||
<div className="w-2 h-2 rounded-full bg-accent-positive" />
|
||||
)}
|
||||
</div>
|
||||
{conversation.unread > 0 && (
|
||||
<Badge className="bg-destructive">{conversation.unread}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[12px] text-text-secondary truncate mb-1">{conversation.lastMessage}</p>
|
||||
<p className="text-[11px] text-text-secondary">{conversation.time}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Area */}
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Chat Header */}
|
||||
<div className="h-16 px-6 flex items-center justify-between border-b border-border">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-[16px]">{selectedConversation.customer}</p>
|
||||
{selectedConversation.online && (
|
||||
<span className="text-[12px] text-accent-positive">● Online</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
{callState.active ? `${callState.type === 'voice' ? 'Voice' : 'Video'} call in progress...` : 'Active now'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!callState.active && (
|
||||
<>
|
||||
<Button variant="ghost" size="icon" onClick={handleVoiceCall} title="Voice Call">
|
||||
<Phone className="w-5 h-5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIncomingCall(true)}
|
||||
title="Simulate Incoming Video Call (Customer Only)"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<Video className="w-5 h-5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Call Overlay */}
|
||||
{callState.active && (
|
||||
<div className="absolute inset-0 z-10 bg-background flex flex-col">
|
||||
{/* Call Video Area */}
|
||||
<div className="flex-1 bg-gradient-to-br from-primary/10 to-secondary/10 relative">
|
||||
{callState.type === 'video' ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
{/* Customer Video (Main) */}
|
||||
<div className="w-full h-full bg-gradient-to-br from-primary/20 to-secondary/20 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-32 h-32 rounded-full bg-primary/20 mx-auto mb-4 flex items-center justify-center">
|
||||
<span className="text-[48px]">AS</span>
|
||||
</div>
|
||||
<p className="text-[18px]">{selectedConversation.customer}</p>
|
||||
<p className="text-[14px] text-muted-foreground">Video Call Active</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Retailer Video (Picture-in-Picture) */}
|
||||
<div className="absolute top-4 right-4 w-48 h-36 bg-background border-2 border-border rounded-lg overflow-hidden">
|
||||
<div className="w-full h-full bg-gradient-to-br from-accent-positive/20 to-accent-warn/20 flex items-center justify-center">
|
||||
{callState.videoOff ? (
|
||||
<VideoOff className="w-8 h-8 text-muted-foreground" />
|
||||
) : (
|
||||
<span className="text-[24px]">You</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-32 h-32 rounded-full bg-primary/20 mx-auto mb-4 flex items-center justify-center">
|
||||
<Phone className="w-16 h-16 text-primary" />
|
||||
</div>
|
||||
<p className="text-[18px]">{selectedConversation.customer}</p>
|
||||
<p className="text-[14px] text-muted-foreground">Voice Call Active</p>
|
||||
<p className="text-[24px] text-muted-foreground mt-4">03:42</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Call initiated by badge */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<Badge variant="outline" className="bg-background/80 backdrop-blur">
|
||||
{callState.initiatedBy === 'customer' ? 'Incoming from Customer' : 'Outgoing'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Call Controls */}
|
||||
<div className="h-24 border-t border-border bg-background/95 backdrop-blur flex items-center justify-center gap-4">
|
||||
{callState.type === 'video' && (
|
||||
<Button
|
||||
variant={callState.videoOff ? "destructive" : "outline"}
|
||||
size="icon"
|
||||
className="w-14 h-14 rounded-full"
|
||||
onClick={toggleVideo}
|
||||
>
|
||||
{callState.videoOff ? <VideoOff className="w-6 h-6" /> : <Video className="w-6 h-6" />}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={callState.muted ? "destructive" : "outline"}
|
||||
size="icon"
|
||||
className="w-14 h-14 rounded-full"
|
||||
onClick={toggleMute}
|
||||
>
|
||||
{callState.muted ? <MicOff className="w-6 h-6" /> : <Mic className="w-6 h-6" />}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="w-16 h-16 rounded-full"
|
||||
onClick={handleEndCall}
|
||||
>
|
||||
<PhoneOff className="w-6 h-6" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Incoming Call Alert */}
|
||||
{incomingCall && (
|
||||
<div className="absolute inset-0 z-20 bg-background/95 backdrop-blur flex items-center justify-center">
|
||||
<Card className="p-8 max-w-md text-center space-y-6">
|
||||
<div className="w-24 h-24 rounded-full bg-primary/20 mx-auto flex items-center justify-center animate-pulse">
|
||||
<Video className="w-12 h-12 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[22px] mb-2">Incoming Video Call</h3>
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
{selectedConversation.customer} is calling you
|
||||
</p>
|
||||
<Badge variant="outline" className="mt-2">
|
||||
Initiated by Customer
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Button variant="outline" onClick={handleRejectCall} className="gap-2">
|
||||
<PhoneOff className="w-4 h-4" />
|
||||
Decline
|
||||
</Button>
|
||||
<Button onClick={handleAcceptCall} className="gap-2">
|
||||
<Video className="w-4 h-4" />
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={`flex ${msg.sender === "associate" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[70%] rounded-[16px] p-3 ${
|
||||
msg.sender === "associate"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card border border-border"
|
||||
}`}
|
||||
>
|
||||
<p className="text-[14px] leading-[20px] mb-1">{msg.text}</p>
|
||||
<p
|
||||
className={`text-[11px] ${
|
||||
msg.sender === "associate" ? "text-primary-foreground/70" : "text-text-secondary"
|
||||
}`}
|
||||
>
|
||||
{msg.time}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="px-6 py-3 border-t border-border">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Button variant="outline" size="sm">
|
||||
<Paperclip className="w-4 h-4 mr-2" />
|
||||
Attach SKU
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Create Quote
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Book Appointment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="px-6 pb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button size="icon" disabled={!message}>
|
||||
<Send className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
src/components/retailer/pages/CurationPage.tsx
Normal file
92
src/components/retailer/pages/CurationPage.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Grid, Plus, Eye, Share2, Copy, Calendar } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
const collections = [
|
||||
{ id: "1", name: "Bridal 2025", items: 48, status: "Published", audience: "Public", updated: "2 days ago" },
|
||||
{ id: "2", name: "Daily Edit", items: 36, status: "Published", audience: "Public", updated: "1 week ago" },
|
||||
{ id: "3", name: "Festive Picks", items: 22, status: "Draft", audience: "Private", updated: "3 days ago" },
|
||||
{ id: "4", name: "Premium Collection", items: 15, status: "Published", audience: "Specific Customers", updated: "5 days ago" },
|
||||
];
|
||||
|
||||
export function CurationPage() {
|
||||
const handleShare = (collection: typeof collections[0]) => {
|
||||
toast.success(`Link copied: https://nova-jewels.hellojewellers.com/${collection.id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Curation & Collections</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Create and manage curated collections for your customers
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Collection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total Collections</p>
|
||||
<p className="text-[24px] leading-[32px]">12</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Published</p>
|
||||
<p className="text-[24px] leading-[32px]">9</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Draft</p>
|
||||
<p className="text-[24px] leading-[32px]">3</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total Items</p>
|
||||
<p className="text-[24px] leading-[32px]">186</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Collections Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{collections.map((collection) => (
|
||||
<Card key={collection.id} className="overflow-hidden">
|
||||
<div className="aspect-video bg-gradient-to-br from-primary/20 to-secondary/20" />
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-[18px] leading-[28px] mb-1">{collection.name}</h3>
|
||||
<p className="text-[14px] text-text-secondary">{collection.items} items</p>
|
||||
</div>
|
||||
<Badge variant={collection.status === "Published" ? "default" : "secondary"}>
|
||||
{collection.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-[12px] text-text-secondary">
|
||||
<span>{collection.audience}</span>
|
||||
<span>•</span>
|
||||
<span>Updated {collection.updated}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-border">
|
||||
<Button size="sm" variant="outline" className="flex-1">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleShare(collection)}>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
156
src/components/retailer/pages/CustomersPage.tsx
Normal file
156
src/components/retailer/pages/CustomersPage.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Users, UserPlus, Search, Filter, MessageCircle, FileText, Calendar } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
const customers = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Aditi Sharma",
|
||||
phone: "+91-98XXXXXX12",
|
||||
email: "aditi.s@example.com",
|
||||
segment: "Premium",
|
||||
associate: "Rohan K.",
|
||||
lastActivity: "2 hours ago",
|
||||
quotes: 3,
|
||||
orders: 1,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Rohan Mehta",
|
||||
phone: "+91-98XXXXXX34",
|
||||
email: "rohan.m@example.com",
|
||||
segment: "Mid-range",
|
||||
associate: "Priya S.",
|
||||
lastActivity: "1 day ago",
|
||||
quotes: 1,
|
||||
orders: 0,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Sana Khan",
|
||||
phone: "+91-98XXXXXX56",
|
||||
email: "sana.k@example.com",
|
||||
segment: "Luxury",
|
||||
associate: "Rohan K.",
|
||||
lastActivity: "3 days ago",
|
||||
quotes: 5,
|
||||
orders: 2,
|
||||
},
|
||||
];
|
||||
|
||||
export function CustomersPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Customers</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage your customer relationships and track engagement
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Segments
|
||||
</Button>
|
||||
<Button>
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
Add Customer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total Customers</p>
|
||||
<p className="text-[24px] leading-[32px]">248</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Active (30d)</p>
|
||||
<p className="text-[24px] leading-[32px]">142</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Avg Quotes</p>
|
||||
<p className="text-[24px] leading-[32px]">2.4</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Conversion Rate</p>
|
||||
<p className="text-[24px] leading-[32px]">38%</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<Card className="p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
||||
<Input placeholder="Search customers by name, phone, or email..." className="pl-10" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Customers Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead>Contact</TableHead>
|
||||
<TableHead>Segment</TableHead>
|
||||
<TableHead>Associate</TableHead>
|
||||
<TableHead>Last Activity</TableHead>
|
||||
<TableHead>Quotes</TableHead>
|
||||
<TableHead>Orders</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{customers.map((customer) => (
|
||||
<TableRow key={customer.id}>
|
||||
<TableCell>{customer.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[12px]">{customer.phone}</p>
|
||||
<p className="text-[12px] text-text-secondary">{customer.email}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={customer.segment === "Luxury" ? "default" : "outline"}>
|
||||
{customer.segment}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{customer.associate}</TableCell>
|
||||
<TableCell className="text-text-secondary">{customer.lastActivity}</TableCell>
|
||||
<TableCell>{customer.quotes}</TableCell>
|
||||
<TableCell>{customer.orders}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="sm" variant="ghost">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost">
|
||||
<FileText className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost">
|
||||
<Calendar className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
165
src/components/retailer/pages/DashboardPage.tsx
Normal file
165
src/components/retailer/pages/DashboardPage.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Eye, MessageCircle, FileText, ShoppingCart, TrendingUp, Clock, Package, Factory, Calendar } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { RetailerPage } from "../../../RetailerApp";
|
||||
|
||||
interface DashboardPageProps {
|
||||
onNavigate: (page: RetailerPage) => void;
|
||||
}
|
||||
|
||||
const kpis = [
|
||||
{ label: "Store Views", value: "2,847", change: "+12.5%", trend: "up", icon: Eye },
|
||||
{ label: "Inquiries", value: "142", change: "+8.2%", trend: "up", icon: MessageCircle },
|
||||
{ label: "Quotes Sent", value: "89", change: "+15.3%", trend: "up", icon: FileText },
|
||||
{ label: "Orders", value: "34", change: "+22.1%", trend: "up", icon: ShoppingCart },
|
||||
{ label: "Conversion", value: "38.2%", change: "+5.1%", trend: "up", icon: TrendingUp },
|
||||
{ label: "Avg Response", value: "2.3h", change: "-18%", trend: "up", icon: Clock },
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ id: "inventory", label: "Sync Inventory", description: "Import or update SKUs", icon: Package },
|
||||
{ id: "curation", label: "Create Collection", description: "Curate new collection", icon: Package },
|
||||
{ id: "team", label: "Invite Team", description: "Add associates", icon: Factory },
|
||||
{ id: "customers", label: "Open CRM", description: "View customers", icon: Factory },
|
||||
{ id: "quotes-orders", label: "Build Quote", description: "Create new quote", icon: FileText },
|
||||
{ id: "analytics", label: "View Analytics", description: "Performance reports", icon: TrendingUp },
|
||||
];
|
||||
|
||||
const recentActivity = [
|
||||
{ id: 1, text: "Auric Foundry shared 120 SKUs", time: "2 hours ago", type: "sync" },
|
||||
{ id: 2, text: "Quote Q-2025-0912 approved by Aditi Sharma", time: "4 hours ago", type: "quote" },
|
||||
{ id: 3, text: "New collection 'Bridal 2025' published", time: "1 day ago", type: "collection" },
|
||||
{ id: 4, text: "Rohan Mehta requested appointment", time: "1 day ago", type: "appointment" },
|
||||
{ id: 5, text: "Order O-2025-0048 moved to 'Under Making'", time: "2 days ago", type: "order" },
|
||||
];
|
||||
|
||||
export function DashboardPage({ onNavigate }: DashboardPageProps) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Dashboard</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Welcome back! Here's what's happening with your store today.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
|
||||
{kpis.map((kpi) => {
|
||||
const Icon = kpi.icon;
|
||||
return (
|
||||
<Card key={kpi.label} className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-primary/10 flex items-center justify-center">
|
||||
<Icon className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<span
|
||||
className={`text-[12px] px-2 py-0.5 rounded-full ${
|
||||
kpi.trend === "up"
|
||||
? "bg-accent-positive/10 text-accent-positive"
|
||||
: "bg-accent-error/10 text-accent-error"
|
||||
}`}
|
||||
>
|
||||
{kpi.change}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[24px] leading-[32px] mb-1">{kpi.value}</p>
|
||||
<p className="text-[12px] text-text-secondary">{kpi.label}</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions & Recent Activity */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Quick Actions */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Quick Actions</h2>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{quickActions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
return (
|
||||
<button
|
||||
key={action.id}
|
||||
onClick={() => onNavigate(action.id as RetailerPage)}
|
||||
className="p-4 rounded-[12px] border border-border hover:border-primary hover:bg-primary/5 transition-colors text-left"
|
||||
>
|
||||
<Icon className="w-5 h-5 text-primary mb-2" />
|
||||
<p className="text-[14px] mb-1">{action.label}</p>
|
||||
<p className="text-[12px] text-text-secondary">{action.description}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-[18px] leading-[28px]">Recent Activity</h2>
|
||||
<Button variant="ghost" size="sm">View All</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{recentActivity.map((activity) => (
|
||||
<div key={activity.id} className="flex items-start gap-3 p-3 rounded-[10px] hover:bg-accent/5">
|
||||
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[14px]">{activity.text}</p>
|
||||
<p className="text-[12px] text-text-secondary">{activity.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Pending Actions */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-[18px] leading-[28px] mb-4">Pending Actions</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-warn/10 flex items-center justify-center">
|
||||
<MessageCircle className="w-5 h-5 text-accent-warn" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">3 unanswered inquiries</p>
|
||||
<p className="text-[12px] text-text-secondary">Respond to customer questions</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("chat")}>View</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Calendar className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">2 appointment requests</p>
|
||||
<p className="text-[12px] text-text-secondary">Review and confirm slots</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("appointments")}>Review</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 rounded-[12px] border border-border">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<Package className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[14px]">48 SKUs out of sync</p>
|
||||
<p className="text-[12px] text-text-secondary">Manufacturer updated inventory</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={() => onNavigate("inventory")}>Sync</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
439
src/components/retailer/pages/InventoryPage.tsx
Normal file
439
src/components/retailer/pages/InventoryPage.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
import { useState } from "react";
|
||||
import { Package, Upload, Download, Filter, Plus, Search, Eye, EyeOff, Edit, Trash2, RefreshCw, Factory } from "lucide-react";
|
||||
import { AddFromManufacturerDialog } from "../AddFromManufacturerDialog";
|
||||
import { AddSkuDialog } from "../AddSkuDialog";
|
||||
import { BulkUploadDialog } from "../../BulkUploadDialog";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
import { Checkbox } from "../../ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../ui/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "../../ui/sheet";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
|
||||
const inventory = [
|
||||
{
|
||||
id: "1",
|
||||
sku: "AU-22K-BR-0192",
|
||||
title: "Heritage Bridal Ring",
|
||||
source: "Auric Foundry",
|
||||
category: "Rings",
|
||||
metal: "22K Gold",
|
||||
weight: "6.8g",
|
||||
price: "₹2.8L",
|
||||
status: "In Stock",
|
||||
visibility: "public",
|
||||
updated: "2 days ago",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
sku: "AU-18K-NK-0441",
|
||||
title: "Aurora Pendant",
|
||||
source: "Local",
|
||||
category: "Necklaces",
|
||||
metal: "18K Gold",
|
||||
weight: "3.2g",
|
||||
price: "₹45K",
|
||||
status: "Made to Order",
|
||||
visibility: "public",
|
||||
updated: "1 week ago",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
sku: "AU-22K-BG-1023",
|
||||
title: "Classic Bangle 2-6",
|
||||
source: "Zephyr Casting",
|
||||
category: "Bangles",
|
||||
metal: "22K Gold",
|
||||
weight: "18.4g",
|
||||
price: "₹1.2L",
|
||||
status: "In Stock",
|
||||
visibility: "private",
|
||||
updated: "3 days ago",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
sku: "AU-18K-ER-0234",
|
||||
title: "Diamond Stud Earrings",
|
||||
source: "BlueRay Gems",
|
||||
category: "Earrings",
|
||||
metal: "18K Gold",
|
||||
weight: "2.4g",
|
||||
price: "₹68K",
|
||||
status: "In Stock",
|
||||
visibility: "public",
|
||||
updated: "1 day ago",
|
||||
},
|
||||
];
|
||||
|
||||
export function InventoryPage() {
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>([]);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [addSkuOpen, setAddSkuOpen] = useState(false);
|
||||
const [bulkUploadOpen, setBulkUploadOpen] = useState(false);
|
||||
const [addFromManufacturerOpen, setAddFromManufacturerOpen] = useState(false);
|
||||
const [selectedSku, setSelectedSku] = useState<typeof inventory[0] | null>(null);
|
||||
const [filterSource, setFilterSource] = useState("all");
|
||||
const [filterCategory, setFilterCategory] = useState("all");
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (selectedItems.length === inventory.length) {
|
||||
setSelectedItems([]);
|
||||
} else {
|
||||
setSelectedItems(inventory.map((item) => item.id));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectItem = (id: string) => {
|
||||
if (selectedItems.includes(id)) {
|
||||
setSelectedItems(selectedItems.filter((item) => item !== id));
|
||||
} else {
|
||||
setSelectedItems([...selectedItems, id]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (item: typeof inventory[0]) => {
|
||||
setSelectedSku(item);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const handleBulkAction = (action: string) => {
|
||||
toast.success(`${action} applied to ${selectedItems.length} items`);
|
||||
setSelectedItems([]);
|
||||
};
|
||||
|
||||
const handleAddFromManufacturer = (skus: any[]) => {
|
||||
// SKUs are added - this would typically update state or make an API call
|
||||
console.log("Adding SKUs from manufacturer:", skus);
|
||||
};
|
||||
|
||||
const handleAddSku = (skuData: any) => {
|
||||
console.log("Adding new SKU:", skuData);
|
||||
};
|
||||
|
||||
const handleBulkUpload = (file: File) => {
|
||||
console.log("Bulk uploading file:", file.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Inventory</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage your product catalog and sync with manufacturers
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" onClick={() => setBulkUploadOpen(true)}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Bulk Upload
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Sync All
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setAddFromManufacturerOpen(true)}>
|
||||
<Factory className="w-4 h-4 mr-2" />
|
||||
Add from Manufacturers
|
||||
</Button>
|
||||
<Button onClick={() => setAddSkuOpen(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add SKU
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Total SKUs</p>
|
||||
<p className="text-[24px] leading-[32px]">248</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">In Stock</p>
|
||||
<p className="text-[24px] leading-[32px]">186</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Made to Order</p>
|
||||
<p className="text-[24px] leading-[32px]">62</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Out of Sync</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-warn">48</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Filters & Search */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-text-secondary" />
|
||||
<Input placeholder="Search by SKU, title, or category..." className="pl-10" />
|
||||
</div>
|
||||
<Select value={filterSource} onValueChange={setFilterSource}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="Source" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Sources</SelectItem>
|
||||
<SelectItem value="auric">Auric Foundry</SelectItem>
|
||||
<SelectItem value="zephyr">Zephyr Casting</SelectItem>
|
||||
<SelectItem value="blueray">BlueRay Gems</SelectItem>
|
||||
<SelectItem value="local">Local</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="Category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="rings">Rings</SelectItem>
|
||||
<SelectItem value="necklaces">Necklaces</SelectItem>
|
||||
<SelectItem value="bangles">Bangles</SelectItem>
|
||||
<SelectItem value="earrings">Earrings</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
More Filters
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Bulk Actions */}
|
||||
{selectedItems.length > 0 && (
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[14px]">
|
||||
{selectedItems.length} item{selectedItems.length > 1 ? "s" : ""} selected
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Publish")}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Publish
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Hide")}>
|
||||
<EyeOff className="w-4 h-4 mr-2" />
|
||||
Hide
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Apply Pricing Rule")}>
|
||||
Apply Rule
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleBulkAction("Delete")}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Inventory Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<Checkbox checked={selectedItems.length === inventory.length} onCheckedChange={handleSelectAll} />
|
||||
</TableHead>
|
||||
<TableHead>SKU</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Source</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Metal</TableHead>
|
||||
<TableHead>Weight</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Visibility</TableHead>
|
||||
<TableHead>Updated</TableHead>
|
||||
<TableHead className="w-24"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{inventory.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedItems.includes(item.id)}
|
||||
onCheckedChange={() => handleSelectItem(item.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-[12px]">{item.sku}</TableCell>
|
||||
<TableCell>{item.title}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{item.source}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{item.category}</TableCell>
|
||||
<TableCell>{item.metal}</TableCell>
|
||||
<TableCell>{item.weight}</TableCell>
|
||||
<TableCell>{item.price}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={item.status === "In Stock" ? "default" : "outline"}>
|
||||
{item.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={item.visibility === "public" ? "default" : "secondary"}>
|
||||
{item.visibility}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-text-secondary">{item.updated}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost" onClick={() => handleViewDetail(item)}>
|
||||
View
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* Add SKU Dialog */}
|
||||
<AddSkuDialog
|
||||
open={addSkuOpen}
|
||||
onClose={() => setAddSkuOpen(false)}
|
||||
onAdd={handleAddSku}
|
||||
/>
|
||||
|
||||
{/* Bulk Upload Dialog */}
|
||||
<BulkUploadDialog
|
||||
open={bulkUploadOpen}
|
||||
onClose={() => setBulkUploadOpen(false)}
|
||||
onUpload={handleBulkUpload}
|
||||
title="Bulk Import Inventory"
|
||||
description="Upload multiple SKUs at once using our Excel template"
|
||||
templateType="inventory"
|
||||
/>
|
||||
|
||||
{/* Add from Manufacturer Dialog */}
|
||||
<AddFromManufacturerDialog
|
||||
open={addFromManufacturerOpen}
|
||||
onClose={() => setAddFromManufacturerOpen(false)}
|
||||
onAdd={handleAddFromManufacturer}
|
||||
/>
|
||||
|
||||
{/* SKU Detail Sheet */}
|
||||
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
|
||||
<SheetContent className="w-[600px] sm:max-w-[600px]">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{selectedSku?.title}</SheetTitle>
|
||||
<SheetDescription>SKU: {selectedSku?.sku}</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{selectedSku && (
|
||||
<Tabs defaultValue="overview" className="mt-6">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="pricing">Pricing</TabsTrigger>
|
||||
<TabsTrigger value="availability">Availability</TabsTrigger>
|
||||
<TabsTrigger value="notes">Notes</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4 mt-4">
|
||||
<div className="aspect-square bg-gradient-to-br from-secondary/20 to-primary/20 rounded-[12px]" />
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Category</span>
|
||||
<span>{selectedSku.category}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Metal</span>
|
||||
<span>{selectedSku.metal}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Weight</span>
|
||||
<span>{selectedSku.weight}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Source</span>
|
||||
<Badge variant="outline">{selectedSku.source}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pricing" className="space-y-4 mt-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Cost Price</span>
|
||||
<span>₹2.1L</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Making Charges</span>
|
||||
<span>₹0.4L</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Tax (GST 3%)</span>
|
||||
<span>₹0.08L</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Margin (12%)</span>
|
||||
<span>₹0.22L</span>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-border flex justify-between">
|
||||
<span>Final MRP</span>
|
||||
<span className="text-[18px]">{selectedSku.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Override Price
|
||||
</Button>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="availability" className="space-y-4 mt-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Status</span>
|
||||
<Badge>{selectedSku.status}</Badge>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Lead Time</span>
|
||||
<span>{selectedSku.status === "In Stock" ? "Ready to ship" : "7-10 days"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary">Last Sync</span>
|
||||
<span>{selectedSku.updated}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="notes" className="space-y-4 mt-4">
|
||||
<textarea
|
||||
placeholder="Add internal notes about this SKU..."
|
||||
className="w-full min-h-[200px] p-3 rounded-[12px] border border-border bg-background text-[14px] resize-none"
|
||||
/>
|
||||
<Button className="w-full">Save Notes</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
177
src/components/retailer/pages/QuotesOrdersPage.tsx
Normal file
177
src/components/retailer/pages/QuotesOrdersPage.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { FileText, Plus, Clock, CheckCircle, Package, Truck } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
const quotes = [
|
||||
{ id: "Q-2025-0912", customer: "Aditi Sharma", items: 2, total: "₹4.2L", status: "pending", validUntil: "Jan 28", expiresIn: "3 days" },
|
||||
{ id: "Q-2025-0913", customer: "Rohan Mehta", items: 1, total: "₹2.8L", status: "approved", validUntil: "Jan 25", expiresIn: null },
|
||||
{ id: "Q-2025-0914", customer: "Sana Khan", items: 3, total: "₹5.6L", status: "pending", validUntil: "Jan 30", expiresIn: "5 days" },
|
||||
];
|
||||
|
||||
const orders = [
|
||||
{ id: "O-2025-0048", customer: "Aditi Sharma", items: 2, total: "₹4.2L", status: "Under Making", eta: "Feb 5, 2025", milestone: 2 },
|
||||
{ id: "O-2025-0039", customer: "Sana Khan", items: 1, total: "₹2.8L", status: "Ready", eta: "Jan 18, 2025", milestone: 3 },
|
||||
];
|
||||
|
||||
export function QuotesOrdersPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Quotes & Orders</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage quotations, orders, and purchase orders
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New Quote
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Pending Quotes</p>
|
||||
<p className="text-[24px] leading-[32px]">8</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Quote Value</p>
|
||||
<p className="text-[24px] leading-[32px]">₹42L</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Active Orders</p>
|
||||
<p className="text-[24px] leading-[32px]">12</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Order Value</p>
|
||||
<p className="text-[24px] leading-[32px]">₹86L</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs defaultValue="quotes" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="quotes">Quotations</TabsTrigger>
|
||||
<TabsTrigger value="orders">Orders</TabsTrigger>
|
||||
<TabsTrigger value="pos">Purchase Orders</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="quotes">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Quote #</TableHead>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead>Items</TableHead>
|
||||
<TableHead>Total</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Valid Until</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{quotes.map((quote) => (
|
||||
<TableRow key={quote.id}>
|
||||
<TableCell className="font-mono">{quote.id}</TableCell>
|
||||
<TableCell>{quote.customer}</TableCell>
|
||||
<TableCell>{quote.items}</TableCell>
|
||||
<TableCell>{quote.total}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={quote.status === "approved" ? "default" : "outline"}>
|
||||
{quote.status === "pending" ? (
|
||||
<>
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
Pending
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-3 h-3 mr-1" />
|
||||
Approved
|
||||
</>
|
||||
)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p>{quote.validUntil}</p>
|
||||
{quote.expiresIn && (
|
||||
<p className="text-[12px] text-accent-warn">Expires in {quote.expiresIn}</p>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline">
|
||||
View
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="orders">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Order #</TableHead>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead>Items</TableHead>
|
||||
<TableHead>Total</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>ETA</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{orders.map((order) => (
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-mono">{order.id}</TableCell>
|
||||
<TableCell>{order.customer}</TableCell>
|
||||
<TableCell>{order.items}</TableCell>
|
||||
<TableCell>{order.total}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-primary">{order.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{order.eta}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline">
|
||||
Track
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="pos">
|
||||
<Card className="p-12 text-center">
|
||||
<Package className="w-12 h-12 text-text-secondary mx-auto mb-4" />
|
||||
<h3 className="text-[18px] leading-[28px] mb-2">No Purchase Orders</h3>
|
||||
<p className="text-text-secondary mb-4">
|
||||
Purchase orders to manufacturers will appear here
|
||||
</p>
|
||||
<Button>Create PO</Button>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
238
src/components/retailer/pages/SetupPage.tsx
Normal file
238
src/components/retailer/pages/SetupPage.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { useState } from "react";
|
||||
import { Store, MapPin, Factory, CheckCircle } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
|
||||
interface SetupPageProps {
|
||||
onComplete: (data: any) => void;
|
||||
}
|
||||
|
||||
export function SetupPage({ onComplete }: SetupPageProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [brandName, setBrandName] = useState("Nova Jewels");
|
||||
const [subdomain, setSubdomain] = useState("nova-jewels");
|
||||
const [storeName, setStoreName] = useState("Mumbai Showroom");
|
||||
const [storeLocation, setStoreLocation] = useState("Mumbai, Maharashtra");
|
||||
|
||||
const handleNext = () => {
|
||||
if (step < 3) {
|
||||
setStep(step + 1);
|
||||
} else {
|
||||
onComplete({
|
||||
name: brandName,
|
||||
logo: null,
|
||||
subdomain,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-page flex items-center justify-center p-6">
|
||||
<Card className="w-full max-w-2xl p-8">
|
||||
{/* Progress */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-[14px] text-text-secondary">Step {step} of 3</span>
|
||||
<span className="text-[14px] text-text-secondary">{Math.round((step / 3) * 100)}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-border rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-300"
|
||||
style={{ width: `${(step / 3) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
{step === 1 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Store className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">Brand Settings</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Set up your brand identity and store name
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="brandName">Brand Name</Label>
|
||||
<Input
|
||||
id="brandName"
|
||||
value={brandName}
|
||||
onChange={(e) => setBrandName(e.target.value)}
|
||||
placeholder="Your Jewellery Store"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subdomain">Store URL</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="subdomain"
|
||||
value={subdomain}
|
||||
onChange={(e) => setSubdomain(e.target.value)}
|
||||
placeholder="your-store"
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-[14px] text-text-secondary">.hellojewellers.com</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 bg-primary/5 border-primary/20">
|
||||
<p className="text-[14px]">
|
||||
Your store will be accessible at:{" "}
|
||||
<strong>https://{subdomain}.hellojewellers.com</strong>
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-secondary/10 flex items-center justify-center">
|
||||
<MapPin className="w-8 h-8 text-secondary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">Store Location</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Add your physical store details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="storeName">Store Name</Label>
|
||||
<Input
|
||||
id="storeName"
|
||||
value={storeName}
|
||||
onChange={(e) => setStoreName(e.target.value)}
|
||||
placeholder="Main Showroom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="location">Location</Label>
|
||||
<Input
|
||||
id="location"
|
||||
value={storeLocation}
|
||||
onChange={(e) => setStoreLocation(e.target.value)}
|
||||
placeholder="City, State"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Phone</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
placeholder="+91-XXXXXXXXXX"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">Address</Label>
|
||||
<textarea
|
||||
id="address"
|
||||
className="w-full min-h-[100px] p-3 rounded-[12px] border border-border bg-background text-[14px] resize-none"
|
||||
placeholder="Street address, area, landmark..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
<div className="w-16 h-16 rounded-full bg-accent-positive/10 flex items-center justify-center">
|
||||
<Factory className="w-8 h-8 text-accent-positive" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[24px] leading-[32px] mb-2">Connect Manufacturers</h2>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Connect with manufacturers to sync inventory (optional)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-primary/10 flex items-center justify-center">
|
||||
<Factory className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Auric Foundry</p>
|
||||
<p className="text-[12px] text-text-secondary">Gold jewellery manufacturer</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Connect</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-secondary/10 flex items-center justify-center">
|
||||
<Factory className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">Zephyr Casting</p>
|
||||
<p className="text-[12px] text-text-secondary">Custom casting services</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Connect</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[12px] bg-accent-positive/10 flex items-center justify-center">
|
||||
<Factory className="w-5 h-5 text-accent-positive" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-[14px] mb-1">BlueRay Gems</p>
|
||||
<p className="text-[12px] text-text-secondary">Diamond and gemstone supplier</p>
|
||||
</div>
|
||||
<Button size="sm" variant="outline">Connect</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-4 bg-accent/5">
|
||||
<p className="text-[14px] text-text-secondary">
|
||||
You can skip this step and connect manufacturers later from settings
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3 mt-8">
|
||||
{step > 1 && (
|
||||
<Button variant="outline" onClick={() => setStep(step - 1)} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleNext} className="flex-1">
|
||||
{step === 3 ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Complete Setup
|
||||
</>
|
||||
) : (
|
||||
"Continue"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
528
src/components/retailer/pages/StorefrontPage.tsx
Normal file
528
src/components/retailer/pages/StorefrontPage.tsx
Normal file
@@ -0,0 +1,528 @@
|
||||
import {
|
||||
Store,
|
||||
Eye,
|
||||
Palette,
|
||||
Link as LinkIcon,
|
||||
QrCode,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
Sparkles,
|
||||
Upload,
|
||||
Image as ImageIcon,
|
||||
Wand2,
|
||||
Save,
|
||||
RefreshCw
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { LiveStorefrontPreview } from "../LiveStorefrontPreview";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Label } from "../../ui/label";
|
||||
import { Switch } from "../../ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import { Textarea } from "../../ui/textarea";
|
||||
import { toast } from "sonner@2.0.3";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Separator } from "../../ui/separator";
|
||||
import { copyWithToast } from "../../utils/clipboard";
|
||||
|
||||
export function StorefrontPage() {
|
||||
const [previewMode, setPreviewMode] = useState<'desktop' | 'mobile'>('mobile');
|
||||
const [livePreviewOpen, setLivePreviewOpen] = useState(false);
|
||||
const [aiGenerating, setAiGenerating] = useState(false);
|
||||
const [storeDescription, setStoreDescription] = useState("Discover exquisite handcrafted jewellery pieces that tell your unique story.");
|
||||
const [selectedTheme, setSelectedTheme] = useState("elegant");
|
||||
const [heroBannerImage, setHeroBannerImage] = useState<string | null>(null);
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
await copyWithToast("https://nova-jewels.hellojewellers.com", "Store link copied!");
|
||||
};
|
||||
|
||||
const handleAIGenerate = (type: 'description' | 'banner' | 'product') => {
|
||||
setAiGenerating(true);
|
||||
setTimeout(() => {
|
||||
if (type === 'description') {
|
||||
setStoreDescription("Experience the timeless elegance of Nova Jewels, where every piece is meticulously crafted to perfection. Our collection features stunning designs that blend traditional artistry with contemporary aesthetics, creating jewellery that resonates with your personal style and celebrates life's precious moments.");
|
||||
toast.success("AI enhanced your store description!");
|
||||
} else if (type === 'banner') {
|
||||
// Simulate AI banner generation - in real app this would call an AI image generation API
|
||||
setHeroBannerImage("ai-generated");
|
||||
toast.success("AI generated hero banner successfully! (Preview mode)");
|
||||
} else if (type === 'product') {
|
||||
toast.success("AI will generate compelling descriptions for all your products!");
|
||||
}
|
||||
setAiGenerating(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const handleBannerUpload = () => {
|
||||
// Simulate file upload
|
||||
setHeroBannerImage("uploaded");
|
||||
toast.success("Banner image uploaded successfully!");
|
||||
};
|
||||
|
||||
const themes = [
|
||||
{ id: 'elegant', name: 'Elegant Gold', primary: '#D4AF37', secondary: '#0B0D10' },
|
||||
{ id: 'modern', name: 'Modern Blue', primary: '#5B8DEF', secondary: '#F4B400' },
|
||||
{ id: 'royal', name: 'Royal Purple', primary: '#7C3AED', secondary: '#EC4899' },
|
||||
{ id: 'luxury', name: 'Luxury Black', primary: '#0B0D10', secondary: '#FBBF24' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Storefront Customization</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Customize your customer-facing store with AI-powered tools
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" onClick={() => setLivePreviewOpen(true)}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Live Preview
|
||||
</Button>
|
||||
<Button onClick={() => toast.success("Changes saved and published!")}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save & Publish
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
{/* Settings */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Store Status */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-[18px] leading-[28px] mb-1">Virtual Store</h3>
|
||||
<p className="text-[14px] text-text-secondary">Your online storefront is live</p>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Store URL</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input value="https://nova-jewels.hellojewellers.com" readOnly className="flex-1" />
|
||||
<Button variant="outline" onClick={handleCopyLink}>
|
||||
<LinkIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
<QrCode className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* AI-Powered Theme Selection */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px]">AI Theme Assistant</h3>
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
AI-Powered
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{themes.map((theme) => (
|
||||
<button
|
||||
key={theme.id}
|
||||
onClick={() => {
|
||||
setSelectedTheme(theme.id);
|
||||
toast.success(`Theme changed to ${theme.name}`);
|
||||
}}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
selectedTheme === theme.id
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-primary/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border-2 border-border"
|
||||
style={{ backgroundColor: theme.primary }}
|
||||
/>
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border-2 border-border"
|
||||
style={{ backgroundColor: theme.secondary }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[14px] font-medium text-left">{theme.name}</p>
|
||||
{selectedTheme === theme.id && (
|
||||
<Badge variant="outline" className="mt-2 text-[10px] px-1.5 py-0">
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
💡 Your theme selection will be reflected in the live preview and customer app
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Banner Images with AI */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px]">Hero Banner</h3>
|
||||
{heroBannerImage && (
|
||||
<Badge variant="outline" className="gap-1 text-accent-positive border-accent-positive/20">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className={`aspect-[16/9] rounded-lg border-2 transition-all ${
|
||||
heroBannerImage
|
||||
? 'border-accent-positive bg-accent-positive/5'
|
||||
: 'border-dashed border-border bg-gradient-to-br from-primary/5 to-secondary/5'
|
||||
} flex flex-col items-center justify-center gap-3`}
|
||||
>
|
||||
{heroBannerImage ? (
|
||||
<div className="text-center">
|
||||
<CheckCircle className="w-12 h-12 text-accent-positive mx-auto mb-2" />
|
||||
<p className="text-[14px] font-medium mb-1">Banner {heroBannerImage === 'ai-generated' ? 'Generated' : 'Uploaded'}</p>
|
||||
<p className="text-[12px] text-muted-foreground mb-3">
|
||||
{heroBannerImage === 'ai-generated'
|
||||
? 'AI-generated promotional banner with your brand theme'
|
||||
: 'Custom uploaded banner image'}
|
||||
</p>
|
||||
<Button variant="outline" size="sm" onClick={() => setHeroBannerImage(null)}>
|
||||
Change Banner
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ImageIcon className="w-12 h-12 text-muted-foreground" />
|
||||
<div className="text-center">
|
||||
<p className="text-[14px] text-muted-foreground mb-2">
|
||||
Upload banner image or generate with AI
|
||||
</p>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button variant="outline" size="sm" onClick={handleBannerUpload}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAIGenerate('banner')}
|
||||
disabled={aiGenerating}
|
||||
>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
{aiGenerating ? 'Generating...' : 'AI Generate'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={handleBannerUpload}
|
||||
className="aspect-square rounded-lg border border-border bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center hover:border-primary transition-colors"
|
||||
>
|
||||
<div className="text-center">
|
||||
<ImageIcon className="w-6 h-6 text-muted-foreground mx-auto mb-1" />
|
||||
<p className="text-[10px] text-muted-foreground">Banner {i}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* AI Content Generation */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px]">Store Description</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAIGenerate('description')}
|
||||
disabled={aiGenerating}
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
{aiGenerating ? 'Generating...' : 'AI Enhance'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Textarea
|
||||
value={storeDescription}
|
||||
onChange={(e) => setStoreDescription(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="Describe your store and what makes it unique..."
|
||||
/>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
This description will appear on your storefront and help customers discover you.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Product Description AI Assistant */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px]">AI Product Assistant</h3>
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Wand2 className="w-3 h-3" />
|
||||
Smart Tools
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-[14px] text-muted-foreground">
|
||||
Generate compelling product descriptions, SEO-optimized titles, and engaging content for your jewellery items.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => handleAIGenerate('product')}>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Generate Descriptions
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Bulk Enhance
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Theme Settings */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Advanced Customization</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Brand Logo</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-20 h-20 rounded-lg border-2 border-dashed border-border flex items-center justify-center">
|
||||
<Store className="w-6 h-6 text-text-secondary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Button variant="outline" size="sm">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload Logo
|
||||
</Button>
|
||||
<p className="text-[12px] text-muted-foreground mt-1">
|
||||
PNG or SVG recommended (max 2MB)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Primary Color</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary border border-border" />
|
||||
<Input defaultValue="#5B8DEF" className="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Accent Color</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-secondary border border-border" />
|
||||
<Input defaultValue="#F4B400" className="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Price Display</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="radio" name="price" value="exact" defaultChecked />
|
||||
<span className="text-[14px]">Show exact prices</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input type="radio" name="price" value="range" />
|
||||
<span className="text-[14px]">Show price ranges</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Home Layout */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Homepage Sections</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="p-3 rounded-lg border border-border flex items-center justify-between">
|
||||
<span className="text-[14px]">Hero Banner</span>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border flex items-center justify-between">
|
||||
<span className="text-[14px]">Featured Collections</span>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border flex items-center justify-between">
|
||||
<span className="text-[14px]">New Arrivals</span>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border flex items-center justify-between">
|
||||
<span className="text-[14px]">Occasion Bands</span>
|
||||
<Switch />
|
||||
</div>
|
||||
<div className="p-3 rounded-lg border border-border flex items-center justify-between">
|
||||
<span className="text-[14px]">Customer Reviews</span>
|
||||
<Switch defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Mobile Preview */}
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] leading-[28px]">Live Preview</h3>
|
||||
<Tabs value={previewMode} onValueChange={(v) => setPreviewMode(v as 'desktop' | 'mobile')} className="w-auto">
|
||||
<TabsList>
|
||||
<TabsTrigger value="desktop">
|
||||
<Monitor className="w-4 h-4" />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mobile">
|
||||
<Smartphone className="w-4 h-4" />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Mobile Phone Mockup */}
|
||||
{previewMode === 'mobile' ? (
|
||||
<div className="relative mx-auto" style={{ width: '300px' }}>
|
||||
{/* Phone Frame */}
|
||||
<div className="relative bg-background border-[14px] border-border rounded-[36px] shadow-2xl">
|
||||
{/* Notch */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-32 h-6 bg-border rounded-b-3xl z-10" />
|
||||
|
||||
{/* Screen Content */}
|
||||
<div className="relative bg-background rounded-[22px] overflow-hidden" style={{ height: '600px' }}>
|
||||
{/* Status Bar */}
|
||||
<div className="h-12 bg-background flex items-center justify-between px-6 text-[11px]">
|
||||
<span>9:41</span>
|
||||
<span>●●●●</span>
|
||||
</div>
|
||||
|
||||
{/* Store Header */}
|
||||
<div className="bg-primary text-primary-foreground p-4 text-center">
|
||||
<h2 className="text-[18px] font-medium">Nova Jewels</h2>
|
||||
<p className="text-[12px] opacity-90">Mumbai, Maharashtra</p>
|
||||
</div>
|
||||
|
||||
{/* Store Content */}
|
||||
<div className="p-4 space-y-4 overflow-y-auto" style={{ height: '480px' }}>
|
||||
{/* Hero Banner */}
|
||||
<div className="aspect-[16/9] rounded-lg bg-gradient-to-br from-primary/20 to-secondary/20 flex items-center justify-center">
|
||||
<ImageIcon className="w-8 h-8 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
{/* Store Description */}
|
||||
<div>
|
||||
<p className="text-[12px] text-muted-foreground line-clamp-2">
|
||||
{storeDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Featured Products Grid */}
|
||||
<div>
|
||||
<h3 className="text-[14px] font-medium mb-2">Featured Collection</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="aspect-square rounded-lg border border-border bg-card flex items-center justify-center">
|
||||
<Store className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div>
|
||||
<h3 className="text-[14px] font-medium mb-2">Categories</h3>
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
{['Rings', 'Necklaces', 'Earrings', 'Bracelets'].map((cat) => (
|
||||
<div key={cat} className="px-4 py-2 rounded-full bg-accent/10 text-[12px] whitespace-nowrap">
|
||||
{cat}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="aspect-[16/10] bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg border border-border flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Monitor className="w-12 h-12 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-[14px] text-muted-foreground">Desktop Preview</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Quick Stats</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-[14px]">Total Views</span>
|
||||
<span className="text-[14px]">2,847</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-[14px]">Unique Visitors</span>
|
||||
<span className="text-[14px]">1,423</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-[14px]">Avg Time</span>
|
||||
<span className="text-[14px]">4m 32s</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-secondary text-[14px]">Mobile Visitors</span>
|
||||
<span className="text-[14px] text-accent-positive">87%</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 bg-gradient-to-br from-primary/5 to-secondary/5 border-primary/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<Sparkles className="w-5 h-5 text-primary shrink-0 mt-1" />
|
||||
<div>
|
||||
<h4 className="text-[14px] font-medium mb-1">AI Recommendations</h4>
|
||||
<p className="text-[12px] text-muted-foreground">
|
||||
Based on your store performance, we recommend adding more product images and enabling customer reviews to increase engagement by 40%.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Storefront Preview Modal */}
|
||||
<LiveStorefrontPreview
|
||||
open={livePreviewOpen}
|
||||
onClose={() => setLivePreviewOpen(false)}
|
||||
storeName="Nova Jewels"
|
||||
storeLocation="Mumbai, Maharashtra"
|
||||
theme={selectedTheme}
|
||||
description={storeDescription}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
165
src/components/retailer/pages/TeamPage.tsx
Normal file
165
src/components/retailer/pages/TeamPage.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Users, UserPlus, Settings } from "lucide-react";
|
||||
import { Card } from "../../ui/card";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../ui/table";
|
||||
|
||||
const team = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Aditi Rao",
|
||||
email: "aditi.r@novajewels.com",
|
||||
role: "Retailer Admin",
|
||||
store: "Mumbai Showroom",
|
||||
status: "Active",
|
||||
lastActive: "Just now",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Rohan Kumar",
|
||||
email: "rohan.k@novajewels.com",
|
||||
role: "Associate",
|
||||
store: "Mumbai Showroom",
|
||||
status: "Active",
|
||||
lastActive: "2 hours ago",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Priya Sharma",
|
||||
email: "priya.s@novajewels.com",
|
||||
role: "Associate",
|
||||
store: "Mumbai Showroom",
|
||||
status: "Active",
|
||||
lastActive: "1 day ago",
|
||||
},
|
||||
];
|
||||
|
||||
export function TeamPage() {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-[28px] leading-[36px] mb-2">Team & Settings</h1>
|
||||
<p className="text-[16px] text-text-secondary">
|
||||
Manage your team members, roles, and store settings
|
||||
</p>
|
||||
</div>
|
||||
<Button>
|
||||
<UserPlus className="w-4 h-4 mr-2" />
|
||||
Invite Member
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Team Members</p>
|
||||
<p className="text-[24px] leading-[32px]">8</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Active</p>
|
||||
<p className="text-[24px] leading-[32px] text-accent-positive">7</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Stores</p>
|
||||
<p className="text-[24px] leading-[32px]">1</p>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<p className="text-[12px] text-text-secondary mb-1">Roles</p>
|
||||
<p className="text-[24px] leading-[32px]">3</p>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Team Members */}
|
||||
<Card>
|
||||
<div className="p-4 border-b border-border flex items-center justify-between">
|
||||
<h3 className="text-[18px] leading-[28px]">Team Members</h3>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Manage Roles
|
||||
</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Store</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Last Active</TableHead>
|
||||
<TableHead className="w-32"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{team.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>{member.name}</TableCell>
|
||||
<TableCell className="text-text-secondary">{member.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{member.role}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{member.store}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className="bg-accent-positive">{member.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-text-secondary">{member.lastActive}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="ghost">
|
||||
Edit
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* Store Settings */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-[18px] leading-[28px] mb-4">Store Settings</h3>
|
||||
<div className="space-y-3">
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[12px] border border-border hover:bg-accent/5 transition-colors text-left">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Brand Settings</p>
|
||||
<p className="text-[12px] text-text-secondary">Logo, colors, subdomain</p>
|
||||
</div>
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[12px] border border-border hover:bg-accent/5 transition-colors text-left">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Store Locations</p>
|
||||
<p className="text-[12px] text-text-secondary">Manage physical locations</p>
|
||||
</div>
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[12px] border border-border hover:bg-accent/5 transition-colors text-left">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Manufacturer Connections</p>
|
||||
<p className="text-[12px] text-text-secondary">Connected: Auric Foundry, Zephyr Casting</p>
|
||||
</div>
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center justify-between p-4 rounded-[12px] border border-border hover:bg-accent/5 transition-colors text-left">
|
||||
<div>
|
||||
<p className="text-[14px] mb-1">Pricing Rules</p>
|
||||
<p className="text-[12px] text-text-secondary">Configure automatic pricing</p>
|
||||
</div>
|
||||
<Settings className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/ui/accordion.tsx
Normal file
66
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion@1.2.3";
|
||||
import { ChevronDownIcon } from "lucide-react@0.487.0";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
157
src/components/ui/alert-dialog.tsx
Normal file
157
src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog@1.1.6";
|
||||
|
||||
import { cn } from "./utils";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
66
src/components/ui/alert.tsx
Normal file
66
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
11
src/components/ui/aspect-ratio.tsx
Normal file
11
src/components/ui/aspect-ratio.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio@1.1.2";
|
||||
|
||||
function AspectRatio({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
|
||||
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
|
||||
}
|
||||
|
||||
export { AspectRatio };
|
||||
53
src/components/ui/avatar.tsx
Normal file
53
src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar@1.1.3";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-10 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
109
src/components/ui/breadcrumb.tsx
Normal file
109
src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react@0.487.0";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
58
src/components/ui/button.tsx
Normal file
58
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot@1.1.2";
|
||||
import { cva, type VariantProps } from "class-variance-authority@0.7.1";
|
||||
|
||||
import { cn } from "./utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9 rounded-md",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Button = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user