Initial commit

This commit is contained in:
2026-05-30 14:33:11 +01:00
commit a8c372177f
156 changed files with 38163 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET single purchase invoice
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const invoice = await db.factureAchat.findUnique({
where: { id: id },
include: {
fournisseur: true,
lignes: {
include: {
produit: true
}
},
fichiers: true
}
})
if (!invoice) {
return NextResponse.json(
{ error: 'Facture non trouvée' },
{ status: 404 }
)
}
return NextResponse.json(invoice)
} catch (error) {
console.error('Error fetching purchase invoice:', error)
return NextResponse.json(
{ error: 'Impossible de récupérer la facture' },
{ status: 500 }
)
}
}
// DELETE purchase invoice (only if BROUILLON)
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Check if invoice exists and is in BROUILLON status
const invoice = await db.factureAchat.findUnique({
where: { id: id }
})
if (!invoice) {
return NextResponse.json(
{ error: 'Facture non trouvée' },
{ status: 404 }
)
}
if (invoice.statut !== 'BROUILLON') {
return NextResponse.json(
{ error: 'Seules les factures en brouillon peuvent être supprimées' },
{ status: 400 }
)
}
// Delete invoice (lines will be cascade deleted)
await db.factureAchat.delete({
where: { id: id }
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting purchase invoice:', error)
return NextResponse.json(
{ error: 'Impossible de supprimer la facture' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// POST validate invoice and update stock
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Get invoice with lines
const invoice = await db.factureAchat.findUnique({
where: { id: id },
include: {
lignes: true
}
})
if (!invoice) {
return NextResponse.json(
{ error: 'Facture non trouvée' },
{ status: 404 }
)
}
if (invoice.statut !== 'BROUILLON') {
return NextResponse.json(
{ error: 'Cette facture a déjà été validée' },
{ status: 400 }
)
}
// Update stock for each line
for (const ligne of invoice.lignes) {
await db.produit.update({
where: { id: ligne.produitId },
data: {
stock: {
increment: ligne.quantite
}
}
})
}
// Update invoice status and set reception date
const updatedInvoice = await db.factureAchat.update({
where: { id: id },
data: {
statut: 'VALIDE',
dateReception: new Date()
},
include: {
fournisseur: true,
lignes: {
include: {
produit: true
}
}
}
})
return NextResponse.json(updatedInvoice)
} catch (error) {
console.error('Error validating purchase invoice:', error)
return NextResponse.json(
{ error: 'Impossible de valider la facture' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET all purchase invoices
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const statut = searchParams.get('statut')
const invoices = await db.factureAchat.findMany({
where: statut ? { statut: statut as any } : undefined,
include: {
fournisseur: true,
lignes: {
include: {
produit: true
}
},
fichiers: true
},
orderBy: {
date: 'desc'
}
})
return NextResponse.json(invoices)
} catch (error) {
console.error('Error fetching purchase invoices:', error)
return NextResponse.json(
{ error: 'Impossible de récupérer les factures d\'achat' },
{ status: 500 }
)
}
}
// POST create new purchase invoice
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
fournisseurId,
date,
lignes,
montantHT,
montantTVA,
montantTTC,
notes
} = body
// Validation
if (!fournisseurId) {
return NextResponse.json(
{ error: 'Le fournisseur est requis' },
{ status: 400 }
)
}
if (!lignes || lignes.length === 0) {
return NextResponse.json(
{ error: 'Au moins une ligne est requise' },
{ status: 400 }
)
}
// Generate invoice number
const now = new Date(date)
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
// Count invoices for this month
const monthInvoices = await db.factureAchat.count({
where: {
date: {
gte: new Date(year, now.getMonth(), 1),
lt: new Date(year, now.getMonth() + 1, 1)
}
}
})
const numero = `A${year}${month}${String(monthInvoices + 1).padStart(4, '0')}`
// Create invoice with lines in a transaction
const invoice = await db.factureAchat.create({
data: {
fournisseurId,
numero,
date: new Date(date),
statut: 'BROUILLON',
montantHT: Number(montantHT),
montantTVA: Number(montantTVA),
montantTTC: Number(montantTTC),
notes: notes || null,
lignes: {
create: lignes.map((ligne: any) => ({
produitId: ligne.produitId,
quantite: ligne.quantite,
prixUnitaireHT: ligne.prixUnitaireHT,
tauxTVA: ligne.tauxTVA,
montantHT: ligne.montantHT,
montantTVA: ligne.montantTVA,
montantTTC: ligne.montantTTC
}))
}
},
include: {
fournisseur: true,
lignes: {
include: {
produit: true
}
}
}
})
return NextResponse.json(invoice, { status: 201 })
} catch (error) {
console.error('Error creating purchase invoice:', error)
return NextResponse.json(
{ error: 'Impossible de créer la facture d\'achat' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from 'next/server'
import { writeFile, mkdir } from 'fs/promises'
import { db } from '@/lib/db'
import { existsSync } from 'fs'
import path from 'path'
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
const factureAchatId = formData.get('factureAchatId') as string
const type = formData.get('type') as string
const nom = formData.get('nom') as string
if (!file) {
return NextResponse.json(
{ error: 'Aucun fichier fourni' },
{ status: 400 }
)
}
if (!factureAchatId) {
return NextResponse.json(
{ error: 'ID de facture requis' },
{ status: 400 }
)
}
// Validate file type (PDF only)
if (file.type !== 'application/pdf') {
return NextResponse.json(
{ error: 'Seuls les fichiers PDF sont acceptés' },
{ status: 400 }
)
}
// Validate file size (max 10MB)
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
return NextResponse.json(
{ error: 'La taille du fichier ne peut pas dépasser 10MB' },
{ status: 400 }
)
}
// Create upload directory if it doesn't exist
const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'purchases')
if (!existsSync(uploadDir)) {
await mkdir(uploadDir, { recursive: true })
}
// Generate unique filename
const timestamp = Date.now()
const filename = `${factureAchatId}_${timestamp}_${file.name}`
const filepath = path.join(uploadDir, filename)
// Write file
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
await writeFile(filepath, buffer)
// Create file record in database
const fichier = await db.fichier.create({
data: {
nom: nom || file.name,
type: type as any,
url: `/uploads/purchases/${filename}`,
taille: file.size,
mimeType: file.type,
factureAchatId: factureAchatId
}
})
return NextResponse.json(fichier, { status: 201 })
} catch (error) {
console.error('Error uploading file:', error)
return NextResponse.json(
{ error: 'Impossible de télécharger le fichier' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import ZAI from 'z-ai-web-dev-sdk';
import fs from 'fs';
export async function POST(req: NextRequest) {
try {
const { imagePath } = await req.json();
if (!imagePath) {
return NextResponse.json({ error: 'imagePath is required' }, { status: 400 });
}
const imageBuffer = fs.readFileSync(imagePath);
const base64Image = imageBuffer.toString('base64');
const mimeType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
const zai = await ZAI.create();
const response = await zai.chat.completions.createVision({
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: 'Analyze this image and describe what it shows in detail. If it contains text, extract all the text. If it\'s a UI design or application screenshot, describe all the features, components, and functionality shown.'
},
{
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${base64Image}`
}
}
]
}
],
thinking: { type: 'disabled' }
});
const content = response.choices[0]?.message?.content;
return NextResponse.json({
success: true,
analysis: content
});
} catch (error) {
console.error('Image analysis error:', error);
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { StatutAtelier } from '@prisma/client'
// POST /api/atelier/orders/[id] - Update workshop status
// Note: Using POST instead of PATCH because the gateway blocks PATCH requests
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const { statutAtelier } = body
console.log('API: POST /api/atelier/orders/[id]')
console.log('API: orderId =', id)
console.log('API: statutAtelier =', statutAtelier)
console.log('API: Valid StatutAtelier values:', Object.values(StatutAtelier))
// Validate status
if (!statutAtelier || !Object.values(StatutAtelier).includes(statutAtelier)) {
console.log('API: Invalid status - returning 400')
return NextResponse.json(
{ error: 'Invalid workshop status' },
{ status: 400 }
)
}
// Check if order exists
const existingOrder = await db.vente.findUnique({
where: { id: id }
})
if (!existingOrder) {
return NextResponse.json(
{ error: 'Order not found' },
{ status: 404 }
)
}
// Update order status
const updateData: any = {
statutAtelier: statutAtelier as StatutAtelier,
dateAtelier: new Date()
}
// Add dateRetrait if status is RETIRE
if (statutAtelier === 'RETIRE') {
updateData.dateRetrait = new Date()
}
const updatedOrder = await db.vente.update({
where: { id: id },
data: updateData,
include: {
client: true,
lignes: {
include: {
produit: true
},
where: {
produit: {
categorie: {
in: ['MONTURE', 'VERRE']
}
}
}
},
paiements: true
}
})
console.log('API: Order updated successfully, new status:', updatedOrder.statutAtelier)
// Fetch patients for the client
let patients = []
if (updatedOrder.clientId) {
patients = await db.patient.findMany({
where: {
clientId: updatedOrder.clientId
},
orderBy: {
dateCreation: 'desc'
},
take: 2
})
}
return NextResponse.json({
...updatedOrder,
patients
})
} catch (error) {
console.error('Error updating work order:', error)
return NextResponse.json(
{ error: 'Failed to update work order' },
{ status: 500 }
)
}
}
// GET /api/atelier/orders/[id] - Get specific work order details
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const order = await db.vente.findUnique({
where: { id: id },
include: {
client: true,
lignes: {
include: {
produit: true
}
},
paiements: true
}
})
if (!order) {
return NextResponse.json(
{ error: 'Order not found' },
{ status: 404 }
)
}
// Fetch patients for the client
let patients = []
if (order.clientId) {
patients = await db.patient.findMany({
where: {
clientId: order.clientId
},
orderBy: {
dateCreation: 'desc'
},
take: 2
})
}
return NextResponse.json({
...order,
patients
})
} catch (error) {
console.error('Error fetching work order:', error)
return NextResponse.json(
{ error: 'Failed to fetch work order' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,86 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { StatutAtelier } from '@prisma/client'
// GET /api/atelier/orders - Get all work orders (sales that need mounting)
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const status = searchParams.get('status') as StatutAtelier | null
// Build where clause
const where: any = {
statut: 'PAYEE', // Only paid sales go to workshop
lignes: {
some: {
produit: {
categorie: {
in: ['MONTURE', 'VERRE'] // Only sales with frames or lenses
}
}
}
}
}
// Filter by status if provided
if (status && status !== 'ALL') {
where.statutAtelier = status
}
const workOrders = await db.vente.findMany({
where,
include: {
client: true,
lignes: {
include: {
produit: true
},
where: {
produit: {
categorie: {
in: ['MONTURE', 'VERRE']
}
}
}
},
paiements: true
},
orderBy: [
{ statutAtelier: 'asc' }, // EN_ATTENTE first, then EN_COURS, etc.
{ date: 'desc' }
]
})
// Fetch patients for each client to get vision measurements
const workOrdersWithPatients = await Promise.all(
workOrders.map(async (order) => {
if (!order.clientId) {
return { ...order, patients: [] }
}
const patients = await db.patient.findMany({
where: {
clientId: order.clientId
},
orderBy: {
dateCreation: 'desc'
},
take: 2 // Get the 2 most recent vision measurements
})
return {
...order,
patients
}
})
)
return NextResponse.json(workOrdersWithPatients)
} catch (error) {
console.error('Error fetching work orders:', error)
return NextResponse.json(
{ error: 'Failed to fetch work orders' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,172 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { StatutVente, StatutAtelier } from '@prisma/client'
// POST /api/atelier/seed - Seed sample work order data for testing
export async function POST(request: NextRequest) {
try {
// Check if we already have test data
const existingOrders = await db.vente.count({
where: {
statut: StatutVente.PAYEE
}
})
if (existingOrders > 0) {
return NextResponse.json(
{
message: 'Test data already exists',
count: existingOrders
},
{ status: 200 }
)
}
// Get or create sample products
const frames = await db.produit.findMany({
where: { categorie: 'MONTURE' },
take: 3
})
const lenses = await db.produit.findMany({
where: { categorie: 'VERRE' },
take: 3
})
// Get or create a sample client
let client = await db.client.findFirst()
if (!client) {
client = await db.client.create({
data: {
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@email.com',
telephone: '0612345678',
adresse: '123 Rue de la République',
ville: 'Paris',
codePostal: '75001'
}
})
}
// Create vision measurements for the client
let patient = await db.patient.findFirst({
where: { clientId: client.id }
})
if (!patient) {
patient = await db.patient.create({
data: {
clientId: client.id,
odSphere: -2.00,
odCylindre: -0.50,
odAxe: 180,
ogSphere: -1.75,
ogCylindre: -0.75,
ogAxe: 175,
addition: 1.50,
pd: 63,
hauteur: 22
}
})
}
// Create sample sales with different workshop statuses
const workOrderData = [
{
statutAtelier: StatutAtelier.EN_ATTENTE,
products: [
frames[0],
lenses[0]
]
},
{
statutAtelier: StatutAtelier.EN_COURS,
products: [
frames[1] || frames[0],
lenses[1] || lenses[0]
]
},
{
statutAtelier: StatutAtelier.TERMINE,
products: [
frames[2] || frames[0],
lenses[2] || lenses[0]
]
},
{
statutAtelier: StatutAtelier.PRET,
products: [
frames[0],
lenses[1] || lenses[0]
]
}
]
const createdOrders = []
for (const orderData of workOrderData) {
const totalHT = orderData.products.reduce((sum, p) => sum + p.prixVenteTTC / 1.2, 0)
const totalTVA = orderData.products.reduce((sum, p) => sum + p.prixVenteTTC - p.prixVenteTTC / 1.2, 0)
const totalTTC = orderData.products.reduce((sum, p) => sum + p.prixVenteTTC, 0)
const order = await db.vente.create({
data: {
clientId: client.id,
numero: `V${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(createdOrders.length + 1).padStart(4, '0')}`,
statut: StatutVente.PAYEE,
statutAtelier: orderData.statutAtelier,
montantHT: totalHT,
montantTVA: totalTVA,
montantTTC: totalTTC,
dateAtelier: new Date(),
lignes: {
create: orderData.products.map(product => ({
produitId: product.id,
quantite: 1,
prixUnitaireHT: product.prixVenteTTC / 1.2,
prixUnitaireTTC: product.prixVenteTTC,
remise: 0,
montantHT: product.prixVenteTTC / 1.2,
montantTTC: product.prixVenteTTC
}))
},
paiements: {
create: {
mode: 'CARTE',
montant: totalTTC,
reference: `TEST${createdOrders.length + 1}`
}
}
},
include: {
client: true,
lignes: {
include: {
produit: true
}
},
paiements: true
}
})
createdOrders.push(order)
}
return NextResponse.json({
message: 'Sample work order data created successfully',
count: createdOrders.length,
orders: createdOrders.map(o => ({
id: o.id,
numero: o.numero,
statutAtelier: o.statutAtelier
}))
})
} catch (error) {
console.error('Error seeding work order data:', error)
return NextResponse.json(
{ error: 'Failed to seed work order data', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/clients/[id]/patients - Get all patients for a client
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
console.log('API: GET /api/clients/[id]/patients')
console.log('API: params.id =', id)
console.log('API: typeof params.id =', typeof id)
const patients = await db.patient.findMany({
where: { clientId: id },
include: {
ordonnances: {
include: {
fichiers: true
}
}
},
orderBy: {
dateCreation: 'desc'
}
})
console.log('API: Patients trouvés =', patients.length)
patients.forEach((p, i) => {
console.log(`API: Patient ${i}: id=${p.id}, clientId=${p.clientId}`)
})
return NextResponse.json(patients)
} catch (error) {
console.error('Error fetching patients:', error)
return NextResponse.json(
{ error: 'Failed to fetch patients' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,146 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/clients/[id] - Get a specific client
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const client = await db.client.findUnique({
where: { id: id },
include: {
patients: {
include: {
ordonnances: {
include: {
fichiers: true
}
}
}
},
ventes: true
}
})
if (!client) {
return NextResponse.json(
{ error: 'Client not found' },
{ status: 404 }
)
}
return NextResponse.json(client)
} catch (error) {
console.error('Error fetching client:', error)
return NextResponse.json(
{ error: 'Failed to fetch client' },
{ status: 500 }
)
}
}
// PUT /api/clients/[id] - Update a client
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const {
nom,
prenom,
email,
telephone,
adresse,
ville,
codePostal,
dateNaissance,
notes
} = body
// Validate required fields
if (!nom || !prenom || !telephone) {
return NextResponse.json(
{ error: 'Missing required fields: nom, prenom, telephone' },
{ status: 400 }
)
}
// Check if telephone is used by another client
const existingClient = await db.client.findFirst({
where: {
telephone,
NOT: {
id: id
}
}
})
if (existingClient) {
return NextResponse.json(
{ error: 'Un autre client utilise déjà ce numéro de téléphone' },
{ status: 400 }
)
}
const client = await db.client.update({
where: { id: id },
data: {
nom,
prenom,
email: email || null,
telephone,
adresse: adresse || null,
ville: ville || null,
codePostal: codePostal || null,
dateNaissance: dateNaissance ? new Date(dateNaissance) : null,
notes: notes || null
}
})
return NextResponse.json(client)
} catch (error) {
console.error('Error updating client:', error)
return NextResponse.json(
{ error: 'Failed to update client' },
{ status: 500 }
)
}
}
// DELETE /api/clients/[id] - Delete a client
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Check if client has sales
const salesCount = await db.vente.count({
where: { clientId: id }
})
if (salesCount > 0) {
return NextResponse.json(
{ error: 'Impossible de supprimer un client qui a des ventes associées' },
{ status: 400 }
)
}
await db.client.delete({
where: { id: id }
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting client:', error)
return NextResponse.json(
{ error: 'Failed to delete client' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,89 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/clients - Get all clients
export async function GET() {
try {
const clients = await db.client.findMany({
include: {
patients: {
include: {
ordonnances: true
}
}
},
orderBy: {
createdAt: 'desc'
}
})
return NextResponse.json(clients)
} catch (error) {
console.error('Error fetching clients:', error)
return NextResponse.json(
{ error: 'Failed to fetch clients' },
{ status: 500 }
)
}
}
// POST /api/clients - Create a new client
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
nom,
prenom,
email,
telephone,
adresse,
ville,
codePostal,
dateNaissance,
notes
} = body
// Validate required fields
if (!nom || !prenom || !telephone) {
return NextResponse.json(
{ error: 'Missing required fields: nom, prenom, telephone' },
{ status: 400 }
)
}
// Check if telephone already exists
const existingClient = await db.client.findUnique({
where: { telephone }
})
if (existingClient) {
return NextResponse.json(
{ error: 'Un client avec ce numéro de téléphone existe déjà' },
{ status: 400 }
)
}
const client = await db.client.create({
data: {
nom,
prenom,
email: email || null,
telephone,
adresse: adresse || null,
ville: ville || null,
codePostal: codePostal || null,
dateNaissance: dateNaissance ? new Date(dateNaissance) : null,
notes: notes || null
}
})
return NextResponse.json(client, { status: 201 })
} catch (error) {
console.error('Error creating client:', error)
return NextResponse.json(
{ error: 'Failed to create client' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { unlink } from 'fs/promises'
import path from 'path'
// DELETE /api/fichiers/[id] - Delete a file
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Get file info
const fichier = await db.fichier.findUnique({
where: { id: id },
})
if (!fichier) {
return NextResponse.json(
{ error: 'File not found' },
{ status: 404 }
)
}
// Delete from database
await db.fichier.delete({
where: { id: id },
})
// Try to delete the file from disk
try {
const filePath = path.join(process.cwd(), 'public', fichier.url)
await unlink(filePath)
} catch (error) {
// File might not exist, but that's okay
console.warn('File not found on disk:', fichier.url)
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting fichier:', error)
return NextResponse.json(
{ error: 'Failed to delete file' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/fournisseurs/[id]/factures - Get all purchase invoices for a supplier
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Check if supplier exists
const supplier = await db.fournisseur.findUnique({
where: { id: id },
})
if (!supplier) {
return NextResponse.json(
{ error: 'Supplier not found' },
{ status: 404 }
)
}
// Get all invoices for this supplier
const factures = await db.factureAchat.findMany({
where: {
fournisseurId: id,
},
orderBy: {
date: 'desc',
},
})
return NextResponse.json(factures)
} catch (error) {
console.error('Error fetching supplier invoices:', error)
return NextResponse.json(
{ error: 'Failed to fetch supplier invoices' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,149 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/fournisseurs/[id] - Get a single supplier
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const supplier = await db.fournisseur.findUnique({
where: { id: id },
include: {
_count: {
select: {
produits: true,
facturesAchat: true,
},
},
},
})
if (!supplier) {
return NextResponse.json(
{ error: 'Supplier not found' },
{ status: 404 }
)
}
return NextResponse.json(supplier)
} catch (error) {
console.error('Error fetching supplier:', error)
return NextResponse.json(
{ error: 'Failed to fetch supplier' },
{ status: 500 }
)
}
}
// PUT /api/fournisseurs/[id] - Update a supplier
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
// Check if supplier exists
const existingSupplier = await db.fournisseur.findUnique({
where: { id: id },
})
if (!existingSupplier) {
return NextResponse.json(
{ error: 'Supplier not found' },
{ status: 404 }
)
}
// Validate required fields
if (!body.nom) {
return NextResponse.json(
{ error: 'Missing required field: nom' },
{ status: 400 }
)
}
const supplier = await db.fournisseur.update({
where: { id: id },
data: {
nom: body.nom,
contact: body.contact !== undefined ? body.contact : existingSupplier.contact,
email: body.email !== undefined ? body.email : existingSupplier.email,
telephone: body.telephone !== undefined ? body.telephone : existingSupplier.telephone,
adresse: body.adresse !== undefined ? body.adresse : existingSupplier.adresse,
ville: body.ville !== undefined ? body.ville : existingSupplier.ville,
codePostal: body.codePostal !== undefined ? body.codePostal : existingSupplier.codePostal,
notes: body.notes !== undefined ? body.notes : existingSupplier.notes,
actif: body.actif !== undefined ? body.actif : existingSupplier.actif,
},
})
return NextResponse.json(supplier)
} catch (error) {
console.error('Error updating supplier:', error)
return NextResponse.json(
{ error: 'Failed to update supplier' },
{ status: 500 }
)
}
}
// DELETE /api/fournisseurs/[id] - Delete a supplier
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Check if supplier exists
const existingSupplier = await db.fournisseur.findUnique({
where: { id: id },
include: {
_count: {
select: {
produits: true,
facturesAchat: true,
},
},
},
})
if (!existingSupplier) {
return NextResponse.json(
{ error: 'Supplier not found' },
{ status: 404 }
)
}
// Check if supplier has associated products or invoices
if (existingSupplier._count.produits > 0) {
return NextResponse.json(
{ error: 'Cannot delete supplier with associated products. Please remove or reassign products first.' },
{ status: 400 }
)
}
if (existingSupplier._count.facturesAchat > 0) {
return NextResponse.json(
{ error: 'Cannot delete supplier with associated purchase invoices. Please archive the supplier instead.' },
{ status: 400 }
)
}
// Delete the supplier
await db.fournisseur.delete({
where: { id: id },
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting supplier:', error)
return NextResponse.json(
{ error: 'Failed to delete supplier' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/fournisseurs - Get all suppliers
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const actif = searchParams.get('actif')
const where: any = {}
if (actif !== null && actif !== 'all') {
where.actif = actif === 'true'
}
const fournisseurs = await db.fournisseur.findMany({
where,
orderBy: {
nom: 'asc',
},
include: {
_count: {
select: {
produits: true,
facturesAchat: true,
},
},
},
})
return NextResponse.json(fournisseurs)
} catch (error) {
console.error('Error fetching fournisseurs:', error)
return NextResponse.json(
{ error: 'Failed to fetch suppliers' },
{ status: 500 }
)
}
}
// POST /api/fournisseurs - Create a new supplier
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate required fields
if (!body.nom) {
return NextResponse.json(
{ error: 'Missing required field: nom' },
{ status: 400 }
)
}
const fournisseur = await db.fournisseur.create({
data: {
nom: body.nom,
contact: body.contact || null,
email: body.email || null,
telephone: body.telephone || null,
adresse: body.adresse || null,
ville: body.ville || null,
codePostal: body.codePostal || null,
notes: body.notes || null,
actif: body.actif !== undefined ? body.actif : true,
},
})
return NextResponse.json(fournisseur)
} catch (error) {
console.error('Error creating fournisseur:', error)
return NextResponse.json(
{ error: 'Failed to create supplier' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,123 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/ordonnances/[id] - Get a specific ordonnance
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const ordonnance = await db.ordonnance.findUnique({
where: { id: id },
include: {
patient: {
include: {
client: true
}
},
fichiers: true
}
})
if (!ordonnance) {
return NextResponse.json(
{ error: 'Ordonnance not found' },
{ status: 404 }
)
}
return NextResponse.json(ordonnance)
} catch (error) {
console.error('Error fetching ordonnance:', error)
return NextResponse.json(
{ error: 'Failed to fetch ordonnance' },
{ status: 500 }
)
}
}
// PUT /api/ordonnances/[id] - Update an ordonnance
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const {
patientId,
numero,
dateEmission,
medecin,
notes
} = body
// Check if numero is used by another ordonnance
if (numero) {
const existingOrdonnance = await db.ordonnance.findFirst({
where: {
numero,
NOT: {
id: id
}
}
})
if (existingOrdonnance) {
return NextResponse.json(
{ error: 'Une autre ordonnance utilise déjà ce numéro' },
{ status: 400 }
)
}
}
const ordonnance = await db.ordonnance.update({
where: { id: id },
data: {
patientId: patientId || undefined,
numero: numero || undefined,
dateEmission: dateEmission ? new Date(dateEmission) : undefined,
medecin: medecin !== undefined ? medecin : undefined,
notes: notes !== undefined ? notes : undefined
},
include: {
patient: {
include: {
client: true
}
}
}
})
return NextResponse.json(ordonnance)
} catch (error) {
console.error('Error updating ordonnance:', error)
return NextResponse.json(
{ error: 'Failed to update ordonnance' },
{ status: 500 }
)
}
}
// DELETE /api/ordonnances/[id] - Delete an ordonnance
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
await db.ordonnance.delete({
where: { id: id }
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting ordonnance:', error)
return NextResponse.json(
{ error: 'Failed to delete ordonnance' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/ordonnances - Get all ordonnances
export async function GET() {
try {
const ordonnances = await db.ordonnance.findMany({
include: {
patient: {
include: {
client: true
}
},
fichiers: true
},
orderBy: {
dateEmission: 'desc'
}
})
return NextResponse.json(ordonnances)
} catch (error) {
console.error('Error fetching ordonnances:', error)
return NextResponse.json(
{ error: 'Failed to fetch ordonnances' },
{ status: 500 }
)
}
}
// POST /api/ordonnances - Create a new ordonnance
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
patientId,
numero,
dateEmission,
medecin,
notes
} = body
// Validate required fields
if (!patientId || !numero || !dateEmission) {
return NextResponse.json(
{ error: 'Missing required fields: patientId, numero, dateEmission' },
{ status: 400 }
)
}
// Check if patient exists
const patient = await db.patient.findUnique({
where: { id: patientId }
})
if (!patient) {
return NextResponse.json(
{ error: 'Patient not found' },
{ status: 404 }
)
}
// Check if numero already exists
const existingOrdonnance = await db.ordonnance.findUnique({
where: { numero }
})
if (existingOrdonnance) {
return NextResponse.json(
{ error: 'Une ordonnance avec ce numéro existe déjà' },
{ status: 400 }
)
}
const ordonnance = await db.ordonnance.create({
data: {
patientId,
numero,
dateEmission: new Date(dateEmission),
medecin: medecin || null,
notes: notes || null
},
include: {
patient: {
include: {
client: true
}
}
}
})
return NextResponse.json(ordonnance, { status: 201 })
} catch (error) {
console.error('Error creating ordonnance:', error)
return NextResponse.json(
{ error: 'Failed to create ordonnance' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/patients/[id]/ordonnances - Get all ordonnances for a patient
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const ordonnances = await db.ordonnance.findMany({
where: { patientId: id },
include: {
fichiers: true,
patient: {
include: {
client: true
}
}
},
orderBy: {
dateEmission: 'desc'
}
})
return NextResponse.json(ordonnances)
} catch (error) {
console.error('Error fetching ordonnances:', error)
return NextResponse.json(
{ error: 'Failed to fetch ordonnances' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/patients/[id] - Get a specific patient
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const patient = await db.patient.findUnique({
where: { id },
include: {
client: true,
ordonnances: {
include: {
fichiers: true
}
}
}
})
if (!patient) {
return NextResponse.json(
{ error: 'Patient not found' },
{ status: 404 }
)
}
return NextResponse.json(patient)
} catch (error) {
console.error('Error fetching patient:', error)
return NextResponse.json(
{ error: 'Failed to fetch patient' },
{ status: 500 }
)
}
}
// PUT /api/patients/[id] - Update a patient
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const {
clientId,
odSphere,
odCylindre,
odAxe,
ogSphere,
ogCylindre,
ogAxe,
addition,
pd,
hauteur,
notes
} = body
const patient = await db.patient.update({
where: { id },
data: {
clientId: clientId || undefined,
odSphere: odSphere !== undefined ? parseFloat(odSphere) : undefined,
odCylindre: odCylindre !== undefined ? parseFloat(odCylindre) : undefined,
odAxe: odAxe !== undefined ? parseInt(odAxe) : undefined,
ogSphere: ogSphere !== undefined ? parseFloat(ogSphere) : undefined,
ogCylindre: ogCylindre !== undefined ? parseFloat(ogCylindre) : undefined,
ogAxe: ogAxe !== undefined ? parseInt(ogAxe) : undefined,
addition: addition !== undefined ? parseFloat(addition) : undefined,
pd: pd !== undefined ? parseFloat(pd) : undefined,
hauteur: hauteur !== undefined ? parseFloat(hauteur) : undefined,
notes: notes !== undefined ? notes : undefined
},
include: {
client: true,
ordonnances: true
}
})
return NextResponse.json(patient)
} catch (error) {
console.error('Error updating patient:', error)
return NextResponse.json(
{ error: 'Failed to update patient' },
{ status: 500 }
)
}
}
// DELETE /api/patients/[id] - Delete a patient
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
await db.patient.delete({
where: { id }
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting patient:', error)
return NextResponse.json(
{ error: 'Failed to delete patient' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/patients - Get all patients
export async function GET() {
try {
const patients = await db.patient.findMany({
include: {
client: true,
ordonnances: true
},
orderBy: {
dateCreation: 'desc'
}
})
return NextResponse.json(patients)
} catch (error) {
console.error('Error fetching patients:', error)
return NextResponse.json(
{ error: 'Failed to fetch patients' },
{ status: 500 }
)
}
}
// POST /api/patients - Create a new patient
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
clientId,
odSphere,
odCylindre,
odAxe,
ogSphere,
ogCylindre,
ogAxe,
addition,
pd,
hauteur,
notes
} = body
// Validate required field
if (!clientId) {
return NextResponse.json(
{ error: 'Missing required field: clientId' },
{ status: 400 }
)
}
// Check if client exists
const client = await db.client.findUnique({
where: { id: clientId }
})
if (!client) {
return NextResponse.json(
{ error: 'Client not found' },
{ status: 404 }
)
}
const patient = await db.patient.create({
data: {
clientId,
odSphere: odSphere !== undefined ? parseFloat(odSphere) : null,
odCylindre: odCylindre !== undefined ? parseFloat(odCylindre) : null,
odAxe: odAxe !== undefined ? parseInt(odAxe) : null,
ogSphere: ogSphere !== undefined ? parseFloat(ogSphere) : null,
ogCylindre: ogCylindre !== undefined ? parseFloat(ogCylindre) : null,
ogAxe: ogAxe !== undefined ? parseInt(ogAxe) : null,
addition: addition !== undefined ? parseFloat(addition) : null,
pd: pd !== undefined ? parseFloat(pd) : null,
hauteur: hauteur !== undefined ? parseFloat(hauteur) : null,
notes: notes || null
},
include: {
client: true,
ordonnances: true
}
})
return NextResponse.json(patient, { status: 201 })
} catch (error) {
console.error('Error creating patient:', error)
return NextResponse.json(
{ error: 'Failed to create patient' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/pos/clients - Get all clients
export async function GET(request: NextRequest) {
try {
const clients = await db.client.findMany({
orderBy: {
nom: 'asc'
}
})
return NextResponse.json(clients)
} catch (error) {
console.error('Error fetching clients:', error)
return NextResponse.json(
{ error: 'Failed to fetch clients' },
{ status: 500 }
)
}
}
// POST /api/pos/clients - Create a new client
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { nom, prenom, email, telephone } = body
// Validate required fields
if (!nom || !prenom || !telephone) {
return NextResponse.json(
{ error: 'Missing required fields: nom, prenom, telephone' },
{ status: 400 }
)
}
// Check if telephone already exists
const existingClient = await db.client.findUnique({
where: { telephone }
})
if (existingClient) {
return NextResponse.json(
{ error: 'Un client avec ce numéro de téléphone existe déjà' },
{ status: 400 }
)
}
// Create client
const client = await db.client.create({
data: {
nom,
prenom,
email: email || null,
telephone
}
})
return NextResponse.json(client, { status: 201 })
} catch (error) {
console.error('Error creating client:', error)
return NextResponse.json(
{ error: 'Failed to create client' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/pos/products - Get all active products
export async function GET(request: NextRequest) {
try {
const products = await db.produit.findMany({
where: {
actif: true,
stock: {
gt: 0
}
},
orderBy: {
designation: 'asc'
}
})
return NextResponse.json(products)
} catch (error) {
console.error('Error fetching products:', error)
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,165 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { StatutVente } from '@prisma/client'
// GET /api/pos/sales/[id] - Get a specific sale
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const sale = await db.vente.findUnique({
where: { id: id },
include: {
client: true,
employe: true,
lignes: {
include: {
produit: true
}
},
paiements: true,
fichiers: true
}
})
if (!sale) {
return NextResponse.json(
{ error: 'Sale not found' },
{ status: 404 }
)
}
return NextResponse.json(sale)
} catch (error) {
console.error('Error fetching sale:', error)
return NextResponse.json(
{ error: 'Failed to fetch sale' },
{ status: 500 }
)
}
}
// PATCH /api/pos/sales/[id] - Update sale status
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const { statut, notes } = body
// Validate status
if (statut && !Object.values(StatutVente).includes(statut)) {
return NextResponse.json(
{ error: 'Invalid status value' },
{ status: 400 }
)
}
// Check if sale exists
const existingSale = await db.vente.findUnique({
where: { id: id }
})
if (!existingSale) {
return NextResponse.json(
{ error: 'Sale not found' },
{ status: 404 }
)
}
// Update sale
const updatedSale = await db.vente.update({
where: { id: id },
data: {
...(statut && { statut }),
...(notes !== undefined && { notes })
},
include: {
client: true,
lignes: {
include: {
produit: true
}
},
paiements: true
}
})
return NextResponse.json(updatedSale)
} catch (error) {
console.error('Error updating sale:', error)
return NextResponse.json(
{ error: 'Failed to update sale' },
{ status: 500 }
)
}
}
// DELETE /api/pos/sales/[id] - Cancel/Delete a sale
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Check if sale exists
const existingSale = await db.vente.findUnique({
where: { id: id },
include: {
lignes: true
}
})
if (!existingSale) {
return NextResponse.json(
{ error: 'Sale not found' },
{ status: 404 }
)
}
// If sale is already paid, we can't delete it, only cancel
if (existingSale.statut === StatutVente.PAYEE) {
// Update status to ANNULEE
const cancelledSale = await db.vente.update({
where: { id: id },
data: {
statut: StatutVente.ANNULEE
}
})
return NextResponse.json(cancelledSale)
}
// If sale is not paid, we can delete it and restore stock
await db.$transaction(async (tx) => {
// Restore product stock
for (const ligne of existingSale.lignes) {
await tx.produit.update({
where: { id: ligne.produitId },
data: {
stock: {
increment: ligne.quantite
}
}
})
}
// Delete sale (cascade will delete lines and payments)
await tx.vente.delete({
where: { id: id }
})
})
return NextResponse.json({ message: 'Sale deleted successfully' })
} catch (error) {
console.error('Error deleting sale:', error)
return NextResponse.json(
{ error: 'Failed to delete sale' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,192 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { StatutVente, ModePaiement } from '@prisma/client'
// Helper function to generate sale number
async function generateSaleNumber(): Promise<string> {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
// Count sales for this month
const salesThisMonth = await db.vente.count({
where: {
date: {
gte: new Date(year, today.getMonth(), 1),
lt: new Date(year, today.getMonth() + 1, 1)
}
}
})
const sequence = String(salesThisMonth + 1).padStart(4, '0')
return `V${year}${month}${sequence}`
}
// GET /api/pos/sales - Get all sales with relations
export async function GET(request: NextRequest) {
try {
const sales = await db.vente.findMany({
include: {
client: true,
employe: true,
lignes: {
include: {
produit: true
}
},
paiements: true
},
orderBy: {
date: 'desc'
},
take: 50 // Limit to last 50 sales
})
return NextResponse.json(sales)
} catch (error) {
console.error('Error fetching sales:', error)
return NextResponse.json(
{ error: 'Failed to fetch sales' },
{ status: 500 }
)
}
}
// POST /api/pos/sales - Create a new sale
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
clientId,
lignes,
paiements,
remise,
montantHT,
montantTVA,
montantTTC,
notes
} = body
// Validate required fields
if (!lignes || lignes.length === 0) {
return NextResponse.json(
{ error: 'Sale must have at least one line' },
{ status: 400 }
)
}
if (!paiements || paiements.length === 0) {
return NextResponse.json(
{ error: 'Sale must have at least one payment' },
{ status: 400 }
)
}
// Verify product availability
for (const ligne of lignes) {
const product = await db.produit.findUnique({
where: { id: ligne.produitId }
})
if (!product) {
return NextResponse.json(
{ error: `Product ${ligne.produitId} not found` },
{ status: 400 }
)
}
if (product.stock < ligne.quantite) {
return NextResponse.json(
{ error: `Insufficient stock for product ${product.designation}` },
{ status: 400 }
)
}
}
// Generate sale number
const numero = await generateSaleNumber()
// Create sale with transaction
const sale = await db.$transaction(async (tx) => {
// Create sale
const vente = await tx.vente.create({
data: {
numero,
clientId: clientId || null,
statut: StatutVente.PAYEE,
montantHT,
montantTVA,
montantTTC,
remise: remise || 0,
notes: notes || null,
employeId: null // Can be updated with authentication
}
})
// Create sale lines and update product stock
for (const ligne of lignes) {
await tx.ligneVente.create({
data: {
venteId: vente.id,
produitId: ligne.produitId,
quantite: ligne.quantite,
prixUnitaireHT: ligne.prixUnitaireHT,
prixUnitaireTTC: ligne.prixUnitaireTTC,
remise: ligne.remise || 0,
montantHT: ligne.montantHT,
montantTTC: ligne.montantTTC
}
})
// Update product stock
await tx.produit.update({
where: { id: ligne.produitId },
data: {
stock: {
decrement: ligne.quantite
}
}
})
}
// Create payments
for (const paiement of paiements) {
await tx.paiement.create({
data: {
venteId: vente.id,
mode: paiement.mode as ModePaiement,
montant: paiement.montant,
reference: paiement.reference || null,
notes: paiement.notes || null,
employeId: null // Can be updated with authentication
}
})
}
return vente
})
// Fetch the complete sale with relations
const completeSale = await db.vente.findUnique({
where: { id: sale.id },
include: {
client: true,
employe: true,
lignes: {
include: {
produit: true
}
},
paiements: true
}
})
return NextResponse.json(completeSale, { status: 201 })
} catch (error) {
console.error('Error creating sale:', error)
return NextResponse.json(
{ error: 'Failed to create sale' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,218 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
// POST /api/pos/seed - Seed sample data for testing
export async function POST() {
try {
// Create sample clients
const clients = await Promise.all([
db.client.create({
data: {
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@email.com',
telephone: '0612345678',
ville: 'Paris'
}
}),
db.client.create({
data: {
nom: 'Martin',
prenom: 'Marie',
email: 'marie.martin@email.com',
telephone: '0623456789',
ville: 'Lyon'
}
}),
db.client.create({
data: {
nom: 'Bernard',
prenom: 'Pierre',
telephone: '0634567890',
ville: 'Marseille'
}
})
])
// Create sample products
const products = await Promise.all([
// Montures
db.produit.create({
data: {
reference: 'MNT001',
designation: 'Monture Ray-Ban Aviator',
categorie: 'MONTURE',
prixAchatHT: 80,
prixVenteTTC: 150,
tva: 20,
stock: 15,
stockMin: 5,
marque: 'Ray-Ban',
typeMonture: 'COMPLET',
materiau: 'Métal',
couleur: 'Or',
emplacement: 'A1-01'
}
}),
db.produit.create({
data: {
reference: 'MNT002',
designation: 'Monture Oakley Holbrook',
categorie: 'MONTURE',
prixAchatHT: 60,
prixVenteTTC: 120,
tva: 20,
stock: 20,
stockMin: 5,
marque: 'Oakley',
typeMonture: 'COMPLET',
materiau: 'Acétate',
couleur: 'Noir',
emplacement: 'A1-02'
}
}),
db.produit.create({
data: {
reference: 'MNT003',
designation: 'Monture Vogue VO5352S',
categorie: 'MONTURE',
prixAchatHT: 45,
prixVenteTTC: 95,
tva: 20,
stock: 25,
stockMin: 5,
marque: 'Vogue',
typeMonture: 'NATUREL',
materiau: 'Acétate',
couleur: 'Rouge',
emplacement: 'A1-03'
}
}),
// Verres
db.produit.create({
data: {
reference: 'VRG001',
designation: 'Verre Simple Vision 1.5',
categorie: 'VERRE',
prixAchatHT: 15,
prixVenteTTC: 45,
tva: 20,
stock: 100,
stockMin: 20,
typeVerre: 'SIMPLE',
indice: 1.5,
emplacement: 'B1-01'
}
}),
db.produit.create({
data: {
reference: 'VRG002',
designation: 'Verre Bifocal 1.5',
categorie: 'VERRE',
prixAchatHT: 30,
prixVenteTTC: 75,
tva: 20,
stock: 50,
stockMin: 10,
typeVerre: 'BIFOCAL',
indice: 1.5,
emplacement: 'B1-02'
}
}),
db.produit.create({
data: {
reference: 'VRG003',
designation: 'Verre Progressif 1.6',
categorie: 'VERRE',
prixAchatHT: 60,
prixVenteTTC: 150,
tva: 20,
stock: 30,
stockMin: 10,
typeVerre: 'PROGRESSIF',
indice: 1.6,
emplacement: 'B1-03'
}
}),
// Lentilles
db.produit.create({
data: {
reference: 'LEN001',
designation: 'Lentilles Journalières',
categorie: 'LENTILLE',
prixAchatHT: 0.20,
prixVenteTTC: 0.50,
tva: 20,
stock: 500,
stockMin: 100,
emplacement: 'C1-01'
}
}),
db.produit.create({
data: {
reference: 'LEN002',
designation: 'Lentilles Mensuelles',
categorie: 'LENTILLE',
prixAchatHT: 8,
prixVenteTTC: 20,
tva: 20,
stock: 100,
stockMin: 20,
emplacement: 'C1-02'
}
}),
// Accessoires
db.produit.create({
data: {
reference: 'ACC001',
designation: 'Étui Lunettes Luxe',
categorie: 'ACCESSOIRE',
prixAchatHT: 10,
prixVenteTTC: 25,
tva: 20,
stock: 40,
stockMin: 10,
emplacement: 'D1-01'
}
}),
db.produit.create({
data: {
reference: 'ACC002',
designation: 'Kit Nettoyage',
categorie: 'ACCESSOIRE',
prixAchatHT: 3,
prixVenteTTC: 8,
tva: 20,
stock: 80,
stockMin: 20,
emplacement: 'D1-02'
}
}),
db.produit.create({
data: {
reference: 'ACC003',
designation: 'Chaînette Lunettes',
categorie: 'ACCESSOIRE',
prixAchatHT: 2,
prixVenteTTC: 6,
tva: 20,
stock: 60,
stockMin: 15,
emplacement: 'D1-03'
}
})
])
return NextResponse.json({
message: 'Sample data seeded successfully',
clients: clients.length,
products: products.length
})
} catch (error) {
console.error('Error seeding data:', error)
return NextResponse.json(
{ error: 'Failed to seed data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/produits/[id]/images - Get all images for a product
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const images = await db.fichier.findMany({
where: {
produitId: id,
type: 'IMAGE_PRODUIT',
},
orderBy: {
createdAt: 'desc',
},
})
return NextResponse.json(images)
} catch (error) {
console.error('Error fetching product images:', error)
return NextResponse.json(
{ error: 'Failed to fetch images' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,244 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/produits/[id] - Get a single product
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const produit = await db.produit.findUnique({
where: { id: id },
include: {
fournisseur: {
select: {
id: true,
nom: true,
},
},
fichiers: {
where: {
type: 'IMAGE_PRODUIT',
},
},
},
})
if (!produit) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
return NextResponse.json({
...produit,
fournisseurNom: produit.fournisseur?.nom || null,
fournisseurId: produit.fournisseur?.id || null,
images: produit.fichiers || [],
})
} catch (error) {
console.error('Error fetching produit:', error)
return NextResponse.json(
{ error: 'Failed to fetch product' },
{ status: 500 }
)
}
}
// PUT /api/produits/[id] - Update a product (full update)
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
// Check if product exists
const existingProduit = await db.produit.findUnique({
where: { id: id },
})
if (!existingProduit) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
// Check if reference is being changed and if it already exists
if (body.reference && body.reference !== existingProduit.reference) {
const referenceExists = await db.produit.findUnique({
where: { reference: body.reference },
})
if (referenceExists) {
return NextResponse.json(
{ error: 'Un produit avec cette référence existe déjà' },
{ status: 400 }
)
}
}
// Check if barcode is being changed and if it already exists
if (body.codeBarre && body.codeBarre !== existingProduit.codeBarre) {
const barcodeExists = await db.produit.findUnique({
where: { codeBarre: body.codeBarre },
})
if (barcodeExists) {
return NextResponse.json(
{ error: 'Un produit avec ce code-barres existe déjà' },
{ status: 400 }
)
}
}
const produit = await db.produit.update({
where: { id: id },
data: {
...(body.reference && { reference: body.reference }),
...(body.designation && { designation: body.designation }),
...(body.categorie && { categorie: body.categorie }),
...(body.fournisseurId !== undefined && { fournisseurId: body.fournisseurId || null }),
...(body.prixAchatHT !== undefined && { prixAchatHT: parseFloat(body.prixAchatHT) }),
...(body.prixVenteTTC !== undefined && { prixVenteTTC: parseFloat(body.prixVenteTTC) }),
...(body.tva !== undefined && { tva: parseFloat(body.tva) }),
...(body.stock !== undefined && { stock: parseInt(body.stock) }),
...(body.stockMin !== undefined && { stockMin: parseInt(body.stockMin) }),
...(body.emplacement !== undefined && { emplacement: body.emplacement || null }),
...(body.marque !== undefined && { marque: body.marque || null }),
...(body.typeMonture !== undefined && { typeMonture: body.typeMonture || null }),
...(body.typeVerre !== undefined && { typeVerre: body.typeVerre || null }),
...(body.indice !== undefined && { indice: body.indice ? parseFloat(body.indice) : null }),
...(body.materiau !== undefined && { materiau: body.materiau || null }),
...(body.couleur !== undefined && { couleur: body.couleur || null }),
...(body.dimensions !== undefined && { dimensions: body.dimensions || null }),
...(body.description !== undefined && { description: body.description || null }),
...(body.codeBarre !== undefined && { codeBarre: body.codeBarre || null }),
...(body.actif !== undefined && { actif: body.actif }),
},
include: {
fournisseur: {
select: {
id: true,
nom: true,
},
},
},
})
return NextResponse.json({
...produit,
fournisseurNom: produit.fournisseur?.nom || null,
fournisseurId: produit.fournisseur?.id || null,
})
} catch (error) {
console.error('Error updating produit:', error)
return NextResponse.json(
{ error: 'Failed to update product' },
{ status: 500 }
)
}
}
// PATCH /api/produits/[id] - Partial update of a product
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
// Check if product exists
const existingProduit = await db.produit.findUnique({
where: { id: id },
})
if (!existingProduit) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
const produit = await db.produit.update({
where: { id: id },
data: body,
include: {
fournisseur: {
select: {
id: true,
nom: true,
},
},
},
})
return NextResponse.json({
...produit,
fournisseurNom: produit.fournisseur?.nom || null,
fournisseurId: produit.fournisseur?.id || null,
})
} catch (error) {
console.error('Error patching produit:', error)
return NextResponse.json(
{ error: 'Failed to update product' },
{ status: 500 }
)
}
}
// DELETE /api/produits/[id] - Delete a product
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// Check if product exists
const existingProduit = await db.produit.findUnique({
where: { id: id },
include: {
ligneVente: true,
ligneFacture: true,
},
})
if (!existingProduit) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
// Check if product is used in sales or purchases
if (existingProduit.ligneVente.length > 0 || existingProduit.ligneFacture.length > 0) {
return NextResponse.json(
{ error: 'Ce produit est utilisé dans des ventes ou des achats et ne peut pas être supprimé' },
{ status: 400 }
)
}
// Delete associated files first
await db.fichier.deleteMany({
where: { produitId: id },
})
// Delete the product
await db.produit.delete({
where: { id: id },
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error deleting produit:', error)
return NextResponse.json(
{ error: 'Failed to delete product' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,164 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/produits - Get all products with filters
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const search = searchParams.get('search')
const categorie = searchParams.get('categorie')
const stockStatus = searchParams.get('stockStatus')
const actif = searchParams.get('actif')
const where: any = {}
if (search) {
where.OR = [
{ reference: { contains: search, mode: 'insensitive' } },
{ designation: { contains: search, mode: 'insensitive' } },
{ marque: { contains: search, mode: 'insensitive' } },
{ codeBarre: { contains: search, mode: 'insensitive' } },
]
}
if (categorie && categorie !== 'all') {
where.categorie = categorie
}
if (actif !== null && actif !== 'all') {
where.actif = actif === 'true'
}
let produits = await db.produit.findMany({
where,
include: {
fournisseur: {
select: {
id: true,
nom: true,
},
},
fichiers: {
where: {
type: 'IMAGE_PRODUIT',
},
},
},
orderBy: {
createdAt: 'desc',
},
})
// Apply stock status filter (client-side for complex logic)
if (stockStatus && stockStatus !== 'all') {
produits = produits.filter((produit) => {
if (stockStatus === 'low') return produit.stock < produit.stockMin
if (stockStatus === 'ok') return produit.stock >= produit.stockMin
if (stockStatus === 'out') return produit.stock <= 0
return true
})
}
// Transform data to match frontend expectations
const transformedProduits = produits.map((produit) => ({
...produit,
fournisseurNom: produit.fournisseur?.nom || null,
fournisseurId: produit.fournisseur?.id || null,
images: produit.fichiers || [],
}))
return NextResponse.json(transformedProduits)
} catch (error) {
console.error('Error fetching produits:', error)
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
)
}
}
// POST /api/produits - Create a new product
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate required fields
if (!body.reference || !body.designation || !body.categorie) {
return NextResponse.json(
{ error: 'Missing required fields: reference, designation, categorie' },
{ status: 400 }
)
}
// Check if reference already exists
const existingProduit = await db.produit.findUnique({
where: { reference: body.reference },
})
if (existingProduit) {
return NextResponse.json(
{ error: 'Un produit avec cette référence existe déjà' },
{ status: 400 }
)
}
// Check if barcode already exists (if provided)
if (body.codeBarre) {
const existingBarcode = await db.produit.findUnique({
where: { codeBarre: body.codeBarre },
})
if (existingBarcode) {
return NextResponse.json(
{ error: 'Un produit avec ce code-barres existe déjà' },
{ status: 400 }
)
}
}
const produit = await db.produit.create({
data: {
reference: body.reference,
designation: body.designation,
categorie: body.categorie,
fournisseurId: body.fournisseurId || null,
prixAchatHT: parseFloat(body.prixAchatHT) || 0,
prixVenteTTC: parseFloat(body.prixVenteTTC) || 0,
tva: parseFloat(body.tva) || 20,
stock: parseInt(body.stock) || 0,
stockMin: parseInt(body.stockMin) || 5,
emplacement: body.emplacement || null,
marque: body.marque || null,
typeMonture: body.typeMonture || null,
typeVerre: body.typeVerre || null,
indice: body.indice ? parseFloat(body.indice) : null,
materiau: body.materiau || null,
couleur: body.couleur || null,
dimensions: body.dimensions || null,
description: body.description || null,
codeBarre: body.codeBarre || null,
actif: body.actif !== undefined ? body.actif : true,
},
include: {
fournisseur: {
select: {
id: true,
nom: true,
},
},
},
})
return NextResponse.json({
...produit,
fournisseurNom: produit.fournisseur?.nom || null,
fournisseurId: produit.fournisseur?.id || null,
})
} catch (error) {
console.error('Error creating produit:', error)
return NextResponse.json(
{ error: 'Failed to create product' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { writeFile, mkdir } from 'fs/promises'
import path from 'path'
import { v4 as uuidv4 } from 'uuid'
// POST /api/produits/upload-image - Upload an image for a product
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
const produitId = formData.get('produitId') as string
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}
if (!produitId) {
return NextResponse.json(
{ error: 'No product ID provided' },
{ status: 400 }
)
}
// Check if product exists
const produit = await db.produit.findUnique({
where: { id: produitId },
})
if (!produit) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
)
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: 'Invalid file type. Only JPEG, PNG, WebP, and GIF are allowed' },
{ status: 400 }
)
}
// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024 // 5MB
if (file.size > maxSize) {
return NextResponse.json(
{ error: 'File too large. Maximum size is 5MB' },
{ status: 400 }
)
}
// Read file buffer
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
// Generate unique filename
const fileExtension = path.extname(file.name)
const uniqueFileName = `${uuidv4()}${fileExtension}`
// Create uploads directory if it doesn't exist
const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'products')
await mkdir(uploadDir, { recursive: true })
// Write file to disk
const filePath = path.join(uploadDir, uniqueFileName)
await writeFile(filePath, buffer)
// Save file info to database
const fichier = await db.fichier.create({
data: {
nom: file.name,
type: 'IMAGE_PRODUIT',
url: `/uploads/products/${uniqueFileName}`,
taille: file.size,
mimeType: file.type,
produitId: produitId,
},
})
return NextResponse.json(fichier)
} catch (error) {
console.error('Error uploading image:', error)
return NextResponse.json(
{ error: 'Failed to upload image' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,165 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfYear, endOfYear } from 'date-fns'
export async function GET(request: NextRequest) {
try {
const now = new Date()
// Time ranges
const todayStart = startOfDay(now)
const todayEnd = endOfDay(now)
const weekStart = startOfWeek(now, { weekStartsOn: 1 })
const weekEnd = endOfWeek(now, { weekStartsOn: 1 })
const monthStart = startOfMonth(now)
const monthEnd = endOfMonth(now)
const yearStart = startOfYear(now)
const yearEnd = endOfYear(now)
// Total sales count
const [salesToday, salesWeek, salesMonth, salesYear] = await Promise.all([
db.vente.count({
where: {
date: { gte: todayStart, lte: todayEnd },
statut: 'PAYEE'
}
}),
db.vente.count({
where: {
date: { gte: weekStart, lte: weekEnd },
statut: 'PAYEE'
}
}),
db.vente.count({
where: {
date: { gte: monthStart, lte: monthEnd },
statut: 'PAYEE'
}
}),
db.vente.count({
where: {
date: { gte: yearStart, lte: yearEnd },
statut: 'PAYEE'
}
})
])
// Revenue calculations
const [revenueToday, revenueMonth] = await Promise.all([
db.vente.aggregate({
where: {
date: { gte: todayStart, lte: todayEnd },
statut: 'PAYEE'
},
_sum: {
montantHT: true,
montantTTC: true
}
}),
db.vente.aggregate({
where: {
date: { gte: monthStart, lte: monthEnd },
statut: 'PAYEE'
},
_sum: {
montantHT: true,
montantTTC: true
}
})
])
// Total clients
const totalClients = await db.client.count()
// Top selling products this month
const topProducts = await db.ligneVente.groupBy({
by: ['produitId'],
where: {
vente: {
date: { gte: monthStart, lte: monthEnd },
statut: 'PAYEE'
}
},
_sum: {
quantite: true,
montantTTC: true
},
orderBy: {
_sum: {
quantite: 'desc'
}
},
take: 5
})
// Get product details for top products
const topProductsWithDetails = await Promise.all(
topProducts.map(async (item) => {
const product = await db.produit.findUnique({
where: { id: item.produitId }
})
return {
designation: product?.designation || 'Inconnu',
quantity: item._sum.quantite || 0,
revenue: item._sum.montantTTC || 0
}
})
)
// Low stock items
const lowStockItems = await db.produit.findMany({
where: {
actif: true,
stock: {
lt: db.produit.fields.stockMin
}
},
select: {
id: true,
reference: true,
designation: true,
stock: true,
stockMin: true
},
orderBy: {
stock: 'asc'
},
take: 10
})
// Pending workshop orders (EN_ATTENTE)
const pendingWorkshopOrders = await db.vente.count({
where: {
statut: 'PAYEE',
statutAtelier: 'EN_ATTENTE'
}
})
const dashboardData = {
totalSales: {
today: salesToday,
week: salesWeek,
month: salesMonth,
year: salesYear
},
revenue: {
htToday: revenueToday._sum.montantHT || 0,
ttcToday: revenueToday._sum.montantTTC || 0,
htMonth: revenueMonth._sum.montantHT || 0,
ttcMonth: revenueMonth._sum.montantTTC || 0
},
totalClients,
topProducts: topProductsWithDetails,
lowStockItems,
pendingWorkshopOrders
}
return NextResponse.json(dashboardData)
} catch (error) {
console.error('Error fetching dashboard data:', error)
return NextResponse.json(
{ error: 'Failed to fetch dashboard data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,76 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: NextRequest) {
try {
const products = await db.produit.findMany({
where: {
actif: true
},
include: {
fournisseur: {
select: {
nom: true
}
}
},
orderBy: {
categorie: 'asc'
}
})
const headers = [
'Référence',
'Désignation',
'Catégorie',
'Marque',
'Fournisseur',
'Stock actuel',
'Stock minimum',
'Prix achat HT',
'Prix vente TTC',
'TVA %',
'Valeur stock HT',
'Emplacement',
'Code barre',
'Statut stock'
]
const rows = products.map((product) => {
const stockValue = product.stock * product.prixAchatHT
const stockStatus = product.stock < product.stockMin ? 'FAIBLE' : 'OK'
return [
product.reference,
`"${product.designation}"`,
product.categorie,
product.marque || '',
product.fournisseur?.nom || '',
product.stock.toString(),
product.stockMin.toString(),
product.prixAchatHT.toFixed(2),
product.prixVenteTTC.toFixed(2),
product.tva.toFixed(2),
stockValue.toFixed(2),
product.emplacement || '',
product.codeBarre || '',
stockStatus
].join(',')
})
const csvContent = [headers.join(','), ...rows].join('\n')
return new NextResponse(csvContent, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="inventaire.csv"'
}
})
} catch (error) {
console.error('Error exporting inventory data:', error)
return NextResponse.json(
{ error: 'Failed to export inventory data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: NextRequest) {
try {
const lowStockItems = await db.produit.findMany({
where: {
actif: true,
stock: {
lt: db.produit.fields.stockMin
}
},
include: {
fournisseur: {
select: {
nom: true
}
}
},
orderBy: {
stock: 'asc'
}
})
const headers = [
'Référence',
'Désignation',
'Catégorie',
'Marque',
'Fournisseur',
'Stock actuel',
'Stock minimum',
'Déficit',
'Prix achat HT',
'Valeur à commander',
'Emplacement',
'Code barre'
]
const rows = lowStockItems.map((product) => {
const deficit = product.stockMin - product.stock
const orderValue = deficit * product.prixAchatHT
return [
product.reference,
`"${product.designation}"`,
product.categorie,
product.marque || '',
product.fournisseur?.nom || '',
product.stock.toString(),
product.stockMin.toString(),
deficit.toString(),
product.prixAchatHT.toFixed(2),
orderValue.toFixed(2),
product.emplacement || '',
product.codeBarre || ''
].join(',')
})
const csvContent = [headers.join(','), ...rows].join('\n')
return new NextResponse(csvContent, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="stock_faible.csv"'
}
})
} catch (error) {
console.error('Error exporting low stock data:', error)
return NextResponse.json(
{ error: 'Failed to export low stock data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,127 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfYear, endOfYear, format } from 'date-fns'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const range = searchParams.get('range') || 'month'
const now = new Date()
let startDate: Date
let endDate: Date
switch (range) {
case 'today':
startDate = startOfDay(now)
endDate = endOfDay(now)
break
case 'week':
startDate = startOfWeek(now, { weekStartsOn: 1 })
endDate = endOfWeek(now, { weekStartsOn: 1 })
break
case 'year':
startDate = startOfYear(now)
endDate = endOfYear(now)
break
case 'month':
default:
startDate = startOfMonth(now)
endDate = endOfMonth(now)
break
}
// Get sales with details
const sales = await db.vente.findMany({
where: {
date: { gte: startDate, lte: endDate },
statut: 'PAYEE'
},
include: {
client: {
select: {
nom: true,
prenom: true
}
},
employe: {
select: {
nom: true,
prenom: true
}
},
lignes: {
include: {
produit: {
select: {
reference: true,
designation: true,
categorie: true
}
}
}
},
paiements: true
},
orderBy: {
date: 'desc'
}
})
// Generate CSV content
const headers = [
'Numéro vente',
'Date',
'Client',
'Employé',
'Produits',
'Quantité totale',
'Montant HT',
'Montant TVA',
'Montant TTC',
'Remise',
'Modes de paiement',
'Statut atelier'
]
const rows = sales.map((sale) => {
const products = sale.lignes.map((l) =>
`${l.produit.designation} (${l.produit.categorie})`
).join('; ')
const quantities = sale.lignes.reduce((sum, l) => sum + l.quantite, 0)
const paymentMethods = sale.paiements.map((p) => p.mode).join(', ')
const atelierStatus = sale.statutAtelier
return [
sale.numero,
format(sale.date, 'dd/MM/yyyy HH:mm'),
sale.client ? `${sale.client.prenom} ${sale.client.nom}` : '',
sale.employe ? `${sale.employe.prenom} ${sale.employe.nom}` : '',
`"${products}"`,
quantities.toString(),
sale.montantHT.toFixed(2),
sale.montantTVA.toFixed(2),
sale.montantTTC.toFixed(2),
sale.remise.toFixed(2),
paymentMethods,
atelierStatus
].join(',')
})
const csvContent = [headers.join(','), ...rows].join('\n')
// Return as CSV file
return new NextResponse(csvContent, {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': `attachment; filename="ventes_${format(now, 'yyyy-MM-dd')}.csv"`
}
})
} catch (error) {
console.error('Error exporting sales data:', error)
return NextResponse.json(
{ error: 'Failed to export sales data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,114 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
export async function GET(request: NextRequest) {
try {
// Stock valuation by category
const stockValuationRaw = await db.produit.groupBy({
by: ['categorie'],
where: {
actif: true
},
_sum: {
stock: true
},
_count: true
})
// Calculate value for each category
const stockValuationByCategory = await Promise.all(
stockValuationRaw.map(async (item) => {
const products = await db.produit.findMany({
where: {
categorie: item.categorie,
actif: true
},
select: {
stock: true,
prixAchatHT: true
}
})
const totalValue = products.reduce((sum, p) => sum + (p.stock * p.prixAchatHT), 0)
return {
category: item.categorie,
value: totalValue,
count: item._count
}
})
)
const totalValue = stockValuationByCategory.reduce((sum, item) => sum + item.value, 0)
// Low stock items
const lowStockItems = await db.produit.findMany({
where: {
actif: true,
stock: {
lt: db.produit.fields.stockMin
}
},
select: {
id: true,
reference: true,
designation: true,
categorie: true,
stock: true,
stockMin: true,
prixAchatHT: true
},
orderBy: {
stock: 'asc'
}
})
const lowStockItemsWithValue = lowStockItems.map((item) => ({
...item,
value: item.stock * item.prixAchatHT
}))
// Category breakdown
const categoryBreakdown = await Promise.all(
['MONTURE', 'VERRE', 'LENTILLE', 'ACCESSOIRE'].map(async (category) => {
const [totalProducts, activeProducts] = await Promise.all([
db.produit.count({ where: { categorie: category } }),
db.produit.count({ where: { categorie: category, actif: true } })
])
const products = await db.produit.findMany({
where: { categorie: category, actif: true },
select: { stock: true, prixAchatHT: true }
})
const totalStock = products.reduce((sum, p) => sum + p.stock, 0)
const stockValue = products.reduce((sum, p) => sum + (p.stock * p.prixAchatHT), 0)
return {
category,
totalProducts,
activeProducts,
totalStock,
stockValue
}
})
)
const inventoryData = {
stockValuation: {
totalValue,
byCategory: stockValuationByCategory
},
lowStockItems: lowStockItemsWithValue,
categoryBreakdown
}
return NextResponse.json(inventoryData)
} catch (error) {
console.error('Error fetching inventory data:', error)
return NextResponse.json(
{ error: 'Failed to fetch inventory data' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,184 @@
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, startOfYear, endOfYear, format } from 'date-fns'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const range = searchParams.get('range') || 'month'
const now = new Date()
let startDate: Date
let endDate: Date
let dateFormat: string
switch (range) {
case 'today':
startDate = startOfDay(now)
endDate = endOfDay(now)
dateFormat = 'HH:mm'
break
case 'week':
startDate = startOfWeek(now, { weekStartsOn: 1 })
endDate = endOfWeek(now, { weekStartsOn: 1 })
dateFormat = 'dd/MM'
break
case 'year':
startDate = startOfYear(now)
endDate = endOfYear(now)
dateFormat = 'MMM'
break
case 'month':
default:
startDate = startOfMonth(now)
endDate = endOfMonth(now)
dateFormat = 'dd/MM'
break
}
// Sales by date
const salesByDate = await db.vente.findMany({
where: {
date: { gte: startDate, lte: endDate },
statut: 'PAYEE'
},
select: {
date: true,
montantHT: true,
montantTTC: true
},
orderBy: {
date: 'asc'
}
})
// Group sales by date
const salesByDateGrouped = salesByDate.reduce((acc, sale) => {
const dateKey = format(sale.date, dateFormat)
const existing = acc.find((item) => item.date === dateKey)
if (existing) {
existing.sales += 1
existing.revenue += sale.montantTTC
} else {
acc.push({
date: dateKey,
sales: 1,
revenue: sale.montantTTC
})
}
return acc
}, [] as { date: string; sales: number; revenue: number }[])
// Sales by category (via products)
const salesByCategoryRaw = await db.ligneVente.groupBy({
by: ['produitId'],
where: {
vente: {
date: { gte: startDate, lte: endDate },
statut: 'PAYEE'
}
},
_sum: {
quantite: true,
montantTTC: true
}
})
// Get product categories and group
const salesByCategoryMap = new Map<string, { count: number; revenue: number }>()
for (const item of salesByCategoryRaw) {
const product = await db.produit.findUnique({
where: { id: item.produitId },
select: { categorie: true }
})
if (product) {
const existing = salesByCategoryMap.get(product.categorie) || { count: 0, revenue: 0 }
salesByCategoryMap.set(product.categorie, {
count: existing.count + (item._sum.quantite || 0),
revenue: existing.revenue + (item._sum.montantTTC || 0)
})
}
}
const salesByCategory = Array.from(salesByCategoryMap.entries()).map(([category, data]) => ({
category,
count: data.count,
revenue: data.revenue
}))
// Sales by employee
const salesByEmployeeRaw = await db.vente.groupBy({
by: ['employeId'],
where: {
date: { gte: startDate, lte: endDate },
statut: 'PAYEE',
employeId: { not: null }
},
_count: true,
_sum: {
montantTTC: true
}
})
const salesByEmployee = await Promise.all(
salesByEmployeeRaw.map(async (item) => {
const employee = await db.employe.findUnique({
where: { id: item.employeId! },
select: { nom: true, prenom: true }
})
return {
employee: employee ? `${employee.prenom} ${employee.nom}` : 'Inconnu',
sales: item._count,
revenue: item._sum.montantTTC || 0
}
})
)
// Sales by payment method
const salesByPaymentMethodRaw = await db.paiement.groupBy({
by: ['mode'],
where: {
date: { gte: startDate, lte: endDate },
vente: {
statut: 'PAYEE'
}
},
_count: true,
_sum: {
montant: true
}
})
const paymentMethodLabels: Record<string, string> = {
ESPECES: 'Espèces',
CARTE: 'Carte bancaire',
CHEQUE: 'Chèque',
VIREMENT: 'Virement',
BON_CAISSE: 'Bon de caisse'
}
const salesByPaymentMethod = salesByPaymentMethodRaw.map((item) => ({
method: paymentMethodLabels[item.mode] || item.mode,
count: item._count,
amount: item._sum.montant || 0
}))
const salesData = {
salesByDate: salesByDateGrouped,
salesByCategory,
salesByEmployee,
salesByPaymentMethod
}
return NextResponse.json(salesData)
} catch (error) {
console.error('Error fetching sales data:', error)
return NextResponse.json(
{ error: 'Failed to fetch sales data' },
{ status: 500 }
)
}
}

5
src/app/api/route.ts Normal file
View File

@@ -0,0 +1,5 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello, world!" });
}

122
src/app/globals.css Normal file
View File

@@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

53
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,53 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Toaster } from "@/components/ui/toaster";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Z.ai Code Scaffold - AI-Powered Development",
description: "Modern Next.js scaffold optimized for AI-powered development with Z.ai. Built with TypeScript, Tailwind CSS, and shadcn/ui.",
keywords: ["Z.ai", "Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "AI development", "React"],
authors: [{ name: "Z.ai Team" }],
icons: {
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
},
openGraph: {
title: "Z.ai Code Scaffold",
description: "AI-powered development with modern React stack",
url: "https://chat.z.ai",
siteName: "Z.ai",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Z.ai Code Scaffold",
description: "AI-powered development with modern React stack",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
>
{children}
<Toaster />
</body>
</html>
);
}

378
src/app/page.tsx Normal file
View File

@@ -0,0 +1,378 @@
'use client'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Users,
Package,
Truck,
ShoppingCart,
FileText,
BarChart3,
Eye,
LayoutDashboard,
Wrench
} from 'lucide-react'
import POSModule from '@/components/pos/POSModule'
import { ProduitListe } from '@/components/products/ProduitListe'
import { ClientList } from '@/components/clients/client-list'
import AtelierModule from '@/components/atelier/AtelierModule'
import { SupplierList } from '@/components/suppliers/SupplierList'
import PurchaseModule from '@/components/purchases/PurchaseModule'
import ReportsModule from '@/components/reports/ReportsModule'
type Module = 'HOME' | 'CLIENTS' | 'PRODUITS' | 'FOURNISSEURS' | 'ACHATS' | 'VENTE' | 'RAPPORTS' | 'ATELIER'
interface ModuleCard {
id: Module
title: string
description: string
icon: React.ReactNode
badge?: string
color: string
}
const modules: ModuleCard[] = [
{
id: 'CLIENTS',
title: 'Gestion Clients',
description: 'Fiches clients, mesures de vision, ordonnances',
icon: <Users className="h-8 w-8" />,
color: 'bg-blue-500'
},
{
id: 'PRODUITS',
title: 'Gestion Produits',
description: 'Catalogue, stock, images, QR codes',
icon: <Package className="h-8 w-8" />,
badge: 'Alertes',
color: 'bg-emerald-500'
},
{
id: 'FOURNISSEURS',
title: 'Fournisseurs',
description: 'Gestion des fournisseurs et contacts',
icon: <Truck className="h-8 w-8" />,
color: 'bg-orange-500'
},
{
id: 'ACHATS',
title: 'Achats & Stock',
description: 'Réception, factures fournisseurs, entrées stock',
icon: <ShoppingCart className="h-8 w-8" />,
color: 'bg-purple-500'
},
{
id: 'VENTE',
title: 'Point de Vente',
description: 'Encaissement, facturation, POS',
icon: <ShoppingCart className="h-8 w-8" />,
badge: 'Actif',
color: 'bg-green-500'
},
{
id: 'ATELIER',
title: 'Atelier',
description: 'Montage de lunettes, commandes en cours',
icon: <Wrench className="h-8 w-8" />,
badge: 'En cours',
color: 'bg-amber-500'
},
{
id: 'RAPPORTS',
title: 'Rapports',
description: 'Statistiques, exports Excel/CSV/PDF',
icon: <BarChart3 className="h-8 w-8" />,
color: 'bg-cyan-500'
}
]
export default function Home() {
const [currentModule, setCurrentModule] = useState<Module>('HOME')
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>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{modules.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"
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>
{module.badge && (
<Badge variant="secondary" className="text-xs">
{module.badge}
</Badge>
)}
</div>
<CardTitle className="text-lg">{module.title}</CardTitle>
<CardDescription className="text-sm">
{module.description}
</CardDescription>
</CardHeader>
</Card>
))}
</div>
</div>
)
}
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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</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>
)
}
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="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">
<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>
</div>
</div>
<div className="flex items-center gap-4">
<Badge variant="outline" className="text-sm">
v1.0.0
</Badge>
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
OP
</div>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-4 py-8">
{renderModule()}
</main>
<footer className="bg-white border-t mt-auto">
<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>
<span>Version 1.0.0</span>
</div>
</div>
</div>
</footer>
</div>
)
}