2025-12-22 16:54:19 +07:00
|
|
|
<?= $this->extend('layouts/v2') ?>
|
|
|
|
|
|
|
|
|
|
<?= $this->section('content') ?>
|
|
|
|
|
|
|
|
|
|
<div x-data="organizationManager()">
|
|
|
|
|
<div class="flex items-center gap-4 mb-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 class="text-2xl font-bold">Organization</h2>
|
|
|
|
|
<p class="text-base-content/60">Manage <span x-text="activeTab + 's'"></span></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Removed Tab List -->
|
|
|
|
|
<!-- The tab is now determined by the URL parameter passed from controller -->
|
|
|
|
|
|
|
|
|
|
<!-- Generic Data Table -->
|
|
|
|
|
<div class="card bg-base-100 shadow">
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
|
|
|
|
<div class="flex justify-between items-center mb-4">
|
|
|
|
|
<h3 class="card-title capitalize" x-text="activeTab + 's'"></h3>
|
|
|
|
|
<button @click="openModal()" class="btn btn-primary btn-sm gap-2">
|
|
|
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
|
|
|
Add <span class="capitalize" x-text="activeTab"></span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Loading -->
|
|
|
|
|
<template x-if="isLoading">
|
|
|
|
|
<div class="flex justify-center p-8">
|
|
|
|
|
<span class="loading loading-spinner text-primary"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- Table -->
|
|
|
|
|
<div class="overflow-x-auto" x-show="!isLoading">
|
|
|
|
|
<table class="table table-zebra">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>ID</th>
|
|
|
|
|
<th>Name</th>
|
|
|
|
|
<!-- Dynamic Headers based on type -->
|
|
|
|
|
<template x-if="activeTab === 'account'"><th>Parent Account</th></template>
|
|
|
|
|
<template x-if="activeTab === 'site'"><th>Account ID</th></template>
|
|
|
|
|
<template x-if="activeTab === 'department'"><th>Site ID</th></template>
|
|
|
|
|
<template x-if="activeTab === 'workstation'"><th>Department ID</th></template>
|
|
|
|
|
<th>Actions</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<template x-for="row in data" :key="getRowId(row)">
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="font-mono text-xs" x-text="getRowId(row)"></td>
|
|
|
|
|
<td class="font-bold" x-text="getRowName(row)"></td>
|
|
|
|
|
|
|
|
|
|
<!-- Account specific -->
|
|
|
|
|
<template x-if="activeTab === 'account'">
|
|
|
|
|
<td x-text="row.Parent || '-'"></td>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- Site specific -->
|
|
|
|
|
<template x-if="activeTab === 'site'">
|
|
|
|
|
<td x-text="row.AccountID || '-'"></td>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- Department specific -->
|
|
|
|
|
<template x-if="activeTab === 'department'">
|
|
|
|
|
<td x-text="row.SiteID || '-'"></td>
|
|
|
|
|
</template>
|
|
|
|
|
<!-- Workstation specific -->
|
|
|
|
|
<template x-if="activeTab === 'workstation'">
|
|
|
|
|
<td x-text="row.DepartmentID || '-'"></td>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<td>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button @click="editRow(row)" class="btn btn-xs btn-ghost btn-square">
|
|
|
|
|
<i data-lucide="pencil" class="w-4 h-4"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<button @click="deleteRow(row)" class="btn btn-xs btn-ghost btn-square text-error">
|
|
|
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
<template x-if="data.length === 0">
|
|
|
|
|
<tr>
|
|
|
|
|
<td colspan="4" class="text-center text-base-content/50 py-4">No records found</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Generic Modal -->
|
2025-12-23 06:29:01 +07:00
|
|
|
<template x-teleport="body">
|
|
|
|
|
<dialog id="orgModal" class="modal" :class="{ 'modal-open': isModalOpen }">
|
|
|
|
|
<div class="modal-box">
|
|
|
|
|
<h3 class="font-bold text-lg mb-4" x-text="isEdit ? 'Edit ' + activeTab : 'New ' + activeTab"></h3>
|
2025-12-22 16:54:19 +07:00
|
|
|
|
2025-12-23 06:29:01 +07:00
|
|
|
<form @submit.prevent="save">
|
2025-12-22 16:54:19 +07:00
|
|
|
<div class="form-control w-full mb-4">
|
2025-12-23 06:29:01 +07:00
|
|
|
<label class="label"><span class="label-text">Name</span></label>
|
|
|
|
|
<input type="text" x-model="form.Name" class="input input-bordered w-full" required />
|
2025-12-22 16:54:19 +07:00
|
|
|
</div>
|
2025-12-23 06:29:01 +07:00
|
|
|
|
|
|
|
|
<!-- Account Fields -->
|
|
|
|
|
<template x-if="activeTab === 'account'">
|
|
|
|
|
<div class="form-control w-full mb-4">
|
|
|
|
|
<label class="label"><span class="label-text">Parent Account</span></label>
|
|
|
|
|
<input type="text" x-model="form.Parent" class="input input-bordered w-full" placeholder="Optional" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- Site Fields: Account ID Link -->
|
|
|
|
|
<template x-if="activeTab === 'site'">
|
|
|
|
|
<div class="form-control w-full mb-4">
|
|
|
|
|
<label class="label"><span class="label-text">Account ID</span></label>
|
|
|
|
|
<input type="number" x-model="form.AccountID" class="input input-bordered w-full" required />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- Department Fields: Site ID Link -->
|
|
|
|
|
<template x-if="activeTab === 'department'">
|
|
|
|
|
<div class="form-control w-full mb-4">
|
|
|
|
|
<label class="label"><span class="label-text">Site ID</span></label>
|
|
|
|
|
<input type="number" x-model="form.SiteID" class="input input-bordered w-full" required />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- Workstation Fields: Department ID Link -->
|
|
|
|
|
<template x-if="activeTab === 'workstation'">
|
|
|
|
|
<div class="form-control w-full mb-4">
|
|
|
|
|
<label class="label"><span class="label-text">Department ID</span></label>
|
|
|
|
|
<input type="number" x-model="form.DepartmentID" class="input input-bordered w-full" required />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div class="modal-action">
|
|
|
|
|
<button type="button" class="btn" @click="closeModal">Cancel</button>
|
|
|
|
|
<button type="submit" class="btn btn-primary" :disabled="isSaving">
|
|
|
|
|
<span x-show="isSaving" class="loading loading-spinner loading-xs"></span>
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
2025-12-22 16:54:19 +07:00
|
|
|
</div>
|
2025-12-23 06:29:01 +07:00
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</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('organizationManager', () => ({
|
|
|
|
|
// Initialize with the type passed from PHP view
|
|
|
|
|
activeTab: '<?= $type ?? 'account' ?>',
|
|
|
|
|
data: [],
|
|
|
|
|
isLoading: false,
|
|
|
|
|
isModalOpen: false,
|
|
|
|
|
isEdit: false,
|
|
|
|
|
isSaving: false,
|
|
|
|
|
form: {},
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
this.loadData();
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// NOTE: setTab removed as we use routing now
|
|
|
|
|
|
|
|
|
|
async loadData() {
|
|
|
|
|
this.isLoading = true;
|
|
|
|
|
this.data = [];
|
|
|
|
|
try {
|
|
|
|
|
const response = await Utils.api(`<?= site_url('api/organization/') ?>${this.activeTab}`);
|
|
|
|
|
this.data = response.data || [];
|
|
|
|
|
// Re-render icons
|
|
|
|
|
setTimeout(() => window.lucide?.createIcons(), 50);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Alpine.store('toast').error(e.message);
|
|
|
|
|
} finally {
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getRowId(row) {
|
|
|
|
|
if(this.activeTab === 'account') return row.AccountID;
|
|
|
|
|
if(this.activeTab === 'site') return row.SiteID;
|
|
|
|
|
if(this.activeTab === 'discipline') return row.DisciplineID;
|
|
|
|
|
if(this.activeTab === 'department') return row.DepartmentID;
|
|
|
|
|
if(this.activeTab === 'workstation') return row.WorkstationID;
|
|
|
|
|
return row.ID;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getRowName(row) {
|
|
|
|
|
if(this.activeTab === 'account') return row.AccountName;
|
|
|
|
|
if(this.activeTab === 'site') return row.SiteName;
|
|
|
|
|
if(this.activeTab === 'discipline') return row.DisciplineName;
|
|
|
|
|
if(this.activeTab === 'department') return row.DepartmentName;
|
|
|
|
|
if(this.activeTab === 'workstation') return row.WorkstationName;
|
|
|
|
|
return row.Name;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
openModal() {
|
|
|
|
|
this.isEdit = false;
|
|
|
|
|
this.form = { Name: '' };
|
|
|
|
|
this.isModalOpen = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
editRow(row) {
|
|
|
|
|
this.isEdit = true;
|
|
|
|
|
this.form = { ...row };
|
|
|
|
|
// Map specific name fields to generic 'Name' for the form input
|
|
|
|
|
if(this.activeTab === 'account') this.form.Name = row.AccountName;
|
|
|
|
|
if(this.activeTab === 'site') this.form.Name = row.SiteName;
|
|
|
|
|
if(this.activeTab === 'discipline') this.form.Name = row.DisciplineName;
|
|
|
|
|
if(this.activeTab === 'department') this.form.Name = row.DepartmentName;
|
|
|
|
|
if(this.activeTab === 'workstation') this.form.Name = row.WorkstationName;
|
|
|
|
|
|
|
|
|
|
this.isModalOpen = true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
closeModal() {
|
|
|
|
|
this.isModalOpen = false;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async save() {
|
|
|
|
|
this.isSaving = true;
|
|
|
|
|
try {
|
|
|
|
|
const payload = { ...this.form };
|
|
|
|
|
|
|
|
|
|
// Map generic Name back to specific field
|
|
|
|
|
if(this.activeTab === 'account') payload.AccountName = this.form.Name;
|
|
|
|
|
if(this.activeTab === 'site') payload.SiteName = this.form.Name;
|
|
|
|
|
if(this.activeTab === 'discipline') payload.DisciplineName = this.form.Name;
|
|
|
|
|
if(this.activeTab === 'department') payload.DepartmentName = this.form.Name;
|
|
|
|
|
if(this.activeTab === 'workstation') payload.WorkstationName = this.form.Name;
|
|
|
|
|
|
|
|
|
|
// ID for updates
|
|
|
|
|
if(this.isEdit) {
|
|
|
|
|
if(this.activeTab === 'account') payload.AccountID = this.form.AccountID;
|
|
|
|
|
if(this.activeTab === 'site') payload.SiteID = this.form.SiteID;
|
|
|
|
|
if(this.activeTab === 'discipline') payload.DisciplineID = this.form.DisciplineID;
|
|
|
|
|
if(this.activeTab === 'department') payload.DepartmentID = this.form.DepartmentID;
|
|
|
|
|
if(this.activeTab === 'workstation') payload.WorkstationID = this.form.WorkstationID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const method = this.isEdit ? 'PATCH' : 'POST';
|
|
|
|
|
await Utils.api(`<?= site_url('api/organization/') ?>${this.activeTab}`, {
|
|
|
|
|
method,
|
|
|
|
|
body: JSON.stringify(payload)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Alpine.store('toast').success('Saved successfully');
|
|
|
|
|
this.closeModal();
|
|
|
|
|
this.loadData();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Alpine.store('toast').error(e.message);
|
|
|
|
|
} finally {
|
|
|
|
|
this.isSaving = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async deleteRow(row) {
|
|
|
|
|
if(!confirm('Are you sure you want to delete this item?')) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const id = this.getRowId(row);
|
|
|
|
|
const idField = this.activeTab === 'account' ? 'AccountID' :
|
|
|
|
|
this.activeTab === 'site' ? 'SiteID' :
|
|
|
|
|
this.activeTab === 'discipline' ? 'DisciplineID' :
|
|
|
|
|
this.activeTab === 'department' ? 'DepartmentID' :
|
|
|
|
|
this.activeTab === 'workstation' ? 'WorkstationID' : 'ID';
|
|
|
|
|
|
|
|
|
|
await Utils.api(`<?= site_url('api/organization/') ?>${this.activeTab}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
body: JSON.stringify({ [idField]: id })
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Alpine.store('toast').success('Deleted successfully');
|
|
|
|
|
this.loadData();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
Alpine.store('toast').error(e.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
Alpine.start();
|
|
|
|
|
</script>
|
|
|
|
|
<?= $this->endSection() ?>
|