diff --git a/next.config.ts b/next.config.ts index 0bd2f11..208fad2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + allowedDevOrigins: ["192.168.1.30:3000"], /* config options here */ typescript: { ignoreBuildErrors: true, diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..ee785f1 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "OptiqueStock", + "short_name": "OptiqueStock", + "description": "Gestion de magasin d'optique", + "start_url": "/", + "scope": "/", + "display": "fullscreen", + "orientation": "any", + "background_color": "#020617", + "theme_color": "#020617", + "icons": [ + { + "src": "/logo.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/src/app/api/ai/assistant/route.ts b/src/app/api/ai/assistant/route.ts new file mode 100644 index 0000000..df3f26d --- /dev/null +++ b/src/app/api/ai/assistant/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from 'next/server' +import ZAI from 'z-ai-web-dev-sdk' +import { db } from '@/lib/db' +import { requireAuth } from '@/lib/auth-utils' + +export async function POST(request: NextRequest) { + const authError = await requireAuth() + if (authError) return authError + + try { + const body = await request.json() + const prompt = String(body.prompt || '').trim() + + if (!prompt) { + return NextResponse.json({ error: 'Votre demande est vide' }, { status: 400 }) + } + + const [clients, produits, ventes, lowStock, pendingWorkshop] = await Promise.all([ + db.client.count(), + db.produit.count({ where: { actif: true } }), + db.vente.count(), + db.produit.findMany({ + where: { + actif: true, + }, + orderBy: { + stock: 'asc', + }, + take: 8, + select: { + reference: true, + designation: true, + stock: true, + stockMin: true, + categorie: true, + }, + }), + db.vente.count({ + where: { + statutAtelier: { + in: ['EN_ATTENTE', 'EN_COURS', 'TERMINE', 'PRET'], + }, + }, + }), + ]) + + const storeContext = { + clients, + produitsActifs: produits, + ventes, + commandesAtelierEnCours: pendingWorkshop, + produitsARevoir: lowStock.filter((item) => item.stock <= item.stockMin), + } + + const zai = await ZAI.create() + const response = await zai.chat.completions.create({ + messages: [ + { + role: 'system', + content: + 'Tu es un assistant IA pour OptiqueStock, un logiciel de gestion de magasin d optique. Reponds en francais, avec des conseils pratiques, courts et directement exploitables. Ne promets pas d actions automatiques.', + }, + { + role: 'user', + content: `Contexte du magasin: ${JSON.stringify(storeContext)}\n\nDemande: ${prompt}`, + }, + ], + thinking: { type: 'disabled' }, + }) + + return NextResponse.json({ + answer: response.choices[0]?.message?.content || 'Aucune reponse generee.', + context: storeContext, + }) + } catch (error) { + console.error('AI assistant error:', error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Erreur IA inconnue' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/employes/[id]/route.ts b/src/app/api/employes/[id]/route.ts new file mode 100644 index 0000000..99585f1 --- /dev/null +++ b/src/app/api/employes/[id]/route.ts @@ -0,0 +1,169 @@ +import { NextRequest, NextResponse } from 'next/server' +import bcrypt from 'bcryptjs' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth-utils' + +const allowedRoles = ['VENDEUR', 'RESPONSABLE', 'ADMIN'] as const + +async function requireAdmin() { + const session = await getSession() + if (!session?.user) { + return NextResponse.json({ error: 'Non authentifie' }, { status: 401 }) + } + + if ((session.user as any).role !== 'ADMIN') { + return NextResponse.json({ error: 'Acces reserve aux administrateurs' }, { status: 403 }) + } + + return null +} + +async function isLastActiveAdmin(id: string) { + const employe = await db.employe.findUnique({ where: { id } }) + if (!employe || employe.role !== 'ADMIN' || !employe.actif) return false + + const activeAdmins = await db.employe.count({ + where: { + role: 'ADMIN', + actif: true, + }, + }) + + return activeAdmins <= 1 +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const authError = await requireAdmin() + if (authError) return authError + + try { + const { id } = await params + const body = await request.json() + const existing = await db.employe.findUnique({ where: { id } }) + + if (!existing) { + return NextResponse.json({ error: 'Employe introuvable' }, { status: 404 }) + } + + if (body.role && !allowedRoles.includes(body.role)) { + return NextResponse.json({ error: 'Role invalide' }, { status: 400 }) + } + + const removingAdminAccess = + existing.role === 'ADMIN' && + existing.actif && + ((body.role && body.role !== 'ADMIN') || body.actif === false) + + if (removingAdminAccess && await isLastActiveAdmin(id)) { + return NextResponse.json( + { error: 'Impossible de retirer le dernier administrateur actif' }, + { status: 400 } + ) + } + + if (body.email && body.email.toLowerCase() !== existing.email) { + const duplicate = await db.employe.findUnique({ + where: { email: body.email.toLowerCase() }, + }) + if (duplicate) { + return NextResponse.json({ error: 'Un employe utilise deja cet email' }, { status: 409 }) + } + } + + const employe = await db.employe.update({ + where: { id }, + data: { + email: body.email !== undefined ? String(body.email).toLowerCase() : existing.email, + nom: body.nom !== undefined ? body.nom : existing.nom, + prenom: body.prenom !== undefined ? body.prenom : existing.prenom, + role: body.role !== undefined ? body.role : existing.role, + actif: body.actif !== undefined ? Boolean(body.actif) : existing.actif, + ...(body.password ? { motDePasse: await bcrypt.hash(body.password, 12) } : {}), + }, + select: { + id: true, + email: true, + nom: true, + prenom: true, + role: true, + actif: true, + createdAt: true, + updatedAt: true, + }, + }) + + return NextResponse.json(employe) + } catch (error) { + console.error('Error updating employe:', error) + return NextResponse.json({ error: 'Failed to update employee' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const authError = await requireAdmin() + if (authError) return authError + + try { + const { id } = await params + const existing = await db.employe.findUnique({ + where: { id }, + include: { + _count: { + select: { + ventes: true, + facturesAchat: true, + paiements: true, + patients: true, + }, + }, + }, + }) + + if (!existing) { + return NextResponse.json({ error: 'Employe introuvable' }, { status: 404 }) + } + + if (await isLastActiveAdmin(id)) { + return NextResponse.json( + { error: 'Impossible de supprimer le dernier administrateur actif' }, + { status: 400 } + ) + } + + const hasHistory = + existing._count.ventes > 0 || + existing._count.facturesAchat > 0 || + existing._count.paiements > 0 || + existing._count.patients > 0 + + if (hasHistory) { + const employe = await db.employe.update({ + where: { id }, + data: { actif: false }, + select: { + id: true, + email: true, + nom: true, + prenom: true, + role: true, + actif: true, + createdAt: true, + updatedAt: true, + }, + }) + return NextResponse.json({ success: true, archived: true, employe }) + } + + await db.employe.delete({ where: { id } }) + return NextResponse.json({ success: true }) + } catch (error) { + console.error('Error deleting employe:', error) + return NextResponse.json({ error: 'Failed to delete employee' }, { status: 500 }) + } +} diff --git a/src/app/api/employes/route.ts b/src/app/api/employes/route.ts new file mode 100644 index 0000000..6b124d7 --- /dev/null +++ b/src/app/api/employes/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from 'next/server' +import bcrypt from 'bcryptjs' +import { db } from '@/lib/db' +import { getSession } from '@/lib/auth-utils' + +const allowedRoles = ['VENDEUR', 'RESPONSABLE', 'ADMIN'] as const + +async function requireAdmin() { + const session = await getSession() + if (!session?.user) { + return NextResponse.json({ error: 'Non authentifie' }, { status: 401 }) + } + + if ((session.user as any).role !== 'ADMIN') { + return NextResponse.json({ error: 'Acces reserve aux administrateurs' }, { status: 403 }) + } + + return null +} + +export async function GET() { + const authError = await requireAdmin() + if (authError) return authError + + try { + const employes = await db.employe.findMany({ + orderBy: [{ actif: 'desc' }, { nom: 'asc' }, { prenom: 'asc' }], + select: { + id: true, + email: true, + nom: true, + prenom: true, + role: true, + actif: true, + createdAt: true, + updatedAt: true, + _count: { + select: { + ventes: true, + facturesAchat: true, + paiements: true, + patients: true, + }, + }, + }, + }) + + return NextResponse.json(employes) + } catch (error) { + console.error('Error fetching employes:', error) + return NextResponse.json({ error: 'Failed to fetch employees' }, { status: 500 }) + } +} + +export async function POST(request: NextRequest) { + const authError = await requireAdmin() + if (authError) return authError + + try { + const body = await request.json() + + if (!body.email || !body.nom || !body.prenom || !body.password) { + return NextResponse.json( + { error: 'Email, nom, prenom et mot de passe sont obligatoires' }, + { status: 400 } + ) + } + + if (!allowedRoles.includes(body.role)) { + return NextResponse.json({ error: 'Role invalide' }, { status: 400 }) + } + + const existing = await db.employe.findUnique({ + where: { email: String(body.email).toLowerCase() }, + }) + + if (existing) { + return NextResponse.json({ error: 'Un employe utilise deja cet email' }, { status: 409 }) + } + + const employe = await db.employe.create({ + data: { + email: String(body.email).toLowerCase(), + nom: body.nom, + prenom: body.prenom, + role: body.role, + actif: body.actif !== undefined ? Boolean(body.actif) : true, + motDePasse: await bcrypt.hash(body.password, 12), + }, + select: { + id: true, + email: true, + nom: true, + prenom: true, + role: true, + actif: true, + createdAt: true, + updatedAt: true, + }, + }) + + return NextResponse.json(employe) + } catch (error) { + console.error('Error creating employe:', error) + return NextResponse.json({ error: 'Failed to create employee' }, { status: 500 }) + } +} diff --git a/src/app/api/pos/wizard-sale/route.ts b/src/app/api/pos/wizard-sale/route.ts new file mode 100644 index 0000000..ea511d2 --- /dev/null +++ b/src/app/api/pos/wizard-sale/route.ts @@ -0,0 +1,238 @@ +import { NextRequest, NextResponse } from 'next/server' +import { ModePaiement, StatutVente } from '@prisma/client' +import { db } from '@/lib/db' +import { getCurrentUserId, requireAuth } from '@/lib/auth-utils' + +const SERVICE_TVA = 20 + +async function generateSaleNumber(): Promise { + const today = new Date() + const year = today.getFullYear() + const month = String(today.getMonth() + 1).padStart(2, '0') + const salesThisMonth = await db.vente.count({ + where: { + date: { + gte: new Date(year, today.getMonth(), 1), + lt: new Date(year, today.getMonth() + 1, 1), + }, + }, + }) + + return `V${year}${month}${String(salesThisMonth + 1).padStart(4, '0')}` +} + +async function getServiceProduct(reference: string, designation: string) { + return db.produit.upsert({ + where: { reference }, + update: { + designation, + actif: true, + stock: { + increment: 1, + }, + }, + create: { + reference, + designation, + categorie: 'SERVICE', + prixAchatHT: 0, + prixVenteTTC: 0, + tva: SERVICE_TVA, + stock: 999999, + stockMin: 0, + actif: true, + }, + }) +} + +function toHt(ttc: number, tva = SERVICE_TVA) { + return ttc / (1 + tva / 100) +} + +export async function POST(request: NextRequest) { + const authError = await requireAuth() + if (authError) return authError + + try { + const body = await request.json() + const clientId = body.clientId || null + const serviceType = body.serviceType as 'COMBO' | 'REPAIR' + const paymentMode = (body.paymentMode || 'ESPECES') as ModePaiement + const additionalCharges = Math.max(0, Number(body.additionalCharges || 0)) + const employeId = await getCurrentUserId() + + const lines: Array<{ + produitId: string + quantite: number + prixUnitaireHT: number + prixUnitaireTTC: number + remise: number + montantHT: number + montantTTC: number + decrementStock: boolean + }> = [] + + const notes: string[] = [] + + if (serviceType === 'COMBO') { + const productLines = Array.isArray(body.products) ? body.products : [] + if (productLines.length === 0) { + return NextResponse.json({ error: 'Selectionnez au moins un produit' }, { status: 400 }) + } + + for (const item of productLines) { + const product = await db.produit.findUnique({ where: { id: item.produitId } }) + const quantite = Math.max(1, Number(item.quantite || 1)) + + if (!product) { + return NextResponse.json({ error: 'Produit introuvable' }, { status: 400 }) + } + + if (product.stock < quantite) { + return NextResponse.json( + { error: `Stock insuffisant pour ${product.designation}` }, + { status: 400 } + ) + } + + const prixUnitaireTTC = Number(item.prixUnitaireTTC ?? product.prixVenteTTC) + const prixUnitaireHT = toHt(prixUnitaireTTC, product.tva) + + lines.push({ + produitId: product.id, + quantite, + prixUnitaireHT, + prixUnitaireTTC, + remise: 0, + montantHT: prixUnitaireHT * quantite, + montantTTC: prixUnitaireTTC * quantite, + decrementStock: true, + }) + } + notes.push('Service: Pack monture + verres') + if (body.details) notes.push(`Details: ${body.details}`) + } else if (serviceType === 'REPAIR') { + const repairType = String(body.repairType || '').trim() + const repairDescription = String(body.repairDescription || '').trim() + const repairPrice = Math.max(0, Number(body.repairPrice || 0)) + + if (!repairType || !repairDescription) { + return NextResponse.json( + { error: 'Type et description de reparation requis' }, + { status: 400 } + ) + } + + const repairProduct = await getServiceProduct('SERVICE_REPAIR', 'Service reparation') + const prixUnitaireHT = toHt(repairPrice) + + lines.push({ + produitId: repairProduct.id, + quantite: 1, + prixUnitaireHT, + prixUnitaireTTC: repairPrice, + remise: 0, + montantHT: prixUnitaireHT, + montantTTC: repairPrice, + decrementStock: false, + }) + + notes.push(`Service: Reparation - ${repairType}`) + notes.push(`Description: ${repairDescription}`) + } else { + return NextResponse.json({ error: 'Service invalide' }, { status: 400 }) + } + + if (additionalCharges > 0) { + const extraProduct = await getServiceProduct('SERVICE_EXTRA', 'Frais supplementaires') + const prixUnitaireHT = toHt(additionalCharges) + lines.push({ + produitId: extraProduct.id, + quantite: 1, + prixUnitaireHT, + prixUnitaireTTC: additionalCharges, + remise: 0, + montantHT: prixUnitaireHT, + montantTTC: additionalCharges, + decrementStock: false, + }) + notes.push(`Frais supplementaires: ${additionalCharges.toFixed(2)} EUR`) + } + + const montantHT = lines.reduce((sum, line) => sum + line.montantHT, 0) + const montantTTC = lines.reduce((sum, line) => sum + line.montantTTC, 0) + const montantTVA = montantTTC - montantHT + const numero = await generateSaleNumber() + + const sale = await db.$transaction(async (tx) => { + const vente = await tx.vente.create({ + data: { + numero, + clientId, + statut: StatutVente.PAYEE, + montantHT, + montantTVA, + montantTTC, + remise: 0, + notes: notes.join('\n'), + employeId, + }, + }) + + for (const line of lines) { + await tx.ligneVente.create({ + data: { + venteId: vente.id, + produitId: line.produitId, + quantite: line.quantite, + prixUnitaireHT: line.prixUnitaireHT, + prixUnitaireTTC: line.prixUnitaireTTC, + remise: line.remise, + montantHT: line.montantHT, + montantTTC: line.montantTTC, + }, + }) + + if (line.decrementStock) { + await tx.produit.update({ + where: { id: line.produitId }, + data: { + stock: { + decrement: line.quantite, + }, + }, + }) + } + } + + await tx.paiement.create({ + data: { + venteId: vente.id, + mode: paymentMode, + montant: montantTTC, + employeId, + }, + }) + + return vente + }) + + const completeSale = await db.vente.findUnique({ + where: { id: sale.id }, + include: { + client: true, + lignes: { + include: { + produit: true, + }, + }, + paiements: true, + }, + }) + + return NextResponse.json(completeSale, { status: 201 }) + } catch (error) { + console.error('Wizard sale error:', error) + return NextResponse.json({ error: 'Impossible d enregistrer la vente' }, { status: 500 }) + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 322271d..e15fdd7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,9 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/toaster"; import SessionProvider from "@/components/auth/SessionProvider"; +import { ThemeProvider } from "@/components/theme-provider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -17,6 +18,21 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "OptiqueStock - Gestion de Magasin d'Optique", description: "Application de gestion de magasin d'optique : clients, produits, ventes, achats, atelier et rapports.", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "OptiqueStock", + }, +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, + viewportFit: "cover", + themeColor: "#020617", }; export default function RootLayout({ @@ -29,9 +45,11 @@ export default function RootLayout({ - - {children} - + + + {children} + + diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7312034..5a6cd6a 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,15 +1,16 @@ 'use client' -import { useState, useEffect } from 'react' +import { Suspense, useState, useEffect } from 'react' import { signIn } from 'next-auth/react' -import { useSearchParams } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { Eye } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -export default function LoginPage() { +function LoginForm() { + const router = useRouter() const searchParams = useSearchParams() const [email, setEmail] = useState('') const [password, setPassword] = useState('') @@ -28,11 +29,22 @@ export default function LoginPage() { setError('') setLoading(true) - await signIn('credentials', { + const callbackUrl = getLocalCallbackPath(searchParams.get('callbackUrl')) + const result = await signIn('credentials', { email, password, - callbackUrl: '/', + redirect: false, + callbackUrl, }) + + if (result?.error) { + setError('Email ou mot de passe incorrect') + setLoading(false) + return + } + + router.replace(callbackUrl) + router.refresh() } return ( @@ -83,3 +95,28 @@ export default function LoginPage() { ) } + +function getLocalCallbackPath(callbackUrl: string | null) { + if (!callbackUrl) return '/' + + try { + const parsed = new URL(callbackUrl, window.location.origin) + return `${parsed.pathname}${parsed.search}${parsed.hash}` || '/' + } catch { + return callbackUrl.startsWith('/') ? callbackUrl : '/' + } +} + +export default function LoginPage() { + return ( + +

Chargement...

+ + } + > + +
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 787bcb3..af8bda1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,23 +1,25 @@ 'use client' -import { useState } from 'react' -import { useSession, signOut } from 'next-auth/react' +import { useEffect, useState } from 'react' +import { signOut, useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { - Users, - Package, - Truck, - ShoppingCart, - FileText, BarChart3, + BrainCircuit, Eye, LayoutDashboard, - Wrench, LogOut, - User + Package, + PanelTop, + ShoppingCart, + Truck, + User, + UserCog, + Users, + Wrench, } from 'lucide-react' import POSModule from '@/components/pos/POSModule' import { ProduitListe } from '@/components/products/ProduitListe' @@ -26,8 +28,24 @@ import AtelierModule from '@/components/atelier/AtelierModule' import { SupplierList } from '@/components/suppliers/SupplierList' import PurchaseModule from '@/components/purchases/PurchaseModule' import ReportsModule from '@/components/reports/ReportsModule' +import { EmployeeManagement } from '@/components/employees/EmployeeManagement' +import { AIAssistant } from '@/components/ai/AIAssistant' +import { ThemeToggle } from '@/components/theme-toggle' +import { SellerWizard } from '@/components/seller-wizard/SellerWizard' -type Module = 'HOME' | 'CLIENTS' | 'PRODUITS' | 'FOURNISSEURS' | 'ACHATS' | 'VENTE' | 'RAPPORTS' | 'ATELIER' +type RoleEmploye = 'VENDEUR' | 'RESPONSABLE' | 'ADMIN' +type Module = + | 'HOME' + | 'CLIENTS' + | 'PRODUITS' + | 'FOURNISSEURS' + | 'ACHATS' + | 'VENTE' + | 'RAPPORTS' + | 'ATELIER' + | 'UTILISATEURS' + | 'IA' + | 'VENDEUR_WIZARD' interface ModuleCard { id: Module @@ -36,15 +54,26 @@ interface ModuleCard { icon: React.ReactNode badge?: string color: string + roles: RoleEmploye[] } const modules: ModuleCard[] = [ + { + id: 'VENDEUR_WIZARD', + title: 'Vendeur Express', + description: 'Parcours rapide client, service, recu', + icon: , + badge: 'Tablette', + color: 'bg-rose-500', + roles: ['VENDEUR', 'RESPONSABLE', 'ADMIN'], + }, { id: 'CLIENTS', title: 'Gestion Clients', description: 'Fiches clients, mesures de vision, ordonnances', icon: , - color: 'bg-blue-500' + color: 'bg-blue-500', + roles: ['VENDEUR', 'RESPONSABLE', 'ADMIN'], }, { id: 'PRODUITS', @@ -52,21 +81,24 @@ const modules: ModuleCard[] = [ description: 'Catalogue, stock, images, QR codes', icon: , badge: 'Alertes', - color: 'bg-emerald-500' + color: 'bg-emerald-500', + roles: ['VENDEUR', 'RESPONSABLE', 'ADMIN'], }, { id: 'FOURNISSEURS', title: 'Fournisseurs', description: 'Gestion des fournisseurs et contacts', icon: , - color: 'bg-orange-500' + color: 'bg-orange-500', + roles: ['RESPONSABLE', 'ADMIN'], }, { id: 'ACHATS', title: 'Achats & Stock', - description: 'Réception, factures fournisseurs, entrées stock', + description: 'Reception, factures fournisseurs, entrees stock', icon: , - color: 'bg-purple-500' + color: 'bg-purple-500', + roles: ['RESPONSABLE', 'ADMIN'], }, { id: 'VENTE', @@ -74,7 +106,8 @@ const modules: ModuleCard[] = [ description: 'Encaissement, facturation, POS', icon: , badge: 'Actif', - color: 'bg-green-500' + color: 'bg-green-500', + roles: ['VENDEUR', 'RESPONSABLE', 'ADMIN'], }, { id: 'ATELIER', @@ -82,48 +115,107 @@ const modules: ModuleCard[] = [ description: 'Montage de lunettes, commandes en cours', icon: , badge: 'En cours', - color: 'bg-amber-500' + color: 'bg-amber-500', + roles: ['VENDEUR', 'RESPONSABLE', 'ADMIN'], }, { id: 'RAPPORTS', title: 'Rapports', description: 'Statistiques, exports Excel/CSV/PDF', icon: , - color: 'bg-cyan-500' - } + color: 'bg-cyan-500', + roles: ['RESPONSABLE', 'ADMIN'], + }, + { + id: 'UTILISATEURS', + title: 'Utilisateurs', + description: 'Employes, roles et niveaux d acces', + icon: , + color: 'bg-indigo-500', + roles: ['ADMIN'], + }, + { + id: 'IA', + title: 'Assistant IA', + description: 'Conseils, priorites et aide a la decision', + icon: , + badge: 'Nouveau', + color: 'bg-sky-500', + roles: ['RESPONSABLE', 'ADMIN'], + }, ] export default function Home() { - const { data: session } = useSession() + const { data: session, status } = useSession() const router = useRouter() const [currentModule, setCurrentModule] = useState('HOME') - if (!session) { - router.push('/login') - return null + const currentRole = ((session?.user as any)?.role || 'VENDEUR') as RoleEmploye + const visibleModules = modules.filter((module) => module.roles.includes(currentRole)) + + useEffect(() => { + if (status === 'unauthenticated') { + router.push('/login') + } + }, [status, router]) + + useEffect(() => { + const current = modules.find((module) => module.id === currentModule) + if (current && !current.roles.includes(currentRole)) { + setCurrentModule('HOME') + } + }, [currentModule, currentRole]) + + if (status === 'loading') { + return ( +
+

Chargement...

+
+ ) } + if (status === 'unauthenticated') return null + + const moduleInfo = modules.find((module) => module.id === currentModule) + + const moduleHeader = ( +
+ +
+ {moduleInfo?.icon} +

{moduleInfo?.title}

+
+
+ ) + const renderModule = () => { if (currentModule === 'HOME') { return (
-
-

OptiqueStock

-

Système de Gestion de Magasin d'Optique

+
+

OptiqueStock

+

+ Systeme de Gestion de Magasin d'Optique +

-
- {modules.map((module) => ( +
+ {visibleModules.map((module) => ( setCurrentModule(module.id)} > -
-
- {module.icon} -
+
+
{module.icon}
{module.badge && ( {module.badge} @@ -131,9 +223,7 @@ export default function Home() { )}
{module.title} - - {module.description} - + {module.description} ))} @@ -142,231 +232,125 @@ export default function Home() { ) } - const moduleInfo = modules.find(m => m.id === currentModule) - - // Render Client Management module if selected if (currentModule === 'CLIENTS') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - // Render POS module if selected if (currentModule === 'VENTE') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - // Render Products module if selected if (currentModule === 'PRODUITS') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - // Render Atelier module if selected if (currentModule === 'ATELIER') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - // Render Suppliers module if selected if (currentModule === 'FOURNISSEURS') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - // Render Purchases module if selected if (currentModule === 'ACHATS') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - // Render Reports module if selected if (currentModule === 'RAPPORTS') { return (
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
-
+ {moduleHeader}
) } - return ( -
-
- -
- {moduleInfo?.icon} -

{moduleInfo?.title}

-
+ if (currentModule === 'UTILISATEURS') { + return ( +
+ {moduleHeader} +
+ ) + } - - -
- -
-

- Module en développement -

-

- Le module {moduleInfo?.title} est actuellement en cours de développement. - Veuillez revenir ultérieurement. -

-
-
-
- ) + if (currentModule === 'IA') { + return ( +
+ {moduleHeader} + +
+ ) + } + + if (currentModule === 'VENDEUR_WIZARD') { + return ( +
+ {moduleHeader} + +
+ ) + } + + return null } return ( -
-
+
+
-
+
-

OptiqueStock

-

Gestion de Magasin d'Optique

+

OptiqueStock

+

Gestion de Magasin d'Optique

-
- - v1.0.0 - -
+
+ +
{session?.user?.name} + {currentRole}
@@ -375,19 +359,12 @@ export default function Home() {
-
- {renderModule()} -
+
{renderModule()}
-