refactor: make employee employment fields nullable and update related salary calculations and display logic

This commit is contained in:
Yoga Pangestu 2026-06-14 15:29:08 +07:00
parent bdeb8cc59f
commit 175cb4acab
9 changed files with 65 additions and 29 deletions

View File

@ -35,7 +35,13 @@ public function label(): string
public function permissions(): array
{
return match ($this) {
self::DEVELOPER, self::OWNER => Permission::cases(),
self::DEVELOPER, self::OWNER => array_values(array_filter(
Permission::cases(),
fn (Permission $permission) => ! in_array($permission, [
Permission::ATTENDANCES_CREATE,
Permission::ATTENDANCES_DELETE,
], true)
)),
self::DIREKTUR => [
Permission::DASHBOARD_VIEW,

View File

@ -25,6 +25,8 @@ public function authorize(): bool
*/
public function rules(): array
{
$anyEmployee = ! empty($this->join_date) || ! empty($this->employment_status) || ! empty($this->base_salary);
return [
'email' => ['required', 'email', 'max:100', Rule::unique('users', 'email')->ignore($this->route('user')?->id)],
'username' => ['required', 'string', 'max:20', 'alpha_dash', Rule::unique('users', 'username')->ignore($this->route('user')?->id)],
@ -33,9 +35,9 @@ public function rules(): array
'gender' => ['nullable', Rule::enum(Gender::class)],
'birth_date' => ['nullable', 'date', 'before:today'],
'address' => ['nullable', 'string'],
'join_date' => ['required', 'date'],
'employment_status' => ['required', Rule::enum(EmploymentStatus::class)],
'base_salary' => ['required', 'integer', 'min:0'],
'join_date' => [$anyEmployee ? 'required' : 'nullable', 'date'],
'employment_status' => [$anyEmployee ? 'required' : 'nullable', Rule::enum(EmploymentStatus::class)],
'base_salary' => [$anyEmployee ? 'required' : 'nullable', 'integer', 'min:0'],
'role' => ['required', Rule::in(Role::assignableValues())],
];
}

View File

@ -86,21 +86,21 @@ public function user(): BelongsTo
public function baseSalaryFormatted(): Attribute
{
return Attribute::make(
get: fn () => 'Rp '.number_format($this->base_salary, 0, ',', '.'),
get: fn () => $this->base_salary !== null ? 'Rp '.number_format($this->base_salary, 0, ',', '.') : '-',
);
}
public function employmentStatusLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->employment_status?->label(),
get: fn () => $this->employment_status?->label() ?? '-',
);
}
public function joinDateFormatted(): Attribute
{
return Attribute::make(
get: fn () => Carbon::parse($this->join_date)->translatedFormat('l, d F Y'),
get: fn () => $this->join_date ? Carbon::parse($this->join_date)->translatedFormat('l, d F Y') : '-',
);
}

View File

@ -70,7 +70,7 @@ public function payrollPeriod(): BelongsTo
public function baseSalaryFormatted(): Attribute
{
return Attribute::make(
get: fn () => 'Rp '.number_format($this->base_salary, 0, ',', '.'),
get: fn () => $this->base_salary !== null ? 'Rp '.number_format($this->base_salary, 0, ',', '.') : '-',
);
}
@ -133,7 +133,7 @@ public function calculateKasbonDeduction(): int
->where('status', EmployeeAdvanceStatus::APPROVED)
->sum('amount');
return min($outstanding, $this->base_salary + (int) $this->adjustments()
return min($outstanding, (int) $this->base_salary + (int) $this->adjustments()
->where('type', PayrollAdjustmentType::BONUS)
->sum('amount'));
}
@ -152,6 +152,6 @@ public function recalculateAmounts(): void
$this->bonus_amount = $bonusAmount;
$this->deduction_amount = $kasbonDeduction + $manualDeduction;
$this->total_amount = max(0, $this->base_salary + $bonusAmount - $this->deduction_amount);
$this->total_amount = max(0, (int) $this->base_salary + $bonusAmount - $this->deduction_amount);
}
}

View File

@ -178,7 +178,7 @@ public function generatePayrollsForPeriod(PayrollPeriod $period): void
$payroll = new Payroll([
'payroll_period_id' => $period->id,
'employee_id' => $employee->id,
'base_salary' => $employee->base_salary,
'base_salary' => $employee->base_salary ?? 0,
'bonus_amount' => 0,
'deduction_amount' => 0,
'total_amount' => 0,

View File

@ -24,7 +24,6 @@ public function paginateForIndex(
): LengthAwarePaginator {
$query = User::query()
->with(['profile', 'employee', 'roles'])
->whereHas('employee')
->when($tableQuery['search'] !== '', function ($query) use ($tableQuery): void {
$search = $tableQuery['search'];
$query->where(function ($query) use ($search): void {
@ -81,12 +80,16 @@ public function create(array $validated): void
'address' => $validated['address'],
]);
Employee::create([
'user_id' => $user->id,
'join_date' => $validated['join_date'],
'employment_status' => $validated['employment_status'],
'base_salary' => $validated['base_salary'],
]);
$hasEmployee = !empty($validated['join_date']) && !empty($validated['employment_status']) && !empty($validated['base_salary']);
if ($hasEmployee) {
Employee::create([
'user_id' => $user->id,
'join_date' => $validated['join_date'],
'employment_status' => $validated['employment_status'],
'base_salary' => $validated['base_salary'],
]);
}
$user->syncRoles([$validated['role']]);
});
@ -112,10 +115,27 @@ public function update(User $user, array $validated): void
$profile->address = $validated['address'];
$profile->save();
$employee->join_date = $validated['join_date'];
$employee->employment_status = $validated['employment_status'];
$employee->base_salary = $validated['base_salary'];
$employee->save();
$hasEmployee = !empty($validated['join_date']) && !empty($validated['employment_status']) && !empty($validated['base_salary']);
if ($hasEmployee) {
if ($employee) {
$employee->join_date = $validated['join_date'];
$employee->employment_status = $validated['employment_status'];
$employee->base_salary = $validated['base_salary'];
$employee->save();
} else {
Employee::create([
'user_id' => $user->id,
'join_date' => $validated['join_date'],
'employment_status' => $validated['employment_status'],
'base_salary' => $validated['base_salary'],
]);
}
} else {
if ($employee) {
$employee->delete();
}
}
$user->syncRoles([$validated['role']]);
});

View File

@ -21,8 +21,8 @@ public function run(): void
Payroll::factory()->create([
'payroll_period_id' => $period->id,
'employee_id' => $employee->id,
'base_salary' => $employee->base_salary,
'total_amount' => $employee->base_salary,
'base_salary' => $employee->base_salary ?? 0,
'total_amount' => $employee->base_salary ?? 0,
]);
});
}

View File

@ -56,12 +56,19 @@ const form = useForm<EmployeeFormData>({
birth_date: props.initialData?.birth_date ?? '',
address: props.initialData?.address ?? '',
join_date: props.initialData?.join_date ?? '',
employment_status: props.initialData?.employment_status ?? 'full_time',
employment_status: props.initialData?.employment_status ?? '',
base_salary: props.initialData?.base_salary ?? '',
role: props.initialData?.role ?? '',
});
function submit() {
form.transform((data) => ({
...data,
join_date: data.join_date === '' ? null : data.join_date,
employment_status: data.employment_status === 'none' || data.employment_status === '' ? null : data.employment_status,
base_salary: data.base_salary === '' ? null : data.base_salary,
}));
const options = {
onError: () => {
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
@ -173,23 +180,24 @@ function submit() {
<FieldGroup>
<FieldSet class="grid gap-4 md:grid-cols-3">
<Field>
<FieldLabel for="join_date" required>Tanggal Bergabung</FieldLabel>
<FieldLabel for="join_date">Tanggal Bergabung</FieldLabel>
<DatePicker id="join_date" v-model="form.join_date"
placeholder="Pilih tanggal bergabung" />
<FieldError :errors="formErrors(form, 'join_date')" />
</Field>
<Field>
<FieldLabel for="base_salary" required>Gaji Pokok</FieldLabel>
<FieldLabel for="base_salary">Gaji Pokok</FieldLabel>
<RupiahInput id="base_salary" v-model="form.base_salary" />
<FieldError :errors="formErrors(form, 'base_salary')" />
</Field>
<Field>
<FieldLabel for="employment_status" required>Status Kepegawaian</FieldLabel>
<FieldLabel for="employment_status">Status Kepegawaian</FieldLabel>
<Select v-model="form.employment_status">
<SelectTrigger id="employment_status" class="w-full">
<SelectValue placeholder="Pilih status kepegawaian" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Pilih status kepegawaian</SelectItem>
<SelectItem v-for="option in employmentStatuses" :key="option.value"
:value="option.value">
{{ option.label }}

View File

@ -23,7 +23,7 @@ const initialData = computed(() => ({
birth_date: props.employee.profile?.birth_date_input ?? '',
address: props.employee.profile?.address ?? '',
join_date: props.employee.employee?.join_date_input ?? '',
employment_status: props.employee.employee?.employment_status ?? 'full_time',
employment_status: props.employee.employee?.employment_status ?? '',
base_salary: props.employee.employee?.base_salary != null ? String(props.employee.employee.base_salary) : '',
role: props.employee.roles?.[0]?.name ?? '',
}));