clqms-be/app/Views/v2/valuesets.php

387 lines
19 KiB
PHP
Raw Normal View History

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 -->
<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>
<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>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button @click="isDefModalOpen = false">close</button>
</form>
</dialog>
<!-- Value Modal -->
<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>
<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>
<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>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button @click="isValueModalOpen = false">close</button>
</form>
</dialog>
</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() ?>