<?php

/**

 * Fichier  : api/orders.php

 * Auteur   : KREMER Régis

 * Version  : 2.6.80

 * Date     : 2026-07-17

 * Description : API REST — CRUD des bons de commande fournisseurs

 *

 * Changelog :

 *   v2.2.0 (2026-02-21) — Création : bons de commande avec statuts draft/sent/partial/received/cancelled
 *   v2.6.80 (2026-07-17) — Brouillons mobiles incomplets et reprenables, révision optimiste, revue serveur des réceptions et validation stricte des quantités.

 *

 * Statuts :

 *   draft     → brouillon, éditable

 *   sent      → envoyé au fournisseur

 *   partial   → réception partielle

 *   received  → entièrement reçu (génère des mouvements 'entry')

 *   cancelled → annulé

 */

require_once dirname(__DIR__).'/config.php';



// -----------------------------------------------------------------------------

// Compat : certains modules utilisent toBaseQty() pour convertir en unité de base.

// Sur MageStock (stock en JSON), si la fonction n'existe pas, on reste en "as-is".

// -----------------------------------------------------------------------------

if (!function_exists('toBaseQty')) {

    function toBaseQty($product, $unit, $qty) {

        // Delegate to config.php toBaseQuantity() for proper unit conversion

        return toBaseQuantity($product, (string)$unit, (float)$qty);

    }

}



// ----------------------------------------------------------------------------

// Compat : findStockIndex() — retourne l'index d'une ligne stock pour (productId, warehouseId)

// Certains anciens modules peuvent l'avoir dans config.php; sinon on fournit une version locale.

// ----------------------------------------------------------------------------

if (!function_exists('findStockIndex')) {

    function findStockIndex($stock, $productId, $warehouseId) {

        $pid = (int)$productId;

        $wid = (int)$warehouseId;

        foreach (($stock ?? []) as $i => $s) {

            if ((int)($s['productId'] ?? 0) === $pid && (int)($s['warehouseId'] ?? 0) === $wid) {

                return $i;

            }

        }

        return null;

    }

}

/**
 * Valide et normalise les lignes d'un brouillon de commande.
 * Les brouillons incomplets peuvent ne contenir aucune ligne, mais toute ligne présente
 * doit déjà respecter le contrat métier historique.
 */
function normalizeOrderDraftLines($lines, $allowEmpty = false) {
    if (!is_array($lines)) jsonError('Format de lignes invalide.');
    if (!$allowEmpty && empty($lines)) jsonError('Au moins une ligne requise.');

    $normalized = [];
    foreach ($lines as $i => $line) {
        $productId = (int)($line['productId'] ?? 0);
        $warehouseId = (int)($line['warehouseId'] ?? 0);
        $quantity = (float)($line['quantity'] ?? 0);
        if ($productId <= 0) jsonError("Ligne $i : productId manquant.");
        if ($warehouseId <= 0) jsonError("Ligne $i : warehouseId manquant.");
        if ($quantity <= 0) jsonError("Ligne $i : quantité invalide.");
        if (!userCanAccessWarehouse($warehouseId)) jsonError("Accès refusé au magasin de la ligne $i.", 403);
        $normalized[] = [
            'productId'        => $productId,
            'warehouseId'      => $warehouseId,
            'quantity'         => $quantity,
            'quantityReceived' => (float)($line['quantityReceived'] ?? 0),
            'unitCost'         => (float)($line['unitCost'] ?? 0),
            'unit'             => trim((string)($line['unit'] ?? 'piece')) ?: 'piece',
            'supplierSku'      => trim((string)($line['supplierSku'] ?? '')),
            'note'             => trim((string)($line['note'] ?? '')),
        ];
    }
    return $normalized;
}

/**
 * Normalise une proposition de réception sans modifier le stock.
 */
function normalizeOrderReceiveLines($order, $receivedLines) {
    if (!is_array($receivedLines) || empty($receivedLines)) jsonError('Lignes de réception requises.');
    $normalized = [];
    $seen = [];
    foreach ($receivedLines as $position => $rl) {
        $lineIndex = (int)($rl['lineIndex'] ?? -1);
        if ($lineIndex < 0 || !isset($order['lines'][$lineIndex])) jsonError("Ligne de réception $position introuvable.");
        if (isset($seen[$lineIndex])) jsonError("La ligne $lineIndex est présente plusieurs fois dans la réception.");
        $seen[$lineIndex] = true;
        $line = $order['lines'][$lineIndex];
        $quantity = (float)($rl['quantityReceived'] ?? 0);
        $remaining = max(0, (float)($line['quantity'] ?? 0) - (float)($line['quantityReceived'] ?? 0));
        if ($quantity <= 0) jsonError("Ligne $lineIndex : quantité reçue invalide.");
        if ($quantity > $remaining + 0.000001) jsonError("Ligne $lineIndex : la quantité reçue dépasse le reliquat de commande.");
        $normalized[] = [
            'lineIndex'        => $lineIndex,
            'quantityReceived' => $quantity,
            'lotNumber'        => trim((string)($rl['lotNumber'] ?? '')),
            'expiryDate'       => trim((string)($rl['expiryDate'] ?? '')),
            'unitCost'         => (float)($rl['unitCost'] ?? $line['unitCost'] ?? 0),
        ];
    }
    return $normalized;
}

function orderReceiveFingerprintPayload($order, $normalizedLines) {
    return [
        'id' => (string)($order['id'] ?? ''),
        'status' => (string)($order['status'] ?? ''),
        'validationStatus' => (string)($order['validationStatus'] ?? 'pending'),
        'lines' => array_map(static fn($line) => [
            'productId' => (int)($line['productId'] ?? 0),
            'warehouseId' => (int)($line['warehouseId'] ?? 0),
            'quantity' => (float)($line['quantity'] ?? 0),
            'quantityReceived' => (float)($line['quantityReceived'] ?? 0),
            'unitCost' => (float)($line['unitCost'] ?? 0),
            'unit' => (string)($line['unit'] ?? 'piece'),
        ], $order['lines'] ?? []),
        'receive' => $normalizedLines,
    ];
}

function orderReceiveReviewToken($order, $normalizedLines) {
    $payload = json_encode(orderReceiveFingerprintPayload($order, $normalizedLines), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    return hash_hmac('sha256', $payload ?: '', csrfToken());
}

startSession(); requireAuth();

header('Content-Type: application/json; charset=utf-8');



// --- Robust JSON error handling (prevents empty/invalid responses on fatal errors) ---

// Convert PHP warnings/notices into exceptions so they are caught by the try/catch below.

set_error_handler(function($severity, $message, $file, $line) {

    if (!(error_reporting() & $severity)) return false;

    throw new ErrorException($message, 0, $severity, $file, $line);

});



// If a fatal error happens (e.g. undefined function), return JSON instead of an empty body.

register_shutdown_function(function() {

    $err = error_get_last();

    if (!$err) return;

    $fatal = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR];

    if (!in_array($err['type'] ?? 0, $fatal, true)) return;

    if (headers_sent()) return;

    http_response_code(500);

    header('Content-Type: application/json; charset=utf-8');

    echo json_encode([

        'error'   => 'Erreur serveur (fatal)',

        'details' => ($err['message'] ?? 'Erreur inconnue'),

        'file'    => basename($err['file'] ?? ''),

        'line'    => $err['line'] ?? null,

    ], JSON_UNESCAPED_UNICODE);

});



$method = $_SERVER['REQUEST_METHOD'];

$body   = json_decode(file_get_contents('php://input'), true) ?? [];



if ($method !== 'GET') csrfCheck();



// Write operations restricted to admin

if (in_array($method, ['POST','PUT','DELETE'])) requireAdmin();



try {

    switch ($method) {



        // ── GET : liste ou détail ────────────────────────────────────────────

        case 'GET':

            $orders = loadData('orders');

            // Filter by warehouse permissions

            $allowedWh = getUserAllowedWarehouses();

            if ($allowedWh !== null) {

                $orders = array_values(array_filter($orders, function($o) use ($allowedWh) {

                    // An order may span several warehouses; check if any line belongs to an allowed warehouse

                    foreach (($o['lines'] ?? []) as $line) {

                        if (in_array((int)($line['warehouseId'] ?? 0), $allowedWh)) return true;

                    }

                    return false;

                }));

            }



            if (isset($_GET['id'])) {

                $id = $_GET['id'];

                $found = null;

                foreach ($orders as $o) { if ((string)($o['id'] ?? '') === (string)$id) { $found = $o; break; } }

                if (!$found) jsonError('Bon de commande introuvable.', 404);

                jsonResponse($found);

            }



            // Optional status filter

            $status = $_GET['status'] ?? '';

            if ($status) $orders = array_values(array_filter($orders, fn($o) => ($o['status'] ?? '') === $status));



            // Sort newest first

            usort($orders, fn($a, $b) => strcmp($b['createdAt'] ?? '', $a['createdAt'] ?? ''));

            jsonResponse($orders);



        // ── POST : créer un bon de commande ──────────────────────────────────

        case 'POST':

            $supplierId = $body['supplierId'] ?? null;
            $lines      = $body['lines'] ?? [];
            $note       = trim((string)($body['note'] ?? ''));
            $ref        = trim((string)($body['ref'] ?? ''));
            $draftMode  = !empty($body['draftMode']);
            $draftWarehouseId = (int)($body['draftWarehouseId'] ?? $body['warehouseId'] ?? 0);
            $draftStep = max(1, min(4, (int)($body['draftStep'] ?? 1)));

            if (!$draftMode && !$supplierId) jsonError('supplierId requis.');
            if ($draftWarehouseId > 0 && !userCanAccessWarehouse($draftWarehouseId)) {
                jsonError('Accès refusé au magasin du brouillon.', 403);
            }
            $normalizedLines = normalizeOrderDraftLines($lines, $draftMode);

            // Generate order number: BC-YYYYMMDD-XXXX
            $orderNum = 'BC-' . date('Ymd') . '-' . strtoupper(substr(uniqid(), -4));
            $now = date('c');
            $incomplete = !$supplierId || $draftWarehouseId <= 0 || empty($normalizedLines);

            $order = addItem('orders', [
                'orderNumber' => $orderNum,
                'ref'         => $ref,
                'supplierId'  => $supplierId ?: null,
                'status'      => 'draft',
                'validationStatus' => 'pending',
                'validationNote'   => '',
                'validatedAt'      => null,
                'refusedAt'        => null,
                'storedAt'         => null,
                'events'           => [[
                    't'    => $now,
                    'type' => 'created',
                    'by'   => currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu',
                    'note' => $draftMode ? 'Brouillon mobile créé' : '',
                ]],
                'lines'       => $normalizedLines,
                'note'        => $note,
                'userId'      => currentUser()['id'],
                'userName'    => currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu',
                'sentAt'      => null,
                'receivedAt'  => null,
                'draftWarehouseId' => $draftWarehouseId > 0 ? $draftWarehouseId : null,
                'draftStep' => $draftStep,
                'draftRevision' => 1,
                'draftUpdatedAt' => $now,
                'isIncompleteDraft' => $incomplete,
            ]);

            jsonResponse($order, 201);



        // ── PUT : modifier statut, lignes, ou réceptionner ───────────────────

        case 'PUT':

            $id = $body['id'] ?? null;

            if (!$id) jsonError('id requis.');



            $orders = loadData('orders');

            $idx    = null;

            foreach ($orders as $i => $o) {

                if ((string)($o['id'] ?? '') === (string)$id) { $idx = $i; break; }

            }

            if ($idx === null) jsonError('Bon de commande introuvable.', 404);



            $order  = $orders[$idx];

            $action = $body['action'] ?? 'update';



            // ── Action: change status ──

            if ($action === 'status') {

                $newStatus = $body['status'] ?? '';

                $allowed   = ['draft', 'sent', 'partial', 'received', 'cancelled'];

                if (!in_array($newStatus, $allowed)) jsonError('Statut invalide.');



                // Cannot go back from received/cancelled

                if (in_array($order['status'] ?? '', ['received', 'cancelled'])) {

                    jsonError('Bon de commande clôturé, modification impossible.');

                }

                if ($newStatus === 'sent') {
                    if (empty($order['supplierId'])) jsonError('Le fournisseur doit être renseigné avant envoi.');
                    normalizeOrderDraftLines($order['lines'] ?? [], false);
                }



                $orders[$idx]['status'] = $newStatus;

                if ($newStatus === 'sent') {

                    $orders[$idx]['sentAt'] = date('c');

                    if (empty($orders[$idx]['validationStatus'])) $orders[$idx]['validationStatus'] = 'pending';

                    $orders[$idx]['events'] = $orders[$idx]['events'] ?? [];

                    $orders[$idx]['events'][] = ['t'=>date('c'),'type'=>'sent','by'=>currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu','note'=>''];
                    $orders[$idx]['isIncompleteDraft'] = false;
                    $orders[$idx]['draftStep'] = 4;
                    $orders[$idx]['draftUpdatedAt'] = date('c');

                }

                if ($newStatus === 'received') {

                    $orders[$idx]['receivedAt'] = date('c');

                }

                if ($newStatus === 'cancelled') {

                    $orders[$idx]['events'] = $orders[$idx]['events'] ?? [];

                    $orders[$idx]['events'][] = ['t'=>date('c'),'type'=>'cancelled','by'=>currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu','note'=>trim($body['note'] ?? '')];

                }



                saveData('orders', $orders);

                jsonResponse($orders[$idx]);

            }



            

            // ── Action: approval (admin only) ──

            if ($action === 'approval') {

                requireAdmin();

                $vs = $body['validationStatus'] ?? '';

                if (!in_array($vs, ['pending','approved','refused'])) jsonError('validationStatus invalide.');

                if (($order['status'] ?? '') !== 'sent') jsonError("Validation possible uniquement sur un BC 'Envoyé'.");

                $note = trim($body['note'] ?? '');

                $orders[$idx]['validationStatus'] = $vs;

                $orders[$idx]['validationNote']   = $note;

                if ($vs === 'approved') {

                    $orders[$idx]['validatedAt'] = date('c');

                    $orders[$idx]['refusedAt']   = null;

                } elseif ($vs === 'refused') {

                    $orders[$idx]['refusedAt']   = date('c');

                    $orders[$idx]['validatedAt'] = null;

                }

                $orders[$idx]['events'] = $orders[$idx]['events'] ?? [];

                $orders[$idx]['events'][] = ['t'=>date('c'),'type'=>$vs,'by'=>currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu','note'=>$note];

                saveData('orders', $orders);

                jsonResponse($orders[$idx]);

            }



            // ── Action: store (after received) ──

            if ($action === 'store') {

                requireAdmin();

                if (($order['status'] ?? '') !== 'received') jsonError('Rangement possible uniquement après réception complète.');

                $orders[$idx]['storedAt'] = date('c');

                $orders[$idx]['events'] = $orders[$idx]['events'] ?? [];

                $orders[$idx]['events'][] = ['t'=>date('c'),'type'=>'stored','by'=>currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu','note'=>''];

                saveData('orders', $orders);

                jsonResponse($orders[$idx]);

            }



// ── Action: review a reception without changing stock ──

            if ($action === 'receive_review') {
                if (($order['validationStatus'] ?? 'pending') !== 'approved') {
                    jsonError("Bon de commande non validé. Un admin doit valider avant réception.", 400);
                }
                if (!in_array(($order['status'] ?? ''), ['sent','partial'])) {
                    jsonError("Réception possible uniquement sur un BC 'Envoyé' ou 'Partiel'.", 400);
                }

                $normalizedLines = normalizeOrderReceiveLines($order, $body['lines'] ?? []);
                $products = loadData('products');
                $warehouses = loadData('warehouses');
                $reviewLines = [];
                $allReceived = true;
                $receivedByIndex = [];
                $totalQuantity = 0.0;
                $totalValue = 0.0;
                $warnings = [];

                foreach ($normalizedLines as $line) $receivedByIndex[$line['lineIndex']] = $line;
                foreach (($order['lines'] ?? []) as $lineIndex => $line) {
                    $prepared = $receivedByIndex[$lineIndex] ?? null;
                    $before = (float)($line['quantityReceived'] ?? 0);
                    $preparedQty = (float)($prepared['quantityReceived'] ?? 0);
                    $after = $before + $preparedQty;
                    $ordered = (float)($line['quantity'] ?? 0);
                    if ($after + 0.000001 < $ordered) $allReceived = false;
                    if (!$prepared) continue;

                    $product = findById($products, (int)($line['productId'] ?? 0));
                    $warehouse = findById($warehouses, (int)($line['warehouseId'] ?? 0));
                    if (!empty($product['trackLot']) && empty($prepared['lotNumber'])) {
                        $warnings[] = 'Lot manquant pour ' . ($product['name'] ?? 'un produit suivi par lot') . '.';
                    }
                    if (!empty($product['trackExpiry']) && empty($prepared['expiryDate'])) {
                        $warnings[] = 'Date limite manquante pour ' . ($product['name'] ?? 'un produit suivi par date') . '.';
                    }
                    $lineValue = $preparedQty * (float)$prepared['unitCost'];
                    $totalQuantity += $preparedQty;
                    $totalValue += $lineValue;
                    $reviewLines[] = [
                        'lineIndex' => $lineIndex,
                        'productId' => (int)($line['productId'] ?? 0),
                        'productName' => $product['name'] ?? 'Produit inconnu',
                        'sku' => $product['sku'] ?? '',
                        'image' => $product['image'] ?? '',
                        'warehouseId' => (int)($line['warehouseId'] ?? 0),
                        'warehouseName' => $warehouse['name'] ?? 'Magasin inconnu',
                        'ordered' => $ordered,
                        'receivedBefore' => $before,
                        'receivedNow' => $preparedQty,
                        'receivedAfter' => $after,
                        'remainingAfter' => max(0, $ordered - $after),
                        'unit' => $line['unit'] ?? 'piece',
                        'unitCost' => (float)$prepared['unitCost'],
                        'lineValue' => $lineValue,
                        'lotNumber' => $prepared['lotNumber'],
                        'expiryDate' => $prepared['expiryDate'],
                    ];
                }

                jsonResponse([
                    'orderId' => $order['id'],
                    'orderNumber' => $order['orderNumber'] ?? '',
                    'reviewToken' => orderReceiveReviewToken($order, $normalizedLines),
                    'willClose' => $allReceived,
                    'nextStatus' => $allReceived ? 'received' : 'partial',
                    'lineCount' => count($reviewLines),
                    'totalQuantity' => $totalQuantity,
                    'totalValue' => $totalValue,
                    'warnings' => array_values(array_unique($warnings)),
                    'lines' => $reviewLines,
                ]);
            }

// ── Action: receive lines (partial or full) ──

            if ($action === 'receive') {

                // Require admin validation before any stock entry

                if (($order['validationStatus'] ?? 'pending') !== 'approved') {
                    jsonError("Bon de commande non validé. Un admin doit valider avant réception.", 400);
                }
                if (!in_array(($order['status'] ?? ''), ['sent','partial'])) {
                    jsonError("Réception possible uniquement sur un BC 'Envoyé' ou 'Partiel'.", 400);
                }

                $receivedLines = normalizeOrderReceiveLines($order, $body['lines'] ?? []);
                $reviewToken = trim((string)($body['reviewToken'] ?? ''));
                if ($reviewToken !== '') {
                    $expectedToken = orderReceiveReviewToken($order, $receivedLines);
                    if (!hash_equals($expectedToken, $reviewToken)) {
                        jsonError('La commande ou la proposition de réception a changé depuis la revue. Recommencez la vérification.', 409);
                    }
                }

                $mvToCreate  = [];
                $allReceived = true;

                foreach ($receivedLines as $rl) {
                    $li  = (int)$rl['lineIndex'];
                    $qr  = (float)$rl['quantityReceived'];
                    $line = $order['lines'][$li];
                    $orders[$idx]['lines'][$li]['quantityReceived'] =
                        (float)($order['lines'][$li]['quantityReceived'] ?? 0) + $qr;

                    // Prepare movement entry
                    $mvToCreate[] = [
                        'productId'   => $line['productId'],
                        'warehouseId' => $line['warehouseId'],
                        'quantity'    => $qr,
                        'unit'        => $line['unit'] ?? 'piece',
                        'unitCost'    => (float)$rl['unitCost'],
                        'lotNumber'   => $rl['lotNumber'],
                        'expiryDate'  => $rl['expiryDate'],
                        'orderId'     => $id,
                        'orderNumber' => $order['orderNumber'] ?? '',
                    ];
                }



                // Check if fully received

                foreach ($orders[$idx]['lines'] as $line) {

                    if (($line['quantityReceived'] ?? 0) < $line['quantity']) {

                        $allReceived = false;

                        break;

                    }

                }



                $orders[$idx]['status']     = $allReceived ? 'received' : 'partial';

                $orders[$idx]['events'] = $orders[$idx]['events'] ?? [];

                $orders[$idx]['events'][] = ['t'=>date('c'),'type'=>($allReceived?'received':'partial'),'by'=>currentUser()['name'] ?? currentUser()['username'] ?? 'Inconnu','note'=>count($receivedLines).' ligne(s) réceptionnée(s)'];
                $orders[$idx]['updatedAt'] = date('c');

                if ($allReceived) $orders[$idx]['receivedAt'] = date('c');

                saveData('orders', $orders);



                // Create entry movements for each received line

                $movements = loadData('movements');

                $stock     = loadData('stock');

                $products  = loadData('products');

                $user      = currentUser();

                $now       = date('c');



                foreach ($mvToCreate as $mv) {

                    $pid  = (int)$mv['productId'];

                    $wid  = (int)$mv['warehouseId'];

                    $qty  = (float)$mv['quantity'];

                    $unit = $mv['unit'];

                    $prod = findById($products, $pid);

                    if (!$prod) continue;



                    $baseQty     = toBaseQty($prod, $unit, $qty);

                    $unitCost    = (float)$mv['unitCost'];

                    $baseUnitCost = toBaseUnitCost($prod, $unit, $unitCost);



                    // Update stock

                    $si = findStockIndex($stock, $pid, $wid);

                    if ($si === null) {

                        $lotEntry = !empty($mv['lotNumber'])
                            ? [['lotNumber'=>$mv['lotNumber'],'expiryDate'=>$mv['expiryDate'],'quantity'=>$baseQty,'createdAt'=>$now]]
                            : [];

                        $stock[] = [

                            'id'            => genId(),

                            'productId'     => $pid,

                            'warehouseId'   => $wid,

                            'quantity'      => $baseQty,

                            'maxAuthorized' => $baseQty,

                            'minStock'      => null,

                            'maxStock'      => null,

                            'safetyStock'   => null,

                            'lots'          => $lotEntry,

                            'locations'     => ['default' => ['quantity' => $baseQty, 'lots' => $lotEntry]],

                            'lastMovementAt'=> $now,

                            'createdAt'     => $now,

                        ];

                    } else {

                        $stock[$si]['quantity']      = ($stock[$si]['quantity'] ?? 0) + $baseQty;

                        $stock[$si]['maxAuthorized'] = max($stock[$si]['maxAuthorized'] ?? 0, $stock[$si]['quantity']);

                        $stock[$si]['lastMovementAt'] = $now;

                        // Sync locations.default quantity
                        if (!isset($stock[$si]['locations']) || !is_array($stock[$si]['locations'])) {
                            $stock[$si]['locations'] = ['default' => ['quantity' => 0, 'lots' => []]];
                        }
                        if (!isset($stock[$si]['locations']['default'])) {
                            $stock[$si]['locations']['default'] = ['quantity' => 0, 'lots' => []];
                        }
                        $stock[$si]['locations']['default']['quantity'] = ($stock[$si]['locations']['default']['quantity'] ?? 0) + $baseQty;

                        // Add lot if tracked

                        if (!empty($mv['lotNumber'])) {

                            $lots = $stock[$si]['lots'] ?? [];

                            $li2  = null;

                            foreach ($lots as $lIdx => $l) {

                                if (($l['lotNumber'] ?? '') === $mv['lotNumber']) { $li2 = $lIdx; break; }

                            }

                            if ($li2 !== null) {

                                $lots[$li2]['quantity'] += $baseQty;

                            } else {

                                $lots[] = ['lotNumber'=>$mv['lotNumber'],'expiryDate'=>$mv['expiryDate'],'quantity'=>$baseQty,'createdAt'=>$now];

                            }

                            $stock[$si]['lots'] = $lots;

                            // Also sync locations.default.lots
                            if (!isset($stock[$si]['locations']['default']['lots'])) {
                                $stock[$si]['locations']['default']['lots'] = [];
                            }
                            $defLots =& $stock[$si]['locations']['default']['lots'];
                            $found2 = false;
                            foreach ($defLots as $di => $dl) {
                                if (($dl['lotNumber'] ?? '') === $mv['lotNumber']) {
                                    $defLots[$di]['quantity'] += $baseQty; $found2 = true; break;
                                }
                            }
                            if (!$found2) {
                                $defLots[] = ['lotNumber'=>$mv['lotNumber'],'expiryDate'=>$mv['expiryDate'],'quantity'=>$baseQty,'createdAt'=>$now];
                            }
                            unset($defLots);

                        }

                    }



                    // CUMP update

                    if ($baseUnitCost > 0) {

                        $currentStock = ($si !== null ? ($stock[$si]['quantity'] ?? 0) : $baseQty);

                        $previousQty  = max(0, $currentStock - $baseQty);

                        $previousCump = (float)($prod['cump'] ?? $prod['cost'] ?? 0);

                        $newCump      = $previousQty > 0

                            ? (($previousQty * $previousCump) + ($baseQty * $baseUnitCost)) / ($previousQty + $baseQty)

                            : $baseUnitCost;

                        $products = array_map(function($p) use ($pid, $newCump) {

                            if ((int)($p['id'] ?? 0) === $pid) $p['cump'] = round($newCump, 6);

                            return $p;

                        }, $products);

                    }



                    // Create movement record

                    $movements[] = [

                        'id'           => genId(),

                        'productId'    => $pid,

                        'warehouseId'  => $wid,

                        'type'         => 'entry',

                        'quantity'     => $baseQty,

                        'unit'         => $unit,

                        'baseQuantity' => $baseQty,

                        'unitCost'     => $unitCost,

                        'baseUnitCost' => $baseUnitCost,

                        'lotNumber'    => $mv['lotNumber'],

                        'expiryDate'   => $mv['expiryDate'],

                        'orderId'      => $id,

                        'orderNumber'  => $mv['orderNumber'],

                        'reason'       => 'Réception BC ' . $mv['orderNumber'],

                        'operator'     => $user['name'] ?? $user['username'] ?? 'Inconnu',

                        'userId'       => $user['id'],

                        'user'         => $user['name'] ?? $user['username'],

                        'date'         => $now,

                        'createdAt'    => $now,

                    ];

                }



                saveData('stock', $stock);

                saveData('movements', $movements);

                saveData('products', $products);



                jsonResponse($orders[$idx]);

            }



            // ── Action: update a resumable draft ──

            if ($action === 'update') {
                if (($order['status'] ?? '') !== 'draft') jsonError('Seuls les brouillons sont modifiables.');

                $clientRevision = isset($body['draftRevision']) ? (int)$body['draftRevision'] : null;
                $serverRevision = max(1, (int)($order['draftRevision'] ?? 1));
                if ($clientRevision !== null && $clientRevision !== $serverRevision) {
                    jsonError('Ce brouillon a été modifié depuis un autre appareil. Rechargez-le avant de continuer.', 409);
                }

                if (array_key_exists('supplierId', $body)) {
                    $orders[$idx]['supplierId'] = $body['supplierId'] ?: null;
                }
                if (isset($body['note'])) $orders[$idx]['note'] = trim((string)$body['note']);
                if (isset($body['ref']))  $orders[$idx]['ref']  = trim((string)$body['ref']);
                if (array_key_exists('draftWarehouseId', $body) || array_key_exists('warehouseId', $body)) {
                    $warehouseId = (int)($body['draftWarehouseId'] ?? $body['warehouseId'] ?? 0);
                    if ($warehouseId > 0 && !userCanAccessWarehouse($warehouseId)) jsonError('Accès refusé au magasin du brouillon.', 403);
                    $orders[$idx]['draftWarehouseId'] = $warehouseId > 0 ? $warehouseId : null;
                }
                if (isset($body['draftStep'])) {
                    $orders[$idx]['draftStep'] = max(1, min(4, (int)$body['draftStep']));
                }
                if (isset($body['lines'])) {
                    $orders[$idx]['lines'] = normalizeOrderDraftLines($body['lines'], true);
                }

                $warehouseId = (int)($orders[$idx]['draftWarehouseId'] ?? 0);
                if ($warehouseId <= 0 && !empty($orders[$idx]['lines'])) {
                    $warehouseId = (int)($orders[$idx]['lines'][0]['warehouseId'] ?? 0);
                    $orders[$idx]['draftWarehouseId'] = $warehouseId > 0 ? $warehouseId : null;
                }
                $orders[$idx]['isIncompleteDraft'] = empty($orders[$idx]['supplierId']) || $warehouseId <= 0 || empty($orders[$idx]['lines']);
                $orders[$idx]['draftRevision'] = $serverRevision + 1;
                $orders[$idx]['draftUpdatedAt'] = date('c');
                $orders[$idx]['updatedAt'] = date('c');

                saveData('orders', $orders);
                jsonResponse($orders[$idx]);
            }



            jsonError('Action inconnue.');



        // ── DELETE : supprimer (brouillons seulement) ────────────────────────

        case 'DELETE':

            $id = $body['id'] ?? $_GET['id'] ?? null;

            if (!$id) jsonError('id requis.');



            $orders = loadData('orders');

            $found  = null;

            foreach ($orders as $o) { if ((string)($o['id'] ?? '') === (string)$id) { $found = $o; break; } }



            if (!$found) jsonError('Bon de commande introuvable.', 404);

            if (!in_array($found['status'] ?? '', ['draft', 'cancelled'])) {

                jsonError('Seuls les brouillons ou annulés peuvent être supprimés.');

            }



            deleteItem('orders', $id);

            jsonResponse(['ok' => true]);



        default:

            jsonError('Méthode non supportée.', 405);

    }



} catch (Throwable $e) {

    http_response_code(500);

    header('Content-Type: application/json; charset=utf-8');

    echo json_encode([

        'error' => $e->getMessage(),

        'type'  => get_class($e),

    ], JSON_UNESCAPED_UNICODE);

    exit;

}

