Add tablet seller wizard and access controls
This commit is contained in:
82
src/app/api/ai/assistant/route.ts
Normal file
82
src/app/api/ai/assistant/route.ts
Normal file
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
169
src/app/api/employes/[id]/route.ts
Normal file
169
src/app/api/employes/[id]/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
107
src/app/api/employes/route.ts
Normal file
107
src/app/api/employes/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
238
src/app/api/pos/wizard-sale/route.ts
Normal file
238
src/app/api/pos/wizard-sale/route.ts
Normal file
@@ -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<string> {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||
>
|
||||
<SessionProvider>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
<ThemeProvider>
|
||||
<SessionProvider>
|
||||
{children}
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<p className="text-gray-500">Chargement...</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
375
src/app/page.tsx
375
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: <PanelTop className="h-8 w-8" />,
|
||||
badge: 'Tablette',
|
||||
color: 'bg-rose-500',
|
||||
roles: ['VENDEUR', 'RESPONSABLE', 'ADMIN'],
|
||||
},
|
||||
{
|
||||
id: 'CLIENTS',
|
||||
title: 'Gestion Clients',
|
||||
description: 'Fiches clients, mesures de vision, ordonnances',
|
||||
icon: <Users className="h-8 w-8" />,
|
||||
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: <Package className="h-8 w-8" />,
|
||||
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: <Truck className="h-8 w-8" />,
|
||||
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: <ShoppingCart className="h-8 w-8" />,
|
||||
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: <ShoppingCart className="h-8 w-8" />,
|
||||
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: <Wrench className="h-8 w-8" />,
|
||||
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: <BarChart3 className="h-8 w-8" />,
|
||||
color: 'bg-cyan-500'
|
||||
}
|
||||
color: 'bg-cyan-500',
|
||||
roles: ['RESPONSABLE', 'ADMIN'],
|
||||
},
|
||||
{
|
||||
id: 'UTILISATEURS',
|
||||
title: 'Utilisateurs',
|
||||
description: 'Employes, roles et niveaux d acces',
|
||||
icon: <UserCog className="h-8 w-8" />,
|
||||
color: 'bg-indigo-500',
|
||||
roles: ['ADMIN'],
|
||||
},
|
||||
{
|
||||
id: 'IA',
|
||||
title: 'Assistant IA',
|
||||
description: 'Conseils, priorites et aide a la decision',
|
||||
icon: <BrainCircuit className="h-8 w-8" />,
|
||||
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<Module>('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 (
|
||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||
<p className="text-muted-foreground">Chargement...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'unauthenticated') return null
|
||||
|
||||
const moduleInfo = modules.find((module) => module.id === currentModule)
|
||||
|
||||
const moduleHeader = (
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour a l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 rounded-lg p-4 ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderModule = () => {
|
||||
if (currentModule === 'HOME') {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-4xl font-bold text-gray-900">OptiqueStock</h1>
|
||||
<p className="text-lg text-gray-600">Système de Gestion de Magasin d'Optique</p>
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-4xl font-bold text-foreground">OptiqueStock</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Systeme de Gestion de Magasin d'Optique
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{modules.map((module) => (
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{visibleModules.map((module) => (
|
||||
<Card
|
||||
key={module.id}
|
||||
className="group cursor-pointer transition-all duration-200 hover:shadow-lg hover:scale-105 border-2 hover:border-primary"
|
||||
className="group cursor-pointer border-2 transition-all duration-200 hover:scale-105 hover:border-primary hover:shadow-lg"
|
||||
onClick={() => setCurrentModule(module.id)}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className={`flex items-center justify-between mb-2`}>
|
||||
<div className={`p-3 rounded-lg ${module.color} text-white`}>
|
||||
{module.icon}
|
||||
</div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className={`rounded-lg p-3 ${module.color} text-white`}>{module.icon}</div>
|
||||
{module.badge && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{module.badge}
|
||||
@@ -131,9 +223,7 @@ export default function Home() {
|
||||
)}
|
||||
</div>
|
||||
<CardTitle className="text-lg">{module.title}</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
{module.description}
|
||||
</CardDescription>
|
||||
<CardDescription className="text-sm">{module.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
))}
|
||||
@@ -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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<ClientList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render POS module if selected
|
||||
if (currentModule === 'VENTE') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<POSModule />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render Products module if selected
|
||||
if (currentModule === 'PRODUITS') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<ProduitListe />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render Atelier module if selected
|
||||
if (currentModule === 'ATELIER') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<AtelierModule />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render Suppliers module if selected
|
||||
if (currentModule === 'FOURNISSEURS') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<SupplierList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render Purchases module if selected
|
||||
if (currentModule === 'ACHATS') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<PurchaseModule />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Render Reports module if selected
|
||||
if (currentModule === 'RAPPORTS') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{moduleHeader}
|
||||
<ReportsModule />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setCurrentModule('HOME')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<LayoutDashboard className="h-4 w-4" />
|
||||
Retour à l'accueil
|
||||
</Button>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg ${moduleInfo?.color} text-white`}>
|
||||
{moduleInfo?.icon}
|
||||
<h2 className="text-2xl font-bold">{moduleInfo?.title}</h2>
|
||||
</div>
|
||||
if (currentModule === 'UTILISATEURS') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{moduleHeader}
|
||||
<EmployeeManagement />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Card className="border-2 border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||||
<div className={`p-6 rounded-full ${moduleInfo?.color} bg-opacity-10 mb-4`}>
|
||||
<Eye className="h-16 w-16 text-gray-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-gray-700 mb-2">
|
||||
Module en développement
|
||||
</h3>
|
||||
<p className="text-gray-500 text-center max-w-md">
|
||||
Le module <strong>{moduleInfo?.title}</strong> est actuellement en cours de développement.
|
||||
Veuillez revenir ultérieurement.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
if (currentModule === 'IA') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{moduleHeader}
|
||||
<AIAssistant />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentModule === 'VENDEUR_WIZARD') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{moduleHeader}
|
||||
<SellerWizard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<header className="bg-white border-b shadow-sm">
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<header className="border-b bg-card shadow-sm">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary rounded-lg">
|
||||
<div className="rounded-lg bg-primary p-2">
|
||||
<Eye className="h-6 w-6 text-primary-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900">OptiqueStock</h1>
|
||||
<p className="text-xs text-gray-500">Gestion de Magasin d'Optique</p>
|
||||
<h1 className="text-xl font-bold text-foreground">OptiqueStock</h1>
|
||||
<p className="text-xs text-muted-foreground">Gestion de Magasin d'Optique</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Badge variant="outline" className="text-sm">
|
||||
v1.0.0
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-3">
|
||||
<ThemeToggle />
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<User className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{session?.user?.name}</span>
|
||||
<Badge variant="secondary">{currentRole}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
title="Se déconnecter"
|
||||
onClick={() => signOut({ redirect: false }).then(() => router.replace('/login'))}
|
||||
title="Se deconnecter"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -375,19 +359,12 @@ export default function Home() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
{renderModule()}
|
||||
</main>
|
||||
<main className="container mx-auto px-4 py-8">{renderModule()}</main>
|
||||
|
||||
<footer className="bg-white border-t mt-auto">
|
||||
<footer className="mt-auto border-t bg-card">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
© 2024 OptiqueStock. Tous droits réservés.
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>Support: support@optiquestock.com</span>
|
||||
<span>•</span>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>Version 1.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user