1127 lines
44 KiB
Svelte
1127 lines
44 KiB
Svelte
<script lang="ts">
|
||
import { supabase } from "$lib/supabaseClient";
|
||
import { formatCurrency } from "$lib/utils/conversion";
|
||
import { onMount } from "svelte";
|
||
import { writable } from "svelte/store";
|
||
import { v4 as uuidv4 } from "uuid";
|
||
|
||
type EmployeeItem = {
|
||
id: string;
|
||
employee_name: string;
|
||
employee_status: string;
|
||
location: string;
|
||
department: string;
|
||
contract_start: Date;
|
||
end_of_contract: Date;
|
||
employee_type: string;
|
||
date_of_birth: Date;
|
||
photo_url: string;
|
||
phone: string;
|
||
mobile: string;
|
||
personal_email: string;
|
||
work_email: string;
|
||
permanent_address: string;
|
||
temporary_address: string;
|
||
job_title: string;
|
||
emergency_contact_name: string;
|
||
emergency_contact_phone: string;
|
||
bank_account: string;
|
||
jamsostek_id: string;
|
||
npwp_id: string;
|
||
remarks: string;
|
||
salary: number;
|
||
last_edu: string;
|
||
document: string;
|
||
hire_date?: Date;
|
||
leaving_date?: Date;
|
||
termination_reason?: string;
|
||
special_notes?: string;
|
||
company?: string;
|
||
blood_type?: string;
|
||
religion?: string;
|
||
marital_status?: string;
|
||
id_number?: string;
|
||
emergency_contact_relation?: string;
|
||
bank_account_name?: string;
|
||
bpjs_kesehatan_id?: string;
|
||
father_name?: string;
|
||
mother_name?: string;
|
||
spouse_name?: string;
|
||
number_of_children?: number;
|
||
child_1?: string;
|
||
child_2?: string;
|
||
child_3?: string;
|
||
place_of_birth?: string;
|
||
created_at?: Date;
|
||
};
|
||
|
||
type EmployeeBenefits = {
|
||
id: string;
|
||
employee_id: string;
|
||
basic_salary: number;
|
||
position_allowance: number;
|
||
meal_allowance: number;
|
||
transportation_allowance: number;
|
||
created_at: Date;
|
||
updated_at: Date;
|
||
};
|
||
|
||
const EmployeeStatus = {
|
||
Active: "Active",
|
||
Inactive: "Inactive",
|
||
};
|
||
|
||
const EmployeeType = {
|
||
DailyWorker: "Daily Worker",
|
||
Contract: "Contract",
|
||
OutSource: "Outsource",
|
||
};
|
||
|
||
const MaritalStatus = {
|
||
Single: "Single",
|
||
Married: "Married",
|
||
Divorced: "Divorced",
|
||
Widowed: "Widowed",
|
||
};
|
||
|
||
let allRows: EmployeeItem[] = [];
|
||
|
||
let offset = 0;
|
||
|
||
type columns = {
|
||
key: string;
|
||
title: string;
|
||
};
|
||
|
||
let search: string = "";
|
||
|
||
const columns: columns[] = [
|
||
{ key: "no", title: "Employee No." },
|
||
{ key: "employee_name", title: "Employee Name" },
|
||
{ key: "employee_status", title: "Status" },
|
||
{ key: "location", title: "Location" },
|
||
{ key: "company", title: "Company" },
|
||
{ key: "department", title: "Department" },
|
||
{ key: "contract_start", title: "Contract Start" },
|
||
{ key: "end_of_contract", title: "End of Contract" },
|
||
{ key: "hire_date", title: "Hire Date" },
|
||
{ key: "leaving_date", title: "Leaving Date" },
|
||
{ key: "termination_reason", title: "Termination/Leaving Reason" },
|
||
{ key: "special_notes", title: "Special Notes" },
|
||
{ key: "employee_type", title: "Type" },
|
||
{ key: "place_of_birth", title: "Place of Birth" },
|
||
{ key: "date_of_birth", title: "Date of Birth" },
|
||
{ key: "id_number", title: "ID Number" },
|
||
{ key: "marital_status", title: "Marital Status" },
|
||
{ key: "photo_url", title: "Photo" },
|
||
{ key: "phone", title: "Phone" },
|
||
{ key: "mobile", title: "Mobile" },
|
||
{ key: "personal_email", title: "Personal 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_contact_relation",
|
||
title: "Emergency Contact Relation",
|
||
},
|
||
{ key: "emergency_contact_phone", title: "Emergency Contact Phone" },
|
||
{ key: "bank_account", title: "Bank Account" },
|
||
{ key: "bank_account_name", title: "Employee Bank Account Name" },
|
||
{ key: "jamsostek_id", title: "Jamsostek ID" },
|
||
{ key: "bpjs_kesehatan_id", title: "BPJS Kesehatan ID" },
|
||
{ key: "npwp_id", title: "NPWP ID" },
|
||
{ key: "remarks", title: "Remarks" },
|
||
{ key: "salary", title: "Salary" },
|
||
{ key: "last_edu", title: "Last Education" },
|
||
{ key: "religion", title: "Religion" },
|
||
{ key: "blood_type", title: "Blood Type" },
|
||
{ key: "father_name", title: "Father’s Name" },
|
||
{ key: "mother_name", title: "Mother’s Name" },
|
||
{ key: "spouse_name", title: "Spouse Name" },
|
||
{ key: "number_of_children", title: "Number of Children" },
|
||
{ key: "child_1", title: "Child 1" },
|
||
{ key: "child_2", title: "Child 2" },
|
||
{ key: "child_3", title: "Child 3" },
|
||
{ key: "document", title: "Document" },
|
||
{ key: "benefits", title: "Benefits" },
|
||
{ key: "created_at", title: "Created At" },
|
||
{ key: "actions", title: "Actions" },
|
||
];
|
||
|
||
const columnBenefits: columns[] = [
|
||
{ key: "basic_salary", title: "Basic Salary" },
|
||
{ key: "position_allowance", title: "Position Allowance" },
|
||
{ key: "meal_allowance", title: "Meal Allowance" },
|
||
{ key: "transportation_allowance", title: "Transportation Allowance" },
|
||
{ key: "total_salary", title: "Total Salary" },
|
||
{ key: "created_at", title: "Created At" },
|
||
{ key: "updated_at", title: "Updated At" },
|
||
];
|
||
|
||
let currentPage = offset + 1;
|
||
let rowsPerPage = 10;
|
||
let totalItems = 0;
|
||
|
||
async function fetchEmployee(
|
||
searchTerm: string | null = null,
|
||
sortColumn: string | null = "created_at",
|
||
sortOrder: "asc" | "desc" = "desc",
|
||
offset: number = 0,
|
||
limit: number = 10,
|
||
) {
|
||
const fromIndex = offset * limit;
|
||
const toIndex = fromIndex + limit - 1;
|
||
|
||
// Inisialisasi query
|
||
let query = supabase
|
||
.from("vb_employee")
|
||
.select("*", { count: "exact" })
|
||
.order(sortColumn || "created_at", {
|
||
ascending: sortOrder === "asc",
|
||
})
|
||
.range(fromIndex, toIndex); // Ini sudah termasuk offset & limit
|
||
|
||
// Tambahkan filter pencarian jika ada
|
||
if (searchTerm) {
|
||
query = query.ilike("employee_name", `%${searchTerm}%`);
|
||
}
|
||
|
||
// Jalankan query
|
||
const { data, count, error } = await query;
|
||
|
||
if (error) {
|
||
console.error("Error fetching PO Employee:", error);
|
||
return;
|
||
}
|
||
|
||
allRows = data as EmployeeItem[];
|
||
totalItems = count || 0;
|
||
|
||
console.log("Fetched Employee:", allRows);
|
||
console.log("Total Items:", totalItems);
|
||
}
|
||
|
||
async function fetchEmployeeBenefits(employeeId: string) {
|
||
const { data, error } = await supabase
|
||
.from("vb_benefits")
|
||
.select("*")
|
||
.eq("employee_id", employeeId)
|
||
.single();
|
||
|
||
if (error) {
|
||
console.error("Error fetching Employee Benefits:", error);
|
||
return null;
|
||
}
|
||
|
||
return data as EmployeeBenefits;
|
||
}
|
||
|
||
$: totalPages = Math.ceil(totalItems / rowsPerPage);
|
||
|
||
function goToPage(page: number) {
|
||
if (page < 1 || page > totalPages) return;
|
||
|
||
currentPage = page;
|
||
offset = (currentPage - 1) * rowsPerPage;
|
||
|
||
fetchEmployee(
|
||
search,
|
||
"created_at",
|
||
"desc",
|
||
currentPage - 1,
|
||
rowsPerPage,
|
||
);
|
||
}
|
||
|
||
function pageRange(
|
||
totalPages: number,
|
||
currentPage: number,
|
||
): (number | string)[] {
|
||
const range: (number | string)[] = [];
|
||
const maxDisplay = 5;
|
||
|
||
if (totalPages <= maxDisplay + 2) {
|
||
for (let i = 1; i <= totalPages; i++) range.push(i);
|
||
} else {
|
||
const start = Math.max(2, currentPage - 2);
|
||
const end = Math.min(totalPages - 1, currentPage + 2);
|
||
|
||
range.push(1);
|
||
if (start > 2) range.push("...");
|
||
|
||
for (let i = start; i <= end; i++) {
|
||
range.push(i);
|
||
}
|
||
|
||
if (end < totalPages - 1) range.push("...");
|
||
range.push(totalPages);
|
||
}
|
||
|
||
return range;
|
||
}
|
||
|
||
function changePage(page: number) {
|
||
if (page < 1 || page > totalPages || page === currentPage) return;
|
||
currentPage = page;
|
||
offset = (currentPage - 1) * rowsPerPage;
|
||
|
||
fetchEmployee(
|
||
search,
|
||
"created_at",
|
||
"desc",
|
||
currentPage - 1,
|
||
rowsPerPage,
|
||
);
|
||
}
|
||
|
||
onMount(() => {
|
||
fetchEmployee();
|
||
});
|
||
|
||
// Initialize the first page
|
||
$: currentPage = 1;
|
||
|
||
let showModal = false;
|
||
let showModalBenefits = false;
|
||
let isEditing = false;
|
||
let currentEditingId: string | null = null;
|
||
let newEmployeeInsert: EmployeeItem = {
|
||
id: "",
|
||
employee_name: "AJITEST",
|
||
employee_status: EmployeeStatus.Active,
|
||
location: "Nusa Penida",
|
||
department: "IT",
|
||
contract_start: new Date(),
|
||
end_of_contract: new Date(),
|
||
hire_date: new Date(),
|
||
leaving_date: new Date(),
|
||
employee_type: "Contract",
|
||
date_of_birth: new Date(),
|
||
photo_url:
|
||
"https://nusapenida-balitour.com/wp-content/uploads/2022/10/a-glance-nusa-penida-scaled.jpg",
|
||
phone: "082177751041",
|
||
mobile: "082177751041",
|
||
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",
|
||
place_of_birth: "Jakarta",
|
||
marital_status: "Single",
|
||
id_number: "3171234567890001",
|
||
emergency_contact_relation: "Brother",
|
||
bank_account_name: "Aji Setiaji",
|
||
bpjs_kesehatan_id: "1122334455",
|
||
father_name: "Budi Setiaji",
|
||
mother_name: "Siti Aminah",
|
||
spouse_name: "",
|
||
number_of_children: 100,
|
||
child_1: "",
|
||
child_2: "",
|
||
child_3: "",
|
||
religion: "Islam",
|
||
blood_type: "O",
|
||
created_at: new Date(),
|
||
};
|
||
|
||
let newEmployeeBenefits: EmployeeBenefits = {
|
||
id: "",
|
||
employee_id: "",
|
||
basic_salary: 0,
|
||
position_allowance: 0,
|
||
meal_allowance: 0,
|
||
transportation_allowance: 0,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
};
|
||
|
||
let employeeBenefits: Record<string, any> = {};
|
||
|
||
const excludedKeys = ["id", "actions", "created_at", "no"];
|
||
const formColumns = columns.filter(
|
||
(col) => !excludedKeys.includes(col.key),
|
||
);
|
||
|
||
const excludedKeysBenefits = [
|
||
"id",
|
||
"employee_id",
|
||
"total_salary",
|
||
"created_at",
|
||
"benefits",
|
||
"updated_at",
|
||
];
|
||
const formColumnsBenefits = columnBenefits.filter(
|
||
(col) => !excludedKeysBenefits.includes(col.key),
|
||
);
|
||
|
||
function openModal(newEmployeeItem?: EmployeeItem, emp?: number) {
|
||
if (newEmployeeItem) {
|
||
isEditing = true;
|
||
currentEditingId = newEmployeeItem.id;
|
||
|
||
// Copy data to avoid direct mutation
|
||
newEmployeeInsert = { ...newEmployeeItem };
|
||
|
||
// Fetch and populate benefits data
|
||
fetchEmployeeBenefits(newEmployeeItem.id).then((benefits) => {
|
||
if (benefits) {
|
||
newEmployeeBenefits = benefits;
|
||
} else {
|
||
newEmployeeBenefits = {
|
||
id: "",
|
||
employee_id: newEmployeeItem.id,
|
||
basic_salary: newEmployeeItem.salary || 0,
|
||
position_allowance: 0,
|
||
meal_allowance: 0,
|
||
transportation_allowance: 0,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
};
|
||
}
|
||
});
|
||
} else {
|
||
isEditing = false;
|
||
currentEditingId = null;
|
||
newEmployeeInsert = {
|
||
id: "",
|
||
employee_name: "",
|
||
employee_status: "",
|
||
location: "",
|
||
department: "",
|
||
contract_start: new Date(),
|
||
end_of_contract: new Date(),
|
||
employee_type: "",
|
||
date_of_birth: new Date(),
|
||
photo_url: "",
|
||
phone: "",
|
||
mobile: "",
|
||
personal_email: "",
|
||
work_email: "",
|
||
permanent_address: "",
|
||
temporary_address: "",
|
||
job_title: "",
|
||
emergency_contact_name: "",
|
||
emergency_contact_phone: "",
|
||
bank_account: "",
|
||
jamsostek_id: "",
|
||
npwp_id: "",
|
||
remarks: "",
|
||
salary: 0,
|
||
last_edu: "",
|
||
document: "",
|
||
hire_date: new Date(),
|
||
leaving_date: new Date(),
|
||
termination_reason: "",
|
||
special_notes: "",
|
||
company: "",
|
||
blood_type: "",
|
||
religion: "",
|
||
marital_status: "",
|
||
id_number: "",
|
||
emergency_contact_relation: "",
|
||
bank_account_name: "",
|
||
bpjs_kesehatan_id: "",
|
||
father_name: "",
|
||
mother_name: "",
|
||
spouse_name: "",
|
||
number_of_children: 0,
|
||
child_1: "",
|
||
child_2: "",
|
||
child_3: "",
|
||
place_of_birth: "",
|
||
created_at: new Date(),
|
||
};
|
||
|
||
newEmployeeBenefits = {
|
||
id: "",
|
||
employee_id: "",
|
||
basic_salary: 0,
|
||
position_allowance: 0,
|
||
meal_allowance: 0,
|
||
transportation_allowance: 0,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
};
|
||
}
|
||
showModal = true;
|
||
}
|
||
|
||
async function openModalBenefits(employee: EmployeeItem) {
|
||
const benefits = await fetchEmployeeBenefits(employee.id);
|
||
|
||
if (benefits) {
|
||
employeeBenefits = benefits;
|
||
|
||
employeeBenefits.total_salary = formatCurrency(
|
||
employeeBenefits.basic_salary +
|
||
employeeBenefits.position_allowance +
|
||
employeeBenefits.meal_allowance +
|
||
employeeBenefits.transportation_allowance,
|
||
);
|
||
|
||
employeeBenefits.basic_salary = formatCurrency(
|
||
employeeBenefits.basic_salary,
|
||
);
|
||
employeeBenefits.position_allowance = formatCurrency(
|
||
employeeBenefits.position_allowance,
|
||
);
|
||
employeeBenefits.meal_allowance = formatCurrency(
|
||
employeeBenefits.meal_allowance,
|
||
);
|
||
employeeBenefits.transportation_allowance = formatCurrency(
|
||
employeeBenefits.transportation_allowance,
|
||
);
|
||
|
||
console.log("Employee Benefits:", employeeBenefits);
|
||
} else {
|
||
employeeBenefits = {
|
||
id: "",
|
||
employee_id: employee.id,
|
||
basic_salary: employee.salary || 0,
|
||
position_allowance: 0,
|
||
meal_allowance: 0,
|
||
transportation_allowance: 0,
|
||
total_salary: 0,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
};
|
||
}
|
||
|
||
showModalBenefits = true;
|
||
}
|
||
|
||
async function saveEmployee(event: Event) {
|
||
event.preventDefault();
|
||
|
||
// validate newEmployeeInsert
|
||
if (!validateForm(newEmployeeInsert)) {
|
||
alert("Please fix the form errors before submitting.");
|
||
return;
|
||
}
|
||
|
||
if (isEditing && currentEditingId) {
|
||
const { error } = await supabase
|
||
.from("vb_employee")
|
||
.update(newEmployeeInsert)
|
||
.eq("id", currentEditingId);
|
||
|
||
if (error) {
|
||
alert("Error updating Employee: " + error.message);
|
||
console.error("Error updating Employee:", error);
|
||
return;
|
||
}
|
||
// cek apakah data vb_benefits sudah ada
|
||
const { data: existingBenefits, error: checkError } = await supabase
|
||
.from("vb_benefits")
|
||
.select("id")
|
||
.eq("employee_id", currentEditingId)
|
||
.maybeSingle();
|
||
|
||
if (checkError) {
|
||
console.error("Error checking vb_benefits:", checkError);
|
||
}
|
||
|
||
if (existingBenefits) {
|
||
// update jika ada
|
||
const { error: benefitsError } = await supabase
|
||
.from("vb_benefits")
|
||
.update(newEmployeeBenefits)
|
||
.eq("employee_id", currentEditingId);
|
||
|
||
if (benefitsError) {
|
||
console.error("Error updating vb_benefits:", benefitsError);
|
||
}
|
||
} else {
|
||
|
||
const idBenefit = uuidv4();
|
||
newEmployeeBenefits.id = idBenefit;
|
||
newEmployeeBenefits.employee_id = currentEditingId;
|
||
|
||
// insert jika belum ada
|
||
const { error: benefitsInsertError } = await supabase
|
||
.from("vb_benefits")
|
||
.insert({
|
||
...newEmployeeBenefits,
|
||
});
|
||
|
||
if (benefitsInsertError) {
|
||
console.error(
|
||
"Error inserting vb_benefits:",
|
||
benefitsInsertError,
|
||
);
|
||
}
|
||
}
|
||
|
||
alert("Employee Updated successfully!");
|
||
} else {
|
||
newEmployeeInsert.id = uuidv4(); // Generate a new UUID for the ID
|
||
|
||
const { error } = await supabase
|
||
.from("vb_employee")
|
||
.insert(newEmployeeInsert);
|
||
|
||
if (error) {
|
||
alert("Error creating New Employee: " + error.message);
|
||
console.error("Error creating New Employee:", error);
|
||
return;
|
||
}
|
||
|
||
newEmployeeBenefits.id = uuidv4();
|
||
newEmployeeBenefits.employee_id = newEmployeeInsert.id;
|
||
|
||
const { error: benefitsError } = await supabase
|
||
.from("vb_employee_benefits")
|
||
.insert(newEmployeeBenefits);
|
||
|
||
if (benefitsError) {
|
||
console.error(
|
||
"Error creating Employee Benefits:",
|
||
benefitsError,
|
||
);
|
||
return;
|
||
} else {
|
||
alert("New Employee created successfully!");
|
||
}
|
||
}
|
||
|
||
await fetchEmployee(
|
||
search,
|
||
"created_at",
|
||
"desc",
|
||
(currentPage - 1) * rowsPerPage,
|
||
rowsPerPage,
|
||
);
|
||
showModal = false;
|
||
}
|
||
|
||
async function deleteEmployee(id: string) {
|
||
if (confirm("Are you sure you want to delete this Employee?")) {
|
||
const { error } = await supabase
|
||
.from("vb_employee")
|
||
.delete()
|
||
.eq("id", id);
|
||
if (error) {
|
||
console.error("Error deleting Employee:", error);
|
||
return;
|
||
}
|
||
await fetchEmployee(
|
||
search,
|
||
"created_at",
|
||
"desc",
|
||
(currentPage - 1) * rowsPerPage,
|
||
rowsPerPage,
|
||
);
|
||
}
|
||
}
|
||
|
||
export let formErrors = writable<{ [key: string]: string }>({});
|
||
|
||
function validateForm(newEmployeeInsert: EmployeeItem): boolean {
|
||
const errors: { [key: string]: string } = {};
|
||
const requiredFields = [
|
||
"employee_name",
|
||
// Add other required fields here if necessary
|
||
];
|
||
|
||
requiredFields.forEach((field) => {
|
||
if (
|
||
!newEmployeeInsert[field as keyof EmployeeItem] ||
|
||
newEmployeeInsert[field as keyof EmployeeItem] === ""
|
||
) {
|
||
errors[field] = `${field.replace(/_/g, " ")} is required.`;
|
||
}
|
||
});
|
||
|
||
formErrors.set(errors);
|
||
return Object.keys(errors).length === 0;
|
||
}
|
||
|
||
function errorClass(field: string): string {
|
||
return $formErrors[field] ? "border-red-500" : "border";
|
||
}
|
||
</script>
|
||
|
||
<div>
|
||
<div
|
||
class="p-6 bg-white shadow-md rounded-2xl mb-4 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
|
||
>
|
||
<div>
|
||
<h2
|
||
class="text-lg font-semibold text-gray-800 flex items-center gap-2"
|
||
>
|
||
<span class="text-blue-600">👨💼</span>
|
||
Employee
|
||
</h2>
|
||
<p class="text-sm text-gray-600">Manage your employee data here.</p>
|
||
</div>
|
||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="🔍 Search by item name..."
|
||
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-64 transition"
|
||
on:input={(e) => {
|
||
search = (e.target as HTMLInputElement).value.toLowerCase();
|
||
|
||
if (search !== "" && search.length > 3) {
|
||
fetchEmployee(search, "created_at", "desc", 0, 10);
|
||
} else if (search === "") {
|
||
fetchEmployee(null, "created_at", "desc", 0, 10);
|
||
}
|
||
}}
|
||
/>
|
||
<button
|
||
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
|
||
on:click={() =>
|
||
fetchEmployee(search, "created_at", "desc", 0, 10)}
|
||
>
|
||
🔄 Reset
|
||
</button>
|
||
<button
|
||
class="bg-blue-600 text-white px-4 py-2 rounded-xl hover:bg-blue-700 text-sm transition"
|
||
on:click={() => openModal()}
|
||
>
|
||
➕ New Employee
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="overflow-x-auto rounded-lg shadow mb-4">
|
||
<table class="w-full divide-y divide-gray-200 text-sm">
|
||
<thead class="bg-gray-100">
|
||
<tr>
|
||
{#each columns as col}
|
||
{#if col.key === "guest_name"}
|
||
<th
|
||
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||
style="background-color: #f0f8ff; z-index: 10;"
|
||
>
|
||
{col.title}
|
||
</th>
|
||
{:else}
|
||
<th
|
||
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
|
||
>
|
||
{col.title}
|
||
</th>
|
||
{/if}
|
||
{/each}
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-200 bg-white">
|
||
{#each allRows as row, i}
|
||
<tr class="hover:bg-gray-50 transition">
|
||
{#each columns as col}
|
||
{#if col.key === "no"}
|
||
<td class="px-4 py-2 font-medium text-gray-800">
|
||
{offset + i + 1}
|
||
</td>
|
||
{:else if col.key === "employee_name"}
|
||
<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" || col.key === "hire_date" || col.key === "leaving_date"}
|
||
<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 === "benefits"}
|
||
<td class="px-4 py-2">
|
||
<button
|
||
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
|
||
on:click={() => openModalBenefits(row)}
|
||
>
|
||
💼 View Benefits
|
||
</button>
|
||
</td>
|
||
{:else if col.key === "created_at"}
|
||
<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 === "actions"}
|
||
<td class="px-4 py-2">
|
||
<button
|
||
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
|
||
on:click={() => openModal(row)}
|
||
>
|
||
✏️ Edit
|
||
</button>
|
||
<button
|
||
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
|
||
on:click={() => deleteEmployee(row.id)}
|
||
>
|
||
🗑️ Delete
|
||
</button>
|
||
</td>
|
||
{:else}
|
||
<td class="px-4 py-2">
|
||
{row[col.key as keyof EmployeeItem]}
|
||
</td>
|
||
{/if}
|
||
{/each}
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Pagination controls -->
|
||
<div class="flex justify-between items-center text-sm">
|
||
<div>
|
||
Showing {(currentPage - 1) * rowsPerPage + 1}–
|
||
{Math.min(currentPage * rowsPerPage, totalItems)} of {totalItems} items
|
||
</div>
|
||
<div class="space-x-2">
|
||
<button
|
||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||
on:click={() => goToPage(currentPage - 1)}
|
||
disabled={currentPage === 1}
|
||
>
|
||
Previous
|
||
</button>
|
||
{#each pageRange(totalPages, currentPage) as page}
|
||
{#if page === "..."}
|
||
<span class="px-2">...</span>
|
||
{:else}
|
||
<button
|
||
on:click={() => changePage(page as number)}
|
||
class="px-2 py-1 border rounded {page === currentPage
|
||
? 'bg-blue-600 text-white border-blue-600'
|
||
: 'bg-white border-gray-300 hover:bg-gray-100'}"
|
||
>
|
||
{page}
|
||
</button>
|
||
{/if}
|
||
{/each}
|
||
<button
|
||
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
|
||
on:click={() => goToPage(currentPage + 1)}
|
||
disabled={currentPage === totalPages}
|
||
>
|
||
Next
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{#if showModal}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||
>
|
||
<div
|
||
class="bg-white rounded-lg shadow-lg p-6 w-full max-w-md max-h-[90vh] overflow-y-auto"
|
||
>
|
||
<h2 class="text-xl font-semibold mb-4">
|
||
{isEditing ? "Edit Employee" : "New Employee"}
|
||
</h2>
|
||
|
||
<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>
|
||
|
||
{#if col.key === "contract_start" || col.key === "end_of_contract" || col.key === "date_of_birth" || col.key === "hire_date" || col.key === "leaving_date"}
|
||
<input
|
||
type="date"
|
||
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,
|
||
)}"
|
||
/>
|
||
{:else if col.key === "salary"}
|
||
<input
|
||
type="number"
|
||
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,
|
||
)}"
|
||
/>
|
||
{: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 if col.key === "marital_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 marital status</option
|
||
>
|
||
{#each Object.entries(MaritalStatus) as [key, value]}
|
||
<option value={key}>{value}</option>
|
||
{/each}
|
||
</select>
|
||
{:else}
|
||
<input
|
||
type="text"
|
||
id={col.key}
|
||
bind:value={
|
||
newEmployeeInsert[
|
||
col.key as keyof EmployeeItem
|
||
]
|
||
}
|
||
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 {errorClass(
|
||
col.key,
|
||
)}"
|
||
/>
|
||
{/if}
|
||
|
||
{#if $formErrors[col.key]}
|
||
<p class="text-red-500 text-xs mt-1">
|
||
{$formErrors[col.key]}
|
||
</p>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
|
||
<h3 class="text-lg font-semibold mb-2">Benefits</h3>
|
||
|
||
{#each formColumnsBenefits as col}
|
||
<div class="mb-4">
|
||
<label
|
||
for={col.key}
|
||
class="block text-sm font-medium text-gray-700 mb-1"
|
||
>
|
||
{col.title}
|
||
</label>
|
||
{#if col.key === "basic_salary"}
|
||
<input
|
||
type="number"
|
||
id={col.key}
|
||
bind:value={newEmployeeBenefits[col.key]}
|
||
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
{:else}
|
||
<input
|
||
type="number"
|
||
id={col.key}
|
||
bind:value={
|
||
newEmployeeBenefits[
|
||
col.key as keyof EmployeeBenefits
|
||
]
|
||
}
|
||
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
/>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
|
||
<div class="flex justify-end space-x-2">
|
||
<button
|
||
type="button"
|
||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300"
|
||
on:click={() => (showModal = false)}
|
||
>
|
||
Cancel
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
class="px-4 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700"
|
||
>
|
||
{isEditing ? "Update" : "Create"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
{#if showModalBenefits}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||
>
|
||
<div
|
||
class="bg-white rounded-lg shadow-lg p-6 w-full max-w-md max-h-[90vh] overflow-y-auto"
|
||
>
|
||
<h2 class="text-xl font-semibold mb-4">Employee Benefits</h2>
|
||
|
||
<div class="space-y-4">
|
||
{#each formColumnsBenefits as col}
|
||
<div class="mb-4">
|
||
<label
|
||
for={col.key}
|
||
class="block text-sm font-medium text-gray-700 mb-1"
|
||
>
|
||
{col.title}
|
||
</label>
|
||
{#if col.key === "basic_salary"}
|
||
<input
|
||
type="text"
|
||
id={col.key}
|
||
bind:value={employeeBenefits[col.key]}
|
||
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
readonly
|
||
/>
|
||
{:else}
|
||
<input
|
||
type="text"
|
||
id={col.key}
|
||
bind:value={
|
||
employeeBenefits[
|
||
col.key as keyof EmployeeBenefits
|
||
]
|
||
}
|
||
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
readonly
|
||
/>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
<div class="mb-4">
|
||
<label
|
||
for="total_salary"
|
||
class="block text-sm font-medium text-gray-700 mb-1"
|
||
>
|
||
Total Salary
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="total_salary"
|
||
bind:value={employeeBenefits.total_salary}
|
||
class="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||
readonly
|
||
/>
|
||
</div>
|
||
|
||
|
||
<div class="flex justify-end">
|
||
<button
|
||
type="button"
|
||
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-xl hover:bg-gray-300"
|
||
on:click={() => (showModalBenefits = false)}
|
||
>
|
||
Close
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|