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:
@@ -181,6 +181,21 @@ export const actions: Actions = {
|
|||||||
taxId: formData.get('taxId')?.toString().trim() || null,
|
taxId: formData.get('taxId')?.toString().trim() || null,
|
||||||
bankName: formData.get('bankName')?.toString().trim() || null,
|
bankName: formData.get('bankName')?.toString().trim() || null,
|
||||||
bankAccount: formData.get('bankAccount')?.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()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(employees.id, params.id));
|
.where(eq(employees.id, params.id));
|
||||||
|
|||||||
@@ -19,6 +19,43 @@
|
|||||||
const fullName = $derived(
|
const fullName = $derived(
|
||||||
emp.displayName ?? `${emp.firstName} ${emp.lastName}`
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -146,6 +183,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Leave Balances -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
@@ -540,6 +639,110 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="flex justify-end gap-2 border-t border-gray-100 dark:border-gray-700 pt-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user