db = $db; } public function tableExists(): bool { return $this->db->tableExists($this->table); } public function normalizeMedicineName(string $name): string { $name = trim($name); // Collapse all whitespace to single space $name = preg_replace('/\s+/', ' ', $name) ?? $name; $name = mb_strtolower($name); // Remove space between number and dosage unit: "8 mg" → "8mg", "10 mcg" → "10mcg" // Covers: mg, mcg, iu, ml, g, kg, mmol, meq, units, tab, cap, drops, inj, puff $name = preg_replace( '/(\d)\s*(mcg|mmol|meq|units|drops|puffs|inj|tab|cap|ml|mg|iu|kg|g)\b/i', '$1$2', $name ) ?? $name; // Strip trailing dosage unit+number suffixes and all-trailing units // e.g. "zerodol th max 8mg" → "zerodol th max 8mg" → kept as-is but normalized // Collapse any double-spaces introduced $name = preg_replace('/\s{2,}/', ' ', $name) ?? $name; return trim($name); } /** * Compute a fuzzy key used for duplicate grouping. * Removes all dosage numbers and units so that "8mg", "8 mg", "8" all produce * the same key for the same base name. */ public function normalizeMedicineNameFuzzy(string $name): string { $name = $this->normalizeMedicineName($name); // Remove standalone numbers and dosage unit combos (e.g. "8mg", "10 mcg", "500") $name = preg_replace( '/\b\d+\.?\d*\s*(mcg|mmol|meq|units|drops|puffs|inj|tab|cap|ml|mg|iu|kg|g)?\b/i', ' ', $name ) ?? $name; // Remove punctuation $name = preg_replace('/[\/\-\(\)\[\]\.]+/', ' ', $name) ?? $name; $name = preg_replace('/\s{2,}/', ' ', $name) ?? $name; return trim($name); } /** * Extract strength tokens so near-identical brands with different strengths * (e.g. 20 MG vs 40 MG) are not treated as duplicates. */ private function extractStrengthSignature(string $name, string $formulation = ''): string { $source = trim($name . ' ' . $formulation); if ($source === '') { return ''; } $source = mb_strtolower($source); $source = preg_replace('/[\(\)\[\],\/\-]+/', ' ', $source) ?? $source; $source = preg_replace('/\s+/', ' ', $source) ?? $source; preg_match_all('/\b(\d+(?:\.\d+)?)\s*(mcg|mg|g|kg|ml|l|iu|units|meq|mmol)?\b/i', $source, $matches, PREG_SET_ORDER); if (empty($matches)) { return ''; } $tokens = []; foreach ($matches as $match) { $number = trim((string) ($match[1] ?? '')); if ($number === '') { continue; } $unit = strtolower(trim((string) ($match[2] ?? ''))); $tokens[] = $number . ($unit !== '' ? $unit : 'n'); } if (empty($tokens)) { return ''; } $tokens = array_values(array_unique($tokens)); sort($tokens, SORT_NATURAL); return implode('|', $tokens); } private function buildDuplicateKey(string $name, string $formulation = ''): string { $base = $this->normalizeMedicineNameFuzzy($name); if ($base === '') { return ''; } $normalizedFormulation = $this->normalizeMedicineName($formulation); if ($normalizedFormulation !== '') { $base .= '::f:' . $normalizedFormulation; } $strength = $this->extractStrengthSignature($name, $formulation); if ($strength === '') { return $base; } return $base . '::' . $strength; } public function findDuplicateByName(string $itemName, string $formulation = '', int $excludeId = 0): ?array { if (! $this->tableExists()) { return null; } $rows = $this->db->table($this->table) ->select('id,item_name,formulation') ->orderBy('id', 'ASC') ->get() ->getResultArray(); $needle = $this->buildDuplicateKey($itemName, $formulation); $needleFormulation = $this->normalizeMedicineName($formulation); if ($needle === '') { return null; } foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); if ($id <= 0 || ($excludeId > 0 && $id === $excludeId)) { continue; } $current = $this->buildDuplicateKey((string) ($row['item_name'] ?? ''), (string) ($row['formulation'] ?? '')); if ($current === '' || $current !== $needle) { continue; } $currentFormulation = $this->normalizeMedicineName((string) ($row['formulation'] ?? '')); if ($currentFormulation !== $needleFormulation) { continue; } return $row; } return null; } public function getDuplicateGroups(): array { if (! $this->tableExists()) { return []; } $rows = $this->db->table($this->table) ->select('id,item_name,formulation,genericname,company_name') ->orderBy('id', 'ASC') ->get() ->getResultArray(); $groups = []; foreach ($rows as $row) { $norm = $this->buildDuplicateKey((string) ($row['item_name'] ?? ''), (string) ($row['formulation'] ?? '')); if ($norm === '') { continue; } if (! isset($groups[$norm])) { $displayName = trim((string) ($row['item_name'] ?? '')); $displayFormulation = trim((string) ($row['formulation'] ?? '')); if ($displayFormulation !== '') { $displayName .= ' (' . $displayFormulation . ')'; } $groups[$norm] = [ 'normalized_name' => $norm, 'display_name' => $displayName, 'rows' => [], ]; } $groups[$norm]['rows'][] = $row; } $result = []; foreach ($groups as $group) { $items = $group['rows']; if (count($items) <= 1) { continue; } usort($items, static function (array $a, array $b): int { return ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0)); }); $keep = $items[0]; $mergeIds = []; $allIds = []; foreach ($items as $idx => $item) { $id = (int) ($item['id'] ?? 0); if ($id > 0) { $allIds[] = $id; if ($idx > 0) { $mergeIds[] = $id; } } } $result[] = [ 'normalized_name' => $group['normalized_name'], 'display_name' => $group['display_name'], 'count' => count($items), 'keep_id' => (int) ($keep['id'] ?? 0), 'all_ids' => $allIds, 'merge_ids' => $mergeIds, ]; } usort($result, static function (array $a, array $b): int { return strcmp((string) ($a['display_name'] ?? ''), (string) ($b['display_name'] ?? '')); }); return $result; } public function mergeDuplicates(int $keepId, array $mergeIds): array { if (! $this->tableExists() || $keepId <= 0) { return ['ok' => false, 'error' => 'Medicine table not found']; } $mergeIds = array_values(array_unique(array_filter(array_map('intval', $mergeIds), static fn(int $id): bool => $id > 0 && $id !== $keepId))); if (empty($mergeIds)) { return ['ok' => false, 'error' => 'No duplicate ids provided']; } $keepRow = $this->db->table($this->table)->where('id', $keepId)->get(1)->getRowArray(); if (empty($keepRow)) { return ['ok' => false, 'error' => 'Keep medicine not found']; } $dupRows = $this->db->table($this->table)->whereIn('id', $mergeIds)->get()->getResultArray(); if (empty($dupRows)) { return ['ok' => false, 'error' => 'Duplicate medicines not found']; } $this->db->transStart(); $patch = []; foreach (['formulation', 'genericname', 'company_name'] as $field) { $current = trim((string) ($keepRow[$field] ?? '')); if ($current !== '') { continue; } foreach ($dupRows as $dup) { $value = trim((string) ($dup[$field] ?? '')); if ($value !== '') { $patch[$field] = $value; break; } } } if (! empty($patch)) { $this->db->table($this->table)->where('id', $keepId)->update($patch); } foreach (['opd_prescrption_prescribed', 'opd_prescription_prescribed', 'opd_prescrption_prescribed_template'] as $refTable) { if (! $this->db->tableExists($refTable)) { continue; } $fields = $this->db->getFieldNames($refTable); if (! in_array('med_id', $fields, true)) { continue; } $this->db->table($refTable) ->whereIn('med_id', $mergeIds) ->set('med_id', $keepId) ->update(); } $this->db->table($this->table)->whereIn('id', $mergeIds)->delete(); $this->db->transComplete(); if (! $this->db->transStatus()) { return ['ok' => false, 'error' => 'Failed to merge duplicates']; } return ['ok' => true, 'merged_count' => count($mergeIds)]; } }