clqms-be/app/Views/v2/result/valueset/resultvalueset_index.php
mahdahar 42a5260f9a feat(valueset): restructure valueset UI and add result-specific CRUD
- Restructure valueset pages from single master page to separate views:
  - Library Valuesets (read-only lookup browser)
  - Result Valuesets (CRUD for valueset table)
  - Valueset Definitions (CRUD for valuesetdef table)
- Add new ResultValueSetController for result-specific valueset operations
- Move views from master/valuesets to result/valueset and result/valuesetdef
- Convert valueset sidebar to collapsible nested menu
- Add search filtering to ValueSetController index
- Remove deprecated welcome_message.php and old nested CRUD view
- Update routes to organize under /result namespace
Summary of changes: This commit reorganizes the valueset management UI by splitting the monolithic master/valuesets page into three distinct sections, adds a new controller for result-related valueset operations, and restructures the sidebar navigation for better usability.
2026-01-14 16:45:58 +07:00

323 lines
9.9 KiB
PHP

<?= $this->extend("v2/layout/main_layout"); ?>
<?= $this->section("content") ?>
<div x-data="resultValueSet()" x-init="init()">
<div class="card-glass p-6 animate-fadeIn mb-6">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-amber-600 to-orange-800 flex items-center justify-center shadow-lg">
<i class="fa-solid fa-list-ul text-2xl text-white"></i>
</div>
<div>
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Result Valuesets</h2>
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage valueset items from database</p>
</div>
</div>
</div>
<div class="card mb-6">
<div class="p-4">
<div class="flex flex-col sm:flex-row gap-4 items-center justify-between">
<div class="flex w-full sm:w-auto gap-2">
<input
type="text"
placeholder="Search valuesets..."
class="input flex-1 sm:w-80"
x-model="keyword"
@keyup.enter="fetchList()"
/>
<button class="btn btn-primary" @click="fetchList()">
<i class="fa-solid fa-search"></i>
</button>
</div>
<button class="btn btn-primary w-full sm:w-auto" @click="showForm()">
<i class="fa-solid fa-plus mr-2"></i>
Add Item
</button>
</div>
</div>
</div>
<div class="card overflow-hidden">
<div x-show="loading" class="p-12 text-center" x-cloak>
<div class="spinner spinner-lg mx-auto mb-4"></div>
<p style="color: rgb(var(--color-text-muted));">Loading valuesets...</p>
</div>
<div class="overflow-x-auto" x-show="!loading">
<table class="table">
<thead>
<tr>
<th class="w-16">ID</th>
<th>Category</th>
<th>Value</th>
<th>Description</th>
<th class="w-20 text-center">Order</th>
<th class="w-24 text-center">Actions</th>
</tr>
</thead>
<tbody>
<template x-if="!list || list.length === 0">
<tr>
<td colspan="6" class="text-center py-12">
<div class="flex flex-col items-center gap-3" style="color: rgb(var(--color-text-muted));">
<i class="fa-solid fa-inbox text-5xl opacity-40"></i>
<p class="text-lg">No valuesets found</p>
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
<i class="fa-solid fa-plus mr-1"></i> Add First Item
</button>
</div>
</td>
</tr>
</template>
<template x-for="item in list" :key="item.VID">
<tr class="hover:bg-opacity-50">
<td>
<span class="badge badge-ghost font-mono text-xs" x-text="item.VID || '-'"></span>
</td>
<td>
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="item.VCategoryName || '-'"></div>
</td>
<td>
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="item.VValue || '-'"></div>
</td>
<td>
<span class="text-sm opacity-70" x-text="item.VDesc || '-'"></span>
</td>
<td class="text-center">
<span class="font-mono text-sm" x-text="item.VOrder || 0"></span>
</td>
<td class="text-center">
<div class="flex items-center justify-center gap-1">
<button class="btn btn-ghost btn-sm btn-square" @click="editItem(item.VID)" title="Edit">
<i class="fa-solid fa-pen text-sky-500"></i>
</button>
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(item)" title="Delete">
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<?= $this->include('v2/result/valueset/resultvalueset_dialog') ?>
<div
x-show="showDeleteModal"
x-cloak
class="modal-overlay"
@click.self="showDeleteModal = false"
>
<div class="modal-content p-6 max-w-md">
<h3 class="font-bold text-lg mb-4 flex items-center gap-2" style="color: rgb(var(--color-error));">
<i class="fa-solid fa-exclamation-triangle"></i>
Confirm Delete
</h3>
<p class="mb-6" style="color: rgb(var(--color-text));">
Are you sure you want to delete item <strong x-text="deleteTarget?.VValue"></strong>?
</p>
<div class="flex gap-2">
<button class="btn btn-ghost flex-1" @click="showDeleteModal = false">Cancel</button>
<button class="btn flex-1" style="background: rgb(var(--color-error)); color: white;" @click="deleteItem()" :disabled="deleting">
<span x-show="deleting" class="spinner spinner-sm"></span>
<span x-show="!deleting">Delete</span>
</button>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section("script") ?>
<script>
function resultValueSet() {
return {
loading: false,
list: [],
keyword: "",
defsList: [],
showModal: false,
isEditing: false,
saving: false,
errors: {},
form: {
VID: null,
VSetID: "",
VOrder: 0,
VValue: "",
VDesc: "",
SiteID: 1
},
showDeleteModal: false,
deleteTarget: null,
deleting: false,
async init() {
await this.fetchList();
await this.fetchDefsList();
},
async fetchList() {
this.loading = true;
try {
const params = new URLSearchParams();
if (this.keyword) params.append('search', this.keyword);
const res = await fetch(`${BASEURL}api/result/valueset?${params}`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.list = data.data || [];
} catch (err) {
console.error(err);
this.list = [];
this.showToast('Failed to load valuesets', 'error');
} finally {
this.loading = false;
}
},
async fetchDefsList() {
try {
const res = await fetch(`${BASEURL}api/result/valuesetdef?limit=1000`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.defsList = data.data || [];
} catch (err) {
console.error('Failed to fetch defs list:', err);
this.defsList = [];
}
},
showForm() {
this.isEditing = false;
this.form = {
VID: null,
VSetID: "",
VOrder: 0,
VValue: "",
VDesc: "",
SiteID: 1
};
this.errors = {};
this.showModal = true;
},
async editItem(id) {
this.isEditing = true;
this.errors = {};
try {
const res = await fetch(`${BASEURL}api/result/valueset/${id}`, {
credentials: 'include'
});
const data = await res.json();
if (data.data) {
this.form = { ...this.form, ...data.data };
this.showModal = true;
}
} catch (err) {
console.error(err);
this.showToast('Failed to load item data', 'error');
}
},
validate() {
const e = {};
if (!this.form.VValue?.trim()) e.VValue = "Value is required";
if (!this.form.VSetID) e.VSetID = "Category is required";
this.errors = e;
return Object.keys(e).length === 0;
},
closeModal() {
this.showModal = false;
this.errors = {};
},
async save() {
if (!this.validate()) return;
this.saving = true;
try {
const method = this.isEditing ? 'PUT' : 'POST';
const url = this.isEditing ? `${BASEURL}api/result/valueset/${this.form.VID}` : `${BASEURL}api/result/valueset`;
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form),
credentials: 'include'
});
if (res.ok) {
this.closeModal();
await this.fetchList();
this.showToast(this.isEditing ? 'Item updated successfully' : 'Item created successfully', 'success');
} else {
const errorData = await res.json().catch(() => ({ message: 'Unknown error' }));
this.errors = { general: errorData.message || 'Failed to save' };
}
} catch (err) {
console.error(err);
this.errors = { general: 'Failed to save item' };
this.showToast('Failed to save item', 'error');
} finally {
this.saving = false;
}
},
confirmDelete(item) {
this.deleteTarget = item;
this.showDeleteModal = true;
},
async deleteItem() {
if (!this.deleteTarget) return;
this.deleting = true;
try {
const res = await fetch(`${BASEURL}api/result/valueset/${this.deleteTarget.VID}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
if (res.ok) {
this.showDeleteModal = false;
await this.fetchList();
this.showToast('Item deleted successfully', 'success');
} else {
this.showToast('Failed to delete item', 'error');
}
} catch (err) {
console.error(err);
this.showToast('Failed to delete item', 'error');
} finally {
this.deleting = false;
this.deleteTarget = null;
}
},
showToast(message, type = 'info') {
if (this.$root && this.$root.showToast) {
this.$root.showToast(message, type);
} else {
alert(message);
}
}
}
}
</script>
<?= $this->endSection() ?>