# Plan: Multiple Reference Ranges with Advanced Dialog ## Overview Refactor the "Reff" tab to support multiple reference ranges using the existing `refnum` table schema. ## Existing Database Schema (refnum table) | Field | Type | Description | |-------|------|-------------| | RefNumID | INT AUTO_INCREMENT | Primary key | | SiteID | INT | Site identifier | | TestSiteID | INT | Links to test | | SpcType | INT | Specimen type | | Sex | INT | Gender (from valueset) | | Criteria | VARCHAR(100) | Additional criteria | | AgeStart | INT | Age range start | | AgeEnd | INT | Age range end | | **NumRefType** | INT | **Input format: 1=NMRC, 2=TH, 3=TEXT, 4=LIST** | | **RangeType** | INT | **Result category: 1=REF, 2=CRTC, 3=VAL, 4=RERUN** | | LowSign | INT | Low operator: 1='<', 2='<=', 3='>=', 4='>', 5='<>' | | Low | INT | Low value | | HighSign | INT | High operator | | High | INT | High value | | Display | INT | Display order | | **Flag** | VARCHAR(10) | **Like Label (e.g., "Negative", "Borderline")** | | Interpretation | VARCHAR(255) | Interpretation text | | Notes | VARCHAR(255) | Notes | | CreateDate | Datetime | Creation timestamp | | StartDate | Datetime | Start date | | EndDate | Datetime | Soft delete | --- ## Key Concept: NumRefType vs RangeType | Aspect | NumRefType | RangeType | |--------|------------|-----------| | **Location** | Main Reff Tab + Advanced Dialog | Advanced Dialog | | **Purpose** | Input format | Result categorization | | **Values** | 1=NMRC, 2=TH, 3=TEXT, 4=LIST | 1=REF, 2=CRTC, 3=VAL, 4=RERUN | | **Database Field** | NumRefType | RangeType | --- ## UI Design ### Main Reff Tab (Simple) ``` ┌─────────────────────────────────────────────────────────────────┐ │ Reference Ranges │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Ref Type: [ Numeric (NMRC) ▼ ] │ │ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ For Numeric (with operators > < <= >=): ││ │ │ Ref Low: [0.00 ] Ref High: [100.00 ] ││ │ │ Crit Low: [<55.00 ] Crit High: [>115.00 ] ││ │ │ ││ │ │ Examples: 0-100, <50, >=100, <>0 (not equal to 0) ││ │ └─────────────────────────────────────────────────────────────┘│ │ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ For Threshold: ││ │ │ Below Text: [Below Normal] Below Value: [<] [50] ││ │ │ Above Text: [Above Normal] Above Value: [>] [150] ││ │ └─────────────────────────────────────────────────────────────┘│ │ │ │ [Advanced Settings ▼] │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Advanced Dialog (Multiple Reference Ranges) ``` ┌───────────────────────────────────────────────────────────────────────────────┐ │ Advanced Reference Ranges [X]Close│ ├───────────────────────────────────────────────────────────────────────────────┤ │ │ │ [Add RefType ▼] [Add Button] │ │ │ │ ┌─────────────────────────────────────────────────────────────────────────┐ │ │ │ RefType │ Flag/Label │ RangeType │ Sex │ Age │ Low │ High │ [×] │ │ │ │─────────┼────────────┼───────────┼─────┼─────┼────────┼────────┼───────│ │ │ │ NMRC │ Negative │ REF (1) │ All │ 0-150│ 0 │ 25 │ [×] │ │ │ │ NMRC │ Borderline │ REF (1) │ All │ 0-150│ 25 │ 50 │ [×] │ │ │ │ NMRC │ Positive │ REF (1) │ All │ 0-150│ 50 │ │ [×] │ │ │ │ TEXT │ Negative │ REF (1) │ All │ 0-150│ │ │ [×] │ │ │ │ TH │ Low │ REF (1) │ All │ 0-150│ <50 │ │ [×] │ │ │ │ NMRC │ Critical │ CRTC (2) │ All │ 0-150│ <55 │ >115 │ [×] │ │ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ │ │ [Cancel] [Save Advanced Ranges] │ │ │ └───────────────────────────────────────────────────────────────────────────────┘ ``` **Key Features:** - **RefType** column: NMRC (1), TH (2), TEXT (3), LIST (4) - **RangeType** column: REF (1), CRTC (2), VAL (3), RERUN (4) - **Flag** column: Display label for the result (e.g., "Negative", "Borderline") - **Low/High** columns: Support operators via LowSign/HighSign fields --- ## Implementation Plan ### Phase 1: Backend Changes (Tests.php Controller) #### 1.1 Add RefType and RangeType constants ```php // At top of Tests.php const REFTYPE_NMRC = 1; const REFTYPE_TH = 2; const REFTYPE_TEXT = 3; const REFTYPE_LIST = 4; const RANGETYPE_REF = 1; const RANGETYPE_CRTC = 2; const RANGETYPE_VAL = 3; const RANGETYPE_RERUN = 4; const LOWSIGN_LT = 1; const LOWSIGN_LTE = 2; const LOWSIGN_GTE = 3; const LOWSIGN_GT = 4; const LOWSIGN_NE = 5; ``` #### 1.2 Update `show()` method to load refnum data ```php // Add after loading testdeftech/testdefcal $row['refnum'] = $this->RefNumModel->where('TestSiteID', $id) ->where('EndDate IS NULL') ->orderBy('Display', 'ASC') ->findAll(); ``` #### 1.3 Update `saveRefNum()` helper method ```php private function saveRefNum($testSiteID, $refRanges, $action, $siteID = 1) { if ($action === 'update') { // Soft delete existing refnums $this->RefNumModel->where('TestSiteID', $testSiteID) ->set('EndDate', date('Y-m-d H:i:s')) ->update(); } foreach ($refRanges as $index => $ref) { $refData = [ 'TestSiteID' => $testSiteID, 'SiteID' => $siteID, 'NumRefType' => $ref['RefType'] ?? self::REFTYPE_NMRC, 'RangeType' => $ref['RangeType'] ?? self::RANGETYPE_REF, 'Flag' => $ref['Flag'] ?? null, // Label for display 'Sex' => $ref['Sex'] ?? 0, // 0=All, 1=M, 2=F (from valueset) 'AgeStart' => $ref['AgeStart'] ?? 0, 'AgeEnd' => $ref['AgeEnd'] ?? 150, 'LowSign' => $this->parseSign($ref['Low'] ?? ''), 'Low' => $this->parseValue($ref['Low'] ?? ''), 'HighSign' => $this->parseSign($ref['High'] ?? ''), 'High' => $this->parseValue($ref['High'] ?? ''), 'Display' => $index, 'CreateDate' => date('Y-m-d H:i:s') ]; $this->RefNumModel->insert($refData); } } // Helper to extract operator from value like "<=50" private function parseSign($value) { if (str_starts_with($value, '<>')) return self::LOWSIGN_NE; if (str_starts_with($value, '<=')) return self::LOWSIGN_LTE; if (str_starts_with($value, '<')) return self::LOWSIGN_LT; if (str_starts_with($value, '>=')) return self::LOWSIGN_GTE; if (str_starts_with($value, '>')) return self::LOWSIGN_GT; return null; } // Helper to extract numeric value from operator-prefixed string private function parseValue($value) { return preg_replace('/^[<>=<>]+/', '', $value) ?: null; } ``` #### 1.4 Update `handleDetails()` to save refnum ```php // Add in handleDetails method, after saving tech/calc details if (isset($input['refnum']) && is_array($input['refnum'])) { $this->saveRefNum($testSiteID, $input['refnum'], $action, $input['SiteID'] ?? 1); } ``` #### 1.5 Update `delete()` to soft delete refnum ```php // Add in delete method $now = date('Y-m-d H:i:s'); $this->RefNumModel->where('TestSiteID', $id) ->set('EndDate', $now) ->update(); ``` --- ### Phase 2: Frontend Changes (tests_index.php) #### 2.1 Update form state to include advanced ref ranges ```javascript form: { // ... existing fields ... // Advanced ranges refRanges: [], // Array of advanced reference range objects // Dialog states showAdvancedRefModal: false, advancedRefRanges: [], newRefType: 1 // Default: NMRC } // RefType options for select refTypeOptions: [ { value: 1, label: 'Numeric (NMRC)' }, { value: 2, label: 'Threshold (TH)' }, { value: 3, label: 'Text (TEXT)' }, { value: 4, label: 'Value Set (LIST)' } ] // RangeType options rangeTypeOptions: [ { value: 1, label: 'REF' }, { value: 2, label: 'CRTC' }, { value: 3, label: 'VAL' }, { value: 4, label: 'RERUN' } ] // Sex options sexOptions: [ { value: 0, label: 'All' }, { value: 1, label: 'Male' }, { value: 2, label: 'Female' } ] ``` #### 2.2 Update `editTest()` to load refnum data ```javascript if (testData.refnum && testData.refnum.length > 0) { this.form.refRanges = testData.refnum.map(r => ({ RefNumID: r.RefNumID, RefType: r.NumRefType || 1, RangeType: r.RangeType || 1, Flag: r.Flag || '', Sex: r.Sex || 0, AgeStart: r.AgeStart || 0, AgeEnd: r.AgeEnd || 150, Low: this.formatValueWithSign(r.LowSign, r.Low), High: this.formatValueWithSign(r.HighSign, r.High) })); } else { this.form.refRanges = []; } // Format value with operator sign for display formatValueWithSign(sign, value) { if (!value && value !== 0) return ''; const signs = { 1: '<', 2: '<=', 3: '>=', 4: '>', 5: '<>' }; return (signs[sign] || '') + value; } ``` #### 2.3 Update `save()` to include refnum in payload ```javascript if (this.form.refRanges && this.form.refRanges.length > 0) { payload.refnum = this.form.refRanges.map(r => ({ RefType: r.RefType, RangeType: r.RangeType, Flag: r.Flag, Sex: r.Sex, AgeStart: r.AgeStart, AgeEnd: r.AgeEnd, Low: r.Low, High: r.High })); } ``` #### 2.4 Add helper methods for advanced ref ranges ```javascript // Open advanced dialog openAdvancedRefDialog() { this.advancedRefRanges = this.form.refRanges.length > 0 ? [...this.form.refRanges] : [{ RefNumID: null, RefType: this.form.RefType || 1, RangeType: 1, Flag: '', Sex: 0, AgeStart: 0, AgeEnd: 150, Low: this.form.RefLow || '', High: this.form.RefHigh || '' }]; // Add CRTC if critical values exist if ((this.form.CritLow || this.form.CritHigh) && !this.advancedRefRanges.some(r => r.RangeType === 2)) { this.advancedRefRanges.push({ RefNumID: null, RefType: 1, RangeType: 2, Flag: 'Critical', Sex: 0, AgeStart: 0, AgeEnd: 150, Low: this.form.CritLow || '', High: this.form.CritHigh || '' }); } this.showAdvancedRefModal = true; }, // Add new advanced range addAdvancedRefRange() { this.advancedRefRanges.push({ RefNumID: null, RefType: this.newRefType, RangeType: 1, Flag: '', Sex: 0, AgeStart: 0, AgeEnd: 150, Low: '', High: '' }); }, // Remove advanced range removeAdvancedRefRange(index) { this.advancedRefRanges.splice(index, 1); }, // Save advanced ranges and close saveAdvancedRefRanges() { this.form.refRanges = [...this.advancedRefRanges]; this.showAdvancedRefModal = false; }, // Cancel advanced dialog cancelAdvancedRefDialog() { this.showAdvancedRefModal = false; } ``` --- ### Phase 3: UI Changes (test_dialog.php) #### 3.1 Keep main Reff tab with RefType selector ```html
Below Threshold:
Above Threshold: