2025-12-22 16:54:19 +07:00
< ? = $this -> extend ( 'layouts/v2' ) ?>
< ? = $this -> section ( 'content' ) ?>
< div class = " flex flex-col lg:flex-row gap-6 h-[calc(100vh-8rem)] " x - data = " valueSetManager() " >
<!-- Left Column : Value Sets ( Defs ) - Fixed width -->
< div class = " w-full lg:w-80 flex flex-col bg-base-100 rounded-box shadow-xl shrink-0 " >
<!-- Header -->
< div class = " p-4 border-b border-base-200 " >
< div class = " flex justify-between items-center mb-4 " >
< h3 class = " font-bold text-lg flex items-center gap-2 " >
< i data - lucide = " book-open " class = " w-5 h-5 text-primary " ></ i >
Value Sets
</ h3 >
< div class = " tooltip tooltip-bottom " data - tip = " New Value Set " >
< button @ click = " openDefModal() " class = " btn btn-sm btn-circle btn-ghost " >
< i data - lucide = " plus " class = " w-5 h-5 " ></ i >
</ button >
</ div >
</ div >
<!-- Search -->
< label class = " input input-sm input-bordered flex items-center gap-2 " >
< input type = " text " class = " grow " placeholder = " Search... " x - model = " searchDef " />
< i data - lucide = " search " class = " w-4 h-4 opacity-70 " ></ i >
</ label >
</ div >
<!-- List -->
< div class = " flex-1 overflow-y-auto p-2 scrollbar-thin " >
< template x - if = " filteredDefs.length === 0 " >
< div class = " text-center p-8 text-base-content/50 text-sm " >
No results found
</ div >
</ template >
< ul class = " menu p-0 gap-1 w-full " >
< template x - for = " def in filteredDefs " : key = " def.VSetID " >
< li >
< a @ click = " selectDef(def) "
: class = " { 'active': selectedDef?.VSetID === def.VSetID } "
class = " flex flex-col items-start gap-1 py-3 group transition-colors " >
<!-- Top Line : Name & Edit -->
< div class = " flex justify-between w-full items-center " >
< span class = " font-medium truncate w-full " x - text = " def.VSName || def.VSetID " ></ span >
<!-- Hover Actions -->
< button @ click . stop = " editDef(def) "
class = " btn btn-xs btn-ghost btn-square opacity-0 group-hover:opacity-100 transition-opacity "
title = " Edit Definition " >
< i data - lucide = " pencil " class = " w-3 h-3 " ></ i >
</ button >
</ div >
<!-- Bottom Line : ID Badge -->
< div class = " flex justify-between w-full items-center " >
< span class = " badge badge-xs badge-neutral badge-outline font-mono opacity-70 " x - text = " 'ID: ' + def.VSetID " ></ span >
</ div >
</ a >
</ li >
</ template >
</ ul >
</ div >
</ div >
<!-- Right Column : Values -->
< div class = " flex-1 flex flex-col bg-base-100 rounded-box shadow-xl overflow-hidden relative " >
<!-- Empty State -->
< div x - show = " !selectedDef " class = " absolute inset-0 flex flex-col items-center justify-center text-base-content/30 bg-base-100 z-10 " >
< i data - lucide = " layout-list " class = " w-24 h-24 mb-4 opacity-20 " ></ i >
< h3 class = " font-bold text-xl " > Select a Value Set </ h3 >
< p > Choose a definition from the left to manage its values </ p >
</ div >
<!-- Content ( Only when selected ) -->
< template x - if = " selectedDef " >
< div class = " flex flex-col h-full animate-in fade-in duration-200 " >
<!-- Main Toolbar / Header -->
< div class = " p-6 border-b border-base-200 bg-base-100/50 backdrop-blur-sm " >
< div class = " flex flex-col md:flex-row justify-between items-start md:items-center gap-4 " >
< div >
< div class = " flex items-center gap-3 " >
< h2 class = " text-2xl font-bold font-display " x - text = " selectedDef.VSName " ></ h2 >
< span class = " badge badge-primary badge-outline font-mono " x - text = " 'ID: ' + selectedDef.VSetID " ></ span >
</ div >
< p class = " text-base-content/70 mt-1 max-w-2xl " x - text = " selectedDef.VSDesc || 'No description provided' " ></ p >
</ div >
< div class = " flex gap-2 shrink-0 " >
< button @ click = " openValueModal() " class = " btn btn-primary gap-2 shadow-lg hover:translate-y-[-1px] transition-transform " >
< i data - lucide = " plus " class = " w-4 h-4 " ></ i >
Add Value
</ button >
</ div >
</ div >
</ div >
<!-- Table -->
< div class = " flex-1 overflow-x-auto bg-base-100 " >
< table class = " table table-pin-rows table-lg " >
< thead >
< tr class = " bg-base-200/50 text-base-content/70 " >
< th class = " w-24 " > VID </ th >
< th class = " w-1/4 " > Value </ th >
< th > Description </ th >
< th class = " w-24 text-center " > Order </ th >
< th class = " w-24 text-right " > Actions </ th >
</ tr >
</ thead >
< tbody >
< template x - for = " val in values " : key = " val.VID " >
< tr class = " hover group transition-colors border-base-100 " >
< td class = " font-mono text-xs opacity-50 " x - text = " val.VID " ></ td >
< td >
< span class = " font-bold " x - text = " val.VValue " ></ span >
</ td >
< td class = " text-base-content/80 " x - text = " val.VDesc || '-' " ></ td >
< td class = " text-center " >
< span class = " badge badge-ghost badge-sm " x - show = " val.SortOrder " x - text = " val.SortOrder " ></ span >
</ td >
< td class = " text-right " >
< div class = " flex justify-end gap-1 opacity-50 group-hover:opacity-100 transition-opacity " >
< button @ click = " editValue(val) " class = " btn btn-sm btn-ghost btn-square text-primary " title = " Edit Value " >
< i data - lucide = " pencil " class = " w-4 h-4 " ></ i >
</ button >
< button @ click = " deleteValue(val) " class = " btn btn-sm btn-ghost btn-square text-error " title = " Delete Value " >
< i data - lucide = " trash-2 " class = " w-4 h-4 " ></ i >
</ button >
</ div >
</ td >
</ tr >
</ template >
</ tbody >
</ table >
<!-- Empty Table State -->
< div x - show = " values.length === 0 " class = " flex flex-col items-center justify-center py-20 text-base-content/40 " >
< i data - lucide = " inbox " class = " w-12 h-12 mb-2 opacity-50 " ></ i >
< p > No values found for this set .</ p >
< button @ click = " openValueModal() " class = " btn btn-link btn-sm mt-2 " > Add first value </ button >
</ div >
</ div >
<!-- Footer count -->
< div class = " p-3 border-t border-base-200 text-xs text-base-content/50 text-right bg-base-100 " >
< span x - text = " values.length " ></ span > values
</ div >
</ div >
</ template >
</ div >
<!-- Def Modal -->
2025-12-23 06:29:01 +07:00
< template x - teleport = " body " >
< dialog class = " modal " : class = " { 'modal-open': isDefModalOpen } " >
< div class = " modal-box transform transition-all " >
< form method = " dialog " >
< button class = " btn btn-sm btn-circle btn-ghost absolute right-2 top-2 " @ click = " isDefModalOpen = false " > ✕ </ button >
</ form >
< h3 class = " font-bold text-xl mb-6 flex items-center gap-2 " >
< div class = " bg-primary/10 p-2 rounded-lg text-primary " >
< i data - lucide = " book-open " class = " w-6 h-6 " ></ i >
</ div >
< span x - text = " isEditDef ? 'Edit Definition' : 'New Definition' " ></ span >
</ h3 >
< form @ submit . prevent = " saveDef " >
< div class = " space-y-4 " >
<!-- ID Field ( Readonly / Hidden Logic ) -->
< div class = " form-control " x - show = " isEditDef " >
< label class = " label " >< span class = " label-text font-medium " > System ID </ span ></ label >
< input type = " text " x - model = " defForm.VSetID " class = " input input-sm input-bordered bg-base-200 font-mono " disabled />
</ div >
< div class = " form-control " >
< label class = " label " >< span class = " label-text font-medium " > Name </ span ></ label >
< input type = " text " x - model = " defForm.VSName " class = " input input-bordered focus:input-primary w-full " placeholder = " e.g. Gender " required />
</ div >
< div class = " form-control " >
< label class = " label " >< span class = " label-text font-medium " > Description </ span ></ label >
< textarea x - model = " defForm.VSDesc " class = " textarea textarea-bordered focus:textarea-primary h-24 " placeholder = " Describe the purpose of this value set... " required ></ textarea >
</ div >
</ div >
2025-12-22 16:54:19 +07:00
< div class = " modal-action " >
< button type = " button " class = " btn " @ click = " isDefModalOpen = false " > Cancel </ button >
< button type = " submit " class = " btn btn-primary px-8 " >
Save Definition
</ button >
</ div >
2025-12-23 06:29:01 +07:00
</ form >
</ div >
< form method = " dialog " class = " modal-backdrop " >
< button @ click = " isDefModalOpen = false " > close </ button >
</ form >
</ dialog >
</ template >
2025-12-22 16:54:19 +07:00
<!-- Value Modal -->
2025-12-23 06:29:01 +07:00
< template x - teleport = " body " >
< dialog class = " modal " : class = " { 'modal-open': isValueModalOpen } " >
< div class = " modal-box transform transition-all " >
< form method = " dialog " >
< button class = " btn btn-sm btn-circle btn-ghost absolute right-2 top-2 " @ click = " isValueModalOpen = false " > ✕ </ button >
</ form >
2025-12-22 16:54:19 +07:00
2025-12-23 06:29:01 +07:00
< h3 class = " font-bold text-xl mb-6 flex items-center gap-2 " >
< div class = " bg-secondary/10 p-2 rounded-lg text-secondary " >
< i data - lucide = " list " class = " w-6 h-6 " ></ i >
</ div >
< span x - text = " isEditValue ? 'Edit Value' : 'Add New Value' " ></ span >
</ h3 >
< form @ submit . prevent = " saveValue " >
< div class = " space-y-4 " >
< div class = " form-control " >
< label class = " label " >< span class = " label-text font-medium " > Display Value </ span ></ label >
< input type = " text " x - model = " valueForm.VValue " class = " input input-bordered focus:input-primary w-full " placeholder = " e.g. Male " required />
</ div >
< div class = " grid grid-cols-2 gap-4 " >
< div class = " form-control " >
< label class = " label " >< span class = " label-text font-medium " > Description </ span ></ label >
< input type = " text " x - model = " valueForm.VDesc " class = " input input-bordered w-full " placeholder = " Optional " />
</ div >
< div class = " form-control " >
< label class = " label " >< span class = " label-text font-medium " > Sort Order </ span ></ label >
< input type = " number " x - model = " valueForm.SortOrder " class = " input input-bordered w-full " placeholder = " 0 " />
</ div >
</ div >
</ div >
2025-12-22 16:54:19 +07:00
< div class = " modal-action " >
< button type = " button " class = " btn " @ click = " isValueModalOpen = false " > Cancel </ button >
< button type = " submit " class = " btn btn-primary px-8 " > Save Value </ button >
</ div >
2025-12-23 06:29:01 +07:00
</ form >
</ div >
< form method = " dialog " class = " modal-backdrop " >
< button @ click = " isValueModalOpen = false " > close </ button >
</ form >
</ dialog >
</ template >
2025-12-22 16:54:19 +07:00
</ div >
< ? = $this -> endSection () ?>
< ? = $this -> section ( 'script' ) ?>
< script type = " module " >
import Alpine , { Utils } from '<?= base_url(' / assets / js / app . js '); ?>' ;
document . addEventListener ( 'alpine:init' , () => {
Alpine . data ( 'valueSetManager' , () => ({
defs : [],
values : [],
searchDef : '' ,
selectedDef : null ,
// Def Modal
isDefModalOpen : false ,
isEditDef : false ,
defForm : { VSetID : '' , VSName : '' , VSDesc : '' },
// Value Modal
isValueModalOpen : false ,
isEditValue : false ,
valueForm : { VID : null , VValue : '' , VDesc : '' , SortOrder : '' },
init () {
this . loadDefs ();
},
async loadDefs () {
try {
const res = await Utils . api ( '<?= site_url(' api / valuesetdef / ') ?>' );
this . defs = res . data || [];
setTimeout (() => window . lucide ? . createIcons (), 50 );
} catch ( e ) { console . error ( e ); }
},
get filteredDefs () {
if ( ! this . searchDef ) return this . defs ;
return this . defs . filter ( d =>
( d . VSName && d . VSName . toLowerCase () . includes ( this . searchDef . toLowerCase ())) ||
( d . VSetID && d . VSetID . toString () . includes ( this . searchDef ))
);
},
async selectDef ( def ) {
this . selectedDef = def ;
this . values = [];
// Load values for this def
try {
// Note: VSetID is an integer (e.g. 1)
const res = await Utils . api ( `<?= site_url('api/valueset/valuesetdef/') ?>${def.VSetID}` );
this . values = res . data || [];
setTimeout (() => window . lucide ? . createIcons (), 50 );
} catch ( e ) {
this . values = [];
}
},
// --- Def Actions ---
openDefModal () {
this . isEditDef = false ;
this . defForm = { VSetID : '' , VSName : '' , VSDesc : '' };
this . isDefModalOpen = true ;
},
editDef ( def ) {
this . isEditDef = true ;
this . defForm = { ... def };
this . isDefModalOpen = true ;
// Since this might be triggered from the list, prevent event bubbling handled in @click.stop
},
async saveDef () {
try {
const method = this . isEditDef ? 'PATCH' : 'POST' ;
await Utils . api ( '<?= site_url(' api / valuesetdef ') ?>' , {
method ,
body : JSON . stringify ( this . defForm )
});
Alpine . store ( 'toast' ) . success ( this . isEditDef ? 'Updated definition' : 'Created definition' );
this . isDefModalOpen = false ;
this . loadDefs ();
// If we edited the currently selected one, update it
if ( this . selectedDef && this . defForm . VSetID === this . selectedDef . VSetID ) {
this . selectedDef = { ... this . defForm };
}
} catch ( e ) { Alpine . store ( 'toast' ) . error ( e . message ); }
},
// --- Value Actions ---
openValueModal () {
this . isEditValue = false ;
this . valueForm = { VID : null , VValue : '' , VDesc : '' , SortOrder : '' , VSetID : this . selectedDef . VSetID };
this . isValueModalOpen = true ;
},
editValue ( val ) {
this . isEditValue = true ;
this . valueForm = { ... val };
this . isValueModalOpen = true ;
},
async saveValue () {
try {
const method = this . isEditValue ? 'PATCH' : 'POST' ;
this . valueForm . VSetID = this . selectedDef . VSetID ;
await Utils . api ( '<?= site_url(' api / valueset ') ?>' , {
method ,
body : JSON . stringify ( this . valueForm )
});
Alpine . store ( 'toast' ) . success ( this . isEditValue ? 'Updated value' : 'Added value' );
this . isValueModalOpen = false ;
this . selectDef ( this . selectedDef ); // Refresh values
} catch ( e ) { Alpine . store ( 'toast' ) . error ( e . message ); }
},
async deleteValue ( val ) {
if ( ! confirm ( 'Delete this value?' )) return ;
try {
await Utils . api ( '<?= site_url(' api / valueset ') ?>' , {
method : 'DELETE' ,
body : JSON . stringify ({ VID : val . VID })
});
Alpine . store ( 'toast' ) . success ( 'Deleted' );
this . selectDef ( this . selectedDef );
} catch ( e ) { Alpine . store ( 'toast' ) . error ( e . message ); }
}
}));
});
Alpine . start ();
</ script >
< ? = $this -> endSection () ?>