369 lines
11 KiB
PHP
Raw Normal View History

<?= $this->extend("v2/layout/main_layout"); ?>
<?= $this->section("content") ?>
<div x-data="sites()" x-init="init()">
<!-- Page Header -->
<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-blue-600 to-blue-900 flex items-center justify-center shadow-lg">
<i class="fa-solid fa-hospital text-2xl text-white"></i>
</div>
<div>
<h2 class="text-2xl font-bold" style="color: rgb(var(--color-text));">Organization Sites</h2>
<p class="text-sm" style="color: rgb(var(--color-text-muted));">Manage physical sites and locations</p>
</div>
</div>
</div>
<!-- Search & Actions Bar -->
<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 sites..."
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 Site
</button>
</div>
</div>
</div>
<!-- Content Card -->
<div class="card overflow-hidden">
<!-- Loading State -->
<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 sites...</p>
</div>
<!-- Table -->
<div class="overflow-x-auto" x-show="!loading">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Site Name</th>
<th>Code</th>
<th>Account</th>
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
<th>Type</th>
<th>Class</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
<!-- Empty State -->
<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 sites found</p>
<button class="btn btn-primary btn-sm mt-2" @click="showForm()">
<i class="fa-solid fa-plus mr-1"></i> Add First Site
</button>
</div>
</td>
</tr>
</template>
<!-- Site Rows -->
<template x-for="site in list" :key="site.SiteID">
<tr class="hover:bg-opacity-50">
<td>
<span class="badge badge-ghost font-mono text-xs" x-text="site.SiteID || '-'"></span>
</td>
<td>
<div class="font-medium" style="color: rgb(var(--color-text));" x-text="site.SiteName || '-'"></div>
</td>
<td x-text="site.SiteCode || '-'"></td>
<td x-text="site.AccountName || '-'"></td>
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
<td x-text="site.SiteTypeText || site.SiteTypeID || '-'"></td>
<td x-text="site.SiteClassText || site.SiteClassID || '-'"></td>
<td class="text-center">
<div class="flex items-center justify-center gap-1">
<button class="btn btn-ghost btn-sm btn-square" @click="editSite(site.SiteID)" title="Edit">
<i class="fa-solid fa-pen text-sky-500"></i>
</button>
<button class="btn btn-ghost btn-sm btn-square" @click="confirmDelete(site)" title="Delete">
<i class="fa-solid fa-trash" style="color: rgb(var(--color-error));"></i>
</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Summary -->
<div class="p-4 flex items-center justify-between" style="border-top: 1px solid rgb(var(--color-border));" x-show="list && list.length > 0">
<span class="text-sm" style="color: rgb(var(--color-text-muted));" x-text="'Showing ' + list.length + ' sites'"></span>
</div>
</div>
<!-- Include Form Dialog -->
<?= $this->include('v2/master/organization/site_dialog') ?>
<!-- Delete Confirmation Modal -->
<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 site <strong x-text="deleteTarget?.SiteName"></strong>?
This action cannot be undone.
</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="deleteSite()" :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 sites() {
return {
// State
loading: false,
list: [],
accountsList: [],
keyword: "",
// Form Modal
showModal: false,
isEditing: false,
saving: false,
errors: {},
form: {
SiteID: null,
SiteCode: "",
SiteName: "",
AccountID: "",
Parent: "",
ME: ""
},
// Delete Modal
showDeleteModal: false,
deleteTarget: null,
deleting: false,
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
// Lookup Options
siteTypeOptions: [],
siteClassOptions: [],
// Lifecycle
async init() {
await this.fetchList();
await this.fetchAccounts();
feat(valueset): refactor from ID-based to name-based lookups Complete overhaul of the valueset system to use human-readable names instead of numeric IDs for improved maintainability and API consistency. - PatientController: Renamed 'Gender' field to 'Sex' in validation rules - ValuesetController: Changed API endpoints from ID-based (/:num) to name-based (/:any) - TestsController: Refactored to use ValueSet library instead of direct valueset queries - Added ValueSet library (app/Libraries/ValueSet.php) with static lookup methods: - getOptions() - returns dropdown format [{value, label}] - getLabel(, ) - returns label for a value - transformLabels(, ) - batch transform records - get() and getRaw() for Lookups compatibility - Added ValueSetApiController for public valueset API endpoints - Added ValueSet refresh endpoint (POST /api/valueset/refresh) - Added DemoOrderController for testing order creation without auth - 2026-01-12-000001: Convert valueset references from VID to VValue - 2026-01-12-000002: Rename patient.Gender column to Sex - OrderTestController: Now uses OrderTestModel with proper model pattern - TestsController: Uses ValueSet library for all lookup operations - ValueSetController: Simplified to use name-based lookups - Updated all organization (account/site/workstation) dialogs and index views - Updated specimen container dialogs and index views - Updated tests_index.php with ValueSet integration - Updated patient dialog form and index views - Removed .factory/config.json and CLAUDE.md (replaced by AGENTS.md) - Consolidated lookups in Lookups.php (removed inline valueset constants) - Updated all test files to match new field names - 32 modified files, 17 new files, 2 deleted files - Net: +661 insertions, -1443 deletions (significant cleanup)
2026-01-12 16:53:41 +07:00
await this.fetchSiteTypeOptions();
await this.fetchSiteClassOptions();
},
// Fetch site type options
async fetchSiteTypeOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/site_type`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.siteTypeOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch site type options:', err);
}
},
// Fetch site class options
async fetchSiteClassOptions() {
try {
const res = await fetch(`${BASEURL}api/valueset/site_class`, {
credentials: 'include'
});
if (!res.ok) throw new Error("HTTP error");
const data = await res.json();
this.siteClassOptions = data.data || [];
} catch (err) {
console.error('Failed to fetch site class options:', err);
}
},
// Fetch site list
async fetchList() {
this.loading = true;
try {
const params = new URLSearchParams();
if (this.keyword) params.append('SiteName', this.keyword);
const res = await fetch(`${BASEURL}api/organization/site?${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 = [];
} finally {
this.loading = false;
}
},
// Fetch account list for dropdown
async fetchAccounts() {
try {
const res = await fetch(`${BASEURL}api/organization/account`, {
credentials: 'include'
});
const data = await res.json();
this.accountsList = data.data || [];
} catch (err) {
console.error('Failed to fetch accounts:', err);
}
},
// Show form for new site
showForm() {
this.isEditing = false;
this.form = {
SiteID: null,
SiteCode: "",
SiteName: "",
AccountID: "",
Parent: "",
ME: ""
};
this.errors = {};
this.showModal = true;
},
// Edit site
async editSite(id) {
this.isEditing = true;
this.errors = {};
try {
const res = await fetch(`${BASEURL}api/organization/site/${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);
alert('Failed to load site data');
}
},
// Validate form
validate() {
const e = {};
if (!this.form.SiteName?.trim()) e.SiteName = "Site name is required";
if (!this.form.SiteCode?.trim()) e.SiteCode = "Site code is required";
this.errors = e;
return Object.keys(e).length === 0;
},
// Close modal
closeModal() {
this.showModal = false;
this.errors = {};
},
// Save site
async save() {
if (!this.validate()) return;
this.saving = true;
try {
const method = this.isEditing ? 'PATCH' : 'POST';
const res = await fetch(`${BASEURL}api/organization/site`, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form),
credentials: 'include'
});
const data = await res.json();
if (res.ok) {
this.closeModal();
await this.fetchList();
} else {
alert(data.message || "Failed to save");
}
} catch (err) {
console.error(err);
alert("Failed to save site");
} finally {
this.saving = false;
}
},
// Confirm delete
confirmDelete(site) {
this.deleteTarget = site;
this.showDeleteModal = true;
},
// Delete site
async deleteSite() {
if (!this.deleteTarget) return;
this.deleting = true;
try {
const res = await fetch(`${BASEURL}api/organization/site`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ SiteID: this.deleteTarget.SiteID }),
credentials: 'include'
});
if (res.ok) {
this.showDeleteModal = false;
await this.fetchList();
} else {
alert("Failed to delete");
}
} catch (err) {
console.error(err);
alert("Failed to delete site");
} finally {
this.deleting = false;
this.deleteTarget = null;
}
}
}
}
</script>
<?= $this->endSection() ?>