<?php

declare(strict_types=1);

require DENTAL_ROOT . '/app/bootstrap.php';
require_login();
require_documentation_schema();
$inventoryCatalogOnly = false;
require_permission('inventory.manage',['super_admin','clinic_admin','inventory_officer']);
require DENTAL_ROOT . '/app/layout.php';
require DENTAL_ROOT . '/app/module_helpers.php';
require_once DENTAL_ROOT . '/app/inventory_integrity.php';

$pdo=db();
if(!inventory_schema_ready()){
    render_header('Inventory');
    echo '<div class="alert danger">Apply Version 0.5.9.8 in Administration → System Setup → Updates & Database before using controlled inventory.</div>';
    render_footer();exit;
}

$user=current_user()??[];$userId=(int)($user['id']??0);$role=active_role();$canApprove=!$inventoryCatalogOnly&&inventory_is_admin();
$retailCatalogReady = schema_column_exists(table('inventory_items'),'is_retail_product') && schema_column_exists(table('inventory_items'),'selling_price');
$allBranches=branch_options();$userBranch=inventory_user_branch_id();
$branches=array_values(array_filter($allBranches,static fn(array $b):bool=>$role==='super_admin'||(int)$b['id']===$userBranch));
$defaultBranch=(int)($_GET['branch_id']??($userBranch?:($branches[0]['id']??0)));
if($role!=='super_admin')$defaultBranch=$userBranch;
if($defaultBranch>0)inventory_require_branch($defaultBranch);

function inventory_validate_request_context(PDO $pdo,int $branchId,string $type,int &$patientId,int &$encounterId,int &$procedureId,int $requestedForId,string $externalReference): void
{
    if(!in_array($type,['issue','return'],true)&&($patientId>0||$encounterId>0||$procedureId>0||$requestedForId>0))throw new RuntimeException('Patient, procedure, encounter, and staff references are only valid for an Issue or Return transaction.');
    if($requestedForId>0){$q=$pdo->prepare('SELECT branch_id,status FROM `' . table('users') . '` WHERE id=?');$q->execute([$requestedForId]);$staff=$q->fetch();if(!$staff||(string)$staff['status']!=='active'||((int)($staff['branch_id']??0)>0&&(int)$staff['branch_id']!==$branchId))throw new RuntimeException('The requested-for staff account is inactive or belongs to another branch.');}
    if($procedureId>0){$branchExpr=inventory_procedure_branch_expression();$q=$pdo->prepare('SELECT pr.patient_id,pr.encounter_id,'.$branchExpr.' effective_branch_id,pr.status FROM `' . table('procedures') . '` pr LEFT JOIN `' . table('clinical_encounters') . '` ce ON ce.id=pr.encounter_id JOIN `' . table('patients') . '` p ON p.id=pr.patient_id WHERE pr.id=?');$q->execute([$procedureId]);$procedure=$q->fetch();if(!$procedure||!in_array((string)$procedure['status'],['in_progress','completed'],true)||(int)($procedure['effective_branch_id']??0)!==$branchId)throw new RuntimeException('The procedure reference must be in progress or completed in this branch.');if($patientId>0&&$patientId!==(int)$procedure['patient_id'])throw new RuntimeException('The selected patient does not match the procedure.');if($encounterId>0&&(int)$procedure['encounter_id']!==$encounterId)throw new RuntimeException('The selected encounter does not match the procedure.');$patientId=(int)$procedure['patient_id'];$encounterId=(int)($procedure['encounter_id']??0);}
    if($encounterId>0){$q=$pdo->prepare('SELECT patient_id,branch_id,status FROM `' . table('clinical_encounters') . '` WHERE id=?');$q->execute([$encounterId]);$encounter=$q->fetch();$requiresActiveEncounter=$type==='issue';if(!$encounter||(int)$encounter['branch_id']!==$branchId||($requiresActiveEncounter&&!in_array((string)$encounter['status'],['draft','pending_review'],true)))throw new RuntimeException($requiresActiveEncounter?'The encounter reference is not active for this branch.':'The source encounter does not belong to this branch.');if($patientId>0&&$patientId!==(int)$encounter['patient_id'])throw new RuntimeException('The selected patient does not match the encounter.');$patientId=(int)$encounter['patient_id'];}
    if($patientId>0){$q=$pdo->prepare('SELECT branch_id,status FROM `' . table('patients') . '` WHERE id=? AND deleted_at IS NULL');$q->execute([$patientId]);$patient=$q->fetch();if(!$patient||!in_array((string)$patient['status'],['active','pending'],true)||((int)($patient['branch_id']??0)>0&&(int)$patient['branch_id']!==$branchId))throw new RuntimeException('The patient reference is inactive or belongs to another branch.');}
    if($type==='issue'&&$requestedForId<=0&&$patientId<=0&&$procedureId<=0)throw new RuntimeException('An Issue must identify the responsible staff member, patient, or clinical procedure.');
    if($type==='return'&&$requestedForId<=0&&$patientId<=0&&$procedureId<=0&&trim($externalReference)==='')throw new RuntimeException('A Return must identify the responsible staff/patient/procedure or an external source reference.');
}


function inventory_batch_header_key(string $value): string
{
    $value = strtolower(trim($value));
    $value = preg_replace('/[^a-z0-9]+/', '_', $value) ?? '';
    $value = trim($value, '_');
    $aliases = [
        'item_code'=>'sku','code'=>'sku','stock_code'=>'sku','product_code'=>'sku',
        'description'=>'name','item_name'=>'name','product_name'=>'name','supply_name'=>'name',
        'scanner_code'=>'barcode','bar_code'=>'barcode',
        'cost'=>'purchase_cost','buy_cost'=>'purchase_cost','purchase_price'=>'purchase_cost',
        'usage_price'=>'usage_cost','issue_cost'=>'usage_cost',
        'reorder'=>'reorder_level','minimum_stock'=>'reorder_level','min_stock'=>'reorder_level',
        'opening_stock'=>'opening_quantity','quantity'=>'opening_quantity','qty'=>'opening_quantity','current_stock'=>'opening_quantity',
        'batch_no'=>'lot_number','lot'=>'lot_number','supplier_lot'=>'lot_number',
        'expiration'=>'expiry_date','expiry'=>'expiry_date','expiration_date'=>'expiry_date',
        'branch_name'=>'branch','clinic_branch'=>'branch',
        'clinical_supply'=>'is_clinical_supply','clinical'=>'is_clinical_supply',
    ];
    return $aliases[$value] ?? $value;
}

function inventory_batch_read_rows(array $file, string $pasted): array
{
    $raw = trim($pasted);
    $uploadError = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
    if ($uploadError !== UPLOAD_ERR_NO_FILE) {
        if ($uploadError !== UPLOAD_ERR_OK) throw new RuntimeException('The inventory import file could not be uploaded.');
        $tmp = (string)($file['tmp_name'] ?? '');
        if ($tmp === '' || !is_uploaded_file($tmp)) throw new RuntimeException('The inventory import upload is invalid.');
        if ((int)($file['size'] ?? 0) > 2 * 1024 * 1024) throw new RuntimeException('Inventory import files must be 2 MB or smaller.');
        $raw = (string)file_get_contents($tmp);
    }
    $raw = trim($raw);
    if ($raw === '') throw new RuntimeException('Paste inventory rows or upload a CSV file.');
    $firstLine = strtok($raw, "\r\n") ?: '';
    $delimiter = substr_count($firstLine, "\t") > substr_count($firstLine, ',') ? "\t" : ',';
    $handle = fopen('php://temp', 'r+');
    if (!$handle) throw new RuntimeException('Unable to read the inventory import data.');
    fwrite($handle, $raw); rewind($handle);
    $headers = fgetcsv($handle, 0, $delimiter);
    if (!$headers || count($headers) < 2) throw new RuntimeException('Inventory import needs a header row with at least SKU and name.');
    $keys = array_map(static fn($h): string => inventory_batch_header_key((string)$h), $headers);
    $rows = [];
    $lineNo = 1;
    while (($data = fgetcsv($handle, 0, $delimiter)) !== false) {
        $lineNo++;
        if (count(array_filter($data, static fn($v): bool => trim((string)$v) !== '')) === 0) continue;
        $row = ['_line' => $lineNo];
        foreach ($keys as $idx => $key) {
            if ($key !== '') $row[$key] = trim((string)($data[$idx] ?? ''));
        }
        $rows[] = $row;
        if (count($rows) > 500) throw new RuntimeException('Import only up to 500 inventory rows at a time.');
    }
    fclose($handle);
    if (!$rows) throw new RuntimeException('No inventory rows were found after the header.');
    return $rows;
}

function inventory_batch_get(array $row, array $keys, string $default=''): string
{
    foreach ($keys as $key) {
        if (array_key_exists($key, $row) && trim((string)$row[$key]) !== '') return trim((string)$row[$key]);
    }
    return $default;
}

function inventory_batch_number(array $row, array $keys, float $default=0.0): float
{
    $value = inventory_batch_get($row, $keys, '');
    if ($value === '') return $default;
    $value = str_replace([',','₱','PHP','php',' '], '', $value);
    if (!is_numeric($value)) throw new RuntimeException('Line ' . (int)($row['_line'] ?? 0) . ' has an invalid number: ' . $value);
    return round((float)$value, 2);
}

function inventory_batch_bool(array $row, string $key, bool $default=true): int
{
    $value = strtolower(inventory_batch_get($row, [$key], $default ? 'yes' : 'no'));
    return in_array($value, ['1','y','yes','true','clinical','active'], true) ? 1 : 0;
}

function inventory_batch_date(array $row, string $key): ?string
{
    $value = inventory_batch_get($row, [$key], '');
    if ($value === '') return null;
    $date = DateTimeImmutable::createFromFormat('!Y-m-d', $value) ?: DateTimeImmutable::createFromFormat('!m/d/Y', $value) ?: DateTimeImmutable::createFromFormat('!d/m/Y', $value);
    if (!$date) throw new RuntimeException('Line ' . (int)($row['_line'] ?? 0) . ' has an invalid expiry date. Use YYYY-MM-DD.');
    $formatted = $date->format('Y-m-d');
    if ($formatted < date('Y-m-d')) throw new RuntimeException('Line ' . (int)($row['_line'] ?? 0) . ' has an expired opening-stock date. Dispose or exclude expired stock first.');
    return $formatted;
}

function inventory_batch_branch_id(array $row, array $branches, int $defaultBranch, string $role): int
{
    $value = inventory_batch_get($row, ['branch_id','branch'], '');
    $branchId = $defaultBranch;
    if ($value !== '') {
        if (ctype_digit($value)) {
            $branchId = (int)$value;
        } else {
            foreach ($branches as $branch) {
                if (strcasecmp((string)$branch['name'], $value) === 0) { $branchId = (int)$branch['id']; break; }
            }
        }
    }
    inventory_require_branch($branchId);
    if ($role !== 'super_admin' && $branchId !== $defaultBranch) throw new RuntimeException('Line ' . (int)($row['_line'] ?? 0) . ' targets another branch.');
    return $branchId;
}

function inventory_import_template_csv(): string
{
    return "sku,name,category,unit,barcode,supplier_name,purchase_cost,usage_cost,reorder_level,opening_quantity,lot_number,expiry_date,branch\nSUP-GLOVES-M,Nitrile Gloves Medium,PPE,box,123456789,ABC Dental Supply,250,250,5,10,OPEN-GLOVES-M,2027-12-31,Main Branch\nSUP-MASK,Surgical Face Masks,PPE,box,,ABC Dental Supply,120,120,10,25,OPEN-MASK,2027-12-31,Main Branch";
}

function inventory_batch_import(PDO $pdo, array $rows, array $branches, int $defaultBranch, string $role, int $userId, bool $canApprove, string $openingMode, string $evidenceReference): array
{
    $postOpening = $openingMode === 'post_migration';
    if ($postOpening && !$canApprove) throw new RuntimeException('Only Superuser or Admin can post opening-stock migration quantities. Inventory Officers may import catalog rows only.');
    if ($postOpening && trim($evidenceReference) === '') throw new RuntimeException('Opening-stock migration requires a source or approval reference.');
    $created = 0; $updated = 0; $stockRows = 0; $documents = [];
    $openingLinesByBranch = [];
    $pdo->beginTransaction();
    try {
        $findBySku = $pdo->prepare('SELECT id FROM `' . table('inventory_items') . '` WHERE sku=? LIMIT 1');
        $findBarcode = $pdo->prepare('SELECT id,sku FROM `' . table('inventory_items') . '` WHERE barcode=? LIMIT 1');
        $insertItem = $pdo->prepare('INSERT INTO `' . table('inventory_items') . '` (branch_id,sku,barcode,name,category,unit,supplier_name,track_lots,is_clinical_supply,purchase_cost,usage_cost,reorder_level,current_stock,status,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0,"active",NOW(),NOW())');
        $updateItem = $pdo->prepare('UPDATE `' . table('inventory_items') . '` SET branch_id=COALESCE(branch_id,?),barcode=?,name=?,category=?,unit=?,supplier_name=?,is_clinical_supply=?,purchase_cost=?,usage_cost=?,reorder_level=?,status="active",updated_at=NOW() WHERE id=?');
        $upsertBranchStock = $pdo->prepare('INSERT INTO `' . table('inventory_branch_stocks') . '` (item_id,branch_id,current_stock,reorder_level,last_reconciled_at,updated_at) VALUES (?,?,0,?,NOW(),NOW()) ON DUPLICATE KEY UPDATE reorder_level=VALUES(reorder_level),updated_at=NOW()');
        foreach ($rows as $row) {
            $line = (int)($row['_line'] ?? 0);
            $sku = strtoupper(inventory_batch_get($row, ['sku']));
            $name = inventory_batch_get($row, ['name']);
            if ($sku === '' || $name === '') throw new RuntimeException('Line ' . $line . ' needs both SKU and item name.');
            if (mb_strlen($sku) > 80 || mb_strlen($name) > 190) throw new RuntimeException('Line ' . $line . ' SKU or name is too long.');
            $branchId = inventory_batch_branch_id($row, $branches, $defaultBranch, $role);
            $barcode = inventory_batch_get($row, ['barcode'], '');
            if ($barcode !== '') {
                $findBarcode->execute([$barcode]);
                $barcodeOwner = $findBarcode->fetch();
                if ($barcodeOwner && strtoupper((string)$barcodeOwner['sku']) !== $sku) throw new RuntimeException('Line ' . $line . ' barcode already belongs to SKU ' . (string)$barcodeOwner['sku'] . '.');
            }
            $category = mb_substr(inventory_batch_get($row, ['category'], ''), 0, 120);
            $unit = mb_substr(inventory_batch_get($row, ['unit'], 'unit'), 0, 40) ?: 'unit';
            $supplier = mb_substr(inventory_batch_get($row, ['supplier_name'], ''), 0, 190);
            $purchaseCost = inventory_batch_number($row, ['purchase_cost'], 0);
            $usageCost = inventory_batch_number($row, ['usage_cost'], $purchaseCost);
            $reorder = inventory_batch_number($row, ['reorder_level'], 0);
            if ($purchaseCost < 0 || $usageCost < 0 || $reorder < 0) throw new RuntimeException('Line ' . $line . ' has negative cost or reorder level.');
            $clinical = inventory_batch_bool($row, 'is_clinical_supply', true);
            $findBySku->execute([$sku]);
            $itemId = (int)$findBySku->fetchColumn();
            if ($itemId > 0) {
                $updateItem->execute([$branchId,$barcode !== '' ? $barcode : null,$name,$category ?: null,$unit,$supplier ?: null,$clinical,$purchaseCost,$usageCost,$reorder,$itemId]);
                $updated++;
            } else {
                $insertItem->execute([$branchId,$sku,$barcode !== '' ? $barcode : null,$name,$category ?: null,$unit,$supplier ?: null,1,$clinical,$purchaseCost,$usageCost,$reorder]);
                $itemId = (int)$pdo->lastInsertId();
                $created++;
            }
            $upsertBranchStock->execute([$itemId,$branchId,$reorder]);
            $stockRows++;
            $qty = inventory_batch_number($row, ['opening_quantity'], 0);
            if ($qty < 0) throw new RuntimeException('Line ' . $line . ' opening quantity cannot be negative.');
            if ($postOpening && $qty > 0) {
                $lot = inventory_batch_get($row, ['lot_number'], 'OPEN-' . date('Ymd') . '-' . preg_replace('/[^A-Z0-9]+/', '-', $sku));
                $expiry = inventory_batch_date($row, 'expiry_date');
                $openingLinesByBranch[$branchId][] = [
                    'item_id' => $itemId,
                    'lot_number' => mb_substr($lot, 0, 100),
                    'expiry_date' => $expiry,
                    'quantity' => $qty,
                    'unit_cost' => $purchaseCost,
                    'notes' => 'Batch opening-stock import line ' . $line,
                ];
            }
        }
        if ($postOpening) {
            $insertDoc = $pdo->prepare('INSERT INTO `' . table('inventory_documents') . '` (document_no,document_type,branch_id,external_reference,reason,evidence_reference,status,requested_by,requested_at,approved_by,approved_at,created_at,updated_at) VALUES (? ,"goods_receipt",?,?,?,?,"approved",?,NOW(),?,NOW(),NOW(),NOW())');
            $insertLine = $pdo->prepare('INSERT INTO `' . table('inventory_document_lines') . '` (document_id,item_id,lot_number,expiry_date,quantity,unit_cost,notes) VALUES (?,?,?,?,?,?,?)');
            foreach ($openingLinesByBranch as $branchId => $lines) {
                if (!$lines) continue;
                $no = inventory_document_number($pdo, 'goods_receipt');
                $insertDoc->execute([$no,$branchId,'BATCH-OPENING-' . date('YmdHis'),'Opening stock migration from batch inventory upload.',$evidenceReference,$userId,$userId]);
                $docId = (int)$pdo->lastInsertId();
                foreach ($lines as $line) $insertLine->execute([$docId,$line['item_id'],$line['lot_number'],$line['expiry_date'],$line['quantity'],$line['unit_cost'],$line['notes']]);
                inventory_record_history($pdo,$docId,null,'approved','Batch opening stock migration prepared from inventory upload.');
                inventory_post_document($pdo,$docId);
                $documents[] = $no;
            }
        }
        $pdo->commit();
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $e;
    }
    return ['created'=>$created,'updated'=>$updated,'branch_rows'=>$stockRows,'documents'=>$documents,'opening_lines'=>array_sum(array_map('count',$openingLinesByBranch))];
}


function inventory_store_evidence(array $file): array
{
    $error=(int)($file['error']??UPLOAD_ERR_NO_FILE);
    if($error===UPLOAD_ERR_NO_FILE)return [null,null];
    if($error!==UPLOAD_ERR_OK)throw new RuntimeException('The evidence file could not be uploaded.');
    $size=(int)($file['size']??0);if($size<1||$size>8*1024*1024)throw new RuntimeException('Evidence files must be between 1 byte and 8 MB.');
    $tmp=(string)($file['tmp_name']??'');if($tmp===''||!is_uploaded_file($tmp))throw new RuntimeException('The evidence upload is invalid.');
    $mime=(new finfo(FILEINFO_MIME_TYPE))->file($tmp)?:'';
    $allowed=['application/pdf'=>'pdf','image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp'];
    if(!isset($allowed[$mime]))throw new RuntimeException('Evidence must be a PDF, JPG, PNG, or WebP file.');
    $dir=DENTAL_ROOT.'/storage/private/inventory_evidence';
    if(!is_dir($dir)&&!mkdir($dir,0770,true)&&!is_dir($dir))throw new RuntimeException('The protected evidence folder could not be created.');
    $stored=bin2hex(random_bytes(24)).'.'.$allowed[$mime];$target=$dir.'/'.$stored;
    if(!move_uploaded_file($tmp,$target))throw new RuntimeException('The evidence file could not be stored.');
    @chmod($target,0640);
    return ['inventory_evidence/'.$stored,basename((string)($file['name']??('evidence.'.$allowed[$mime])))];
}

function inventory_remove_evidence(?string $relativePath): void
{
    $relativePath=trim((string)$relativePath);if($relativePath==='')return;
    $base=realpath(DENTAL_ROOT.'/storage/private');$file=realpath(DENTAL_ROOT.'/storage/private/'.$relativePath);
    if($base&&$file&&str_starts_with($file,$base.DIRECTORY_SEPARATOR)&&is_file($file))@unlink($file);
}

function inventory_post_document(PDO $pdo,int $documentId): void
{
    [$doc,$lines]=inventory_load_document($pdo,$documentId,true);
    if((string)$doc['status']!=='approved')throw new RuntimeException('Only an approved inventory document can be posted.');
    if(!$lines)throw new RuntimeException('This document has no lines.');
    $type=(string)$doc['document_type'];$branchId=(int)$doc['branch_id'];$approvedBy=(int)($doc['approved_by']??0)?:null;
    if($type==='purchase_order'||$type==='transfer')throw new RuntimeException('Use the dedicated receiving or dispatch action for this document type.');
    foreach($lines as $line){
        $itemId=(int)$line['item_id'];$lotId=(int)($line['lot_id']??0);$quantity=round((float)$line['quantity'],2);
        if($type==='goods_receipt'){
            $lotId=inventory_find_or_create_lot($pdo,$itemId,$branchId,(string)$line['lot_number'],$line['expiry_date']?:null,(float)($line['unit_cost']??0),$documentId);
            $pdo->prepare('UPDATE `' . table('inventory_document_lines') . '` SET lot_id=? WHERE id=? AND document_id=?')->execute([$lotId,(int)$line['id'],$documentId]);$line['lot_id']=$lotId;
            $delta=$quantity;$movement='purchase';
        }elseif($type==='return'){
            $sourceDocumentId=(int)($doc['source_document_id']??0);$sourceLineId=(int)($line['source_line_id']??0);
            if($sourceDocumentId<=0||$sourceLineId<=0)throw new RuntimeException('Every return line must remain linked to its original posted issue.');
            $source=$pdo->prepare('SELECT l.quantity,l.item_id,l.lot_id FROM `' . table('inventory_document_lines') . '` l JOIN `' . table('inventory_documents') . '` d ON d.id=l.document_id WHERE l.id=? AND l.document_id=? AND d.document_type="issue" AND d.status="posted"');
            $source->execute([$sourceLineId,$sourceDocumentId]);$sourceRow=$source->fetch();
            if(!$sourceRow||(int)$sourceRow['item_id']!==$itemId||(int)$sourceRow['lot_id']!==$lotId)throw new RuntimeException('A return line no longer matches the original issue.');
            $used=$pdo->prepare('SELECT COALESCE(SUM(rl.quantity),0) FROM `' . table('inventory_document_lines') . '` rl JOIN `' . table('inventory_documents') . '` rd ON rd.id=rl.document_id WHERE rd.source_document_id=? AND rd.document_type="return" AND rd.status IN ("submitted","approved","posted") AND rd.id<>? AND rl.source_line_id=?');
            $used->execute([$sourceDocumentId,$documentId,$sourceLineId]);
            if($quantity>(float)$sourceRow['quantity']-(float)$used->fetchColumn()+0.001)throw new RuntimeException('This return would exceed the unreturned quantity from the original issue.');
            $delta=$quantity;$movement='return';
        }
        elseif($type==='issue'){$delta=-$quantity;$movement='issue';}
        elseif(in_array($type,['damage','expiry','disposal'],true)){$delta=-$quantity;$movement=$type==='expiry'?'expiration':$type;}
        elseif($type==='stock_count'){
            if($lotId<=0)throw new RuntimeException('Every count line must identify a lot.');
            $lock=$pdo->prepare('SELECT quantity_remaining FROM `' . table('inventory_lots') . '` WHERE id=? AND item_id=? AND branch_id=? FOR UPDATE');$lock->execute([$lotId,$itemId,$branchId]);$actual=$lock->fetchColumn();
            if($actual===false)throw new RuntimeException('A counted lot no longer exists.');
            if(abs((float)$actual-(float)$line['recorded_quantity'])>0.001)throw new RuntimeException('Stock changed after this count was submitted. Cancel it and perform a fresh count.');
            $delta=round((float)$line['counted_quantity']-(float)$actual,2);$movement='stock_count';
            $pdo->prepare('UPDATE `' . table('inventory_document_lines') . '` SET variance_quantity=? WHERE id=?')->execute([$delta,(int)$line['id']]);
            if(abs($delta)<0.01)continue;
        }else{throw new RuntimeException('Unsupported stock document type.');}
        if($lotId<=0)throw new RuntimeException('Every stock movement must identify a lot.');
        inventory_post_movement($pdo,[
            'item_id'=>$itemId,'lot_id'=>$lotId,'branch_id'=>$branchId,'transaction_type'=>$movement,'quantity'=>$delta,
            'unit_cost'=>$line['unit_cost'],'document_id'=>$documentId,'document_line_id'=>(int)$line['id'],'approved_by'=>$approvedBy,
            'reference_type'=>'inventory_document','reference_id'=>(string)$doc['document_no'],'notes'=>(string)($line['notes']?:$doc['reason']),
        ]);
    }
    [, $finalLines]=inventory_load_document($pdo,$documentId,true);
    $hash=inventory_document_snapshot(array_merge($doc,['status'=>'posted','posted_by'=>(int)(current_user()['id']??0)]),$finalLines);
    $stmt=$pdo->prepare('UPDATE `' . table('inventory_documents') . '` SET status="posted",posted_by=?,posted_at=NOW(),document_hash=?,updated_at=NOW() WHERE id=? AND status="approved"');
    $stmt->execute([(int)(current_user()['id']??0),$hash,$documentId]);if($stmt->rowCount()!==1)throw new RuntimeException('The document changed before posting.');
    inventory_record_history($pdo,$documentId,'approved','posted','Stock movement posted.');
}

if($_SERVER['REQUEST_METHOD']==='POST'){
    verify_csrf();$action=(string)($_POST['action']??'');
    if ($inventoryCatalogOnly) {
        require_task_action('inventory.catalog');
        if ($action !== 'item') throw new RuntimeException('Dental Assistants may add product and supply catalogue records only. Stock receiving, issuing, transferring, adjusting, approval, and disposal remain controlled by Inventory or Administration.');
    } else {
        require_task_action('inventory.manage');
    }
    try{
        if($action==='batch_import'){
            $rows = inventory_batch_read_rows((array)($_FILES['inventory_csv'] ?? []), (string)($_POST['inventory_paste'] ?? ''));
            $openingMode = (string)($_POST['opening_mode'] ?? 'catalog_only');
            if (!in_array($openingMode, ['catalog_only','post_migration'], true)) $openingMode = 'catalog_only';
            $summary = inventory_batch_import($pdo, $rows, $allBranches, $defaultBranch, $role, $userId, $canApprove, $openingMode, trim((string)($_POST['evidence_reference'] ?? '')));
            audit('inventory_batch_imported','inventory_item',null,$summary);
            $message = 'Inventory batch import complete: ' . (int)$summary['created'] . ' created, ' . (int)$summary['updated'] . ' updated, ' . (int)$summary['branch_rows'] . ' branch stock rows ready.';
            if (!empty($summary['documents'])) $message .= ' Opening stock posted through ' . implode(', ', $summary['documents']) . '.';
            module_return('inventory.php?view=batch&branch_id=' . $defaultBranch, 'success', $message);
        }
        if($action==='supplier'){
            $name=trim((string)($_POST['name']??''));if($name==='')throw new RuntimeException('Supplier name is required.');
            $pdo->prepare('INSERT INTO `' . table('inventory_suppliers') . '` (name,contact_person,email,phone,address,tax_id,status,created_by,created_at) VALUES (?,?,?,?,?,?,"active",?,NOW())')
                ->execute([$name,trim((string)$_POST['contact_person']),trim((string)$_POST['email']),trim((string)$_POST['phone']),trim((string)$_POST['address']),trim((string)$_POST['tax_id']),$userId]);
            audit('inventory_supplier_created','inventory_supplier',(int)$pdo->lastInsertId());module_return('inventory.php','success','Supplier added.');
        }
        if($action==='item'){
            if (!$retailCatalogReady) throw new RuntimeException('Apply Version 0.7.6.2 before adding products or supplies.');
            $branchId=(int)($_POST['branch_id']??0);inventory_require_branch($branchId);$sku=strtoupper(trim((string)($_POST['sku']??'')));$barcode=trim((string)($_POST['barcode']??''));$name=trim((string)($_POST['name']??''));
            if($sku===''||$name==='')throw new RuntimeException('SKU and item name are required.');
            $isRetail=isset($_POST['is_retail_product'])?1:0;$sellingPrice=round((float)($_POST['selling_price']??0),2);
            if($isRetail&&$sellingPrice<=0)throw new RuntimeException('A retail product needs a positive selling price before the cashier can sell it.');
            if($barcode!==''){$check=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_items') . '` WHERE barcode=?');$check->execute([$barcode]);if((int)$check->fetchColumn()>0)throw new RuntimeException('That barcode is already assigned to another inventory item.');}
            $pdo->beginTransaction();
            $stmt=$pdo->prepare('INSERT INTO `' . table('inventory_items') . '` (branch_id,sku,barcode,name,category,unit,supplier_name,default_supplier_id,track_lots,is_clinical_supply,is_retail_product,purchase_cost,selling_price,usage_cost,reorder_level,current_stock,status,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,"active",NOW())');
            $stmt->execute([$branchId,$sku,$barcode!==''?$barcode:null,$name,trim((string)($_POST['category']??'')),trim((string)($_POST['unit']??'unit')),trim((string)($_POST['supplier_name']??'')),((int)($_POST['default_supplier_id']??0))?:null,1,isset($_POST['is_clinical_supply'])?1:0,$isRetail,(float)($_POST['purchase_cost']??0),$sellingPrice,(float)($_POST['usage_cost']??0),(float)($_POST['reorder_level']??0)]);
            $id=(int)$pdo->lastInsertId();
            $pdo->prepare('INSERT INTO `' . table('inventory_branch_stocks') . '` (item_id,branch_id,current_stock,reorder_level,last_reconciled_at,updated_at) VALUES (?,?,0,?,NOW(),NOW())')->execute([$id,$branchId,(float)($_POST['reorder_level']??0)]);
            $pdo->commit();audit('inventory_item_created','inventory_item',$id,['retail_product'=>$isRetail===1,'selling_price'=>$sellingPrice,'catalog_only_role'=>$inventoryCatalogOnly]);module_return('inventory.php?view=stock','success',($isRetail?'Retail product':'Supply').' added with zero stock. Inventory or Administration must receive stock through the controlled receiving workflow before it can be sold or issued.');
        }
        if($action==='create_document'){
            $type=(string)($_POST['document_type']??'');
            $allowed=['purchase_order','issue','return','transfer','stock_count','damage','expiry','disposal'];
            if(!in_array($type,$allowed,true))throw new RuntimeException('Choose a valid inventory workflow.');
            $submitMode=(string)($_POST['submit_mode']??'submit');
            $documentStatus=$submitMode==='draft'?'draft':'submitted';
            $branchId=(int)($_POST['branch_id']??0);
            $sourceBranch=$type==='transfer'?((int)($_POST['source_branch_id']??0)?:$branchId):$branchId;
            $destination=$type==='transfer'?(int)($_POST['destination_branch_id']??0):null;
            inventory_require_branch($sourceBranch);
            if($type==='transfer'&&($destination<=0||$destination===$sourceBranch))throw new RuntimeException('Select a different destination branch.');

            $reason=trim((string)($_POST['reason']??''));
            if(strlen($reason)<5)throw new RuntimeException('Enter a specific transaction reason.');
            $supplierId=(int)($_POST['supplier_id']??0);
            $requestedForId=(int)($_POST['requested_for_user_id']??0);
            $patientId=(int)($_POST['patient_id']??0);
            $encounterId=(int)($_POST['encounter_id']??0);
            $procedureId=(int)($_POST['procedure_id']??0);
            $chairId=(int)($_POST['chair_id']??0);
            $usageDestination=trim((string)($_POST['usage_destination']??''));
            $sourceDocumentId=$type==='return'?(int)($_POST['source_document_id']??0):0;
            $externalReference=trim((string)($_POST['external_reference']??''));
            $evidenceReference=trim((string)($_POST['evidence_reference']??''));
            [$evidencePath,$evidenceName]=inventory_store_evidence((array)($_FILES['evidence_file']??[]));

            try{
                if($type==='purchase_order'){
                    $q=$pdo->prepare('SELECT status FROM `' . table('inventory_suppliers') . '` WHERE id=?');$q->execute([$supplierId]);
                    if($supplierId<=0||(string)$q->fetchColumn()!=='active')throw new RuntimeException('Select an active supplier for the purchase request.');
                }
                if(in_array($type,['purchase_order','transfer','stock_count','damage','expiry','disposal'],true)&&$evidenceReference===''&&$evidencePath===null){
                    throw new RuntimeException('Attach or reference the supporting evidence for this controlled transaction.');
                }

                if($type==='issue'){
                    $allowedDestinations=['active_treatment','staff','chair','sterilization','department'];
                    if(!in_array($usageDestination,$allowedDestinations,true))throw new RuntimeException('Choose where the supplies are being issued.');
                    if($usageDestination==='active_treatment'&&($encounterId<=0||$procedureId<=0))throw new RuntimeException('Treatment consumption must identify the active encounter and performed procedure.');
                    if(in_array($usageDestination,['staff','department'],true)&&$requestedForId<=0)throw new RuntimeException('Select the responsible staff member for this issue.');
                    if($usageDestination==='chair'&&$chairId<=0)throw new RuntimeException('Select the destination chair.');
                }else{$usageDestination='';$chairId=0;}

                if($chairId>0){
                    $chair=$pdo->prepare('SELECT branch_id,status FROM `' . table('clinic_chairs') . '` WHERE id=?');$chair->execute([$chairId]);$chairRow=$chair->fetch();
                    if(!$chairRow||(int)$chairRow['branch_id']!==$sourceBranch||(string)$chairRow['status']==='retired')throw new RuntimeException('The selected chair is unavailable for this branch.');
                }

                $originalLines=[];
                if($type==='return'){
                    if($sourceDocumentId<=0)throw new RuntimeException('Select the original posted issue being returned.');
                    $original=$pdo->prepare('SELECT * FROM `' . table('inventory_documents') . '` WHERE id=? AND branch_id=? AND document_type="issue" AND status="posted" LIMIT 1');
                    $original->execute([$sourceDocumentId,$sourceBranch]);$sourceDoc=$original->fetch();
                    if(!$sourceDoc)throw new RuntimeException('The original posted issue was not found for this branch.');
                    $patientId=(int)($sourceDoc['patient_id']??0);$encounterId=(int)($sourceDoc['encounter_id']??0);$procedureId=(int)($sourceDoc['procedure_id']??0);
                    $requestedForId=(int)($sourceDoc['requested_for_user_id']??0);$usageDestination=(string)($sourceDoc['usage_destination']??'');$chairId=(int)($sourceDoc['chair_id']??0);if($externalReference==='')$externalReference=(string)$sourceDoc['document_no'];
                    $sourceLineStmt=$pdo->prepare('SELECT * FROM `' . table('inventory_document_lines') . '` WHERE document_id=?');$sourceLineStmt->execute([$sourceDocumentId]);
                    foreach($sourceLineStmt->fetchAll() as $sourceLine)$originalLines[(int)$sourceLine['id']]=$sourceLine;
                }

                if($type==='issue'&&in_array($usageDestination,['chair','sterilization'],true)&&$requestedForId<=0)$requestedForId=$userId;
                inventory_validate_request_context($pdo,$sourceBranch,$type,$patientId,$encounterId,$procedureId,$requestedForId,$externalReference);

                if($type==='issue'&&$encounterId>0){
                    $enc=$pdo->prepare('SELECT c.patient_id,c.status,c.dentist_user_id,c.assistant_user_id,q.chair_id FROM `' . table('clinical_encounters') . '` c LEFT JOIN `' . table('queue_entries') . '` q ON q.id=c.queue_entry_id WHERE c.id=? AND c.branch_id=? LIMIT 1');
                    $enc->execute([$encounterId,$sourceBranch]);$encRow=$enc->fetch();
                    if(!$encRow||!in_array((string)$encRow['status'],['draft','pending_review'],true))throw new RuntimeException('The selected encounter is no longer active.');
                    $patientId=(int)$encRow['patient_id'];if($chairId<=0)$chairId=(int)($encRow['chair_id']??0);
                    if($requestedForId<=0)$requestedForId=(int)($encRow['assistant_user_id']??0)?:((int)($encRow['dentist_user_id']??0));
                }

                $itemIds=(array)($_POST['item_id']??[]);$quantities=(array)($_POST['quantity']??[]);$lotIds=(array)($_POST['lot_id']??[]);
                $unitCosts=(array)($_POST['unit_cost']??[]);$counted=(array)($_POST['counted_quantity']??[]);$notes=(array)($_POST['line_notes']??[]);$sourceLineIds=(array)($_POST['source_line_id']??[]);
                $lines=[];
                $itemCheck=$pdo->prepare('SELECT status FROM `' . table('inventory_items') . '` WHERE id=?');
                $lotCheck=$pdo->prepare('SELECT item_id,branch_id,quantity_remaining,expiry_date,status,unit_cost FROM `' . table('inventory_lots') . '` WHERE id=?');
                $lineCount=max(count($itemIds),count($sourceLineIds));
                for($k=0;$k<$lineCount;$k++){
                    $sourceLineId=(int)($sourceLineIds[$k]??0);
                    if($type==='return'){
                        if($sourceLineId<=0)continue;
                        $sourceLine=$originalLines[$sourceLineId]??null;if(!$sourceLine)throw new RuntimeException('A return line does not belong to the original issue.');
                        $itemId=(int)$sourceLine['item_id'];$lotId=(int)$sourceLine['lot_id'];$unitCost=$sourceLine['unit_cost']!==null?(float)$sourceLine['unit_cost']:null;
                    }else{
                        $itemId=(int)($itemIds[$k]??0);if($itemId<=0)continue;$lotId=(int)($lotIds[$k]??0);
                        $unitCost=isset($unitCosts[$k])&&$unitCosts[$k]!==''?round((float)$unitCosts[$k],2):null;
                    }
                    $itemCheck->execute([$itemId]);if((string)$itemCheck->fetchColumn()!=='active')throw new RuntimeException('A selected inventory item is missing or inactive.');
                    $qty=round((float)($quantities[$k]??0),2);$count=isset($counted[$k])&&$counted[$k]!==''?round((float)$counted[$k],2):null;if($type==='return'&&$qty<=0)continue;
                    if($unitCost!==null&&$unitCost<0)throw new RuntimeException('Unit cost cannot be negative.');
                    if($type==='purchase_order'&&$unitCost===null)throw new RuntimeException('Every purchase-request line requires a unit cost.');
                    $lotRow=null;
                    if($lotId>0){$lotCheck->execute([$lotId]);$lotRow=$lotCheck->fetch();if(!$lotRow||(int)$lotRow['item_id']!==$itemId||(int)$lotRow['branch_id']!==$sourceBranch)throw new RuntimeException('A selected lot does not match its item and source branch.');if((string)$lotRow['status']==='disposed')throw new RuntimeException('A disposed lot cannot be used in a new transaction.');}

                    if($type==='stock_count'){
                        if($lotId<=0||!$lotRow||$count===null||$count<0)throw new RuntimeException('Each count line requires a lot and non-negative counted quantity.');
                        $qty=abs($count-(float)$lotRow['quantity_remaining']);$recorded=(float)$lotRow['quantity_remaining'];$variance=round($count-$recorded,2);
                    }else{
                        if($qty<=0)throw new RuntimeException('Every line requires a quantity greater than zero.');$recorded=$count=$variance=null;
                        if($type!=='purchase_order'&&($lotId<=0||!$lotRow))throw new RuntimeException('Every stock movement line requires a valid lot.');
                        if(in_array($type,['issue','transfer'],true)&&((string)$lotRow['status']!=='active'||(!empty($lotRow['expiry_date'])&&(string)$lotRow['expiry_date']<date('Y-m-d'))))throw new RuntimeException('Issued or transferred stock must come from an active, unexpired lot.');
                        if($type==='return'){
                            if(!in_array((string)$lotRow['status'],['active','depleted'],true))throw new RuntimeException('Returns may only restore an active or depleted lot.');
                            $returned=$pdo->prepare('SELECT COALESCE(SUM(rl.quantity),0) FROM `' . table('inventory_document_lines') . '` rl JOIN `' . table('inventory_documents') . '` rd ON rd.id=rl.document_id WHERE rd.source_document_id=? AND rd.document_type="return" AND rd.status IN ("submitted","approved","posted") AND rl.source_line_id=?');
                            $returned->execute([$sourceDocumentId,$sourceLineId]);$available=round((float)$sourceLine['quantity']-(float)$returned->fetchColumn(),2);
                            if($qty>$available+0.001)throw new RuntimeException('The return quantity exceeds the unreturned amount from the original issue.');
                        }
                        if($type==='expiry'&&(empty($lotRow['expiry_date'])||(string)$lotRow['expiry_date']>date('Y-m-d')))throw new RuntimeException('Only a lot that has reached its expiry date can be posted as expired.');
                        if($type==='expiry'&&abs($qty-(float)$lotRow['quantity_remaining'])>0.001)throw new RuntimeException('An expiry document must remove the entire remaining quantity of the expired lot.');
                        if(in_array($type,['issue','transfer','damage','expiry','disposal'],true)&&$qty>(float)$lotRow['quantity_remaining']+0.001)throw new RuntimeException('A transaction quantity exceeds the selected lot balance.');
                    }
                    $lines[]=['source_line_id'=>$sourceLineId?:null,'item_id'=>$itemId,'lot_id'=>$lotId?:null,'quantity'=>$qty,'unit_cost'=>$unitCost,'recorded'=>$recorded,'counted'=>$count,'variance'=>$variance,'notes'=>trim((string)($notes[$k]??''))];
                }
                if(!$lines)throw new RuntimeException('Add at least one valid transaction line.');

                $pdo->beginTransaction();$no=inventory_document_number($pdo,$type);
                $pdo->prepare('INSERT INTO `' . table('inventory_documents') . '` (document_no,document_type,source_document_id,branch_id,source_branch_id,destination_branch_id,supplier_id,requested_for_user_id,patient_id,encounter_id,procedure_id,usage_destination,chair_id,evidence_file_path,evidence_original_name,external_reference,reason,evidence_reference,status,requested_by,requested_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),NOW())')
                    ->execute([$no,$type,$sourceDocumentId?:null,$sourceBranch,$type==='transfer'?$sourceBranch:null,$destination,$supplierId?:null,$requestedForId?:null,$patientId?:null,$encounterId?:null,$procedureId?:null,$usageDestination!==''?$usageDestination:null,$chairId?:null,$evidencePath,$evidenceName,$externalReference,$reason,$evidenceReference,$documentStatus,$userId]);
                $docId=(int)$pdo->lastInsertId();
                $ins=$pdo->prepare('INSERT INTO `' . table('inventory_document_lines') . '` (document_id,source_line_id,item_id,lot_id,quantity,unit_cost,recorded_quantity,counted_quantity,variance_quantity,notes) VALUES (?,?,?,?,?,?,?,?,?,?)');
                foreach($lines as $line)$ins->execute([$docId,$line['source_line_id'],$line['item_id'],$line['lot_id'],$line['quantity'],$line['unit_cost'],$line['recorded'],$line['counted'],$line['variance'],$line['notes']]);
                inventory_record_history($pdo,$docId,null,$documentStatus,$documentStatus==='draft'?'Saved as draft.':'Submitted for independent approval.');
                $pdo->commit();audit('inventory_document_'.$documentStatus,'inventory_document',$docId,['type'=>$type,'document_no'=>$no]);
                module_return('inventory.php?view=documents','success',$no.($documentStatus==='draft'?' saved as draft.':' submitted for approval.'));
            }catch(Throwable $documentError){
                if($pdo->inTransaction())$pdo->rollBack();inventory_remove_evidence($evidencePath);throw $documentError;
            }
        }
        if($action==='submit_draft'){
            $id=(int)($_POST['id']??0);$pdo->beginTransaction();[$doc,$lines]=inventory_load_document($pdo,$id,true);
            if((string)$doc['status']!=='draft')throw new RuntimeException('Only a draft document can be submitted.');
            if((int)$doc['requested_by']!==$userId&&!$canApprove)throw new RuntimeException('Only the draft owner or an Administrator can submit this document.');
            if(!$lines)throw new RuntimeException('The draft has no transaction lines.');
            $stmt=$pdo->prepare('UPDATE `' . table('inventory_documents') . '` SET status="submitted",requested_at=NOW(),updated_at=NOW() WHERE id=? AND status="draft"');$stmt->execute([$id]);
            if($stmt->rowCount()!==1)throw new RuntimeException('The draft changed in another session.');
            inventory_record_history($pdo,$id,'draft','submitted','Draft submitted for independent approval.');$pdo->commit();
            audit('inventory_document_submitted','inventory_document',$id,['from'=>'draft']);module_return('inventory.php?view=documents','success','Draft submitted for approval.');
        }
        if($action==='review'){
            if(!$canApprove)throw new RuntimeException('Only an Administrator may approve or reject controlled inventory documents.');
            $id=(int)$_POST['id'];$decision=(string)$_POST['decision'];$note=trim((string)$_POST['review_note']);if(!in_array($decision,['approved','rejected'],true))throw new RuntimeException('Invalid review decision.');if(strlen($note)<5)throw new RuntimeException('Enter a review note.');
            $pdo->beginTransaction();[$doc,$lines]=inventory_load_document($pdo,$id,true);if((string)$doc['status']!=='submitted')throw new RuntimeException('Only submitted documents can be reviewed.');if((int)$doc['requested_by']===$userId)throw new RuntimeException('The requester cannot approve the same inventory document.');
            $reviewHash=inventory_document_snapshot(array_merge($doc,['status'=>$decision,'approved_by'=>$userId]),$lines);$pdo->prepare('UPDATE `' . table('inventory_documents') . '` SET status=?,approved_by=?,approved_at=NOW(),review_note=?,document_hash=?,updated_at=NOW() WHERE id=? AND status="submitted"')->execute([$decision,$userId,$note,$reviewHash,$id]);inventory_record_history($pdo,$id,'submitted',$decision,$note);$pdo->commit();audit('inventory_document_reviewed','inventory_document',$id,['decision'=>$decision]);module_return('inventory.php','success','Inventory document '.$decision.'.');
        }
        if($action==='post'){
            $id=(int)$_POST['id'];$pdo->beginTransaction();[$doc]=inventory_load_document($pdo,$id,true);inventory_require_branch((int)$doc['branch_id']);if((int)($doc['approved_by']??0)===$userId)throw new RuntimeException('The approver cannot post the same inventory adjustment.');inventory_post_document($pdo,$id);$pdo->commit();audit('inventory_document_posted','inventory_document',$id);module_return('inventory.php','success','Approved stock movement posted and reconciled by lot and branch.');
        }
        if($action==='create_receipt'){
            $poId=(int)$_POST['purchase_order_id'];$branchId=(int)$_POST['branch_id'];inventory_require_branch($branchId);$deliveryReference=trim((string)($_POST['external_reference']??''));$receivingEvidence=trim((string)($_POST['evidence_reference']??''));if($deliveryReference===''||$receivingEvidence==='')throw new RuntimeException('Delivery/invoice reference and receiving evidence are required.');$pdo->beginTransaction();[$po,$poLines]=inventory_load_document($pdo,$poId,true);if((string)$po['document_type']!=='purchase_order'||!in_array((string)$po['status'],['approved','posted'],true))throw new RuntimeException('Receive only against an approved purchase order.');if((int)$po['branch_id']!==$branchId)throw new RuntimeException('The receipt branch does not match the purchase order.');if((int)$po['approved_by']===$userId)throw new RuntimeException('The purchase-order approver cannot also receive the same delivery.');
            $qtys=(array)($_POST['receive_quantity']??[]);$lots=(array)($_POST['lot_number']??[]);$expires=(array)($_POST['expiry_date']??[]);$receiptLines=[];
            foreach($poLines as $line){$lineId=(int)$line['id'];$qty=round((float)($qtys[$lineId]??0),2);if($qty<=0)continue;$used=$pdo->prepare('SELECT COALESCE(SUM(rl.quantity),0) FROM `' . table('inventory_document_lines') . '` rl JOIN `' . table('inventory_documents') . '` rd ON rd.id=rl.document_id WHERE rd.source_document_id=? AND rd.document_type="goods_receipt" AND rd.status="posted" AND rl.source_line_id=?');$used->execute([$poId,$lineId]);$remaining=round((float)$line['quantity']-(float)$used->fetchColumn(),2);if($qty>$remaining+0.001)throw new RuntimeException('Receipt quantity exceeds the remaining ordered quantity for '.$line['item_name'].'.');$lot=trim((string)($lots[$lineId]??''));if($lot==='')throw new RuntimeException('Lot number is required for '.$line['item_name'].'.');$expiry=($expires[$lineId]??'')?:null;if($expiry!==null&&$expiry<date('Y-m-d'))throw new RuntimeException('Expired stock cannot be accepted for '.$line['item_name'].'.');$receiptLines[]=['source_line_id'=>$lineId,'item_id'=>(int)$line['item_id'],'quantity'=>$qty,'unit_cost'=>(float)($line['unit_cost']??0),'lot_number'=>$lot,'expiry_date'=>$expiry];}
            if(!$receiptLines)throw new RuntimeException('Enter at least one quantity received.');$no=inventory_document_number($pdo,'goods_receipt');$reason=trim((string)($_POST['reason']??''));if($reason==='')$reason='Goods received against '.$po['document_no'];
            $pdo->prepare('INSERT INTO `' . table('inventory_documents') . '` (document_no,document_type,source_document_id,branch_id,supplier_id,external_reference,reason,evidence_reference,status,requested_by,requested_at,approved_by,approved_at,created_at) VALUES (?,"goods_receipt",?,?,?,?,?,?,"approved",?,NOW(),?,NOW(),NOW())')
                ->execute([$no,$poId,$branchId,$po['supplier_id'],$deliveryReference,$reason,$receivingEvidence,$userId,(int)$po['approved_by']]);
            $docId=(int)$pdo->lastInsertId();$ins=$pdo->prepare('INSERT INTO `' . table('inventory_document_lines') . '` (document_id,source_line_id,item_id,lot_number,expiry_date,quantity,unit_cost,notes) VALUES (?,?,?,?,?,?,?,?)');foreach($receiptLines as $line)$ins->execute([$docId,$line['source_line_id'],$line['item_id'],$line['lot_number'],$line['expiry_date'],$line['quantity'],$line['unit_cost'],'PO line '.$line['source_line_id']]);inventory_record_history($pdo,$docId,null,'approved','Receiving transaction created from approved PO.');inventory_post_document($pdo,$docId);
            $remainingTotal=0.0;foreach($poLines as $line){$used=$pdo->prepare('SELECT COALESCE(SUM(rl.quantity),0) FROM `' . table('inventory_document_lines') . '` rl JOIN `' . table('inventory_documents') . '` rd ON rd.id=rl.document_id WHERE rd.source_document_id=? AND rd.document_type="goods_receipt" AND rd.status="posted" AND rl.source_line_id=?');$used->execute([$poId,(int)$line['id']]);$remainingTotal+=max(0,(float)$line['quantity']-(float)$used->fetchColumn());}
            $poStatus=$remainingTotal<0.01?'posted':'approved';$poHash=$poStatus==='posted'?inventory_document_snapshot(array_merge($po,['status'=>'posted','posted_by'=>$userId]),$poLines):(string)($po['document_hash']??'');$pdo->prepare('UPDATE `' . table('inventory_documents') . '` SET status=?,posted_by=CASE WHEN ?="posted" THEN ? ELSE posted_by END,posted_at=CASE WHEN ?="posted" THEN NOW() ELSE posted_at END,document_hash=CASE WHEN ?="posted" THEN ? ELSE document_hash END,updated_at=NOW() WHERE id=?')->execute([$poStatus,$poStatus,$userId,$poStatus,$poStatus,$poHash,$poId]);if($poStatus==='posted')inventory_record_history($pdo,$poId,'approved','posted','Purchase order fully received.');
            $pdo->commit();audit('inventory_goods_received','inventory_document',$docId,['purchase_order_id'=>$poId]);module_return('inventory.php','success',$no.' posted. Lot and branch balances were reconciled.');
        }
        if($action==='dispatch_transfer'){
            $id=(int)$_POST['id'];$pdo->beginTransaction();[$doc,$lines]=inventory_load_document($pdo,$id,true);if((string)$doc['document_type']!=='transfer'||(string)$doc['status']!=='approved')throw new RuntimeException('Only an approved transfer can be dispatched.');inventory_require_branch((int)$doc['source_branch_id']);if((int)$doc['approved_by']===$userId)throw new RuntimeException('The approver cannot dispatch the same transfer.');foreach($lines as $line)inventory_post_movement($pdo,['item_id'=>(int)$line['item_id'],'lot_id'=>(int)$line['lot_id'],'branch_id'=>(int)$doc['source_branch_id'],'transaction_type'=>'transfer_out','quantity'=>-(float)$line['quantity'],'unit_cost'=>$line['unit_cost'],'document_id'=>$id,'document_line_id'=>(int)$line['id'],'approved_by'=>(int)$doc['approved_by'],'reference_type'=>'inventory_transfer','reference_id'=>(string)$doc['document_no'],'notes'=>(string)$doc['reason']]);$pdo->prepare('UPDATE `' . table('inventory_documents') . '` SET status="dispatched",posted_by=?,posted_at=NOW(),updated_at=NOW() WHERE id=? AND status="approved"')->execute([$userId,$id]);inventory_record_history($pdo,$id,'approved','dispatched','Source stock dispatched.');$pdo->commit();audit('inventory_transfer_dispatched','inventory_document',$id);module_return('inventory.php','success','Transfer dispatched. Destination stock will not increase until separately received.');
        }
        if($action==='receive_transfer'){
            $id=(int)$_POST['id'];$pdo->beginTransaction();[$doc,$lines]=inventory_load_document($pdo,$id,true);if((string)$doc['document_type']!=='transfer'||(string)$doc['status']!=='dispatched')throw new RuntimeException('Only a dispatched transfer can be received.');$dest=(int)$doc['destination_branch_id'];inventory_require_branch($dest);if((int)$doc['posted_by']===$userId)throw new RuntimeException('The person who dispatched the transfer cannot receive it.');if((int)$doc['approved_by']===$userId)throw new RuntimeException('The transfer approver cannot also confirm destination receipt.');foreach($lines as $line){$sourceLot=$pdo->prepare('SELECT lot_number,expiry_date,unit_cost FROM `' . table('inventory_lots') . '` WHERE id=?');$sourceLot->execute([(int)$line['lot_id']]);$lot=$sourceLot->fetch();if(!$lot)throw new RuntimeException('Source lot not found.');$destLot=inventory_find_or_create_lot($pdo,(int)$line['item_id'],$dest,(string)$lot['lot_number'],$lot['expiry_date']?:null,(float)($lot['unit_cost']??$line['unit_cost']??0),$id);$out=$pdo->prepare('SELECT id FROM `' . table('inventory_transactions') . '` WHERE document_id=? AND document_line_id=? AND transaction_type="transfer_out" AND status="posted" ORDER BY id DESC LIMIT 1');$out->execute([$id,(int)$line['id']]);$outId=(int)$out->fetchColumn();$inId=inventory_post_movement($pdo,['item_id'=>(int)$line['item_id'],'lot_id'=>$destLot,'branch_id'=>$dest,'transaction_type'=>'transfer_in','quantity'=>(float)$line['quantity'],'unit_cost'=>$line['unit_cost'],'document_id'=>$id,'document_line_id'=>(int)$line['id'],'counterpart_transaction_id'=>$outId?:null,'approved_by'=>(int)$doc['approved_by'],'reference_type'=>'inventory_transfer','reference_id'=>(string)$doc['document_no'],'notes'=>(string)$doc['reason']]);if($outId)$pdo->prepare('UPDATE `' . table('inventory_transactions') . '` SET counterpart_transaction_id=? WHERE id=?')->execute([$inId,$outId]);}
            $pdo->prepare('UPDATE `' . table('inventory_documents') . '` SET status="received",received_by=?,received_at=NOW(),document_hash=?,updated_at=NOW() WHERE id=? AND status="dispatched"')->execute([$userId,inventory_document_snapshot(array_merge($doc,['status'=>'received','received_by'=>$userId]),$lines),$id]);inventory_record_history($pdo,$id,'dispatched','received','Destination independently received all transfer lines.');$pdo->commit();audit('inventory_transfer_received','inventory_document',$id);module_return('inventory.php','success','Transfer received. Source and destination movements are paired and reconciled.');
        }
    }catch(Throwable $e){if($pdo->inTransaction())$pdo->rollBack();throw $e;}
}

$view=(string)($_GET['view']??'stock');
if(!in_array($view,['stock','lots','movements','documents','batch'],true))$view='stock';
if($inventoryCatalogOnly)$view='stock';
$state=module_management_state('page','per_page',25);$page=$state['page'];$perPage=$state['per_page'];$offset=$state['offset'];
$q=trim((string)($_GET['q']??''));
$statusFilter=trim((string)($_GET['status']??''));
$categoryFilter=trim((string)($_GET['category']??''));
$expiryFilter=trim((string)($_GET['expiry']??''));
$documentTypeFilter=trim((string)($_GET['document_type']??''));
$dateFrom=trim((string)($_GET['date_from']??''));
$dateTo=trim((string)($_GET['date_to']??''));
$isValidDate=static function(string $value): bool {
    if($value==='') return true;
    $date=DateTimeImmutable::createFromFormat('!Y-m-d',$value);
    return $date instanceof DateTimeImmutable && $date->format('Y-m-d')===$value;
};
if(!$isValidDate($dateFrom))$dateFrom='';
if(!$isValidDate($dateTo))$dateTo='';
if($dateFrom!==''&&$dateTo!==''&&$dateFrom>$dateTo){$swap=$dateFrom;$dateFrom=$dateTo;$dateTo=$swap;}
$manageId=$inventoryCatalogOnly?0:max(0,(int)($_GET['manage_id']??0));

$metricWhere=' WHERE i.status="active"';$metricParams=[];
if($defaultBranch>0){$metricWhere.=' AND bs.branch_id=?';$metricParams[]=$defaultBranch;}elseif($role!=='super_admin'){$metricWhere.=' AND 1=0';}
$metricFrom=' FROM `' . table('inventory_branch_stocks') . '` bs JOIN `' . table('inventory_items') . '` i ON i.id=bs.item_id';
$metric=function(string $expression)use($pdo,$metricFrom,$metricWhere,$metricParams){$stmt=$pdo->prepare('SELECT '.$expression.$metricFrom.$metricWhere);$stmt->execute($metricParams);return $stmt->fetchColumn();};
$activeItems=(int)$metric('COUNT(*)');
$stockedItems=(int)$metric('SUM(CASE WHEN bs.current_stock>0 THEN 1 ELSE 0 END)');
$low=(int)$metric('SUM(CASE WHEN bs.reorder_level>0 AND bs.current_stock>0 AND bs.current_stock<=bs.reorder_level THEN 1 ELSE 0 END)');
$outOfStock=(int)$metric('SUM(CASE WHEN bs.current_stock<=0 THEN 1 ELSE 0 END)');
$expStmt=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_lots') . '` WHERE quantity_remaining>0 AND status="active" AND expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(),INTERVAL 60 DAY)'.($defaultBranch>0?' AND branch_id=?':''));$expStmt->execute($defaultBranch>0?[$defaultBranch]:[]);$expiring=(int)$expStmt->fetchColumn();
$pendingStmt=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_documents') . '` WHERE status="submitted"'.($defaultBranch>0?' AND (branch_id=? OR destination_branch_id=?)':''));$pendingStmt->execute($defaultBranch>0?[$defaultBranch,$defaultBranch]:[]);$pending=(int)$pendingStmt->fetchColumn();
$transitStmt=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_documents') . '` WHERE document_type="transfer" AND status="dispatched"'.($defaultBranch>0?' AND (branch_id=? OR destination_branch_id=?)':''));$transitStmt->execute($defaultBranch>0?[$defaultBranch,$defaultBranch]:[]);$transit=(int)$transitStmt->fetchColumn();

$categoryOptions=[];
$categorySql='SELECT DISTINCT i.category FROM `' . table('inventory_items') . '` i JOIN `' . table('inventory_branch_stocks') . '` bs ON bs.item_id=i.id WHERE i.status="active" AND i.category IS NOT NULL AND TRIM(i.category)<>""';
$categoryParams=[];
if($defaultBranch>0){$categorySql.=' AND bs.branch_id=?';$categoryParams[]=$defaultBranch;}elseif($role!=="super_admin"){$categorySql.=' AND 1=0';}
$categorySql.=' ORDER BY i.category';
$categoryStmt=$pdo->prepare($categorySql);$categoryStmt->execute($categoryParams);$categoryOptions=array_values(array_filter(array_map('strval',$categoryStmt->fetchAll(PDO::FETCH_COLUMN))));

$rows=[];$rowsTotal=0;
if($view==='stock'){
    $where=['i.status="active"'];$params=[];
    if($defaultBranch>0){$where[]='bs.branch_id=?';$params[]=$defaultBranch;}elseif($role!=='super_admin')$where[]='1=0';
    if($q!==''){$like=module_like_term($q);$where[]='(i.sku LIKE ? OR i.barcode LIKE ? OR i.name LIKE ? OR i.category LIKE ? OR b.name LIKE ?)';array_push($params,$like,$like,$like,$like,$like);}
    if($categoryFilter!==''&&in_array($categoryFilter,$categoryOptions,true)){$where[]='i.category=?';$params[]=$categoryFilter;}
    if($statusFilter==='out')$where[]='bs.current_stock<=0';
    elseif($statusFilter==='low')$where[]='bs.reorder_level>0 AND bs.current_stock>0 AND bs.current_stock<=bs.reorder_level';
    elseif($statusFilter==='healthy')$where[]='bs.current_stock>0 AND (bs.reorder_level<=0 OR bs.current_stock>bs.reorder_level)';
    $whereSql=implode(' AND ',$where);
    $count=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_branch_stocks') . '` bs JOIN `' . table('inventory_items') . '` i ON i.id=bs.item_id JOIN `' . table('branches') . '` b ON b.id=bs.branch_id WHERE '.$whereSql);$count->execute($params);$rowsTotal=(int)$count->fetchColumn();
    $stmt=$pdo->prepare('SELECT bs.*,i.sku,i.barcode,i.name,i.category,i.unit,i.is_retail_product,i.selling_price,b.name branch,'
        . '(SELECT MIN(expiry_date) FROM `' . table('inventory_lots') . '` l WHERE l.item_id=bs.item_id AND l.branch_id=bs.branch_id AND l.quantity_remaining>0 AND l.status="active") next_expiry,'
        . '(SELECT MAX(created_at) FROM `' . table('inventory_transactions') . '` t WHERE t.item_id=bs.item_id AND t.branch_id=bs.branch_id AND t.status="posted") last_movement_at '
        . 'FROM `' . table('inventory_branch_stocks') . '` bs JOIN `' . table('inventory_items') . '` i ON i.id=bs.item_id JOIN `' . table('branches') . '` b ON b.id=bs.branch_id WHERE '.$whereSql.' ORDER BY (bs.current_stock<=0) DESC,(bs.reorder_level>0 AND bs.current_stock<=bs.reorder_level) DESC,i.name LIMIT '.$perPage.' OFFSET '.$offset);$stmt->execute($params);$rows=$stmt->fetchAll();
}elseif($view==='lots'){
    $where=['1=1'];$params=[];if($defaultBranch>0){$where[]='l.branch_id=?';$params[]=$defaultBranch;}
    if($q!==''){$like=module_like_term($q);$where[]='(i.sku LIKE ? OR i.barcode LIKE ? OR i.name LIKE ? OR l.lot_number LIKE ? OR b.name LIKE ?)';array_push($params,$like,$like,$like,$like,$like);}
    if(in_array($statusFilter,['active','quarantined','depleted','expired','disposed'],true)){$where[]='l.status=?';$params[]=$statusFilter;}
    if($expiryFilter==='expiring_30')$where[]='l.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(),INTERVAL 30 DAY)';
    elseif($expiryFilter==='expiring_60')$where[]='l.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(),INTERVAL 60 DAY)';
    elseif($expiryFilter==='expired')$where[]='l.expiry_date<CURDATE()';
    elseif($expiryFilter==='no_expiry')$where[]='l.expiry_date IS NULL';
    $whereSql=implode(' AND ',$where);
    $count=$pdo->prepare('SELECT COUNT(*) FROM `' . table('inventory_lots') . '` l JOIN `' . table('inventory_items') . '` i ON i.id=l.item_id JOIN `' . table('branches') . '` b ON b.id=l.branch_id WHERE '.$whereSql);$count->execute($params);$rowsTotal=(int)$count->fetchColumn();
    $stmt=$pdo->prepare('SELECT l.*,i.name item_name,i.sku,i.barcode,i.unit,b.name branch FROM `' . table('inventory_lots') . '` l JOIN `' . table('inventory_items') . '` i ON i.id=l.item_id JOIN `' . table('branches') . '` b ON b.id=l.branch_id WHERE '.$whereSql.' ORDER BY (l.status<>"active"),i.name,l.expiry_date IS NULL,l.expiry_date,l.lot_number LIMIT '.$perPage.' OFFSET '.$offset);$stmt->execute($params);$rows=$stmt->fetchAll();
}elseif($view==='movements'){
    $where=['t.status="posted"'];$params=[];if($defaultBranch>0){$where[]='t.branch_id=?';$params[]=$defaultBranch;}
    if($q!==''){$like=module_like_term($q);$where[]='(t.movement_no LIKE ? OR i.sku LIKE ? OR i.barcode LIKE ? OR i.name LIKE ? OR lot.lot_number LIKE ? OR d.document_no LIKE ?)';array_push($params,$like,$like,$like,$like,$like,$like);}
    if(in_array($statusFilter,['purchase','procedure_consumption','issue','return','transfer_in','transfer_out','stock_count','adjustment','damage','expiration','disposal'],true)){$where[]='t.transaction_type=?';$params[]=$statusFilter;}
    if($dateFrom!==''){$where[]='DATE(t.created_at)>=?';$params[]=$dateFrom;}
    if($dateTo!==''){$where[]='DATE(t.created_at)<=?';$params[]=$dateTo;}
    $whereSql=implode(' AND ',$where);
    $from=' FROM `' . table('inventory_transactions') . '` t JOIN `' . table('inventory_items') . '` i ON i.id=t.item_id LEFT JOIN `' . table('inventory_lots') . '` lot ON lot.id=t.lot_id LEFT JOIN `' . table('inventory_documents') . '` d ON d.id=t.document_id LEFT JOIN `' . table('users') . '` u ON u.id=t.user_id LEFT JOIN `' . table('branches') . '` b ON b.id=t.branch_id ';
    $count=$pdo->prepare('SELECT COUNT(*)'.$from.'WHERE '.$whereSql);$count->execute($params);$rowsTotal=(int)$count->fetchColumn();
    $stmt=$pdo->prepare('SELECT t.*,i.sku,i.name item_name,i.unit,lot.lot_number,d.document_no,u.name user_name,b.name branch'.$from.'WHERE '.$whereSql.' ORDER BY t.created_at DESC,t.id DESC LIMIT '.$perPage.' OFFSET '.$offset);$stmt->execute($params);$rows=$stmt->fetchAll();
}elseif($view==='documents'){
    $where=['1=1'];$params=[];if($defaultBranch>0){$where[]='(d.branch_id=? OR d.destination_branch_id=?)';array_push($params,$defaultBranch,$defaultBranch);}
    if($q!==''){$like=module_like_term($q);$where[]='(d.document_no LIKE ? OR d.external_reference LIKE ? OR d.reason LIKE ? OR s.name LIKE ? OR pt.patient_no LIKE ? OR pt.first_name LIKE ? OR pt.last_name LIKE ?)';array_push($params,$like,$like,$like,$like,$like,$like,$like);}
    if(in_array($statusFilter,['draft','submitted','approved','rejected','posted','dispatched','received','cancelled'],true)){$where[]='d.status=?';$params[]=$statusFilter;}
    if(in_array($documentTypeFilter,['purchase_order','goods_receipt','issue','return','transfer','stock_count','adjustment','damage','expiry','disposal'],true)){$where[]='d.document_type=?';$params[]=$documentTypeFilter;}
    if($dateFrom!==''){$where[]='DATE(d.created_at)>=?';$params[]=$dateFrom;}
    if($dateTo!==''){$where[]='DATE(d.created_at)<=?';$params[]=$dateTo;}
    $whereSql=implode(' AND ',$where);
    $from=' FROM `' . table('inventory_documents') . '` d JOIN `' . table('branches') . '` b ON b.id=d.branch_id LEFT JOIN `' . table('branches') . '` db ON db.id=d.destination_branch_id LEFT JOIN `' . table('inventory_suppliers') . '` s ON s.id=d.supplier_id LEFT JOIN `' . table('users') . '` r ON r.id=d.requested_by LEFT JOIN `' . table('users') . '` a ON a.id=d.approved_by LEFT JOIN `' . table('users') . '` p ON p.id=d.posted_by LEFT JOIN `' . table('patients') . '` pt ON pt.id=d.patient_id ';
    $count=$pdo->prepare('SELECT COUNT(*)'.$from.'WHERE '.$whereSql);$count->execute($params);$rowsTotal=(int)$count->fetchColumn();
    $stmt=$pdo->prepare('SELECT d.*,b.name branch,s.name supplier,r.name requester,a.name approver,p.name poster,db.name destination_branch,pt.patient_no,pt.first_name patient_first_name,pt.last_name patient_last_name'.$from.'WHERE '.$whereSql.' ORDER BY d.created_at DESC LIMIT '.$perPage.' OFFSET '.$offset);$stmt->execute($params);$rows=$stmt->fetchAll();
}

$managedDocument=null;$managedLines=[];
if($manageId>0){
    $stmt=$pdo->prepare('SELECT d.*,b.name branch,s.name supplier,r.name requester,a.name approver,p.name poster,db.name destination_branch,rf.name requested_for,pt.patient_no,pt.first_name patient_first_name,pt.last_name patient_last_name,ch.chair_name chair_name FROM `' . table('inventory_documents') . '` d JOIN `' . table('branches') . '` b ON b.id=d.branch_id LEFT JOIN `' . table('branches') . '` db ON db.id=d.destination_branch_id LEFT JOIN `' . table('inventory_suppliers') . '` s ON s.id=d.supplier_id LEFT JOIN `' . table('users') . '` r ON r.id=d.requested_by LEFT JOIN `' . table('users') . '` a ON a.id=d.approved_by LEFT JOIN `' . table('users') . '` p ON p.id=d.posted_by LEFT JOIN `' . table('users') . '` rf ON rf.id=d.requested_for_user_id LEFT JOIN `' . table('patients') . '` pt ON pt.id=d.patient_id LEFT JOIN `' . table('clinic_chairs') . '` ch ON ch.id=d.chair_id WHERE d.id=?'.($defaultBranch>0?' AND (d.branch_id=? OR d.destination_branch_id=?)':'').' LIMIT 1');
    $stmt->execute($defaultBranch>0?[$manageId,$defaultBranch,$defaultBranch]:[$manageId]);$managedDocument=$stmt->fetch()?:null;
    if($managedDocument){$stmt=$pdo->prepare('SELECT l.*,i.name item_name,i.sku,i.unit,lot.lot_number actual_lot,lot.expiry_date actual_expiry FROM `' . table('inventory_document_lines') . '` l JOIN `' . table('inventory_items') . '` i ON i.id=l.item_id LEFT JOIN `' . table('inventory_lots') . '` lot ON lot.id=l.lot_id WHERE l.document_id=? ORDER BY l.id');$stmt->execute([$manageId]);$managedLines=$stmt->fetchAll();}
}

$chairOptions=[];
if($defaultBranch>0&&schema_table_exists(table('clinic_chairs'))){$stmt=$pdo->prepare('SELECT id,chair_name AS name,status FROM `' . table('clinic_chairs') . '` WHERE branch_id=? AND status="available" ORDER BY chair_name');$stmt->execute([$defaultBranch]);$chairOptions=$stmt->fetchAll();}
$lookupEndpoint=app_absolute_url('api/management_lookup.php');$contextEndpoint=app_absolute_url('api/inventory_context.php');
$inventorySafetySummary = function_exists('inventory_completion_risk_summary') ? inventory_completion_risk_summary($pdo, $defaultBranch) : [];

render_header($inventoryCatalogOnly?'Products & Supplies':'Inventory');
?>
<?php if($inventoryCatalogOnly): ?>
<section class="card assistant-product-catalog-head"><div><span class="eyebrow">Dental Assistant catalogue access</span><h2>Add products and supplies used by the clinic</h2><p>Add toothbrushes, mouthwash, dental floss, take-home kits, and supply records. Mark items for retail sale and enter the protected selling price. Inventory or Administration must receive stock before Cashier can sell it.</p></div><button type="button" class="btn primary" data-open="item-form">+ Add product or supply</button></section>
<?php if(!$retailCatalogReady):?><div class="alert danger">Apply Version 0.7.6.2 before adding retail products.</div><?php endif;?>
<?php else: ?>
<section class="inventory-metrics" aria-label="Inventory status">
    <a href="inventory.php?view=stock&amp;branch_id=<?=$defaultBranch?>"><span>Active items</span><strong><?=number_format($activeItems)?></strong><small>Catalog items monitored</small></a>
    <a href="inventory.php?view=stock&amp;status=healthy&amp;branch_id=<?=$defaultBranch?>"><span>Stocked items</span><strong><?=number_format($stockedItems)?></strong><small>Positive available balance</small></a>
    <a class="warning" href="inventory.php?view=stock&amp;status=low&amp;branch_id=<?=$defaultBranch?>"><span>Low stock</span><strong><?=number_format($low)?></strong><small>Positive stock at reorder level</small></a>
    <a class="danger" href="inventory.php?view=stock&amp;status=out&amp;branch_id=<?=$defaultBranch?>"><span>Out of stock</span><strong><?=number_format($outOfStock)?></strong><small>Zero available balance</small></a>
    <a class="warning" href="inventory.php?view=lots&amp;branch_id=<?=$defaultBranch?>"><span>Expiring lots</span><strong><?=number_format($expiring)?></strong><small>Within the next 60 days</small></a>
    <a href="inventory.php?view=documents&amp;status=submitted&amp;branch_id=<?=$defaultBranch?>"><span>Awaiting approval</span><strong><?=number_format($pending)?></strong><small>Independent review required</small></a>
    <a href="inventory.php?view=documents&amp;status=dispatched&amp;branch_id=<?=$defaultBranch?>"><span>Transfers in transit</span><strong><?=number_format($transit)?></strong><small>Dispatched, not received</small></a>
</section>

<?php if($outOfStock>0||$low>0||$expiring>0||$pending>0||$transit>0):?>
<section class="inventory-alert-strip" aria-label="Inventory actions required">
    <strong>Action required</strong>
    <?php if($outOfStock>0):?><a href="inventory.php?view=stock&amp;status=out&amp;branch_id=<?=$defaultBranch?>"><?=$outOfStock?> out of stock</a><?php endif;?>
    <?php if($low>0):?><a href="inventory.php?view=stock&amp;status=low&amp;branch_id=<?=$defaultBranch?>"><?=$low?> below reorder target</a><?php endif;?>
    <?php if($expiring>0):?><a href="inventory.php?view=lots&amp;branch_id=<?=$defaultBranch?>"><?=$expiring?> expiring soon</a><?php endif;?>
    <?php if($pending>0):?><a href="inventory.php?view=documents&amp;status=submitted&amp;branch_id=<?=$defaultBranch?>"><?=$pending?> awaiting approval</a><?php endif;?><?php if($transit>0):?><a href="inventory.php?view=documents&amp;status=dispatched&amp;branch_id=<?=$defaultBranch?>"><?=$transit?> transfer<?= $transit===1?'':'s' ?> awaiting destination receipt</a><?php endif;?>
</section>
<?php endif;?>
<?php if($inventorySafetySummary): ?>
<?= inventory_completion_card_html($inventorySafetySummary) ?>
<?php endif; ?>

<section class="inventory-command-bar">
    <div><span class="eyebrow">Inventory work queue</span><h2>Resolve alerts first, then start the required stock transaction</h2><p>Clinical procedure consumption is posted from Dentist-verified treatment. Use manual Issue supplies only for stock that was not already consumed by a linked clinical procedure.</p></div>
    <div class="inventory-command-grid">
        <button type="button" data-inventory-workflow="issue">Issue supplies</button>
        <button type="button" data-inventory-workflow="purchase_order">Purchase request</button>
        <button type="button" data-inventory-workflow="transfer">Transfer stock</button>
        <button type="button" data-inventory-workflow="return">Return unused</button>
        <button type="button" data-inventory-workflow="stock_count">Physical count</button>
        <button type="button" data-inventory-workflow="damage">Record damage</button>
        <button type="button" data-inventory-workflow="expiry">Record expiry</button>
        <button type="button" data-inventory-workflow="disposal">Controlled disposal</button>
        <button type="button" class="secondary" data-open="item-form">Add item</button>
        <a class="secondary" href="inventory.php?view=batch&amp;branch_id=<?=$defaultBranch?>">Batch upload</a>
        <button type="button" class="secondary" data-open="supplier-form">Manage supplier</button>
    </div>
</section>
<?php endif; ?>

<div class="inventory-workspace-head">
    <nav class="inventory-tabs" aria-label="Inventory records">
        <a class="<?=$view==='stock'?'active':''?>" href="inventory.php?view=stock&amp;branch_id=<?=$defaultBranch?>"><?= $inventoryCatalogOnly?'Product & supply catalogue':'Stock' ?> <span><?=$activeItems?></span></a>
        <?php if(!$inventoryCatalogOnly):?><a class="<?=$view==='lots'?'active':''?>" href="inventory.php?view=lots&amp;branch_id=<?=$defaultBranch?>">Lots</a>
        <a class="<?=$view==='movements'?'active':''?>" href="inventory.php?view=movements&amp;branch_id=<?=$defaultBranch?>">Movements</a>
        <a class="<?=$view==='documents'?'active':''?>" href="inventory.php?view=documents&amp;branch_id=<?=$defaultBranch?>">Controlled documents <?php if($pending):?><span><?=$pending?></span><?php endif;?></a>
        <a class="<?=$view==='batch'?'active':''?>" href="inventory.php?view=batch&amp;branch_id=<?=$defaultBranch?>">Batch upload</a><?php endif;?>
    </nav>
    <?php if($role==='super_admin'&&count($branches)>1):?><form method="get" class="inline-form inventory-branch-switch"><input type="hidden" name="view" value="<?=e($view)?>"><label>Branch<select name="branch_id" onchange="this.form.submit()"><?php foreach($branches as $b):?><option value="<?=(int)$b['id']?>" <?=$defaultBranch===(int)$b['id']?'selected':''?>><?=e((string)$b['name'])?></option><?php endforeach;?></select></label></form><?php endif;?>
</div>
<?php
$searchPlaceholders=[
    'stock'=>'Search item, SKU, barcode, or category',
    'lots'=>'Search item, SKU, barcode, or lot number',
    'movements'=>'Search movement, document, item, SKU, barcode, or lot',
    'documents'=>'Search document, reference, supplier, or patient',
    'batch'=>'Paste or upload CSV inventory rows',
];
$listLabels=['stock'=>'items','lots'=>'lots','movements'=>'movements','documents'=>'documents','batch'=>'batch rows'];
$hasActiveFilters=$q!==''||$statusFilter!==''||$categoryFilter!==''||$expiryFilter!==''||$documentTypeFilter!==''||$dateFrom!==''||$dateTo!=='';
?>
<?php if($view==='batch'): ?>
<section class="card inventory-batch-card">
    <div class="card-head"><div><span class="eyebrow">Inventory migration</span><h2>Batch upload inventory records</h2><p>Use this for existing clinic records. It accepts pasted CSV text or a CSV file. Catalog rows are created or updated; opening quantities are posted only when explicitly selected.</p></div><span class="badge">v0.6.2.1</span></div>
    <form method="post" enctype="multipart/form-data" class="form-grid">
        <input type="hidden" name="csrf" value="<?=e(csrf_token())?>">
        <input type="hidden" name="action" value="batch_import">
        <label>Import mode<select name="opening_mode">
            <option value="catalog_only">Catalog only — do not change stock balances</option>
            <?php if($canApprove):?><option value="post_migration">Catalog + post opening quantities as migration receiving document</option><?php endif;?>
        </select><small>Opening stock posting creates controlled Goods Receipt movement records, not silent stock edits.</small></label>
        <label>Default branch<select name="branch_id" disabled><?php foreach($branches as $b):?><option value="<?=(int)$b['id']?>" <?=$defaultBranch===(int)$b['id']?'selected':''?>><?=e((string)$b['name'])?></option><?php endforeach;?></select><small>Use the branch column only when Superuser imports multiple branches.</small></label>
        <label class="wide">Source / approval reference<input name="evidence_reference" placeholder="Opening count sheet, migration file name, approval note, or supplier ledger reference"><small>Required when posting opening quantities.</small></label>
        <label class="wide">Upload CSV file<input type="file" name="inventory_csv" accept=".csv,text/csv,text/plain"><small>Maximum 2 MB. Header row is required.</small></label>
        <label class="wide">Or paste CSV rows<textarea name="inventory_paste" placeholder="<?=e(inventory_import_template_csv())?>"></textarea></label>
        <div class="status-actions wide"><button class="btn primary">Import inventory batch</button><a class="btn ghost" href="inventory.php?view=stock&amp;branch_id=<?=$defaultBranch?>">Back to stock</a></div>
    </form>
    <div class="inventory-batch-help">
        <article><h3>Accepted columns</h3><p>Required: <strong>sku</strong> and <strong>name</strong>. Optional: category, unit, barcode, supplier_name, purchase_cost, usage_cost, reorder_level, opening_quantity, lot_number, expiry_date, branch.</p></article>
        <article><h3>Template</h3><code class="inventory-template-code"><?=e(inventory_import_template_csv())?></code></article>
    </div>
    <div class="notice warning"><strong>Control rule:</strong> Batch upload is for migration and existing records. Regular daily usage should still use Issue supplies, Purchase request, receiving, return, count, damage, expiry, or disposal workflows.</div>
</section>
<?php else: ?>
<form method="get" class="inventory-filter-toolbar" data-inventory-filter-form>
    <input type="hidden" name="view" value="<?=e($view)?>">
    <input type="hidden" name="branch_id" value="<?=$defaultBranch?>">
    <input type="hidden" name="page" value="1">
    <label class="inventory-filter-search"><span>Search</span><input type="search" name="q" value="<?=e($q)?>" placeholder="<?=e($searchPlaceholders[$view])?>" autocomplete="off" data-inventory-search></label>
    <?php if($view==='stock'):?>
        <label><span>Category</span><select name="category" data-auto-inventory-filter><option value="">All categories</option><?php foreach($categoryOptions as $category):?><option value="<?=e($category)?>" <?=$categoryFilter===$category?'selected':''?>><?=e($category)?></option><?php endforeach;?></select></label>
        <label><span>Stock status</span><select name="status" data-auto-inventory-filter><option value="">All stock</option><option value="healthy" <?=$statusFilter==='healthy'?'selected':''?>>Available</option><option value="low" <?=$statusFilter==='low'?'selected':''?>>Low stock</option><option value="out" <?=$statusFilter==='out'?'selected':''?>>Out of stock</option></select></label>
    <?php elseif($view==='lots'):?>
        <label><span>Lot status</span><select name="status" data-auto-inventory-filter><option value="">All statuses</option><?php foreach(['active','quarantined','depleted','expired','disposed'] as $st):?><option value="<?=$st?>" <?=$statusFilter===$st?'selected':''?>><?=e(module_status($st))?></option><?php endforeach;?></select></label>
        <label><span>Expiry</span><select name="expiry" data-auto-inventory-filter><option value="">Any expiry</option><option value="expiring_30" <?=$expiryFilter==='expiring_30'?'selected':''?>>Within 30 days</option><option value="expiring_60" <?=$expiryFilter==='expiring_60'?'selected':''?>>Within 60 days</option><option value="expired" <?=$expiryFilter==='expired'?'selected':''?>>Past expiry date</option><option value="no_expiry" <?=$expiryFilter==='no_expiry'?'selected':''?>>No expiry recorded</option></select></label>
    <?php elseif($view==='movements'):?>
        <label><span>Movement</span><select name="status" data-auto-inventory-filter><option value="">All movements</option><?php foreach(['purchase','procedure_consumption','issue','return','transfer_in','transfer_out','stock_count','adjustment','damage','expiration','disposal'] as $st):?><option value="<?=$st?>" <?=$statusFilter===$st?'selected':''?>><?=e(module_status($st))?></option><?php endforeach;?></select></label>
        <label><span>From</span><input type="date" name="date_from" value="<?=e($dateFrom)?>" data-auto-inventory-filter></label>
        <label><span>To</span><input type="date" name="date_to" value="<?=e($dateTo)?>" data-auto-inventory-filter></label>
    <?php else:?>
        <label><span>Workflow</span><select name="document_type" data-auto-inventory-filter><option value="">All workflows</option><?php foreach(['purchase_order','goods_receipt','issue','return','transfer','stock_count','adjustment','damage','expiry','disposal'] as $type):?><option value="<?=$type?>" <?=$documentTypeFilter===$type?'selected':''?>><?=e(module_status($type))?></option><?php endforeach;?></select></label>
        <label><span>Status</span><select name="status" data-auto-inventory-filter><option value="">All statuses</option><?php foreach(['draft','submitted','approved','rejected','posted','dispatched','received','cancelled'] as $st):?><option value="<?=$st?>" <?=$statusFilter===$st?'selected':''?>><?=e(module_status($st))?></option><?php endforeach;?></select></label>
        <label><span>From</span><input type="date" name="date_from" value="<?=e($dateFrom)?>" data-auto-inventory-filter></label>
        <label><span>To</span><input type="date" name="date_to" value="<?=e($dateTo)?>" data-auto-inventory-filter></label>
    <?php endif;?>
    <label class="inventory-filter-page-size"><span>Items per page</span><select name="per_page" data-auto-inventory-filter><?php foreach([10,25,50,100] as $size):?><option value="<?=$size?>" <?=$perPage===$size?'selected':''?>><?=$size?></option><?php endforeach;?></select></label>
    <div class="inventory-filter-actions"><button class="btn primary compact" type="submit">Apply</button><?php if($hasActiveFilters):?><a class="btn ghost compact" href="inventory.php?view=<?=e($view)?>&amp;branch_id=<?=$defaultBranch?>">Clear</a><?php endif;?></div>
    <span class="inventory-filter-state" data-inventory-filter-state aria-live="polite">Filters update automatically</span>
</form>

<div class="card server-list-card inventory-list-card"><?php module_render_list_count($rowsTotal,$listLabels[$view]);module_render_server_pagination($rowsTotal,$page,$perPage,'page','per_page','inventory-list');?><div class="table-wrap"><table class="inventory-table"><thead><tr>
<?php if($view==='stock'):?><th>Item</th><th>Retail</th><th>Available</th><th>Reorder</th><th>Next expiry</th><th>Last movement</th><th>Status</th><th></th>
<?php elseif($view==='lots'):?><th>Item</th><th>Lot</th><th>Expiry</th><th>Received</th><th>Remaining</th><th>Status</th>
<?php elseif($view==='movements'):?><th>Date</th><th>Movement</th><th>Item / lot</th><th>Quantity</th><th>Balance after</th><th>Reference</th><th>Posted by</th>
<?php else:?><th>Document</th><th>Workflow / branch</th><th>Linked context</th><th>Status</th><th>Control trail</th><th></th><?php endif;?></tr></thead><tbody>
<?php foreach($rows as $row):?><tr>
<?php if($view==='stock'):$isOut=(float)$row['current_stock']<=0;$isLow=!$isOut&&(float)$row['reorder_level']>0&&(float)$row['current_stock']<=(float)$row['reorder_level'];?>
<td><strong><?=e((string)$row['sku'])?> · <?=e((string)$row['name'])?></strong><small><?=e((string)$row['category'])?><?=!empty($row['barcode'])?' · Barcode '.e((string)$row['barcode']):''?> · <?=e((string)$row['branch'])?></small></td><td><?php if(!empty($row['is_retail_product'])):?><span class="badge success">For sale</span><small>₱<?=number_format((float)$row['selling_price'],2)?></small><?php else:?><span class="muted">Supply only</span><?php endif;?></td><td class="<?=$isOut||$isLow?'danger-text':''?>"><strong><?=e((string)$row['current_stock'])?></strong> <?=e((string)$row['unit'])?></td><td><?=e((string)$row['reorder_level'])?> <?=e((string)$row['unit'])?></td><td><?=e($row['next_expiry']?date('M j, Y',strtotime((string)$row['next_expiry'])):'—')?></td><td><?=e($row['last_movement_at']?date('M j, g:i A',strtotime((string)$row['last_movement_at'])):'No movement')?></td><td><span class="badge <?=$isOut?'danger':($isLow?'warning':'success')?>"><?=$isOut?'Out of stock':($isLow?'Low stock':'Available')?></span></td><td><a class="btn ghost compact" href="inventory.php?view=movements&amp;branch_id=<?=$defaultBranch?>&amp;q=<?=urlencode((string)$row['sku'])?>">History</a></td>
<?php elseif($view==='lots'):?><td><strong><?=e((string)$row['item_name'])?></strong><small><?=e((string)$row['sku'])?> · <?=e((string)$row['branch'])?></small></td><td><?=e((string)$row['lot_number'])?></td><td class="<?=!empty($row['expiry_date'])&&(string)$row['expiry_date']<date('Y-m-d')?'danger-text':''?>"><?=e((string)($row['expiry_date']?date('M j, Y',strtotime((string)$row['expiry_date'])):'No expiry'))?></td><td><?=e((string)$row['quantity_received'])?> <?=e((string)$row['unit'])?></td><td><strong><?=e((string)$row['quantity_remaining'])?></strong> <?=e((string)$row['unit'])?></td><td><span class="badge"><?=e(module_status((string)$row['status']))?></span></td>
<?php elseif($view==='movements'):?><td><?=e(date('M j, Y',strtotime((string)$row['created_at'])))?><small><?=e(date('g:i A',strtotime((string)$row['created_at'])))?></small></td><td><strong><?=e(module_status((string)$row['transaction_type']))?></strong><small><?=e((string)$row['movement_no'])?></small></td><td><?=e((string)$row['item_name'])?><small><?=e((string)$row['sku'])?> · Lot <?=e((string)($row['lot_number']?:'—'))?></small></td><td class="<?=((float)$row['quantity']<0)?'danger-text':'ok-text'?>"><strong><?=((float)$row['quantity']>0?'+':'')?><?=e((string)$row['quantity'])?></strong> <?=e((string)$row['unit'])?></td><td><?=e((string)$row['balance_after'])?> <?=e((string)$row['unit'])?></td><td><?=e((string)($row['document_no']?:$row['reference_id']?:'—'))?></td><td><?=e((string)($row['user_name']?:'System'))?><small><?=e((string)$row['branch'])?></small></td>
<?php else:$patientLabel=!empty($row['patient_no'])?(string)$row['patient_no'].' · '.(string)$row['patient_last_name'].', '.(string)$row['patient_first_name']:'';?>
<td><strong><?=e((string)$row['document_no'])?></strong><small><?=e(date('M j, Y g:i A',strtotime((string)$row['created_at'])))?></small></td><td><?=e(module_status((string)$row['document_type']))?><small><?=e((string)$row['branch'])?><?=!empty($row['destination_branch'])?' → '.e((string)$row['destination_branch']):''?></small></td><td><?=e($patientLabel?:($row['supplier']?:'General stock'))?><small><?=e(module_status((string)($row['usage_destination']?:'controlled inventory')))?></small></td><td><span class="badge"><?=e(module_status((string)$row['status']))?></span></td><td><small>Requested: <?=e((string)($row['requester']?:'—'))?><br>Approved: <?=e((string)($row['approver']?:'—'))?><br>Posted: <?=e((string)($row['poster']?:'—'))?></small></td><td><a class="btn ghost compact" href="<?=e(module_management_url(['manage_id'=>(int)$row['id'],'view'=>'documents','page'=>$page,'per_page'=>$perPage,'branch_id'=>$defaultBranch]))?>">Manage</a></td><?php endif;?></tr><?php endforeach;if(!$rows):?><tr><td colspan="8" class="muted">No records match the current filters.</td></tr><?php endif;?></tbody></table></div><?php module_render_server_pagination($rowsTotal,$page,$perPage,'page','per_page','inventory-list');?></div>
<?php endif; ?>

<?php if($managedDocument):?><dialog id="manage-document" class="wide-dialog" open><div class="card"><a class="dialog-close" href="inventory.php?view=documents&amp;branch_id=<?=$defaultBranch?>">×</a><h2><?=e((string)$managedDocument['document_no'])?></h2><p><?=e(module_status((string)$managedDocument['document_type']))?> · <?=e((string)$managedDocument['branch'])?> · <span class="badge"><?=e(module_status((string)$managedDocument['status']))?></span></p>
<div class="inventory-document-context"><div><span>Destination</span><strong><?=e(module_status((string)($managedDocument['usage_destination']?:'General stock')))?></strong></div><div><span>Patient / staff</span><strong><?=e(!empty($managedDocument['patient_no'])?((string)$managedDocument['patient_no'].' · '.(string)$managedDocument['patient_last_name'].', '.(string)$managedDocument['patient_first_name']):((string)($managedDocument['requested_for']?:'—')))?></strong></div><div><span>Chair</span><strong><?=e((string)($managedDocument['chair_name']?:'—'))?></strong></div><div><span>Source document</span><strong><?=e((string)($managedDocument['source_document_id']?:'—'))?></strong></div></div>
<div class="table-wrap"><table><thead><tr><th>Item</th><th>Quantity</th><th>Lot</th><th>Expiry</th><th>Unit cost</th></tr></thead><tbody><?php foreach($managedLines as $line):?><tr><td><?=e((string)$line['item_name'])?><small><?=e((string)$line['sku'])?></small></td><td><?=e((string)$line['quantity'])?> <?=e((string)$line['unit'])?></td><td><?=e((string)($line['actual_lot']?:$line['lot_number']?:'—'))?></td><td><?=e((string)($line['actual_expiry']?:$line['expiry_date']?:'—'))?></td><td><?=$line['unit_cost']!==null?'₱'.number_format((float)$line['unit_cost'],2):'—'?></td></tr><?php endforeach;?></tbody></table></div>
<div class="notice info"><strong>Reason:</strong> <?=e((string)$managedDocument['reason'])?><br><strong>External reference:</strong> <?=e((string)($managedDocument['external_reference']?:'—'))?><br><strong>Evidence reference:</strong> <?=e((string)($managedDocument['evidence_reference']?:'—'))?><?php if(!empty($managedDocument['evidence_file_path'])):?><br><a href="download_inventory_evidence.php?id=<?=(int)$managedDocument['id']?>">Open attached <?=e((string)($managedDocument['evidence_original_name']?:'evidence'))?></a><?php endif;?></div>
<?php if($managedDocument['status']==='draft'&&((int)$managedDocument['requested_by']===$userId||$canApprove)):?><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="submit_draft"><input type="hidden" name="id" value="<?=(int)$managedDocument['id']?>"><button class="btn primary">Submit draft for approval</button></form>
<?php elseif($managedDocument['status']==='submitted'&&$canApprove&&(int)$managedDocument['requested_by']!==$userId):?><form method="post" class="form-grid"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="review"><input type="hidden" name="id" value="<?=(int)$managedDocument['id']?>"><label class="wide">Review note<input name="review_note" required></label><div class="status-actions wide"><button name="decision" value="approved" class="btn primary">Approve</button><button name="decision" value="rejected" class="btn danger">Reject</button></div></form>
<?php elseif($managedDocument['status']==='approved'&&$managedDocument['document_type']==='purchase_order'&&(int)$managedDocument['approved_by']!==$userId):?><button class="btn primary" data-open="receipt-form">Receive against purchase request</button>
<?php elseif($managedDocument['status']==='approved'&&$managedDocument['document_type']==='transfer'):?><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="dispatch_transfer"><input type="hidden" name="id" value="<?=(int)$managedDocument['id']?>"><button class="btn primary">Dispatch transfer</button></form>
<?php elseif($managedDocument['status']==='dispatched'&&($role==='super_admin'||$userBranch===(int)$managedDocument['destination_branch_id'])):?><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="receive_transfer"><input type="hidden" name="id" value="<?=(int)$managedDocument['id']?>"><button class="btn primary">Receive at destination</button></form>
<?php elseif($managedDocument['status']==='approved'):?><form method="post"><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="post"><input type="hidden" name="id" value="<?=(int)$managedDocument['id']?>"><button class="btn primary">Post stock movement</button></form><?php endif;?></div></dialog><?php endif;?>

<dialog id="supplier-form" class="wide-dialog"><form method="post" class="form-grid"><button type="button" class="dialog-close" data-close>×</button><h2 class="wide">Add supplier</h2><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="supplier"><label>Supplier name<input name="name" required></label><label>Contact person<input name="contact_person"></label><label>Email<input type="email" name="email"></label><label>Phone<input name="phone"></label><label>Tax ID<input name="tax_id"></label><label class="wide">Address<textarea name="address"></textarea></label><button class="btn primary">Add supplier</button></form></dialog>

<dialog id="item-form" class="wide-dialog"<?=((string)($_GET['add']??''))==='product'?' open':''?>><form method="post" class="form-grid"><button type="button" class="dialog-close" data-close>×</button><h2 class="wide"><?= $inventoryCatalogOnly?'Add product or supply':'Add inventory catalogue item' ?></h2><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="item"><label>Branch<select name="branch_id" required><?php foreach($branches as $b):?><option value="<?=(int)$b['id']?>" <?=$defaultBranch===(int)$b['id']?'selected':''?>><?=e((string)$b['name'])?></option><?php endforeach;?></select></label><label>SKU<input name="sku" required placeholder="Example: RET-TB-001"></label><label>Barcode / scanner code<input name="barcode" inputmode="numeric" placeholder="Optional UPC, EAN, or internal barcode"></label><label>Item name<input name="name" required placeholder="Example: Soft-bristle toothbrush"></label><label>Category<input name="category" placeholder="Oral care, take-home product, clinical supply"></label><label>Unit<input name="unit" value="piece" required></label><?php if(!$inventoryCatalogOnly):?><label>Default supplier<?php module_remote_record_picker('default_supplier_id','inventory_supplier',$defaultBranch,false,'Search supplier name or contact');?></label><label>Supplier display name<input name="supplier_name"></label><label>Purchase cost<input type="number" min="0" step="0.01" name="purchase_cost" value="0"></label><label>Usage cost<input type="number" min="0" step="0.01" name="usage_cost" value="0"></label><label>Branch reorder level<input type="number" min="0" step="0.01" name="reorder_level" value="0"></label><?php else:?><input type="hidden" name="purchase_cost" value="0"><input type="hidden" name="usage_cost" value="0"><input type="hidden" name="reorder_level" value="0"><?php endif;?><label class="catalog-choice"><input type="checkbox" name="is_retail_product" value="1" data-retail-product-toggle> Sell this product at Cashier<small>Use for toothbrushes, mouthwash, floss, take-home kits, and other products sold directly to patients.</small></label><label>Selling price<input type="number" min="0" step="0.01" name="selling_price" value="0" data-retail-selling-price><small>Required when marked for cashier sale.</small></label><label class="catalog-choice"><input type="checkbox" name="is_clinical_supply" value="1" checked> May also be used as a clinical supply</label><div class="alert info wide"><strong>Catalogue only:</strong> Adding an item does not create stock. Inventory or Administration must receive and verify stock through the controlled purchasing/receiving workflow.</div><button class="btn primary wide" <?=!$retailCatalogReady?'disabled':''?>>Add with zero opening stock</button></form></dialog>

<dialog id="document-form" class="wide-dialog inventory-transaction-dialog" data-inventory-transaction data-context-endpoint="<?=e($contextEndpoint)?>" data-lookup-endpoint="<?=e($lookupEndpoint)?>"><form method="post" enctype="multipart/form-data" class="inventory-transaction-form"><button type="button" class="dialog-close" data-close>×</button><div class="inventory-dialog-heading"><span class="eyebrow">Controlled inventory</span><h2 data-inventory-title>Issue supplies</h2><p data-inventory-description>Release stock to an active treatment, staff member, chair, sterilization area, or department.</p></div><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="create_document"><input type="hidden" name="document_type" value="issue" data-inventory-type><input type="hidden" name="patient_id" value="" data-inventory-patient-id><input type="hidden" name="chair_id" value="" data-inventory-chair-id>
<div class="inventory-form-grid"><label>Branch / source<select name="branch_id" required data-inventory-branch><?php foreach($branches as $b):?><option value="<?=(int)$b['id']?>" <?=$defaultBranch===(int)$b['id']?'selected':''?>><?=e((string)$b['name'])?></option><?php endforeach;?></select></label>
<div data-inventory-section data-types="purchase_order"><label>Supplier<?php module_remote_record_picker('supplier_id','inventory_supplier',$defaultBranch,false,'Search supplier name or contact');?></label></div>
<div data-inventory-section data-types="transfer"><label>Transfer source<select name="source_branch_id"><option value="">Use branch above</option><?php foreach($branches as $b):?><option value="<?=(int)$b['id']?>"><?=e((string)$b['name'])?></option><?php endforeach;?></select></label></div>
<div data-inventory-section data-types="transfer"><label>Destination branch<select name="destination_branch_id"><option value="">Select destination</option><?php foreach($allBranches as $b):?><option value="<?=(int)$b['id']?>"><?=e((string)$b['name'])?></option><?php endforeach;?></select></label></div>
<div data-inventory-section data-types="issue"><label>Issue destination<select name="usage_destination" data-issue-destination><option value="active_treatment">Active patient treatment</option><option value="staff">Dentist or staff member</option><option value="chair">Chair / treatment room</option><option value="sterilization">Sterilization area</option><option value="department">Department / general use</option></select></label></div>
<div data-inventory-section data-types="issue" data-issue-context="active_treatment"><label>Active encounter<?php module_remote_record_picker('encounter_id','encounter',$defaultBranch,false,'Search active patient, Dentist, or assistant');?></label></div>
<div data-inventory-section data-types="issue" data-issue-context="active_treatment"><label>Performed procedure<select name="procedure_id" data-inventory-procedure><option value="">Select the active or completed procedure</option></select></label></div>
<div data-inventory-section data-types="issue" data-issue-context="staff department"><label>Responsible staff<?php module_remote_record_picker('requested_for_user_id','staff',$defaultBranch,false,'Search staff name, email, or role');?></label></div>
<div data-inventory-section data-types="issue" data-issue-context="chair"><label>Chair / room<select data-chair-select><option value="">Select chair</option><?php foreach($chairOptions as $chair):?><option value="<?=(int)$chair['id']?>"><?=e((string)$chair['name'])?> · <?=e(module_status((string)$chair['status']))?></option><?php endforeach;?></select></label></div>
<div class="inventory-context-summary" data-inventory-section data-types="issue" data-issue-context="active_treatment" hidden><span>Patient</span><strong data-context-patient>—</strong><span>Dentist</span><strong data-context-dentist>—</strong><span>Dental Assistant</span><strong data-context-assistant>—</strong><span>Chair</span><strong data-context-chair>—</strong></div>
<div data-inventory-section data-types="return" class="wide"><label>Original posted issue<?php module_remote_record_picker('source_document_id','inventory_issue',$defaultBranch,false,'Search issue number, patient, or staff');?></label><small>Only quantities that remain unreturned can be restored to the original lot.</small></div>
<label>External reference<input name="external_reference" placeholder="PO number, department request, count sheet, or external source"></label><label>Evidence reference<input name="evidence_reference" placeholder="Invoice, transfer slip, incident report, or certificate"></label><label class="wide">Attach evidence<input type="file" name="evidence_file" accept="application/pdf,image/jpeg,image/png,image/webp"><small>Protected PDF or image, maximum 8 MB.</small></label><label class="wide">Specific reason<textarea name="reason" required placeholder="Explain why this transaction is required and where the stock is going."></textarea></label></div>

<section class="inventory-lines-section" data-standard-lines><div class="section-heading minor"><div><h3>Transaction items</h3><p>Scan or search an item. The earliest valid expiry is suggested automatically.</p></div><div class="inventory-line-tools"><input type="search" data-inventory-scan placeholder="Scan SKU / barcode then press Enter"><button type="button" class="btn ghost" data-add-inventory-line>+ Add item</button></div></div><div class="table-wrap"><table class="inventory-entry-table"><thead><tr><th>Item</th><th data-col="lot">Lot / FEFO</th><th data-col="quantity">Quantity</th><th data-col="count">Counted quantity</th><th data-col="cost">Unit cost</th><th>Stock impact</th><th>Note</th><th></th></tr></thead><tbody data-inventory-lines></tbody><tfoot><tr><td colspan="8"><div class="inventory-impact-total"><span data-impact-label>Projected transaction</span><strong data-inventory-total>0 item lines</strong></div></td></tr></tfoot></table></div></section>
<section class="inventory-lines-section" data-return-lines hidden><div class="section-heading minor"><div><h3>Returnable issue lines</h3><p>Enter only the unused quantity physically returned. The original lot and issue remain linked.</p></div></div><div class="table-wrap"><table class="inventory-entry-table"><thead><tr><th>Item</th><th>Original lot</th><th>Issued</th><th>Already returned</th><th>Return now</th><th>Note</th></tr></thead><tbody data-return-line-body><tr><td colspan="6" class="muted">Select the original posted issue.</td></tr></tbody></table></div></section>
<div class="inventory-form-actions"><button type="submit" name="submit_mode" value="draft" class="btn ghost">Save complete draft</button><button type="submit" name="submit_mode" value="submit" class="btn primary" data-inventory-submit>Submit stock issue</button></div></form></dialog>

<template id="inventory-line-template"><tr data-inventory-line><td><div class="inventory-item-picker" data-inventory-item-picker><input type="search" data-item-query placeholder="Search SKU, barcode, or item" autocomplete="off"><input type="hidden" name="item_id[]" data-item-id><div class="remote-patient-results" data-item-results hidden></div><small data-item-meta>Choose an item.</small></div></td><td data-cell="lot"><select name="lot_id[]" data-lot-select><option value="">Choose an item first</option></select><small data-lot-meta></small></td><td data-cell="quantity"><input type="number" min="0" step="0.01" name="quantity[]" data-line-quantity></td><td data-cell="count"><input type="number" min="0" step="0.01" name="counted_quantity[]" data-line-count></td><td data-cell="cost"><input type="number" min="0" step="0.01" name="unit_cost[]" data-line-cost></td><td><strong data-line-impact>—</strong><small data-line-warning></small></td><td><input name="line_notes[]" placeholder="Optional"></td><td><button type="button" class="icon-btn" data-remove-inventory-line aria-label="Remove line">×</button></td></tr></template>

<?php if($managedDocument&&$managedDocument['status']==='approved'&&$managedDocument['document_type']==='purchase_order'&&(int)$managedDocument['approved_by']!==$userId):?><dialog id="receipt-form" class="wide-dialog"><form method="post" class="form-grid"><button type="button" class="dialog-close" data-close>×</button><h2 class="wide">Receive against <?=e((string)$managedDocument['document_no'])?></h2><input type="hidden" name="csrf" value="<?=e(csrf_token())?>"><input type="hidden" name="action" value="create_receipt"><input type="hidden" name="purchase_order_id" value="<?=(int)$managedDocument['id']?>"><input type="hidden" name="branch_id" value="<?=(int)$managedDocument['branch_id']?>"><label>Delivery / invoice reference<input name="external_reference" required></label><label>Receiving evidence<input name="evidence_reference" required></label><label class="wide">Receiving note<textarea name="reason">Goods received against <?=e((string)$managedDocument['document_no'])?></textarea></label><div class="wide table-wrap"><table><thead><tr><th>Item</th><th>Ordered</th><th>Receive now</th><th>Supplier lot</th><th>Expiry</th></tr></thead><tbody><?php foreach($managedLines as $line):?><tr><td><?=e((string)$line['item_name'])?></td><td><?=e((string)$line['quantity'])?> <?=e((string)$line['unit'])?></td><td><input type="number" min="0" max="<?=e((string)$line['quantity'])?>" step="0.01" name="receive_quantity[<?=(int)$line['id']?>]"></td><td><input name="lot_number[<?=(int)$line['id']?>]"></td><td><input type="date" name="expiry_date[<?=(int)$line['id']?>]"></td></tr><?php endforeach;?></tbody></table></div><button class="btn primary">Post goods receipt</button></form></dialog><?php endif;?>
<?php render_footer();
