Add demo data branch tools and tablet polish
This commit is contained in:
272
src/app/api/demo/seed/route.ts
Normal file
272
src/app/api/demo/seed/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '/'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user