clqms-be/app/Views/v2/patient-form.php
2025-12-22 16:54:19 +07:00

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() ?>