Add demo data branch tools and tablet polish

This commit is contained in:
2026-05-31 21:14:42 +01:00
parent 86beb8a5dd
commit 865c4a78ea
8 changed files with 459 additions and 59 deletions

View File

@@ -0,0 +1,272 @@
import { NextResponse } from 'next/server'
import bcrypt from 'bcryptjs'
import { ModePaiement, StatutAtelier, StatutVente } from '@prisma/client'
import { db } from '@/lib/db'
import { getSession } from '@/lib/auth-utils'
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
}
function htFromTtc(ttc: number, tva = 20) {
return ttc / (1 + tva / 100)
}
export async function POST() {
const authError = await requireAdmin()
if (authError) return authError
try {
const suppliers = await Promise.all([
db.fournisseur.upsert({
where: { id: 'demo-fournisseur-lumioptic' },
update: {
nom: 'LumiOptic Distribution',
contact: 'Nadia Benali',
email: 'contact@demo-lumioptic.local',
telephone: '0522000001',
ville: 'Casablanca',
actif: true,
},
create: {
id: 'demo-fournisseur-lumioptic',
nom: 'LumiOptic Distribution',
contact: 'Nadia Benali',
email: 'contact@demo-lumioptic.local',
telephone: '0522000001',
ville: 'Casablanca',
},
}),
db.fournisseur.upsert({
where: { id: 'demo-fournisseur-clairverre' },
update: {
nom: 'ClairVerre Pro',
contact: 'Yassine El Amrani',
email: 'contact@demo-clairverre.local',
telephone: '0522000002',
ville: 'Rabat',
actif: true,
},
create: {
id: 'demo-fournisseur-clairverre',
nom: 'ClairVerre Pro',
contact: 'Yassine El Amrani',
email: 'contact@demo-clairverre.local',
telephone: '0522000002',
ville: 'Rabat',
},
}),
])
const productsData = [
['DEMO-MNT-001', 'Monture acetate noir Atlas', 'MONTURE', 42, 129, 12, 4, 'Atlas', 'COMPLET', suppliers[0].id],
['DEMO-MNT-002', 'Monture metal fine Sofia', 'MONTURE', 38, 115, 9, 3, 'Sofia', 'COMPLET', suppliers[0].id],
['DEMO-MNT-003', 'Monture enfant Flex Kids', 'MONTURE', 25, 79, 14, 5, 'Flex Kids', 'NATUREL', suppliers[0].id],
['DEMO-VR-001', 'Verres simple vision anti-reflet', 'VERRE', 18, 59, 40, 10, null, null, suppliers[1].id],
['DEMO-VR-002', 'Verres progressifs confort', 'VERRE', 58, 189, 18, 6, null, null, suppliers[1].id],
['DEMO-VR-003', 'Verres anti-lumiere bleue', 'VERRE', 24, 89, 22, 8, null, null, suppliers[1].id],
['DEMO-LEN-001', 'Lentilles mensuelles confort', 'LENTILLE', 9, 29, 30, 8, 'ClearDay', null, suppliers[1].id],
['DEMO-ACC-001', 'Kit nettoyage premium', 'ACCESSOIRE', 3, 12, 50, 12, null, null, suppliers[0].id],
['DEMO-ACC-002', 'Etui rigide signature', 'ACCESSOIRE', 5, 18, 35, 10, null, null, suppliers[0].id],
['DEMO-ACC-003', 'Cordon lunettes sport', 'ACCESSOIRE', 2, 9, 5, 10, null, null, suppliers[0].id],
] as const
const products = await Promise.all(
productsData.map(([reference, designation, categorie, prixAchatHT, prixVenteTTC, stock, stockMin, marque, typeMonture, fournisseurId]) =>
db.produit.upsert({
where: { reference },
update: {
designation,
categorie,
prixAchatHT,
prixVenteTTC,
stock,
stockMin,
marque,
typeMonture: typeMonture as any,
fournisseurId,
actif: true,
},
create: {
reference,
designation,
categorie,
prixAchatHT,
prixVenteTTC,
tva: 20,
stock,
stockMin,
marque,
typeMonture: typeMonture as any,
fournisseurId,
actif: true,
},
})
)
)
const clientsData = [
['demo-client-1', 'Amina', 'Rachid', '0600000101', 'amina.rachid@demo.local', 'Casablanca'],
['demo-client-2', 'Karim', 'Bennani', '0600000102', 'karim.bennani@demo.local', 'Rabat'],
['demo-client-3', 'Sara', 'El Fassi', '0600000103', 'sara.elfassi@demo.local', 'Marrakech'],
['demo-client-4', 'Youssef', 'Idrissi', '0600000104', null, 'Tanger'],
['demo-client-5', 'Leila', 'Mansouri', '0600000105', 'leila.mansouri@demo.local', 'Fes'],
] as const
const clients = await Promise.all(
clientsData.map(([id, prenom, nom, telephone, email, ville]) =>
db.client.upsert({
where: { telephone },
update: {
prenom,
nom,
email,
ville,
},
create: {
id,
prenom,
nom,
telephone,
email,
ville,
},
})
)
)
const password = await bcrypt.hash('demo123', 12)
const employees = await Promise.all([
db.employe.upsert({
where: { email: 'vendeur.demo@optiquestock.local' },
update: {
nom: 'Demo',
prenom: 'Vendeur',
role: 'VENDEUR',
actif: true,
},
create: {
email: 'vendeur.demo@optiquestock.local',
nom: 'Demo',
prenom: 'Vendeur',
role: 'VENDEUR',
actif: true,
motDePasse: password,
},
}),
db.employe.upsert({
where: { email: 'responsable.demo@optiquestock.local' },
update: {
nom: 'Demo',
prenom: 'Responsable',
role: 'RESPONSABLE',
actif: true,
},
create: {
email: 'responsable.demo@optiquestock.local',
nom: 'Demo',
prenom: 'Responsable',
role: 'RESPONSABLE',
actif: true,
motDePasse: password,
},
}),
])
const saleSpecs = [
{
numero: 'DEMO-VENTE-001',
client: clients[0],
statutAtelier: StatutAtelier.EN_COURS,
items: [products[0], products[3], products[7]],
payment: ModePaiement.CARTE,
},
{
numero: 'DEMO-VENTE-002',
client: clients[1],
statutAtelier: StatutAtelier.PRET,
items: [products[1], products[4]],
payment: ModePaiement.ESPECES,
},
{
numero: 'DEMO-VENTE-003',
client: clients[2],
statutAtelier: StatutAtelier.EN_ATTENTE,
items: [products[2], products[5]],
payment: ModePaiement.CHEQUE,
},
]
let salesCreated = 0
for (const spec of saleSpecs) {
const existingSale = await db.vente.findUnique({ where: { numero: spec.numero } })
if (existingSale) continue
const montantTTC = spec.items.reduce((sum, product) => sum + product.prixVenteTTC, 0)
const montantHT = spec.items.reduce((sum, product) => sum + htFromTtc(product.prixVenteTTC), 0)
await db.vente.create({
data: {
numero: spec.numero,
clientId: spec.client.id,
employeId: employees[0].id,
statut: StatutVente.PAYEE,
statutAtelier: spec.statutAtelier,
montantHT,
montantTVA: montantTTC - montantHT,
montantTTC,
notes: 'Vente demo generee automatiquement',
dateAtelier: new Date(),
lignes: {
create: spec.items.map((product) => ({
produitId: product.id,
quantite: 1,
prixUnitaireHT: htFromTtc(product.prixVenteTTC),
prixUnitaireTTC: product.prixVenteTTC,
remise: 0,
montantHT: htFromTtc(product.prixVenteTTC),
montantTTC: product.prixVenteTTC,
})),
},
paiements: {
create: {
mode: spec.payment,
montant: montantTTC,
employeId: employees[0].id,
reference: spec.numero,
},
},
},
})
salesCreated += 1
}
return NextResponse.json({
message: 'Demo data ready',
clients: clients.length,
products: products.length,
suppliers: suppliers.length,
employees: employees.length,
salesCreated,
demoLogins: [
{ email: 'vendeur.demo@optiquestock.local', password: 'demo123' },
{ email: 'responsable.demo@optiquestock.local', password: 'demo123' },
],
})
} catch (error) {
console.error('Demo seed error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to seed demo data' },
{ status: 500 }
)
}
}

View File

@@ -4,6 +4,7 @@ import "./globals.css";
import { Toaster } from "@/components/ui/toaster";
import SessionProvider from "@/components/auth/SessionProvider";
import { ThemeProvider } from "@/components/theme-provider";
import { CurrencyProvider } from "@/components/currency-provider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -46,9 +47,11 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
>
<ThemeProvider>
<SessionProvider>
{children}
</SessionProvider>
<CurrencyProvider>
<SessionProvider>
{children}
</SessionProvider>
</CurrencyProvider>
</ThemeProvider>
<Toaster />
</body>

View File

@@ -29,6 +29,8 @@ function LoginForm() {
setError('')
setLoading(true)
await requestFullscreen()
const callbackUrl = getLocalCallbackPath(searchParams.get('callbackUrl'))
const result = await signIn('credentials', {
email,
@@ -96,6 +98,11 @@ function LoginForm() {
)
}
async function requestFullscreen() {
if (document.fullscreenElement || !document.documentElement.requestFullscreen) return
await document.documentElement.requestFullscreen().catch(() => undefined)
}
function getLocalCallbackPath(callbackUrl: string | null) {
if (!callbackUrl) return '/'

View File

@@ -9,8 +9,10 @@ import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/ca
import {
BarChart3,
BrainCircuit,
Database,
Eye,
LayoutDashboard,
Loader2,
LogOut,
Package,
PanelTop,
@@ -32,6 +34,7 @@ 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'
import { CurrencySelect } from '@/components/currency-select'
type RoleEmploye = 'VENDEUR' | 'RESPONSABLE' | 'ADMIN'
type Module =
@@ -149,6 +152,8 @@ export default function Home() {
const { data: session, status } = useSession()
const router = useRouter()
const [currentModule, setCurrentModule] = useState<Module>('HOME')
const [demoGenerating, setDemoGenerating] = useState(false)
const [demoMessage, setDemoMessage] = useState('')
const currentRole = ((session?.user as any)?.role || 'VENDEUR') as RoleEmploye
const visibleModules = modules.filter((module) => module.roles.includes(currentRole))
@@ -166,6 +171,28 @@ export default function Home() {
}
}, [currentModule, currentRole])
async function generateDemoData() {
setDemoGenerating(true)
setDemoMessage('')
try {
const response = await fetch('/api/demo/seed', { method: 'POST' })
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Generation demo impossible')
}
setDemoMessage(
`Demo prete: ${data.clients} clients, ${data.products} produits, ${data.salesCreated} nouvelles ventes.`
)
} catch (error) {
setDemoMessage(error instanceof Error ? error.message : 'Erreur demo inconnue')
} finally {
setDemoGenerating(false)
}
}
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
@@ -206,6 +233,26 @@ export default function Home() {
</p>
</div>
{currentRole === 'ADMIN' && (
<div className="mx-auto flex max-w-2xl flex-col items-center gap-3 rounded-md border bg-card p-4 text-center">
<Button
variant="outline"
size="lg"
onClick={generateDemoData}
disabled={demoGenerating}
className="h-12 gap-2"
>
{demoGenerating ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Database className="h-5 w-5" />
)}
Generer les donnees demo
</Button>
{demoMessage && <p className="text-sm text-muted-foreground">{demoMessage}</p>}
</div>
)}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{visibleModules.map((module) => (
<Card
@@ -341,6 +388,7 @@ export default function Home() {
</div>
<div className="flex items-center gap-3">
<ThemeToggle />
<CurrencySelect />
<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>