- 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
29 KiB
29 KiB
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
- Basic users - Use global RefLow/RefHigh fields on main tab
- Advanced users - Click "Advanced Settings" to open modal
- Modal - Add/edit/remove multiple ranges with criteria
- Save - Advanced ranges saved to refnum table, global fields saved to testdeftech
Next Steps
- Review this plan
- Provide feedback or request changes
- Once approved, switch to Code mode for implementation