perbaikan HR

This commit is contained in:
Aji Setiaji
2025-09-10 11:46:01 +07:00
parent aa9a285684
commit ca9c2d0901
2 changed files with 481 additions and 69 deletions

View File

@@ -2,6 +2,7 @@
import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte";
import { writable } from "svelte/store";
import { v4 as uuidv4 } from "uuid";
type EmployeeItem = {
id: string;
@@ -17,12 +18,12 @@
phone: string;
mobile: string;
personal_email: string;
wok_email: string;
work_email: string;
permanent_address: string;
temporary_address: string;
job_title: string;
emergency_contact_name: string;
emergency_contract_phone: string;
emergency_contact_phone: string;
bank_account: string;
jamsostek_id: string;
npwp_id: string;
@@ -34,10 +35,15 @@
created_at?: Date;
};
type POItem = {
id: number;
item_name: string;
created_at?: Date;
const EmployeeStatus = {
Active: "Active",
Inactive: "Inactive",
};
const EmployeeType = {
DailyWorker: "Daily Worker",
Contract: "Contract",
OutSource: "Outsource",
};
let allRows: EmployeeItem[] = [];
@@ -63,12 +69,12 @@
{ key: "phone", title: "Phone" },
{ key: "mobile", title: "Mobile" },
{ key: "personal_email", title: "Personal Email" },
{ key: "wok_email", title: "Work Email" },
{ key: "work_email", title: "Work Email" },
{ key: "permanent_address", title: "Permanent Address" },
{ key: "temporary_address", title: "Temporary Address" },
{ key: "job_title", title: "Job Title" },
{ key: "emergency_contact_name", title: "Emergency Contact Name" },
{ key: "emergency_contract_phone", title: "Emergency Contact Phone" },
{ key: "emergency_contact_phone", title: "Emergency Contact Phone" },
{ key: "bank_account", title: "Bank Account" },
{ key: "jamsostek_id", title: "Jamsostek ID" },
{ key: "npwp_id", title: "NPWP ID" },
@@ -132,13 +138,7 @@
currentPage = page;
offset = (currentPage - 1) * rowsPerPage;
fetchEmployee(
null,
"created_at",
"desc",
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
fetchEmployee(null, "created_at", "desc", currentPage - 1, rowsPerPage);
}
function pageRange(
@@ -173,13 +173,7 @@
currentPage = page;
offset = (currentPage - 1) * rowsPerPage;
fetchEmployee(
null,
"created_at",
"desc",
(currentPage - 1) * rowsPerPage,
rowsPerPage,
);
fetchEmployee(null, "created_at", "desc", currentPage - 1, rowsPerPage);
}
onMount(() => {
@@ -194,32 +188,34 @@
let currentEditingId: string | null = null;
let newEmployeeInsert: EmployeeItem = {
id: "",
employee_name: "",
employee_status: "",
location: "",
department: "",
employee_name: "AJITEST",
employee_status: EmployeeStatus.Active,
location: "Nusa Penida",
department: "IT",
contract_start: new Date(),
end_of_contract: new Date(),
employee_type: "",
employee_type: "Contract",
date_of_birth: new Date(),
photo_url: "",
phone: "",
mobile: "",
personal_email: "",
wok_email: "",
permanent_address: "",
temporary_address: "",
job_title: "",
emergency_contact_name: "",
emergency_contract_phone: "",
bank_account: "",
jamsostek_id: "",
npwp_id: "",
remarks: "",
salary: 0,
last_edu: "",
document: "",
url: "",
photo_url:
"https://nusapenida-balitour.com/wp-content/uploads/2022/10/a-glance-nusa-penida-scaled.jpg",
phone: "08123456789",
mobile: "08123456789",
personal_email: "ajitest@example.com",
work_email: "ajitest@workmail.com",
permanent_address: "Jl. Example No. 123, Jakarta",
temporary_address: "Jl. Temporary No. 456, Jakarta",
job_title: "Software Engineer",
emergency_contact_name: "Aji Setiaji",
emergency_contact_phone: "08123456789",
bank_account: "1234567890",
jamsostek_id: "9876543210",
npwp_id: "123456789012345",
remarks: "New employee",
salary: 5000000,
last_edu: "Bachelor's Degree",
document:
"https://nusapenida-balitour.com/wp-content/uploads/2022/10/a-glance-nusa-penida-scaled.jpg",
url: "https://example.com/employee/ajitest",
created_at: new Date(),
};
@@ -255,12 +251,12 @@
phone: "",
mobile: "",
personal_email: "",
wok_email: "",
work_email: "",
permanent_address: "",
temporary_address: "",
job_title: "",
emergency_contact_name: "",
emergency_contract_phone: "",
emergency_contact_phone: "",
bank_account: "",
jamsostek_id: "",
npwp_id: "",
@@ -278,16 +274,12 @@
async function saveEmployee(event: Event) {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
// Validate form data
if (!validateForm(formData)) {
console.error("Form validation failed");
// validate newEmployeeInsert
if (!validateForm(newEmployeeInsert)) {
alert("Please fix the form errors before submitting.");
return;
}
console.log("Saving Employee:", newEmployeeInsert);
if (isEditing && currentEditingId) {
const { error } = await supabase
.from("vb_employee")
@@ -302,6 +294,8 @@
alert("Employee updated successfully!");
}
} else {
newEmployeeInsert.id = uuidv4(); // Generate a new UUID for the ID
const { error } = await supabase
.from("vb_employee")
.insert(newEmployeeInsert);
@@ -347,7 +341,7 @@
export let formErrors = writable<{ [key: string]: string }>({});
function validateForm(formData: FormData): boolean {
function validateForm(newEmployeeInsert: EmployeeItem): boolean {
const errors: { [key: string]: string } = {};
const requiredFields = [
"employee_name",
@@ -355,7 +349,10 @@
];
requiredFields.forEach((field) => {
if (!formData.get(field) || formData.get(field) === "") {
if (
!newEmployeeInsert[field as keyof EmployeeItem] ||
newEmployeeInsert[field as keyof EmployeeItem] === ""
) {
errors[field] = `${field.replace(/_/g, " ")} is required.`;
}
});
@@ -377,7 +374,7 @@
<h2
class="text-lg font-semibold text-gray-800 flex items-center gap-2"
>
<span class="text-blue-600">👥</span>
<span class="text-blue-600">👨‍💼</span>
Employee
</h2>
<p class="text-sm text-gray-600">Manage your employee data here.</p>
@@ -443,6 +440,86 @@
<td class="px-4 py-2 font-medium text-gray-800">
{row[col.key as keyof EmployeeItem]}
</td>
{:else if col.key === "employee_status"}
<td class="px-4 py-2">
{#if row[col.key as keyof EmployeeItem] === EmployeeStatus.Active}
<span
class="inline-block px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800"
>
{EmployeeStatus.Active}
</span>
{:else}
<span
class="inline-block px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800"
>
{EmployeeStatus.Inactive}
</span>
{/if}
</td>
{:else if col.key === "employee_type"}
<td class="px-4 py-2">
{#if row[col.key as keyof EmployeeItem] === EmployeeType.DailyWorker}
<span
class="inline-block px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800"
>
{EmployeeType.DailyWorker}
</span>
{:else if row[col.key as keyof EmployeeItem] === EmployeeType.Contract}
<span
class="inline-block px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800"
>
{EmployeeType.Contract}
</span>
{:else if row[col.key as keyof EmployeeItem] === EmployeeType.OutSource}
<span
class="inline-block px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800"
>
{EmployeeType.OutSource}
</span>
{:else}
<span
class="inline-block px-2 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-800"
>
Unknown
</span>
{/if}
</td>
{:else if col.key === "photo_url" || col.key === "document" || col.key === "url"}
<td class="px-4 py-2">
{#if row[col.key as keyof EmployeeItem]}
<a
href={row[col.key as keyof EmployeeItem] as string}
target="_blank"
class="text-blue-600 hover:underline"
>View</a
>
{:else}
N/A
{/if}
</td>
{:else if col.key === "salary"}
<td class="px-4 py-2">
{row[col.key as keyof EmployeeItem]
? new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
}).format(
row[
col.key as keyof EmployeeItem
] as number,
)
: "N/A"}
</td>
{:else if col.key === "contract_start" || col.key === "end_of_contract" || col.key === "date_of_birth"}
<td class="px-4 py-2">
{row[col.key as keyof EmployeeItem]
? new Date(
row[
col.key as keyof EmployeeItem
] as string | number | Date,
).toLocaleDateString()
: "N/A"}
</td>
{:else if col.key === "created_at"}
<td class="px-4 py-2">
{row[col.key as keyof EmployeeItem]
@@ -529,23 +606,22 @@
<h2 class="text-xl font-semibold mb-4">
{isEditing ? "Edit Employee" : "New Employee"}
</h2>
<form on:submit={saveEmployee} class="space-y-4">
<form on:submit|preventDefault={saveEmployee} class="space-y-4">
{#each formColumns as col}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>{col.title}</label
>
{col.title}
</label>
{#if col.key === "contract_start" || col.key === "end_of_contract" || col.key === "date_of_birth"}
<input
type="date"
id={col.key}
bind:value={
newEmployeeInsert[
col.key as keyof EmployeeItem
]
}
bind:value={newEmployeeInsert[col.key]}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 {errorClass(
col.key,
)}"
@@ -554,15 +630,37 @@
<input
type="number"
id={col.key}
bind:value={
newEmployeeInsert[
col.key as keyof EmployeeItem
]
}
bind:value={newEmployeeInsert[col.key]}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 {errorClass(
col.key,
)}"
/>
{:else if col.key === "employee_type"}
<select
id={col.key}
bind:value={newEmployeeInsert[col.key]}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 {errorClass(
col.key,
)}"
>
<option value="" disabled>Select type</option>
{#each Object.entries(EmployeeType) as [key, value]}
<option value={key}>{value}</option>
{/each}
</select>
{:else if col.key === "employee_status"}
<select
id={col.key}
bind:value={newEmployeeInsert[col.key]}
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 {errorClass(
col.key,
)}"
>
<option value="" disabled>Select status</option>
{#each Object.entries(EmployeeStatus) as [key, value]}
<option value={key}>{value}</option>
{/each}
</select>
{:else}
<input
type="text"
@@ -577,6 +675,7 @@
)}"
/>
{/if}
{#if $formErrors[col.key]}
<p class="text-red-500 text-xs mt-1">
{$formErrors[col.key]}
@@ -584,6 +683,7 @@
{/if}
</div>
{/each}
<div class="flex justify-end space-x-2">
<button
type="button"