clqms-be/plans/ref_range_multiple_support_plan.md
mahdahar 9e0b01e7e2 refactor: reorganize documentation and update test-related files
- Remove deprecated docs folder with outdated documentation
- Add new plans directory with ref_range_multiple_support_plan.md
- Update test migrations, seeds, and views for improved functionality
2026-01-05 07:21:12 +07:00

29 KiB
Raw Blame History

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

// 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

// 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

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

// 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

// 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

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

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

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

// 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

<!-- Reff Tab - Main (Simple) -->
<div x-show="form.dialogTab === 'reff'" class="space-y-4" x-cloak>
    
    <!-- RefType Selector -->
    <div>
        <label class="label"><span class="label-text font-medium">Reference Type</span></label>
        <select class="select w-full" x-model="form.RefType">
            <option value="1">Numeric Range (NMRC)</option>
            <option value="2">Threshold (TH)</option>
            <option value="3">Text Result (TEXT)</option>
            <option value="4">Value Set (LIST)</option>
        </select>
    </div>
    
    <!-- Numeric Range Fields -->
    <template x-if="form.RefType == '1'">
        <div class="space-y-4">
            <div class="grid grid-cols-2 gap-4">
                <div>
                    <label class="label"><span class="label-text">Ref Low</span></label>
                    <input type="text" class="input" x-model="form.RefLow" placeholder="0.00" />
                </div>
                <div>
                    <label class="label"><span class="label-text">Ref High</span></label>
                    <input type="text" class="input" x-model="form.RefHigh" placeholder="10.00" />
                </div>
            </div>
            <div class="grid grid-cols-2 gap-4">
                <div>
                    <label class="label"><span class="label-text text-error">Crit Low</span></label>
                    <input type="text" class="input border-error/30" x-model="form.CritLow" placeholder="0.00" />
                </div>
                <div>
                    <label class="label"><span class="label-text text-error">Crit High</span></label>
                    <input type="text" class="input border-error/30" x-model="form.CritHigh" placeholder="20.00" />
                </div>
            </div>
            <div class="grid grid-cols-2 gap-4">
                <div>
                    <label class="label"><span class="label-text">Unit</span></label>
                    <input type="text" class="input" x-model="form.Unit1" placeholder="mg/dL" />
                </div>
                <div>
                    <label class="label"><span class="label-text">Decimals</span></label>
                    <input type="number" class="input" x-model="form.Decimal" />
                </div>
            </div>
        </div>
    </template>
    
    <!-- Threshold Fields -->
    <template x-if="form.RefType == '2'">
        <div class="space-y-4">
            <div class="p-4 bg-info/10 border border-info/20 rounded-lg">
                <p class="text-sm mb-3"><strong>Below Threshold:</strong></p>
                <div class="grid grid-cols-3 gap-4">
                    <div>
                        <label class="label"><span class="label-text">Below Text</span></label>
                        <input type="text" class="input" x-model="form.RefText" placeholder="Below Normal" />
                    </div>
                    <div>
                        <label class="label"><span class="label-text">Operator</span></label>
                        <select class="select" x-model="form.BelowOp">
                            <option value="<"><</option>
                            <option value="<="><=</option>
                            <option value="<>"><></option>
                        </select>
                    </div>
                    <div>
                        <label class="label"><span class="label-text">Value</span></label>
                        <input type="text" class="input" x-model="form.BelowVal" placeholder="0.00" />
                    </div>
                </div>
            </div>
            <div class="p-4 bg-warning/10 border border-warning/20 rounded-lg">
                <p class="text-sm mb-3"><strong>Above Threshold:</strong></p>
                <div class="grid grid-cols-3 gap-4">
                    <div>
                        <label class="label"><span class="label-text">Above Text</span></label>
                        <input type="text" class="input" x-model="form.AboveText" placeholder="Above Normal" />
                    </div>
                    <div>
                        <label class="label"><span class="label-text">Operator</span></label>
                        <select class="select" x-model="form.AboveOp">
                            <option value=">">></option>
                            <option value=">=">>=</option>
                        </select>
                    </div>
                    <div>
                        <label class="label"><span class="label-text">Value</span></label>
                        <input type="text" class="input" x-model="form.AboveVal" placeholder="0.00" />
                    </div>
                </div>
            </div>
        </div>
    </template>
    
    <!-- Text Result Fields -->
    <template x-if="form.RefType == '3'">
        <div class="space-y-4">
            <div>
                <label class="label"><span class="label-text">Default Text</span></label>
                <input type="text" class="input" x-model="form.RefText" placeholder="e.g., Negative" />
            </div>
        </div>
    </template>
    
    <!-- Value Set Fields -->
    <template x-if="form.RefType == '4'">
        <div class="space-y-4">
            <div>
                <label class="label"><span class="label-text">Value Set</span></label>
                <select class="select w-full" x-model="form.VSetDefID">
                    <option value="">Select Value Set...</option>
                    <template x-for="v in vsetDefsList" :key="v.VSetDefID">
                        <option :value="v.VSetDefID" x-text="v.VSDesc"></option>
                    </template>
                </select>
            </div>
        </div>
    </template>
    
    <!-- Advanced Button -->
    <div class="mt-4 pt-4 border-t" style="border-color: rgb(var(--color-border));">
        <button class="btn btn-outline btn-sm" @click="openAdvancedRefDialog()">
            <i class="fa-solid fa-gear mr-1"></i>
            Advanced Settings
        </button>
        <span class="ml-2 text-xs opacity-60" x-show="form.refRanges.length > 0">
            <i class="fa-solid fa-check text-success mr-1"></i>
            <span x-text="form.refRanges.length + ' advanced ranges configured'"></span>
        </span>
    </div>
</div>

3.2 Add Advanced RefRanges Modal

<!-- Advanced Reference Ranges Modal -->
<div x-show="showAdvancedRefModal" x-cloak class="modal-overlay" @click.self="cancelAdvancedRefDialog()">
    <div class="modal-content p-6 max-w-5xl w-full max-h-[90vh] overflow-y-auto">
        <div class="flex items-center justify-between mb-4">
            <h3 class="font-bold text-lg">Advanced Reference Ranges</h3>
            <button class="btn btn-ghost btn-sm btn-square" @click="cancelAdvancedRefDialog()">
                <i class="fa-solid fa-times"></i>
            </button>
        </div>
        
        <!-- Add Row Controls -->
        <div class="flex gap-2 mb-4 p-3 bg-base-200 rounded-lg">
            <select class="select select-sm" x-model="newRefType">
                <option :value="1">Numeric (NMRC)</option>
                <option :value="2">Threshold (TH)</option>
                <option :value="3">Text (TEXT)</option>
                <option :value="4">Value Set (LIST)</option>
            </select>
            <button class="btn btn-sm btn-outline" @click="addAdvancedRefRange()">
                <i class="fa-solid fa-plus mr-1"></i> Add Range
            </button>
        </div>
        
        <!-- Advanced Ranges Table -->
        <div class="overflow-x-auto mb-4">
            <table class="table table-sm table-compact w-full">
                <thead>
                    <tr>
                        <th style="width: 80px;">RefType</th>
                        <th style="width: 120px;">Flag/Label</th>
                        <th style="width: 80px;">RangeType</th>
                        <th style="width: 60px;">Sex</th>
                        <th style="width: 70px;">Age From</th>
                        <th style="width: 70px;">Age To</th>
                        <th style="width: 100px;">Low</th>
                        <th style="width: 100px;">High</th>
                        <th style="width: 40px;"></th>
                    </tr>
                </thead>
                <tbody>
                    <template x-for="(ref, index) in advancedRefRanges" :key="index">
                        <tr :class="{ 'bg-error/5': ref.RangeType == 2 }">
                            <!-- RefType -->
                            <td>
                                <select 
                                    class="select select-xs w-full"
                                    x-model="ref.RefType"
                                >
                                    <option :value="1">NMRC</option>
                                    <option :value="2">TH</option>
                                    <option :value="3">TEXT</option>
                                    <option :value="4">LIST</option>
                                </select>
                            </td>
                            
                            <!-- Flag/Label -->
                            <td>
                                <input 
                                    type="text" 
                                    class="input input-xs w-full"
                                    x-model="ref.Flag"
                                    placeholder="e.g., Negative"
                                />
                            </td>
                            
                            <!-- RangeType -->
                            <td>
                                <select 
                                    class="select select-xs w-full"
                                    :class="{ 'border-error/30 bg-error/10': ref.RangeType == 2 }"
                                    x-model="ref.RangeType"
                                >
                                    <option :value="1">REF</option>
                                    <option :value="2">CRTC</option>
                                    <option :value="3">VAL</option>
                                    <option :value="4">RERUN</option>
                                </select>
                            </td>
                            
                            <!-- Sex -->
                            <td>
                                <select class="select select-xs w-full" x-model="ref.Sex">
                                    <option :value="0">All</option>
                                    <option :value="1">M</option>
                                    <option :value="2">F</option>
                                </select>
                            </td>
                            
                            <!-- Age From -->
                            <td>
                                <input type="number" class="input input-xs w-full text-center" x-model="ref.AgeStart" />
                            </td>
                            
                            <!-- Age To -->
                            <td>
                                <input type="number" class="input input-xs w-full text-center" x-model="ref.AgeEnd" />
                            </td>
                            
                            <!-- Low -->
                            <td>
                                <input 
                                    type="text" 
                                    class="input input-xs w-full text-center"
                                    :class="{ 'border-error/30': ref.RangeType == 2 }"
                                    x-model="ref.Low"
                                    placeholder="0.00 or <=10"
                                />
                            </td>
                            
                            <!-- High -->
                            <td>
                                <input 
                                    type="text" 
                                    class="input input-xs w-full text-center"
                                    :class="{ 'border-error/30': ref.RangeType == 2 }"
                                    x-model="ref.High"
                                    placeholder="0.00"
                                />
                            </td>
                            
                            <!-- Delete -->
                            <td>
                                <button class="btn btn-ghost btn-xs btn-square text-error" @click="removeAdvancedRefRange(index)">
                                    <i class="fa-solid fa-times"></i>
                                </button>
                            </td>
                        </tr>
                    </template>
                    
                    <!-- Empty State -->
                    <template x-if="advancedRefRanges.length === 0">
                        <tr>
                            <td colspan="9" class="text-center py-8 text-base-400">
                                <i class="fa-solid fa-layer-group mr-2"></i>
                                No advanced ranges. Click "Add Range" to create one.
                            </td>
                        </tr>
                    </template>
                </tbody>
            </table>
        </div>
        
        <!-- Legend -->
        <div class="text-xs opacity-60 p-2 border rounded bg-base-200 flex gap-4 mb-4">
            <span><strong>1</strong>=NMRC</span>
            <span><strong>2</strong>=TH</span>
            <span><strong>3</strong>=TEXT</span>
            <span><strong>4</strong>=LIST</span>
            <span class="ml-4"><strong>1</strong>=REF</span>
            <span><strong>2</strong>=CRTC</span>
            <span><strong>3</strong>=VAL</span>
            <span><strong>4</strong>=RERUN</span>
        </div>
        
        <!-- Actions -->
        <div class="flex gap-2 pt-4 border-t" style="border-color: rgb(var(--color-border));">
            <button class="btn btn-ghost flex-1" @click="cancelAdvancedRefDialog()">Cancel</button>
            <button class="btn btn-primary flex-1" @click="saveAdvancedRefRanges()">
                <i class="fa-solid fa-check mr-1"></i> Save Advanced Ranges
            </button>
        </div>
    </div>
</div>

Summary of Files to Modify

File Changes
app/Controllers/Tests.php Add constants, refnum loading/save helper, delete update
app/Models/RefRange/RefNumModel.php Ensure allowedFields includes all needed fields
app/Views/v2/master/tests/tests_index.php Add refRanges state, helper methods, modal state
app/Views/v2/master/tests/test_dialog.php Update Reff tab with numeric RefType, add Advanced modal

Workflow

  1. Basic users - Use global RefLow/RefHigh fields on main tab
  2. Advanced users - Click "Advanced Settings" to open modal
  3. Modal - Add/edit/remove multiple ranges with criteria
  4. Save - Advanced ranges saved to refnum table, global fields saved to testdeftech

Next Steps

  1. Review this plan
  2. Provide feedback or request changes
  3. Once approved, switch to Code mode for implementation