Show personal/address/emergency on employee detail and in edit modal

Detail page now has three new cards beneath the main employee block:
- Personal: DOB (with computed age), gender, nationality, marital status
- Address: combined one-line address plus a labelled grid for the
  Thai-specific subdistrict/district/province/postal code parts
- Emergency Contact: name, phone, relationship

Edit modal extends with matching sections so HR/admin can update
all 14 new fields. updateEmployee server action passes the new
fields through to the employees table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 10:01:09 +07:00
parent f222ac3989
commit f12c901a97
2 changed files with 218 additions and 0 deletions
@@ -181,6 +181,21 @@ export const actions: Actions = {
taxId: formData.get('taxId')?.toString().trim() || null,
bankName: formData.get('bankName')?.toString().trim() || null,
bankAccount: formData.get('bankAccount')?.toString().trim() || null,
dateOfBirth: formData.get('dateOfBirth')?.toString().trim() || null,
gender: formData.get('gender')?.toString().trim() || null,
nationality: formData.get('nationality')?.toString().trim() || null,
maritalStatus: formData.get('maritalStatus')?.toString().trim() || null,
addressLine1: formData.get('addressLine1')?.toString().trim() || null,
addressLine2: formData.get('addressLine2')?.toString().trim() || null,
subdistrict: formData.get('subdistrict')?.toString().trim() || null,
district: formData.get('district')?.toString().trim() || null,
province: formData.get('province')?.toString().trim() || null,
postalCode: formData.get('postalCode')?.toString().trim() || null,
country: formData.get('country')?.toString().trim() || null,
emergencyContactName: formData.get('emergencyContactName')?.toString().trim() || null,
emergencyContactPhone: formData.get('emergencyContactPhone')?.toString().trim() || null,
emergencyContactRelationship:
formData.get('emergencyContactRelationship')?.toString().trim() || null,
updatedAt: new Date()
})
.where(eq(employees.id, params.id));
@@ -19,6 +19,43 @@
const fullName = $derived(
emp.displayName ?? `${emp.firstName} ${emp.lastName}`
);
const GENDER_LABELS: Record<string, string> = {
male: 'Male',
female: 'Female',
other: 'Other',
prefer_not_to_say: 'Prefer not to say'
};
const MARITAL_LABELS: Record<string, string> = {
single: 'Single',
married: 'Married',
divorced: 'Divorced',
widowed: 'Widowed',
other: 'Other'
};
function ageFromDob(dob: string | null | undefined): number | null {
if (!dob) return null;
const d = new Date(dob);
if (isNaN(d.getTime())) return null;
const now = new Date();
let age = now.getFullYear() - d.getFullYear();
const m = now.getMonth() - d.getMonth();
if (m < 0 || (m === 0 && now.getDate() < d.getDate())) age--;
return age;
}
const fullAddress = $derived(
[
emp.addressLine1,
emp.addressLine2,
[emp.subdistrict, emp.district].filter(Boolean).join(', '),
[emp.province, emp.postalCode].filter(Boolean).join(' '),
emp.country
]
.filter((s): s is string => !!s && s.trim().length > 0)
.join(' • ')
);
</script>
<svelte:head>
@@ -146,6 +183,68 @@
</div>
</div>
<!-- Personal -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Personal</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
<div>
<p class="text-gray-500 dark:text-gray-400">Date of Birth</p>
<p class="font-medium text-gray-900 dark:text-white">
{#if emp.dateOfBirth}
{formatDate(emp.dateOfBirth)} <span class="text-xs text-gray-400">({ageFromDob(emp.dateOfBirth)} y)</span>
{:else}{/if}
</p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Gender</p>
<p class="font-medium text-gray-900 dark:text-white">{emp.gender ? GENDER_LABELS[emp.gender] ?? emp.gender : '—'}</p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Nationality</p>
<p class="font-medium text-gray-900 dark:text-white">{emp.nationality ?? '—'}</p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Marital Status</p>
<p class="font-medium text-gray-900 dark:text-white">{emp.maritalStatus ? MARITAL_LABELS[emp.maritalStatus] ?? emp.maritalStatus : '—'}</p>
</div>
</div>
</div>
<!-- Address -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Address</h3>
{#if fullAddress}
<p class="text-sm text-gray-900 dark:text-white">{fullAddress}</p>
<div class="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-3 text-xs text-gray-500 dark:text-gray-400">
{#if emp.subdistrict}<div><span class="block text-[10px] uppercase">Subdistrict</span>{emp.subdistrict}</div>{/if}
{#if emp.district}<div><span class="block text-[10px] uppercase">District</span>{emp.district}</div>{/if}
{#if emp.province}<div><span class="block text-[10px] uppercase">Province</span>{emp.province}</div>{/if}
{#if emp.postalCode}<div><span class="block text-[10px] uppercase">Postal Code</span>{emp.postalCode}</div>{/if}
</div>
{:else}
<p class="text-sm text-gray-500 dark:text-gray-400"></p>
{/if}
</div>
<!-- Emergency Contact -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<h3 class="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Emergency Contact</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm">
<div>
<p class="text-gray-500 dark:text-gray-400">Name</p>
<p class="font-medium text-gray-900 dark:text-white">{emp.emergencyContactName ?? '—'}</p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Phone</p>
<p class="font-medium text-gray-900 dark:text-white">{emp.emergencyContactPhone ?? '—'}</p>
</div>
<div>
<p class="text-gray-500 dark:text-gray-400">Relationship</p>
<p class="font-medium text-gray-900 dark:text-white">{emp.emergencyContactRelationship ?? '—'}</p>
</div>
</div>
</div>
<!-- Leave Balances -->
<div class="mb-6">
<div class="mb-3 flex items-center justify-between">
@@ -540,6 +639,110 @@
</div>
</div>
<!-- Edit: Personal -->
<h4 class="mt-4 mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Personal</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="edit-dateOfBirth" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Date of Birth</label>
<input type="date" id="edit-dateOfBirth" name="dateOfBirth" value={emp.dateOfBirth ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-gender" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Gender</label>
<select id="edit-gender" name="gender"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm">
<option value="" selected={!emp.gender}>—</option>
<option value="male" selected={emp.gender === 'male'}>Male</option>
<option value="female" selected={emp.gender === 'female'}>Female</option>
<option value="other" selected={emp.gender === 'other'}>Other</option>
<option value="prefer_not_to_say" selected={emp.gender === 'prefer_not_to_say'}>Prefer not to say</option>
</select>
</div>
<div>
<label for="edit-nationality" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Nationality</label>
<input type="text" id="edit-nationality" name="nationality" value={emp.nationality ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-maritalStatus" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Marital Status</label>
<select id="edit-maritalStatus" name="maritalStatus"
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm">
<option value="" selected={!emp.maritalStatus}>—</option>
<option value="single" selected={emp.maritalStatus === 'single'}>Single</option>
<option value="married" selected={emp.maritalStatus === 'married'}>Married</option>
<option value="divorced" selected={emp.maritalStatus === 'divorced'}>Divorced</option>
<option value="widowed" selected={emp.maritalStatus === 'widowed'}>Widowed</option>
<option value="other" selected={emp.maritalStatus === 'other'}>Other</option>
</select>
</div>
</div>
<!-- Edit: Address -->
<h4 class="mt-4 mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Address</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="sm:col-span-2">
<label for="edit-addressLine1" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Address Line 1</label>
<input type="text" id="edit-addressLine1" name="addressLine1" value={emp.addressLine1 ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div class="sm:col-span-2">
<label for="edit-addressLine2" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Address Line 2</label>
<input type="text" id="edit-addressLine2" name="addressLine2" value={emp.addressLine2 ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-subdistrict" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Subdistrict <span class="text-xs text-gray-400">(Tambon)</span>
</label>
<input type="text" id="edit-subdistrict" name="subdistrict" value={emp.subdistrict ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-district" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
District <span class="text-xs text-gray-400">(Amphoe)</span>
</label>
<input type="text" id="edit-district" name="district" value={emp.district ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-province" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">
Province <span class="text-xs text-gray-400">(Changwat)</span>
</label>
<input type="text" id="edit-province" name="province" value={emp.province ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-postalCode" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Postal Code</label>
<input type="text" id="edit-postalCode" name="postalCode" maxlength="10" value={emp.postalCode ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div class="sm:col-span-2">
<label for="edit-country" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Country</label>
<input type="text" id="edit-country" name="country" value={emp.country ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
</div>
<!-- Edit: Emergency Contact -->
<h4 class="mt-4 mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Emergency Contact</h4>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label for="edit-emergencyContactName" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Name</label>
<input type="text" id="edit-emergencyContactName" name="emergencyContactName" value={emp.emergencyContactName ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-emergencyContactPhone" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Phone</label>
<input type="tel" id="edit-emergencyContactPhone" name="emergencyContactPhone" value={emp.emergencyContactPhone ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
<div>
<label for="edit-emergencyContactRelationship" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Relationship</label>
<input type="text" id="edit-emergencyContactRelationship" name="emergencyContactRelationship" value={emp.emergencyContactRelationship ?? ''}
class="w-full rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white px-3 py-2 text-sm" />
</div>
</div>
<div class="flex justify-end gap-2 border-t border-gray-100 dark:border-gray-700 pt-4">
<button
type="button"