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 @@
-
+