688 lines
27 KiB
PHP
688 lines
27 KiB
PHP
<?= $this->extend('layouts/v2') ?>
|
|
|
|
<?= $this->section('content') ?>
|
|
|
|
<div x-data="patientManager()">
|
|
|
|
<!-- Page Header with Actions -->
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-bold">Patients</h2>
|
|
<p class="text-base-content/60">Manage patient records</p>
|
|
</div>
|
|
<button @click="openCreateModal()" class="btn btn-primary gap-2">
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
Add Patient
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Search & Filter Card -->
|
|
<div class="card bg-base-100 shadow mb-6">
|
|
<div class="card-body">
|
|
<div class="flex flex-col lg:flex-row gap-4">
|
|
|
|
<!-- Search by Name -->
|
|
<div class="form-control flex-1">
|
|
<label class="label">
|
|
<span class="label-text">Patient Name</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Search by name..."
|
|
class="input input-bordered w-full"
|
|
x-model="filters.Name"
|
|
@keyup.enter="searchPatients"
|
|
>
|
|
</div>
|
|
|
|
<!-- Search by ID -->
|
|
<div class="form-control w-full lg:w-48">
|
|
<label class="label">
|
|
<span class="label-text">Patient ID</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="MRN..."
|
|
class="input input-bordered w-full"
|
|
x-model="filters.PatientID"
|
|
@keyup.enter="searchPatients"
|
|
>
|
|
</div>
|
|
|
|
<!-- Search by Birthdate -->
|
|
<div class="form-control w-full lg:w-48">
|
|
<label class="label">
|
|
<span class="label-text">Birthdate</span>
|
|
</label>
|
|
<input
|
|
type="date"
|
|
class="input input-bordered w-full"
|
|
x-model="filters.Birthdate"
|
|
>
|
|
</div>
|
|
|
|
<!-- Search Button -->
|
|
<div class="form-control w-full lg:w-auto">
|
|
<label class="label lg:invisible">
|
|
<span class="label-text"> </span>
|
|
</label>
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="btn btn-primary flex-1 lg:flex-none gap-2"
|
|
@click="searchPatients"
|
|
:disabled="isLoading"
|
|
>
|
|
<i data-lucide="search" class="w-4 h-4"></i>
|
|
Search
|
|
</button>
|
|
<button
|
|
class="btn btn-ghost"
|
|
@click="clearFilters"
|
|
:disabled="isLoading"
|
|
>
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Table -->
|
|
<div class="card bg-base-100 shadow">
|
|
<div class="card-body">
|
|
|
|
<!-- Loading State -->
|
|
<template x-if="isLoading">
|
|
<div class="flex justify-center items-center py-12">
|
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Error State -->
|
|
<template x-if="error && !isLoading">
|
|
<div class="alert alert-error">
|
|
<i data-lucide="alert-circle" class="w-5 h-5"></i>
|
|
<span x-text="error"></span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Empty State -->
|
|
<template x-if="!isLoading && !error && patients.length === 0">
|
|
<div class="text-center py-12">
|
|
<i data-lucide="users" class="w-16 h-16 mx-auto text-base-content/20 mb-4"></i>
|
|
<h3 class="text-lg font-semibold">No patients found</h3>
|
|
<p class="text-base-content/60" x-text="hasSearched ? 'Try adjusting your search criteria' : 'Click Search to load patients'"></p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Data Table -->
|
|
<template x-if="!isLoading && !error && patients.length > 0">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th>Patient ID</th>
|
|
<th>Name</th>
|
|
<th>Gender</th>
|
|
<th>Birthdate</th>
|
|
<th>Mobile</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="patient in patients" :key="patient.InternalPID">
|
|
<tr>
|
|
<td>
|
|
<span class="font-mono text-sm" x-text="patient.PatientID || '-'"></span>
|
|
</td>
|
|
<td>
|
|
<div class="font-medium" x-text="patient.FullName || '-'"></div>
|
|
<div class="text-xs text-base-content/50" x-text="patient.Email || ''"></div>
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-ghost" x-text="patient.Gender || '-'"></span>
|
|
</td>
|
|
<td x-text="formatDate(patient.Birthdate)"></td>
|
|
<td x-text="patient.MobilePhone || '-'"></td>
|
|
<td>
|
|
<div class="flex gap-1">
|
|
<button
|
|
@click="openEditModal(patient)"
|
|
class="btn btn-ghost btn-sm btn-square"
|
|
title="Edit"
|
|
>
|
|
<i data-lucide="pencil" class="w-4 h-4"></i>
|
|
</button>
|
|
<button
|
|
class="btn btn-ghost btn-sm btn-square text-error"
|
|
title="Delete"
|
|
@click="confirmDelete(patient)"
|
|
>
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Results Count -->
|
|
<div class="text-sm text-base-content/60 mt-4">
|
|
Showing <span x-text="patients.length" class="font-medium"></span> patients
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Modal -->
|
|
<dialog id="patientModal" class="modal" :class="{ 'modal-open': showFormModal }">
|
|
<div class="modal-box w-11/12 max-w-5xl">
|
|
<form method="dialog">
|
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" @click="closeFormModal()">✕</button>
|
|
</form>
|
|
|
|
<h3 class="font-bold text-lg mb-6" x-text="isEdit ? 'Edit Patient' : 'New Patient'"></h3>
|
|
|
|
<!-- Error Alert in Modal -->
|
|
<template x-if="formError">
|
|
<div class="alert alert-error mb-6">
|
|
<i data-lucide="alert-circle" class="w-5 h-5"></i>
|
|
<span x-text="formError"></span>
|
|
</div>
|
|
</template>
|
|
|
|
<form @submit.prevent="submitForm">
|
|
<!-- Personal Information -->
|
|
<div class="mb-6">
|
|
<h4 class="text-base font-semibold mb-3 flex items-center gap-2">
|
|
<i data-lucide="user" class="w-4 h-4"></i> Personal Information
|
|
</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Patient ID (MRN)</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.PatientID" placeholder="Auto-generated">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Prefix</span></label>
|
|
<select class="select select-bordered" x-model="form.Prefix">
|
|
<option value="">Select...</option>
|
|
<option value="Mr.">Mr.</option>
|
|
<option value="Mrs.">Mrs.</option>
|
|
<option value="Ms.">Ms.</option>
|
|
<option value="Dr.">Dr.</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">First Name *</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.NameFirst" required>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Middle Name</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.NameMiddle">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Last Name</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.NameLast">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Suffix</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.Suffix">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Gender *</span></label>
|
|
<select class="select select-bordered" x-model="form.Gender" required>
|
|
<option value="">Select...</option>
|
|
<template x-for="g in options.gender" :key="g.VID">
|
|
<option :value="g.VID" x-text="g.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Birthdate *</span></label>
|
|
<input type="date" class="input input-bordered" x-model="form.Birthdate" required>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Place of Birth</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.PlaceOfBirth">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Marital Status</span></label>
|
|
<select class="select select-bordered" x-model="form.MaritalStatus">
|
|
<option value="">Select...</option>
|
|
<template x-for="m in options.marital" :key="m.VID">
|
|
<option :value="m.VID" x-text="m.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Religion</span></label>
|
|
<select class="select select-bordered" x-model="form.Religion">
|
|
<option value="">Select...</option>
|
|
<template x-for="r in options.religion" :key="r.VID">
|
|
<option :value="r.VID" x-text="r.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Ethnic</span></label>
|
|
<select class="select select-bordered" x-model="form.Ethnic">
|
|
<option value="">Select...</option>
|
|
<template x-for="e in options.ethnic" :key="e.VID">
|
|
<option :value="e.VID" x-text="e.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Information -->
|
|
<div class="mb-6">
|
|
<h4 class="text-base font-semibold mb-3 flex items-center gap-2">
|
|
<i data-lucide="phone" class="w-4 h-4"></i> Contact Information
|
|
</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Mobile Phone</span></label>
|
|
<input type="tel" class="input input-bordered" x-model="form.MobilePhone">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Phone</span></label>
|
|
<input type="tel" class="input input-bordered" x-model="form.Phone">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Email Address</span></label>
|
|
<input type="email" class="input input-bordered" x-model="form.EmailAddress1">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Alternate Email</span></label>
|
|
<input type="email" class="input input-bordered" x-model="form.EmailAddress2">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Address -->
|
|
<div class="mb-6">
|
|
<h4 class="text-base font-semibold mb-3 flex items-center gap-2">
|
|
<i data-lucide="map-pin" class="w-4 h-4"></i> Address
|
|
</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label"><span class="label-text">Street Address</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.Street_1">
|
|
</div>
|
|
<div class="form-control md:col-span-2">
|
|
<label class="label"><span class="label-text">Street Address 2</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.Street_2">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Province</span></label>
|
|
<select class="select select-bordered" x-model="form.Province" @change="loadCities">
|
|
<option value="">Select Province...</option>
|
|
<template x-for="p in options.provinces" :key="p.AreaGeoID">
|
|
<option :value="p.AreaGeoID" x-text="p.AreaName"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">City</span></label>
|
|
<select class="select select-bordered" x-model="form.City" :disabled="!form.Province">
|
|
<option value="">Select City...</option>
|
|
<template x-for="c in options.cities" :key="c.AreaGeoID">
|
|
<option :value="c.AreaGeoID" x-text="c.AreaName"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">ZIP Code</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.ZIP">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">Country</span></label>
|
|
<select class="select select-bordered" x-model="form.Country">
|
|
<option value="">Select...</option>
|
|
<template x-for="c in options.country" :key="c.VID">
|
|
<option :value="c.VID" x-text="c.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Identifier -->
|
|
<div class="mb-6">
|
|
<h4 class="text-base font-semibold mb-3 flex items-center gap-2">
|
|
<i data-lucide="id-card" class="w-4 h-4"></i> Identifier
|
|
</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">ID Type</span></label>
|
|
<select class="select select-bordered" x-model="form.PatIdt.IdentifierType">
|
|
<option value="">Select...</option>
|
|
<option value="KTP">KTP</option>
|
|
<option value="SIM">SIM</option>
|
|
<option value="Passport">Passport</option>
|
|
<option value="BPJS">BPJS</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text">ID Number</span></label>
|
|
<input type="text" class="input input-bordered" x-model="form.PatIdt.Identifier">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" @click="closeFormModal()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="isSubmitting">
|
|
<span x-show="isSubmitting" class="loading loading-spinner loading-sm"></span>
|
|
<span x-text="isSubmitting ? 'Saving...' : 'Save Patient'"></span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<dialog id="deleteModal" class="modal" :class="{ 'modal-open': showDeleteModal }">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg">Delete Patient</h3>
|
|
<p class="py-4">
|
|
Are you sure you want to delete patient
|
|
<span class="font-semibold" x-text="deleteTarget?.FullName"></span>?
|
|
This action cannot be undone.
|
|
</p>
|
|
<div class="modal-action">
|
|
<button class="btn" @click="showDeleteModal = false">Cancel</button>
|
|
<button
|
|
class="btn btn-error"
|
|
@click="deletePatient"
|
|
:disabled="isDeleting"
|
|
>
|
|
<span x-show="isDeleting" class="loading loading-spinner loading-sm"></span>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button @click="showDeleteModal = 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('patientManager', () => ({
|
|
// List State
|
|
patients: [],
|
|
isLoading: false,
|
|
error: null,
|
|
hasSearched: false,
|
|
filters: {
|
|
Name: '',
|
|
PatientID: '',
|
|
Birthdate: ''
|
|
},
|
|
|
|
// Delete State
|
|
showDeleteModal: false,
|
|
deleteTarget: null,
|
|
isDeleting: false,
|
|
|
|
// Form/Modal State
|
|
showFormModal: false,
|
|
isEdit: false,
|
|
isSubmitting: false,
|
|
formError: null,
|
|
|
|
// Options Cache
|
|
options: {
|
|
gender: [],
|
|
marital: [],
|
|
religion: [],
|
|
ethnic: [],
|
|
country: [],
|
|
provinces: [],
|
|
cities: []
|
|
},
|
|
|
|
// Default Form Data
|
|
defaultForm: {
|
|
InternalPID: null,
|
|
PatientID: '',
|
|
Prefix: '',
|
|
NameFirst: '',
|
|
NameMiddle: '',
|
|
NameLast: '',
|
|
Suffix: '',
|
|
Gender: '',
|
|
Birthdate: '',
|
|
PlaceOfBirth: '',
|
|
MaritalStatus: '',
|
|
Religion: '',
|
|
Ethnic: '',
|
|
Country: '',
|
|
Race: '',
|
|
MobilePhone: '',
|
|
Phone: '',
|
|
EmailAddress1: '',
|
|
EmailAddress2: '',
|
|
Street_1: '',
|
|
Street_2: '',
|
|
Street_3: '',
|
|
Province: '',
|
|
City: '',
|
|
ZIP: '',
|
|
PatIdt: {
|
|
IdentifierType: '',
|
|
Identifier: ''
|
|
}
|
|
},
|
|
form: {}, // Initialized in init
|
|
|
|
init() {
|
|
this.form = JSON.parse(JSON.stringify(this.defaultForm));
|
|
this.loadOptions();
|
|
this.searchPatients(); // Initial load
|
|
},
|
|
|
|
// --- List Actions ---
|
|
|
|
async searchPatients() {
|
|
this.isLoading = true;
|
|
this.error = null;
|
|
this.hasSearched = true;
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (this.filters.Name) params.append('Name', this.filters.Name);
|
|
if (this.filters.PatientID) params.append('PatientID', this.filters.PatientID);
|
|
if (this.filters.Birthdate) params.append('Birthdate', this.filters.Birthdate);
|
|
|
|
const data = await Utils.api('<?= site_url('api/patient') ?>?' + params.toString());
|
|
this.patients = data.data || [];
|
|
|
|
// Re-initialize icons for new rows
|
|
setTimeout(() => {
|
|
if(window.lucide) window.lucide.createIcons();
|
|
}, 50);
|
|
} catch (e) {
|
|
this.error = e.message;
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
|
|
clearFilters() {
|
|
this.filters = { Name: '', PatientID: '', Birthdate: '' };
|
|
this.searchPatients();
|
|
},
|
|
|
|
formatDate(dateStr) {
|
|
return Utils.formatDate(dateStr);
|
|
},
|
|
|
|
// --- Delete Actions ---
|
|
|
|
confirmDelete(patient) {
|
|
this.deleteTarget = patient;
|
|
this.showDeleteModal = true;
|
|
},
|
|
|
|
async deletePatient() {
|
|
if (!this.deleteTarget) return;
|
|
this.isDeleting = true;
|
|
try {
|
|
await Utils.api('<?= site_url('api/patient') ?>', {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ InternalPID: this.deleteTarget.InternalPID })
|
|
});
|
|
Alpine.store('toast').success('Patient deleted successfully');
|
|
this.showDeleteModal = false;
|
|
this.searchPatients();
|
|
} catch (e) {
|
|
Alpine.store('toast').error(e.message || 'Failed to delete patient');
|
|
} finally {
|
|
this.isDeleting = false;
|
|
}
|
|
},
|
|
|
|
// --- Form/Modal Actions ---
|
|
|
|
async loadOptions() {
|
|
try {
|
|
// Check if already loaded
|
|
if(this.options.gender.length > 0) return;
|
|
|
|
const [gender, marital, religion, ethnic, country, provinces] = await Promise.all([
|
|
Utils.api('<?= site_url('api/valueset/valuesetdef/1') ?>'),
|
|
Utils.api('<?= site_url('api/valueset/valuesetdef/2') ?>'),
|
|
Utils.api('<?= site_url('api/religion') ?>'),
|
|
Utils.api('<?= site_url('api/ethnic') ?>'),
|
|
Utils.api('<?= site_url('api/country') ?>'),
|
|
Utils.api('<?= site_url('api/areageo/provinces') ?>')
|
|
]);
|
|
|
|
this.options.gender = gender.data || [];
|
|
this.options.marital = marital.data || [];
|
|
this.options.religion = religion.data || [];
|
|
this.options.ethnic = ethnic.data || [];
|
|
this.options.country = country.data || [];
|
|
this.options.provinces = provinces.data || [];
|
|
} catch (e) {
|
|
console.error('Failed to load options', e);
|
|
Alpine.store('toast').error('Failed to load form options');
|
|
}
|
|
},
|
|
|
|
async loadCities() {
|
|
if (!this.form.Province) {
|
|
this.options.cities = [];
|
|
return;
|
|
}
|
|
try {
|
|
const data = await Utils.api('<?= site_url('api/areageo/cities') ?>?province=' + this.form.Province);
|
|
this.options.cities = data.data || [];
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
},
|
|
|
|
openCreateModal() {
|
|
this.isEdit = false;
|
|
this.formError = null;
|
|
this.form = JSON.parse(JSON.stringify(this.defaultForm));
|
|
this.showFormModal = true;
|
|
},
|
|
|
|
async openEditModal(patient) {
|
|
this.isEdit = true;
|
|
this.formError = null;
|
|
// Fetch full details if needed, or use row data if sufficient.
|
|
// Row data might allow for quicker edit if all fields are present, but it's safer to fetch.
|
|
// Assuming row data is partial:
|
|
|
|
try {
|
|
const fullPatient = await Utils.api('<?= site_url('api/patient/') ?>' + patient.InternalPID);
|
|
this.mapPatientToForm(fullPatient.data || patient);
|
|
this.showFormModal = true;
|
|
} catch(e) {
|
|
Alpine.store('toast').error('Failed to load patient details');
|
|
}
|
|
},
|
|
|
|
mapPatientToForm(patient) {
|
|
// deep copy default first
|
|
const f = JSON.parse(JSON.stringify(this.defaultForm));
|
|
|
|
Object.keys(f).forEach(key => {
|
|
if (key === 'PatIdt') {
|
|
if (patient.PatIdt) f.PatIdt = patient.PatIdt;
|
|
} else if (patient[key] !== undefined && patient[key] !== null) {
|
|
if (patient[key + 'VID']) {
|
|
f[key] = patient[key + 'VID'];
|
|
} else if (key === 'Province' && patient.ProvinceID) {
|
|
f.Province = patient.ProvinceID;
|
|
} else if (key === 'City' && patient.CityID) {
|
|
f.City = patient.CityID;
|
|
} else {
|
|
f[key] = patient[key];
|
|
}
|
|
}
|
|
});
|
|
this.form = f;
|
|
|
|
if (this.form.Province) {
|
|
this.loadCities();
|
|
}
|
|
},
|
|
|
|
closeFormModal() {
|
|
this.showFormModal = false;
|
|
},
|
|
|
|
async submitForm() {
|
|
this.isSubmitting = true;
|
|
this.formError = null;
|
|
|
|
try {
|
|
const url = '<?= site_url('api/patient') ?>';
|
|
const method = this.isEdit ? 'PATCH' : 'POST';
|
|
|
|
// Clean up PatIdt if empty
|
|
const payload = { ...this.form };
|
|
if (!payload.PatIdt.IdentifierType || !payload.PatIdt.Identifier) {
|
|
delete payload.PatIdt;
|
|
}
|
|
|
|
await Utils.api(url, {
|
|
method: method,
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
Alpine.store('toast').success(this.isEdit ? 'Patient updated successfully' : 'Patient created successfully');
|
|
this.closeFormModal();
|
|
this.searchPatients();
|
|
} catch (e) {
|
|
this.formError = e.message;
|
|
Alpine.store('toast').error('Failed to save patient');
|
|
} finally {
|
|
this.isSubmitting = false;
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
|
|
Alpine.start();
|
|
</script>
|
|
<?= $this->endSection() ?>
|