diff --git a/app/assets/utils/classes/BarcodeManager.php b/app/assets/utils/classes/BarcodeManager.php new file mode 100644 index 0000000..09ab4c0 --- /dev/null +++ b/app/assets/utils/classes/BarcodeManager.php @@ -0,0 +1,762 @@ +pdo = $pdo; + $this->company_id = $company_id; + } + + // ───────────────────────────────────────────────────────────── + // Private helpers + // ───────────────────────────────────────────────────────────── + + private function stockTableName(int $warehouse_id): string { + return 'td_stock_' . $warehouse_id; + } + + /** + * Fetch a single active product row by SKU. + */ + private function findProduct(string $sku): array|false { + $sth = $this->pdo->prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND sku = :sku + AND status > 0 + LIMIT 1" + ); + $sth->execute([':company_id' => $this->company_id, ':sku' => $sku]); + return $sth->fetch(PDO::FETCH_ASSOC); + } + + /** + * When a serial is provided, verify it does not already map to a different lot + * across any active warehouse stock table. + * + * Throws if the serial is claimed by a different lot, otherwise returns silently. + */ + private function validateLotSerial(string $sku, string $lot, string $serial): void { + if ($serial === '') return; + + $warehouses = $this->pdo->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $this->company_id]); + + foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { + $table = $this->stockTableName((int)$warehouse_id); + $sth = $this->pdo->prepare( + "SELECT lot_number + FROM `{$table}` + WHERE company_id = :company_id + AND product_sku = :sku + AND serial_number = :serial + AND status != -1 + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':sku' => $sku, + ':serial' => $serial, + ]); + $existing_lot = $sth->fetchColumn(); + if ($existing_lot !== false && (string)$existing_lot !== $lot) { + throw new Exception('Serial number belongs to a different lot.'); + } + } + } + + /** + * Fetch a barcode row from md_barcode by barcode string and type. + * + * Returns false if the row does not exist. + * Throws if the row exists but is disabled (status != 1). + */ + private function findPreparedLabel(string $barcode, string $type): array|false { + $sth = $this->pdo->prepare( + "SELECT * + FROM md_barcode + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = :barcode_type + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':barcode' => $barcode, + ':barcode_type' => $type, + ]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + if ($row && (int)$row['status'] !== 1) { + throw new Exception('Barcode is disabled.'); + } + return $row ?: false; + } + + /** + * Confirm a rack row exists for the given warehouse + zone + aisle + rack. + * Throws if not found. + */ + private function validateLocation(int $warehouse_id, string $zone, string $aisle, string $rack): void { + $sth = $this->pdo->prepare( + "SELECT 1 FROM md_rack + WHERE company_id = :company_id + AND warehouse = :warehouse + AND zone = :zone + AND aisle = :aisle + AND rack = :rack + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':warehouse' => $warehouse_id, + ':zone' => $zone, + ':aisle' => $aisle, + ':rack' => $rack, + ]); + if (!$sth->fetchColumn()) throw new Exception('Location not found.'); + } + + /** + * Check whether any warehouse stock table has a positive balance for a given SKU + lot. + * Used as a fallback when a lot row is missing from md_lot. + */ + private function activeStockLotExists(string $sku, string $lot): bool { + $warehouses = $this->pdo->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $this->company_id]); + + foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { + $table = $this->stockTableName((int)$warehouse_id); + $sth = $this->pdo->prepare( + "SELECT ROUND(SUM(COALESCE(`in`, 0)) - SUM(COALESCE(`out`, 0)), 2) AS balance + FROM `{$table}` + WHERE company_id = :company_id + AND product_sku = :product_sku + AND lot_number = :lot_number + AND status = 1" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':product_sku' => $sku, + ':lot_number' => $lot, + ]); + if ((float)$sth->fetchColumn() > 0) return true; + } + return false; + } + + /** + * Look up a lot by SKU + lot number. + * + * Tries md_lot first; falls back to synthesising a row from active stock + * (handles the case where stock was entered without an md_lot record). + * Returns false if neither source yields a result. + */ + private function findLot(string $sku, string $lot): array|false { + $sth = $this->pdo->prepare( + "SELECT + l.product_sku, + l.lot_number, + l.expiry_date, + p.product_name, + p.uom + FROM md_lot l + INNER JOIN md_product p + ON p.company_id = l.company_id + AND p.sku = l.product_sku + WHERE l.company_id = :company_id + AND l.product_sku = :product_sku + AND l.lot_number = :lot_number + AND p.status > 0 + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':product_sku' => $sku, + ':lot_number' => $lot, + ]); + $row = $sth->fetch(PDO::FETCH_ASSOC); + if ($row) return $row; + + $product = $this->findProduct($sku); + if (!$product || !$this->activeStockLotExists($sku, $lot)) return false; + + return [ + 'product_sku' => $product['sku'], + 'lot_number' => $lot, + 'expiry_date' => null, + 'product_name' => $product['product_name'], + 'uom' => $product['uom'], + ]; + } + + // ───────────────────────────────────────────────────────────── + // SKU LABEL BASIS + // ───────────────────────────────────────────────────────────── + + /** + * Return all SKU barcode labels, optionally filtered by SKU and/or lot. + * + * Joined to md_product for product_name. Ordered newest first. + * + * @param string $sku Filter by product SKU (empty = all). + * @param string $lot Filter by lot number (empty = all). + * @return array + */ + public function listSkuLabels(string $sku = '', string $lot = ''): array { + $params = [':company_id' => $this->company_id]; + $where = 'WHERE b.company_id = :company_id'; + + if ($sku !== '') { + $where .= ' AND b.product_sku = :product_sku'; + $params[':product_sku'] = $sku; + } + if ($lot !== '') { + $where .= ' AND b.lot_number = :lot_number'; + $params[':lot_number'] = $lot; + } + + $sth = $this->pdo->prepare( + "SELECT + b.id, + b.product_sku, + b.lot_number, + b.serial_number, + b.barcode, + b.status, + b.print_count, + b.last_printed_dt, + b.created_dt, + p.product_name + FROM md_barcode b + LEFT JOIN md_product p + ON p.company_id = b.company_id + AND p.sku = b.product_sku + {$where} + AND b.barcode_type = 'sku' + ORDER BY b.created_dt DESC, b.id DESC" + ); + $sth->execute($params); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Create a new SKU barcode label (or re-activate a previously disabled one). + * + * Validates that the lot exists (or active stock exists for it). + * When $allowUnlisted is true, skips the lot requirement and only checks + * that the product SKU is valid — intended for pre-labelling before goods arrive. + * + * @param string $sku Product SKU. + * @param string $lot Lot number. + * @param string $serial Serial number (unique within lot/sku). + * @param bool $allowUnlisted Allow creating a label for a lot not yet in md_lot or stock. + * @return array Keys: product_sku, product_name, lot_number, serial_number, barcode. + * @throws Exception On validation failure. + */ + public function createSkuLabel(string $sku, string $lot, string $serial, bool $allowUnlisted = false): array { + $lot_row = $this->findLot($sku, $lot); + + if (!$lot_row) { + if (!$allowUnlisted) throw new Exception('Lot not found.'); + + $product = $this->findProduct($sku); + if (!$product) throw new Exception('SKU not found.'); + + $lot_row = [ + 'product_sku' => $product['sku'], + 'lot_number' => $lot, + 'expiry_date' => null, + 'product_name' => $product['product_name'], + 'uom' => $product['uom'], + ]; + } + + $barcode = "SKU|{$sku}|{$lot}|{$serial}"; + + $this->pdo->prepare( + "INSERT INTO md_barcode + (company_id, barcode_type, barcode, product_sku, lot_number, serial_number) + VALUES + (:company_id, 'sku', :barcode, :product_sku, :lot_number, :serial_number) + ON DUPLICATE KEY UPDATE + barcode_type = VALUES(barcode_type), + product_sku = VALUES(product_sku), + lot_number = VALUES(lot_number), + serial_number = VALUES(serial_number), + status = 1" + )->execute([ + ':company_id' => $this->company_id, + ':barcode' => $barcode, + ':product_sku' => $sku, + ':lot_number' => $lot, + ':serial_number' => $serial, + ]); + + return [ + 'product_sku' => $sku, + 'product_name' => $lot_row['product_name'], + 'lot_number' => $lot, + 'serial_number' => $serial, + 'barcode' => $barcode, + ]; + } + + // ───────────────────────────────────────────────────────────── + // LOCATION LABEL BASIS + // ───────────────────────────────────────────────────────────── + + /** + * Return all location barcode labels for this company. + * + * @return array + */ + public function listLocationLabels(): array { + $sth = $this->pdo->prepare( + "SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt + FROM md_barcode + WHERE company_id = :company_id + AND barcode_type = 'loc'" + ); + $sth->execute([':company_id' => $this->company_id]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Create a new location barcode label (or re-activate a previously disabled one). + * + * Validates that the rack exists in md_rack before inserting. + * + * @param string $barcode Full barcode string (e.g. "LOC|1|A|1|R1"). + * @param int $warehouse_id Warehouse ID. + * @param string $zone Zone code. + * @param string $aisle Aisle code. + * @param string $rack Rack code. + * @return array Keys: barcode, warehouse_id, zone, aisle, rack. + * @throws Exception If the location is not found in md_rack. + */ + public function createLocationLabel(string $barcode, int $warehouse_id, string $zone, string $aisle, string $rack): array { + $this->validateLocation($warehouse_id, $zone, $aisle, $rack); + + $this->pdo->prepare( + "INSERT INTO md_barcode + (company_id, barcode_type, barcode, warehouse_id, zone, aisle, rack) + VALUES + (:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack) + ON DUPLICATE KEY UPDATE + barcode_type = VALUES(barcode_type), + warehouse_id = VALUES(warehouse_id), + zone = VALUES(zone), + aisle = VALUES(aisle), + rack = VALUES(rack), + status = 1" + )->execute([ + ':company_id' => $this->company_id, + ':barcode' => $barcode, + ':warehouse_id' => $warehouse_id, + ':zone' => $zone, + ':aisle' => $aisle, + ':rack' => $rack, + ]); + + return [ + 'barcode' => $barcode, + 'warehouse_id' => $warehouse_id, + 'zone' => $zone, + 'aisle' => $aisle, + 'rack' => $rack, + ]; + } + + // ───────────────────────────────────────────────────────────── + // SHARED LABEL OPERATIONS + // ───────────────────────────────────────────────────────────── + + /** + * Increment print_count and update last_printed_dt for an active barcode. + * + * @param string $barcode Barcode string. + * @param string $type 'sku' or 'loc'. + */ + public function recordPrint(string $barcode, string $type): void { + $this->pdo->prepare( + "UPDATE md_barcode + SET print_count = print_count + 1, + last_printed_dt = NOW() + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = :barcode_type + AND status = 1" + )->execute([ + ':company_id' => $this->company_id, + ':barcode' => $barcode, + ':barcode_type' => $type, + ]); + } + + /** + * Set the status of a barcode label (0 = disabled, 1 = active). + * + * @param string $barcode Barcode string. + * @param string $type 'sku' or 'loc'. + * @param int $status Target status value. + */ + public function setStatus(string $barcode, string $type, int $status): void { + $this->pdo->prepare( + "UPDATE md_barcode + SET status = :status + WHERE company_id = :company_id + AND barcode = :barcode + AND barcode_type = :barcode_type" + )->execute([ + ':company_id' => $this->company_id, + ':barcode' => $barcode, + ':barcode_type' => $type, + ':status' => $status, + ]); + } + + // ───────────────────────────────────────────────────────────── + // LOOKUP / RESOLUTION BASIS + // ───────────────────────────────────────────────────────────── + + /** + * Resolve a barcode string to its type and fields. + * + * Dispatch order: + * 1. SKU| prefix → validated SKU label + * 2. LOC| prefix → validated location label + * 3. Fallback → raw product barcode or SKU field match in md_product + * + * Returns an array with 'type' set to 'sku', 'loc', or 'raw' plus + * the type-specific fields. + * + * The $advancedLocation flag controls how LOC barcodes are parsed; + * resolve it from the caller via CompanySettingManager::get('advanced_location'). + * + * @param string $barcode Full barcode string to resolve. + * @param bool $advancedLocation True = zone/aisle/rack parts; false = rack only. + * @return array + * @throws Exception On parse or validation failure. + */ + public function lookup(string $barcode, bool $advancedLocation): array { + $parts = explode('|', $barcode); + $label_type = strtoupper($parts[0] ?? ''); + + if ($label_type === 'SKU') { + if (count($parts) !== 4) throw new Exception('Invalid SKU label format.'); + [, $sku, $lot, $serial] = $parts; + $sku = trim($sku); + $lot = trim($lot); + $serial = trim($serial); + if ($sku === '' || $lot === '' || $serial === '') { + throw new Exception('SKU, lot, and serial are required.'); + } + + $label = $this->findPreparedLabel($barcode, 'sku'); + if (!$label) throw new Exception('SKU barcode label was not prepared.'); + if ( + $label['product_sku'] !== $sku || + $label['lot_number'] !== $lot || + $label['serial_number'] !== $serial + ) { + throw new Exception('SKU barcode label does not match its registry.'); + } + + $product = $this->findProduct($sku); + if (!$product) throw new Exception('SKU not found.'); + $this->validateLotSerial($sku, $lot, $serial); + + return [ + 'type' => 'sku', + 'sku' => $product['sku'], + 'product_name' => $product['product_name'], + 'uom' => $product['uom'] ?? 'pcs', + 'lot_number' => $lot, + 'serial_number' => $serial, + ]; + } + + if ($label_type === 'LOC') { + if ($advancedLocation && count($parts) !== 5) throw new Exception('Invalid advanced location label format.'); + if (!$advancedLocation && count($parts) !== 3) throw new Exception('Invalid location label format.'); + + $warehouse_id = (int)($parts[1] ?? 0); + if ($advancedLocation) { + $zone = trim($parts[2]); + $aisle = trim($parts[3]); + $rack = trim($parts[4]); + } else { + $rack = trim($parts[2]); + $zone = $rack; + $aisle = $rack; + } + if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') { + throw new Exception('Location label is incomplete.'); + } + + $label = $this->findPreparedLabel($barcode, 'loc'); + if (!$label) throw new Exception('Location barcode label was not prepared.'); + if ( + (int)$label['warehouse_id'] !== $warehouse_id || + $label['zone'] !== $zone || + $label['aisle'] !== $aisle || + $label['rack'] !== $rack + ) { + throw new Exception('Location barcode label does not match its registry.'); + } + + $this->validateLocation($warehouse_id, $zone, $aisle, $rack); + + return [ + 'type' => 'loc', + 'warehouse_id' => $warehouse_id, + 'zone' => $zone, + 'aisle' => $aisle, + 'rack' => $rack, + ]; + } + + // Fallback: match against md_product's barcode or sku fields + $sth = $this->pdo->prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND (barcode = :barcode OR sku = :sku) + AND status > 0 + ORDER BY (barcode = :barcode_order) DESC + LIMIT 1" + ); + $sth->execute([ + ':company_id' => $this->company_id, + ':barcode' => $barcode, + ':sku' => $barcode, + ':barcode_order' => $barcode, + ]); + $product = $sth->fetch(PDO::FETCH_ASSOC); + + if (!$product) throw new Exception('Barcode not found.'); + + return [ + 'type' => 'raw', + 'sku' => $product['sku'], + 'product_name' => $product['product_name'], + 'uom' => $product['uom'] ?? 'pcs', + ]; + } + + // ───────────────────────────────────────────────────────────── + // DROPDOWN BASIS + // ───────────────────────────────────────────────────────────── + + /** + * Return a merged list of lots for the SKU label creation UI. + * + * Combines three sources in priority order, deduplicating by SKU+lot: + * 1. md_lot rows (canonical lot master) + * 2. Active stock rows whose lot is not in md_lot + * 3. Existing barcode rows whose lot is not covered by 1 or 2 + * + * Each row is augmented with barcode_active_count and barcode_disabled_count. + * Sorted by product_sku → lot_number. + * + * @return array + */ + public function getLabelLots(): array { + $sth = $this->pdo->prepare( + "SELECT + l.id, + l.product_sku, + l.lot_number, + l.expiry_date, + l.description, + p.product_name, + p.uom, + 'md_lot' AS source + FROM md_lot l + INNER JOIN md_product p + ON p.company_id = l.company_id + AND p.sku = l.product_sku + WHERE l.company_id = :company_id + AND p.status > 0 + ORDER BY l.product_sku ASC, l.lot_number ASC" + ); + $sth->execute([':company_id' => $this->company_id]); + $rows = $sth->fetchAll(PDO::FETCH_ASSOC); + $seen = []; + foreach ($rows as $row) { + $seen[$row['product_sku'] . '|' . $row['lot_number']] = true; + } + + // Supplement from active stock tables + $warehouses = $this->pdo->prepare( + "SELECT id FROM md_warehouse + WHERE company_id = :company_id AND status = 1" + ); + $warehouses->execute([':company_id' => $this->company_id]); + + foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { + $table = $this->stockTableName((int)$warehouse_id); + $stock = $this->pdo->prepare( + "SELECT + s.product_sku, + s.lot_number, + p.product_name, + p.uom, + ROUND(SUM(COALESCE(s.`in`, 0)) - SUM(COALESCE(s.`out`, 0)), 2) AS balance + FROM `{$table}` s + INNER JOIN md_product p + ON p.company_id = s.company_id + AND p.sku = s.product_sku + WHERE s.company_id = :company_id + AND s.status = 1 + AND s.lot_number IS NOT NULL + AND s.lot_number <> '' + AND p.status > 0 + GROUP BY s.product_sku, s.lot_number, p.product_name, p.uom + HAVING balance > 0" + ); + $stock->execute([':company_id' => $this->company_id]); + + foreach ($stock->fetchAll(PDO::FETCH_ASSOC) as $row) { + $key = $row['product_sku'] . '|' . $row['lot_number']; + if (isset($seen[$key])) continue; + $seen[$key] = true; + $rows[] = [ + 'id' => null, + 'product_sku' => $row['product_sku'], + 'lot_number' => $row['lot_number'], + 'expiry_date' => null, + 'description' => 'Found in active stock; missing md_lot row.', + 'product_name' => $row['product_name'], + 'uom' => $row['uom'], + 'source' => 'active_stock', + ]; + } + } + + // Supplement from existing barcode rows + $barcodes = $this->pdo->prepare( + "SELECT + b.product_sku, + b.lot_number, + b.serial_number, + b.barcode, + b.status, + b.created_dt, + p.product_name, + p.uom + FROM md_barcode b + INNER JOIN md_product p + ON p.company_id = b.company_id + AND p.sku = b.product_sku + WHERE b.company_id = :company_id + AND b.barcode_type = 'sku' + AND b.product_sku IS NOT NULL AND b.product_sku <> '' + AND b.lot_number IS NOT NULL AND b.lot_number <> '' + AND p.status > 0 + ORDER BY b.created_dt DESC, b.id DESC" + ); + $barcodes->execute([':company_id' => $this->company_id]); + + foreach ($barcodes->fetchAll(PDO::FETCH_ASSOC) as $row) { + $key = $row['product_sku'] . '|' . $row['lot_number']; + if (isset($seen[$key])) continue; + $seen[$key] = true; + $rows[] = [ + 'id' => null, + 'product_sku' => $row['product_sku'], + 'lot_number' => $row['lot_number'], + 'expiry_date' => null, + 'description' => 'Prepared barcode exists; missing md_lot row.', + 'product_name' => $row['product_name'], + 'uom' => $row['uom'], + 'source' => 'md_barcode', + 'serial_number' => $row['serial_number'], + 'barcode' => $row['barcode'], + ]; + } + + // Attach per-lot label counts + $status_rows = $this->pdo->prepare( + "SELECT + product_sku, + lot_number, + SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS active_count, + SUM(CASE WHEN status <> 1 THEN 1 ELSE 0 END) AS disabled_count + FROM md_barcode + WHERE company_id = :company_id + AND barcode_type = 'sku' + AND product_sku IS NOT NULL AND product_sku <> '' + AND lot_number IS NOT NULL AND lot_number <> '' + GROUP BY product_sku, lot_number" + ); + $status_rows->execute([':company_id' => $this->company_id]); + + $label_status = []; + foreach ($status_rows->fetchAll(PDO::FETCH_ASSOC) as $row) { + $label_status[$row['product_sku'] . '|' . $row['lot_number']] = [ + 'active_count' => (int)$row['active_count'], + 'disabled_count' => (int)$row['disabled_count'], + ]; + } + + foreach ($rows as &$row) { + $status = $label_status[$row['product_sku'] . '|' . $row['lot_number']] ?? null; + $row['barcode_active_count'] = $status['active_count'] ?? 0; + $row['barcode_disabled_count'] = $status['disabled_count'] ?? 0; + } + unset($row); + + usort($rows, fn($a, $b) => [$a['product_sku'], $a['lot_number']] <=> [$b['product_sku'], $b['lot_number']]); + + return $rows; + } + + /** + * Return all active products for the label creation product dropdown. + * + * @return array Each row: sku, product_name, uom. + */ + public function getLabelProducts(): array { + $sth = $this->pdo->prepare( + "SELECT sku, product_name, uom + FROM md_product + WHERE company_id = :company_id + AND status > 0 + ORDER BY sku ASC" + ); + $sth->execute([':company_id' => $this->company_id]); + return $sth->fetchAll(PDO::FETCH_ASSOC); + } +} diff --git a/app/ics/api/engine/barcode_lookup.php b/app/ics/api/engine/barcode_lookup.php index 089faae..1418b89 100644 --- a/app/ics/api/engine/barcode_lookup.php +++ b/app/ics/api/engine/barcode_lookup.php @@ -1,6 +1,8 @@ prepare( - "SELECT sku, product_name, uom - FROM md_product - WHERE company_id = :company_id - AND sku = :sku - AND status > 0 - LIMIT 1" - ); - $sth->execute([':company_id' => $company_id, ':sku' => $sku]); - return $sth->fetch(PDO::FETCH_ASSOC); - } - - function validateLotSerial(PDO $pdo, int $company_id, string $sku, string $lot, string $serial): void { - if ($serial === '') return; - - $warehouses = $pdo->prepare( - "SELECT id FROM md_warehouse - WHERE company_id = :company_id AND status = 1" - ); - $warehouses->execute([':company_id' => $company_id]); - - foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { - $table = 'td_stock_' . (int)$warehouse_id; - $sth = $pdo->prepare( - "SELECT lot_number - FROM `{$table}` - WHERE company_id = :company_id - AND product_sku = :sku - AND serial_number = :serial - AND status != -1 - LIMIT 1" - ); - $sth->execute([':company_id' => $company_id, ':sku' => $sku, ':serial' => $serial]); - $existing_lot = $sth->fetchColumn(); - if ($existing_lot !== false && (string)$existing_lot !== $lot) { - throw new Exception('Serial number belongs to a different lot.'); - } - } - } - - function findPreparedLabel(PDO $pdo, int $company_id, string $barcode, string $type): array|false { - $sth = $pdo->prepare( - "SELECT * - FROM md_barcode - WHERE company_id = :company_id - AND barcode = :barcode - AND barcode_type = :barcode_type - LIMIT 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':barcode' => $barcode, - ':barcode_type' => $type, - ]); - $row = $sth->fetch(PDO::FETCH_ASSOC); - if ($row && (int)$row['status'] !== 1) { - throw new Exception('Barcode is disabled.'); - } - return $row ?: false; - } - - function advancedLocation(PDO $pdo, int $company_id): bool { - $sth = $pdo->prepare( - "SELECT value FROM company_setting - WHERE company_id = :company_id AND setting_key = 'advanced_location' - LIMIT 1" - ); - $sth->execute([':company_id' => $company_id]); - $value = $sth->fetchColumn(); - return (int)($value === false ? 0 : $value) === 1; - } - - function validateLocation(PDO $pdo, int $company_id, int $warehouse_id, string $zone, string $aisle, string $rack): void { - $sth = $pdo->prepare( - "SELECT 1 FROM md_rack - WHERE company_id = :company_id - AND warehouse = :warehouse - AND zone = :zone - AND aisle = :aisle - AND rack = :rack - LIMIT 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':warehouse' => $warehouse_id, - ':zone' => $zone, - ':aisle' => $aisle, - ':rack' => $rack, - ]); - if (!$sth->fetchColumn()) throw new Exception('Location not found.'); - } - - $parts = explode('|', $barcode); - $label_type = strtoupper($parts[0] ?? ''); + $csm = new CompanySettingManager($pdo1, $company_id); + $advanced_location = (bool)$csm->get('advanced_location'); + $bm = new BarcodeManager($pdo2, $company_id); try { - if ($label_type === 'SKU') { - if (count($parts) !== 4) throw new Exception('Invalid SKU label format.'); - $sku = trim($parts[1]); - $lot = trim($parts[2]); - $serial = trim($parts[3]); - if ($sku === '' || $lot === '' || $serial === '') throw new Exception('SKU, lot, and serial are required.'); - - $label = findPreparedLabel($pdo2, $company_id, $barcode, 'sku'); - if (!$label) throw new Exception('SKU barcode label was not prepared.'); - if ($label['product_sku'] !== $sku || $label['lot_number'] !== $lot || $label['serial_number'] !== $serial) { - throw new Exception('SKU barcode label does not match its registry.'); - } - - $product = findProduct($pdo2, $company_id, $sku); - if (!$product) throw new Exception('SKU not found.'); - validateLotSerial($pdo2, $company_id, $sku, $lot, $serial); - - $answer['success'] = 1; - $answer['type'] = 'sku'; - $answer['sku'] = $product['sku']; - $answer['product_name'] = $product['product_name']; - $answer['uom'] = $product['uom'] ?? 'pcs'; - $answer['lot_number'] = $lot; - $answer['serial_number'] = $serial; - exit(json_encode($answer)); - } - - if ($label_type === 'LOC') { - $advanced = advancedLocation($pdo1, $company_id); - if ($advanced && count($parts) !== 5) throw new Exception('Invalid advanced location label format.'); - if (!$advanced && count($parts) !== 3) throw new Exception('Invalid location label format.'); - - $warehouse_id = (int)($parts[1] ?? 0); - if ($advanced) { - $zone = trim($parts[2]); - $aisle = trim($parts[3]); - $rack = trim($parts[4]); - } else { - $rack = trim($parts[2]); - $zone = $rack; - $aisle = $rack; - } - if (!$warehouse_id || $zone === '' || $aisle === '' || $rack === '') throw new Exception('Location label is incomplete.'); - - $label = findPreparedLabel($pdo2, $company_id, $barcode, 'loc'); - if (!$label) throw new Exception('Location barcode label was not prepared.'); - if ((int)$label['warehouse_id'] !== $warehouse_id || $label['zone'] !== $zone || $label['aisle'] !== $aisle || $label['rack'] !== $rack) { - throw new Exception('Location barcode label does not match its registry.'); - } - - validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack); - - $answer['success'] = 1; - $answer['type'] = 'loc'; - $answer['warehouse_id'] = $warehouse_id; - $answer['zone'] = $zone; - $answer['aisle'] = $aisle; - $answer['rack'] = $rack; - exit(json_encode($answer)); - } + $result = $bm->lookup($barcode, $advanced_location); + $answer['success'] = 1; + $answer = array_merge($answer, $result); } catch (Exception $e) { $answer['message'] = $e->getMessage(); - http_response_code(400); - exit(json_encode($answer)); + $http_code = ($e->getMessage() === 'Barcode not found.') ? 404 : 400; + http_response_code($http_code); } - $sth = $pdo2->prepare( - "SELECT sku, product_name, uom - FROM md_product - WHERE company_id = :company_id - AND (barcode = :barcode OR sku = :sku) - AND status > 0 - ORDER BY (barcode = :barcode_order) DESC - LIMIT 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':barcode' => $barcode, - ':sku' => $barcode, - ':barcode_order' => $barcode, - ]); - $product = $sth->fetch(PDO::FETCH_ASSOC); - - if (!$product) { - $answer['message'] = 'Barcode not found.'; - http_response_code(404); - exit(json_encode($answer)); - } - - $answer['success'] = 1; - $answer['type'] = 'raw'; - $answer['sku'] = $product['sku']; - $answer['product_name'] = $product['product_name']; - $answer['uom'] = $product['uom'] ?? 'pcs'; exit(json_encode($answer)); ?> diff --git a/app/ics/api/engine/location_barcode_label.php b/app/ics/api/engine/location_barcode_label.php index 71eb837..5d95444 100644 --- a/app/ics/api/engine/location_barcode_label.php +++ b/app/ics/api/engine/location_barcode_label.php @@ -1,30 +1,7 @@ prepare( - "SELECT 1 FROM md_rack - WHERE company_id = :company_id - AND warehouse = :warehouse - AND zone = :zone - AND aisle = :aisle - AND rack = :rack - LIMIT 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':warehouse' => $warehouse_id, - ':zone' => $zone, - ':aisle' => $aisle, - ':rack' => $rack, - ]); - if (!$sth->fetchColumn()) { - $answer = ['success' => 0, 'message' => 'Location not found.']; - http_response_code(404); - exit(json_encode($answer)); - } - } + require '../../../assets/utils/classes/BarcodeManager.php'; $label_action = $data['label_action'] ?? 'create'; $barcode = trim($data['barcode'] ?? ''); @@ -37,17 +14,11 @@ require_role($user_role, ['owner', 'admin', 'staff']); } - if ($label_action === 'list') { - $sth = $pdo2->prepare( - "SELECT barcode, warehouse_id, zone, aisle, rack, status, print_count, last_printed_dt - FROM md_barcode - WHERE company_id = :company_id - AND barcode_type = 'loc'" - ); - $sth->execute([':company_id' => $company_id]); + $bm = new BarcodeManager($pdo2, $company_id); + if ($label_action === 'list') { $answer['success'] = 1; - $answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $answer['output'] = $bm->listLocationLabels(); exit(json_encode($answer)); } @@ -57,18 +28,7 @@ http_response_code(400); exit(json_encode($answer)); } - - $sth = $pdo2->prepare( - "UPDATE md_barcode - SET print_count = print_count + 1, - last_printed_dt = NOW() - WHERE company_id = :company_id - AND barcode = :barcode - AND barcode_type = 'loc' - AND status = 1" - ); - $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); - + $bm->recordPrint($barcode, 'loc'); $answer['success'] = 1; exit(json_encode($answer)); } @@ -79,17 +39,7 @@ http_response_code(400); exit(json_encode($answer)); } - - $sth = $pdo2->prepare( - "UPDATE md_barcode - SET status = 0 - WHERE company_id = :company_id - AND barcode = :barcode - AND barcode_type = 'loc' - AND status = 1" - ); - $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); - + $bm->setStatus($barcode, 'loc', 0); $answer['success'] = 1; exit(json_encode($answer)); } @@ -100,58 +50,26 @@ http_response_code(400); exit(json_encode($answer)); } - - $sth = $pdo2->prepare( - "UPDATE md_barcode - SET status = 1 - WHERE company_id = :company_id - AND barcode = :barcode - AND barcode_type = 'loc' - AND status <> 1" - ); - $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); - + $bm->setStatus($barcode, 'loc', 1); $answer['success'] = 1; exit(json_encode($answer)); } + // Default: create if ($barcode === '' || !$warehouse_id || $zone === '' || $aisle === '' || $rack === '') { $answer['message'] = 'Location barcode data is incomplete.'; http_response_code(400); exit(json_encode($answer)); } - validateLocation($pdo2, $company_id, $warehouse_id, $zone, $aisle, $rack); + try { + $output = $bm->createLocationLabel($barcode, $warehouse_id, $zone, $aisle, $rack); + $answer['success'] = 1; + $answer['output'] = $output; + } catch (Exception $e) { + $answer['message'] = $e->getMessage(); + http_response_code($e->getMessage() === 'Location not found.' ? 404 : 400); + } - $sth = $pdo2->prepare( - "INSERT INTO md_barcode - (company_id, barcode_type, barcode, warehouse_id, zone, aisle, rack) - VALUES - (:company_id, 'loc', :barcode, :warehouse_id, :zone, :aisle, :rack) - ON DUPLICATE KEY UPDATE - barcode_type = VALUES(barcode_type), - warehouse_id = VALUES(warehouse_id), - zone = VALUES(zone), - aisle = VALUES(aisle), - rack = VALUES(rack), - status = 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':barcode' => $barcode, - ':warehouse_id' => $warehouse_id, - ':zone' => $zone, - ':aisle' => $aisle, - ':rack' => $rack, - ]); - - $answer['success'] = 1; - $answer['output'] = [ - 'barcode' => $barcode, - 'warehouse_id' => $warehouse_id, - 'zone' => $zone, - 'aisle' => $aisle, - 'rack' => $rack, - ]; exit(json_encode($answer)); ?> diff --git a/app/ics/api/engine/sku_barcode_label.php b/app/ics/api/engine/sku_barcode_label.php index 49acedf..ca1eeec 100644 --- a/app/ics/api/engine/sku_barcode_label.php +++ b/app/ics/api/engine/sku_barcode_label.php @@ -1,127 +1,7 @@ prepare( - "SELECT sku, product_name, uom - FROM md_product - WHERE company_id = :company_id - AND sku = :sku - AND status > 0 - LIMIT 1" - ); - $sth->execute([':company_id' => $company_id, ':sku' => $sku]); - return $sth->fetch(PDO::FETCH_ASSOC); - } - - function activeStockLotExists(PDO $pdo, int $company_id, string $sku, string $lot): bool { - $warehouses = $pdo->prepare( - "SELECT id FROM md_warehouse - WHERE company_id = :company_id AND status = 1" - ); - $warehouses->execute([':company_id' => $company_id]); - - foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { - $table = stockTableNameFromWarehouseId((int)$warehouse_id); - $sth = $pdo->prepare( - "SELECT ROUND(SUM(COALESCE(`in`, 0)) - SUM(COALESCE(`out`, 0)), 2) AS balance - FROM `{$table}` - WHERE company_id = :company_id - AND product_sku = :product_sku - AND lot_number = :lot_number - AND status = 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':product_sku' => $sku, - ':lot_number' => $lot, - ]); - if ((float)$sth->fetchColumn() > 0) return true; - } - - return false; - } - - function findLot(PDO $pdo, int $company_id, string $sku, string $lot): array|false { - $sth = $pdo->prepare( - "SELECT - l.product_sku, - l.lot_number, - l.expiry_date, - p.product_name, - p.uom - FROM md_lot l - INNER JOIN md_product p - ON p.company_id = l.company_id - AND p.sku = l.product_sku - WHERE l.company_id = :company_id - AND l.product_sku = :product_sku - AND l.lot_number = :lot_number - AND p.status > 0 - LIMIT 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':product_sku' => $sku, - ':lot_number' => $lot, - ]); - $row = $sth->fetch(PDO::FETCH_ASSOC); - if ($row) return $row; - - $product = findProduct($pdo, $company_id, $sku); - if (!$product || !activeStockLotExists($pdo, $company_id, $sku, $lot)) return false; - - return [ - 'product_sku' => $product['sku'], - 'lot_number' => $lot, - 'expiry_date' => null, - 'product_name' => $product['product_name'], - 'uom' => $product['uom'], - ]; - } - - function listLabels(PDO $pdo, int $company_id, string $sku, string $lot): array { - $params = [':company_id' => $company_id]; - $where = "WHERE b.company_id = :company_id"; - - if ($sku !== '') { - $where .= " AND b.product_sku = :product_sku"; - $params[':product_sku'] = $sku; - } - - if ($lot !== '') { - $where .= " AND b.lot_number = :lot_number"; - $params[':lot_number'] = $lot; - } - - $sth = $pdo->prepare( - "SELECT - b.id, - b.product_sku, - b.lot_number, - b.serial_number, - b.barcode, - b.status, - b.print_count, - b.last_printed_dt, - b.created_dt, - p.product_name - FROM md_barcode b - LEFT JOIN md_product p - ON p.company_id = b.company_id - AND p.sku = b.product_sku - {$where} - AND b.barcode_type = 'sku' - ORDER BY b.created_dt DESC, b.id DESC" - ); - $sth->execute($params); - return $sth->fetchAll(PDO::FETCH_ASSOC); - } + require '../../../assets/utils/classes/BarcodeManager.php'; $label_action = $data['label_action'] ?? 'read'; $product_sku = trim($data['product_sku'] ?? ''); @@ -131,6 +11,8 @@ require_role($user_role, ['owner', 'admin', 'staff']); } + $bm = new BarcodeManager($pdo2, $company_id); + if ($label_action === 'print') { $barcode = trim($data['barcode'] ?? ''); if ($barcode === '') { @@ -138,18 +20,7 @@ http_response_code(400); exit(json_encode($answer)); } - - $sth = $pdo2->prepare( - "UPDATE md_barcode - SET print_count = print_count + 1, - last_printed_dt = NOW() - WHERE company_id = :company_id - AND barcode = :barcode - AND barcode_type = 'sku' - AND status = 1" - ); - $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); - + $bm->recordPrint($barcode, 'sku'); $answer['success'] = 1; exit(json_encode($answer)); } @@ -161,23 +32,13 @@ http_response_code(400); exit(json_encode($answer)); } - - $sth = $pdo2->prepare( - "UPDATE md_barcode - SET status = 0 - WHERE company_id = :company_id - AND barcode = :barcode - AND barcode_type = 'sku' - AND status = 1" - ); - $sth->execute([':company_id' => $company_id, ':barcode' => $barcode]); - + $bm->setStatus($barcode, 'sku', 0); $answer['success'] = 1; exit(json_encode($answer)); } if ($label_action === 'create') { - $serial_number = trim($data['serial_number'] ?? ''); + $serial_number = trim($data['serial_number'] ?? ''); $allow_unlisted = !empty($data['allow_unlisted']); if ($product_sku === '' || $lot_number === '' || $serial_number === '') { @@ -186,64 +47,19 @@ exit(json_encode($answer)); } - $lot = findLot($pdo2, $company_id, $product_sku, $lot_number); - if (!$lot) { - if (!$allow_unlisted) { - $answer['message'] = 'Lot not found.'; - http_response_code(404); - exit(json_encode($answer)); - } - - $product = findProduct($pdo2, $company_id, $product_sku); - if (!$product) { - $answer['message'] = 'SKU not found.'; - http_response_code(404); - exit(json_encode($answer)); - } - - $lot = [ - 'product_sku' => $product['sku'], - 'lot_number' => $lot_number, - 'expiry_date' => null, - 'product_name' => $product['product_name'], - 'uom' => $product['uom'], - ]; + try { + $output = $bm->createSkuLabel($product_sku, $lot_number, $serial_number, $allow_unlisted); + $answer['success'] = 1; + $answer['output'] = $output; + } catch (Exception $e) { + $answer['message'] = $e->getMessage(); + http_response_code(($e->getMessage() === 'Lot not found.' || $e->getMessage() === 'SKU not found.') ? 404 : 400); } - $barcode = "SKU|{$product_sku}|{$lot_number}|{$serial_number}"; - - $sth = $pdo2->prepare( - "INSERT INTO md_barcode - (company_id, barcode_type, barcode, product_sku, lot_number, serial_number) - VALUES - (:company_id, 'sku', :barcode, :product_sku, :lot_number, :serial_number) - ON DUPLICATE KEY UPDATE - barcode_type = VALUES(barcode_type), - product_sku = VALUES(product_sku), - lot_number = VALUES(lot_number), - serial_number = VALUES(serial_number), - status = 1" - ); - $sth->execute([ - ':company_id' => $company_id, - ':product_sku' => $product_sku, - ':lot_number' => $lot_number, - ':serial_number' => $serial_number, - ':barcode' => $barcode, - ]); - - $answer['success'] = 1; - $answer['output'] = [ - 'product_sku' => $product_sku, - 'product_name' => $lot['product_name'], - 'lot_number' => $lot_number, - 'serial_number' => $serial_number, - 'barcode' => $barcode, - ]; exit(json_encode($answer)); } $answer['success'] = 1; - $answer['output'] = listLabels($pdo2, $company_id, $product_sku, $lot_number); + $answer['output'] = $bm->listSkuLabels($product_sku, $lot_number); exit(json_encode($answer)); ?> diff --git a/app/ics/api/engine/sku_label_lots.php b/app/ics/api/engine/sku_label_lots.php index ba56836..cac45f8 100644 --- a/app/ics/api/engine/sku_label_lots.php +++ b/app/ics/api/engine/sku_label_lots.php @@ -1,159 +1,11 @@ prepare( - "SELECT - l.id, - l.product_sku, - l.lot_number, - l.expiry_date, - l.description, - p.product_name, - p.uom, - 'md_lot' AS source - FROM md_lot l - INNER JOIN md_product p - ON p.company_id = l.company_id - AND p.sku = l.product_sku - WHERE l.company_id = :company_id - AND p.status > 0 - ORDER BY l.product_sku ASC, l.lot_number ASC" - ); - $sth->execute([':company_id' => $company_id]); - $rows = $sth->fetchAll(PDO::FETCH_ASSOC); - $seen = []; - - foreach ($rows as $row) { - $seen[$row['product_sku'] . '|' . $row['lot_number']] = true; - } - - $warehouses = $pdo2->prepare( - "SELECT id FROM md_warehouse - WHERE company_id = :company_id AND status = 1" - ); - $warehouses->execute([':company_id' => $company_id]); - - foreach ($warehouses->fetchAll(PDO::FETCH_COLUMN) as $warehouse_id) { - $table = stockTableNameFromWarehouseId((int)$warehouse_id); - $stock = $pdo2->query( - "SELECT - s.product_sku, - s.lot_number, - p.product_name, - p.uom, - ROUND(SUM(COALESCE(s.`in`, 0)) - SUM(COALESCE(s.`out`, 0)), 2) AS balance - FROM `{$table}` s - INNER JOIN md_product p - ON p.company_id = s.company_id - AND p.sku = s.product_sku - WHERE s.company_id = " . (int)$company_id . " - AND s.status = 1 - AND s.lot_number IS NOT NULL - AND s.lot_number <> '' - AND p.status > 0 - GROUP BY s.product_sku, s.lot_number, p.product_name, p.uom - HAVING balance > 0" - ); - - foreach ($stock->fetchAll(PDO::FETCH_ASSOC) as $row) { - $key = $row['product_sku'] . '|' . $row['lot_number']; - if (isset($seen[$key])) continue; - $seen[$key] = true; - $rows[] = [ - 'id' => null, - 'product_sku' => $row['product_sku'], - 'lot_number' => $row['lot_number'], - 'expiry_date' => null, - 'description' => 'Found in active stock; missing md_lot row.', - 'product_name'=> $row['product_name'], - 'uom' => $row['uom'], - 'source' => 'active_stock', - ]; - } - } - - $barcodes = $pdo2->prepare( - "SELECT - b.product_sku, - b.lot_number, - b.serial_number, - b.barcode, - b.status, - b.created_dt, - p.product_name, - p.uom - FROM md_barcode b - INNER JOIN md_product p - ON p.company_id = b.company_id - AND p.sku = b.product_sku - WHERE b.company_id = :company_id - AND b.barcode_type = 'sku' - AND b.product_sku IS NOT NULL - AND b.product_sku <> '' - AND b.lot_number IS NOT NULL - AND b.lot_number <> '' - AND p.status > 0 - ORDER BY b.created_dt DESC, b.id DESC" - ); - $barcodes->execute([':company_id' => $company_id]); - - foreach ($barcodes->fetchAll(PDO::FETCH_ASSOC) as $row) { - $key = $row['product_sku'] . '|' . $row['lot_number']; - if (isset($seen[$key])) continue; - $seen[$key] = true; - $rows[] = [ - 'id' => null, - 'product_sku' => $row['product_sku'], - 'lot_number' => $row['lot_number'], - 'expiry_date' => null, - 'description' => 'Prepared barcode exists; missing md_lot row.', - 'product_name' => $row['product_name'], - 'uom' => $row['uom'], - 'source' => 'md_barcode', - 'serial_number' => $row['serial_number'], - 'barcode' => $row['barcode'], - ]; - } - - $status_rows = $pdo2->prepare( - "SELECT - product_sku, - lot_number, - SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS active_count, - SUM(CASE WHEN status <> 1 THEN 1 ELSE 0 END) AS disabled_count - FROM md_barcode - WHERE company_id = :company_id - AND barcode_type = 'sku' - AND product_sku IS NOT NULL - AND product_sku <> '' - AND lot_number IS NOT NULL - AND lot_number <> '' - GROUP BY product_sku, lot_number" - ); - $status_rows->execute([':company_id' => $company_id]); - $label_status = []; - foreach ($status_rows->fetchAll(PDO::FETCH_ASSOC) as $row) { - $label_status[$row['product_sku'] . '|' . $row['lot_number']] = [ - 'active_count' => (int)$row['active_count'], - 'disabled_count' => (int)$row['disabled_count'], - ]; - } - - foreach ($rows as &$row) { - $status = $label_status[$row['product_sku'] . '|' . $row['lot_number']] ?? null; - $row['barcode_active_count'] = $status['active_count'] ?? 0; - $row['barcode_disabled_count'] = $status['disabled_count'] ?? 0; - } - unset($row); - - usort($rows, fn($a, $b) => [$a['product_sku'], $a['lot_number']] <=> [$b['product_sku'], $b['lot_number']]); + $bm = new BarcodeManager($pdo2, $company_id); $answer['success'] = 1; - $answer['output'] = $rows; + $answer['output'] = $bm->getLabelLots(); exit(json_encode($answer)); ?> diff --git a/app/ics/api/engine/sku_label_products.php b/app/ics/api/engine/sku_label_products.php index 4b0b1a0..23ed628 100644 --- a/app/ics/api/engine/sku_label_products.php +++ b/app/ics/api/engine/sku_label_products.php @@ -1,17 +1,11 @@ prepare( - "SELECT sku, product_name, uom - FROM md_product - WHERE company_id = :company_id - AND status > 0 - ORDER BY sku ASC" - ); - $sth->execute([':company_id' => $company_id]); + $bm = new BarcodeManager($pdo2, $company_id); $answer['success'] = 1; - $answer['output'] = $sth->fetchAll(PDO::FETCH_ASSOC); + $answer['output'] = $bm->getLabelProducts(); exit(json_encode($answer)); ?> diff --git a/app/include_master_sidebar.php b/app/include_master_sidebar.php index 0907074..43ad758 100644 --- a/app/include_master_sidebar.php +++ b/app/include_master_sidebar.php @@ -10,7 +10,7 @@ - + diff --git a/app/include_setting_sidebar.php b/app/include_setting_sidebar.php index fd4ea82..f652c1f 100644 --- a/app/include_setting_sidebar.php +++ b/app/include_setting_sidebar.php @@ -10,7 +10,7 @@ - +