457 lines
16 KiB
PHP
457 lines
16 KiB
PHP
<?= $this->extend('layouts/v2') ?>
|
|
|
|
<?= $this->section('content') ?>
|
|
|
|
<div x-data="patientForm">
|
|
|
|
<!-- Page Header -->
|
|
<div class="flex items-center gap-4 mb-6">
|
|
<a href="<?= site_url('v2/patients') ?>" class="btn btn-ghost btn-sm btn-square">
|
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
|
</a>
|
|
<div>
|
|
<h2 class="text-2xl font-bold"><?= isset($patient) ? 'Edit Patient' : 'New Patient' ?></h2>
|
|
<p class="text-base-content/60"><?= isset($patient) ? 'Update patient information' : 'Register a new patient' ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Alert -->
|
|
<template x-if="error">
|
|
<div class="alert alert-error mb-6" x-transition>
|
|
<i data-lucide="alert-circle" class="w-5 h-5"></i>
|
|
<span x-text="error"></span>
|
|
</div>
|
|
</template>
|
|
|
|
<form @submit.prevent="submitForm">
|
|
|
|
<!-- Personal Information -->
|
|
<div class="card bg-base-100 shadow mb-6">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-lg mb-4">
|
|
<i data-lucide="user" class="w-5 h-5"></i>
|
|
Personal Information
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
|
<!-- Patient ID -->
|
|
<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 if empty">
|
|
</div>
|
|
|
|
<!-- Prefix -->
|
|
<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>
|
|
|
|
<!-- First Name -->
|
|
<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>
|
|
|
|
<!-- Middle Name -->
|
|
<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>
|
|
|
|
<!-- Last Name -->
|
|
<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>
|
|
|
|
<!-- Suffix -->
|
|
<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" placeholder="Jr., Sr., III...">
|
|
</div>
|
|
|
|
<!-- Gender -->
|
|
<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 genderOptions" :key="g.VID">
|
|
<option :value="g.VID" x-text="g.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Birthdate -->
|
|
<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>
|
|
|
|
<!-- Place of Birth -->
|
|
<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>
|
|
|
|
<!-- Marital Status -->
|
|
<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 maritalOptions" :key="m.VID">
|
|
<option :value="m.VID" x-text="m.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Religion -->
|
|
<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 religionOptions" :key="r.VID">
|
|
<option :value="r.VID" x-text="r.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Ethnic -->
|
|
<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 ethnicOptions" :key="e.VID">
|
|
<option :value="e.VID" x-text="e.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Information -->
|
|
<div class="card bg-base-100 shadow mb-6">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-lg mb-4">
|
|
<i data-lucide="phone" class="w-5 h-5"></i>
|
|
Contact Information
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
<!-- Mobile Phone -->
|
|
<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" placeholder="+62...">
|
|
</div>
|
|
|
|
<!-- Phone -->
|
|
<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>
|
|
|
|
<!-- Email 1 -->
|
|
<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" placeholder="email@example.com">
|
|
</div>
|
|
|
|
<!-- Email 2 -->
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Address -->
|
|
<div class="card bg-base-100 shadow mb-6">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-lg mb-4">
|
|
<i data-lucide="map-pin" class="w-5 h-5"></i>
|
|
Address
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
<!-- Street 1 -->
|
|
<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>
|
|
|
|
<!-- Street 2 -->
|
|
<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>
|
|
|
|
<!-- Province -->
|
|
<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 provinces" :key="p.AreaGeoID">
|
|
<option :value="p.AreaGeoID" x-text="p.AreaName"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- City -->
|
|
<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 cities" :key="c.AreaGeoID">
|
|
<option :value="c.AreaGeoID" x-text="c.AreaName"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- ZIP -->
|
|
<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>
|
|
|
|
<!-- Country -->
|
|
<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 countryOptions" :key="c.VID">
|
|
<option :value="c.VID" x-text="c.VDesc"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Identifier (PatIdt) -->
|
|
<div class="card bg-base-100 shadow mb-6">
|
|
<div class="card-body">
|
|
<h3 class="card-title text-lg mb-4">
|
|
<i data-lucide="id-card" class="w-5 h-5"></i>
|
|
Identifier
|
|
</h3>
|
|
|
|
<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>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="flex justify-end gap-4">
|
|
<a href="<?= site_url('v2/patients') ?>" class="btn btn-ghost">Cancel</a>
|
|
<button type="submit" class="btn btn-primary gap-2" :disabled="isSubmitting">
|
|
<span x-show="isSubmitting" class="loading loading-spinner loading-sm"></span>
|
|
<i x-show="!isSubmitting" data-lucide="save" class="w-4 h-4"></i>
|
|
<span x-text="isSubmitting ? 'Saving...' : 'Save Patient'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
<?= $this->endSection() ?>
|
|
|
|
<?= $this->section('script') ?>
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('patientForm', () => ({
|
|
isSubmitting: false,
|
|
error: null,
|
|
isEdit: <?= isset($patient) ? 'true' : 'false' ?>,
|
|
|
|
// Form data
|
|
form: {
|
|
InternalPID: <?= isset($patient) ? json_encode($patient['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: ''
|
|
}
|
|
},
|
|
|
|
// Dropdown options
|
|
genderOptions: [],
|
|
maritalOptions: [],
|
|
religionOptions: [],
|
|
ethnicOptions: [],
|
|
countryOptions: [],
|
|
provinces: [],
|
|
cities: [],
|
|
|
|
async init() {
|
|
await this.loadOptions();
|
|
|
|
<?php if (isset($patient)): ?>
|
|
// Load patient data for edit
|
|
this.loadPatientData(<?= json_encode($patient) ?>);
|
|
<?php endif; ?>
|
|
},
|
|
|
|
async loadOptions() {
|
|
try {
|
|
// Load ValueSets for dropdowns
|
|
const [gender, marital, religion, ethnic, country, provinces] = await Promise.all([
|
|
fetch('<?= site_url('api/valueset/valuesetdef/1') ?>', { credentials: 'include' }).then(r => r.json()),
|
|
fetch('<?= site_url('api/valueset/valuesetdef/2') ?>', { credentials: 'include' }).then(r => r.json()),
|
|
fetch('<?= site_url('api/religion') ?>', { credentials: 'include' }).then(r => r.json()),
|
|
fetch('<?= site_url('api/ethnic') ?>', { credentials: 'include' }).then(r => r.json()),
|
|
fetch('<?= site_url('api/country') ?>', { credentials: 'include' }).then(r => r.json()),
|
|
fetch('<?= site_url('api/areageo/provinces') ?>', { credentials: 'include' }).then(r => r.json())
|
|
]);
|
|
|
|
this.genderOptions = gender.data || [];
|
|
this.maritalOptions = marital.data || [];
|
|
this.religionOptions = religion.data || [];
|
|
this.ethnicOptions = ethnic.data || [];
|
|
this.countryOptions = country.data || [];
|
|
this.provinces = provinces.data || [];
|
|
} catch (err) {
|
|
console.error('Failed to load options:', err);
|
|
}
|
|
},
|
|
|
|
async loadCities() {
|
|
if (!this.form.Province) {
|
|
this.cities = [];
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('<?= site_url('api/areageo/cities') ?>?province=' + this.form.Province, {
|
|
credentials: 'include'
|
|
});
|
|
const data = await response.json();
|
|
this.cities = data.data || [];
|
|
} catch (err) {
|
|
console.error('Failed to load cities:', err);
|
|
}
|
|
},
|
|
|
|
loadPatientData(patient) {
|
|
// Map patient data to form
|
|
Object.keys(this.form).forEach(key => {
|
|
if (key === 'PatIdt') {
|
|
if (patient.PatIdt) {
|
|
this.form.PatIdt = patient.PatIdt;
|
|
}
|
|
} else if (patient[key] !== undefined && patient[key] !== null) {
|
|
// Handle VID fields
|
|
if (patient[key + 'VID']) {
|
|
this.form[key] = patient[key + 'VID'];
|
|
} else if (key === 'Province' && patient.ProvinceID) {
|
|
this.form.Province = patient.ProvinceID;
|
|
} else if (key === 'City' && patient.CityID) {
|
|
this.form.City = patient.CityID;
|
|
} else {
|
|
this.form[key] = patient[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
// Load cities if province is set
|
|
if (this.form.Province) {
|
|
this.loadCities();
|
|
}
|
|
},
|
|
|
|
async submitForm() {
|
|
this.isSubmitting = true;
|
|
this.error = 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;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
$store.toast.success(this.isEdit ? 'Patient updated successfully' : 'Patient created successfully');
|
|
// Redirect to patient list
|
|
setTimeout(() => {
|
|
window.location.href = '<?= site_url('v2/patients') ?>';
|
|
}, 500);
|
|
} else {
|
|
this.error = data.message || 'Failed to save patient';
|
|
}
|
|
} catch (err) {
|
|
this.error = 'Connection error: ' + err.message;
|
|
} finally {
|
|
this.isSubmitting = false;
|
|
}
|
|
}
|
|
}));
|
|
});
|
|
</script>
|
|
<?= $this->endSection() ?>
|