feat: add role-based visibility for employee management features

This commit is contained in:
Yoga Pangestu 2026-08-06 13:41:26 +07:00
parent 8eb3e565ec
commit 58d45f8363
6 changed files with 106 additions and 57 deletions

View File

@ -26,13 +26,19 @@ public function index(PaginatedRequest $request): Response
filters: $request->only(['employment_status', 'is_active', 'gender']),
),
'filters' => $request->only(['employment_status', 'is_active', 'gender']),
'canViewAll' => $this->service->canViewAll(),
]);
}
public function create(): Response
{
$user = auth()->user();
return Inertia::render('admin/hr/employee/create', [
'roles' => Role::where('name', '!=', 'Developer')->get(['id', 'name']),
'roles' => $this->service->canViewAll()
? Role::where('name', '!=', 'Developer')->get(['id', 'name'])
: Role::where('name', '=', $user->roles->first()?->name)->get(['id', 'name']),
'canViewAll' => $this->service->canViewAll(),
]);
}

View File

@ -9,6 +9,13 @@
class EmployeeService
{
private const ADMIN_ROLES = ['developer', 'owner', 'direktur', 'admin-toko'];
public function canViewAll(): bool
{
return auth()->user()->hasAnyRole(self::ADMIN_ROLES);
}
public function getAll(array $filters = []): Collection
{
return User::select(['id', 'email', 'username', 'is_active'])
@ -18,6 +25,10 @@ public function getAll(array $filters = []): Collection
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
'roles' => fn ($q) => $q->select(['id', 'name']),
])
->when(! $this->canViewAll(), function ($q) {
$userRoles = auth()->user()->roles->pluck('name');
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
})
->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status)))
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
@ -35,6 +46,10 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
'roles' => fn ($q) => $q->select(['id', 'name']),
])
->when(! $this->canViewAll(), function ($q) {
$userRoles = auth()->user()->roles->pluck('name');
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
})
->when($search, fn ($q) => $q->whereHas('userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status)))
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))

View File

@ -39,6 +39,7 @@ type CreateColumnsParams = {
handleResetPassword: (employee: Employee) => void;
toggleActiveUrl: (id: number) => string;
can: (permission: string) => boolean;
canViewAll: boolean;
};
export function createEmployeeColumns(
@ -50,6 +51,7 @@ export function createEmployeeColumns(
handleResetPassword,
toggleActiveUrl,
can,
canViewAll,
} = params;
return [
@ -93,20 +95,25 @@ export function createEmployeeColumns(
);
},
},
{
id: 'role',
header: () => <span>Role</span>,
cell: ({ row }) => {
const employee = row.original;
const roleName = employee.roles?.[0]?.name ?? '-';
...(canViewAll
? [
{
id: 'role',
header: () => <span>Role</span>,
cell: ({ row }) => {
const employee = row.original;
const roleName =
employee.roles?.[0]?.name ?? '-';
return (
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
{roleName}
</span>
);
},
},
return (
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
{roleName}
</span>
);
},
},
]
: []),
{
accessorKey: 'employee.employment_status',
id: 'employment_status',

View File

@ -36,12 +36,15 @@ type Role = {
type Props = {
roles: Role[];
canViewAll: boolean;
};
export default function EmployeeCreate({ roles }: Props) {
export default function EmployeeCreate({ roles, canViewAll }: Props) {
const [joinDate, setJoinDate] = useState<Date | undefined>(undefined);
const [resignDate, setResignDate] = useState<Date | undefined>(undefined);
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
const [selectedRole, setSelectedRole] = useState<Role | null>(
!canViewAll && roles.length === 1 ? roles[0] : null,
);
return (
<>
@ -117,46 +120,61 @@ export default function EmployeeCreate({ roles }: Props) {
message={errors.username}
/>
</div>
<div className="grid gap-2">
<Label>
Role{' '}
<span className="text-destructive">
*
</span>
</Label>
<Combobox
items={roles}
itemToStringLabel={(r) => r.name}
value={selectedRole}
onValueChange={(value) =>
setSelectedRole(value)
}
>
<ComboboxInput
placeholder="Cari role..."
className="w-full"
{canViewAll ? (
<div className="grid gap-2">
<Label>
Role{' '}
<span className="text-destructive">
*
</span>
</Label>
<Combobox
items={roles}
itemToStringLabel={(r) =>
r.name
}
value={selectedRole}
onValueChange={(value) =>
setSelectedRole(value)
}
>
<ComboboxInput
placeholder="Cari role..."
className="w-full"
/>
<ComboboxContent>
<ComboboxEmpty>
Tidak ada role
ditemukan.
</ComboboxEmpty>
<ComboboxList>
{(role) => (
<ComboboxItem
key={
role.id
}
value={
role
}
>
{role.name}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError
message={errors.role}
/>
<ComboboxContent>
<ComboboxEmpty>
Tidak ada role
ditemukan.
</ComboboxEmpty>
<ComboboxList>
{(role) => (
<ComboboxItem
key={role.id}
value={role}
>
{role.name}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
<InputError
message={errors.role}
/>
</div>
</div>
) : (
<div className="grid gap-2">
<Label>Role</Label>
<div className="flex h-10 w-full items-center rounded-md border border-input bg-muted px-3 py-2 text-sm">
{selectedRole?.name ?? '-'}
</div>
</div>
)}
</CardContent>
</Card>

View File

@ -56,9 +56,10 @@ type EmployeeData = {
type Props = {
employee: EmployeeData;
roles: Role[];
canViewAll: boolean;
};
export default function EmployeeEdit({ employee, roles }: Props) {
export default function EmployeeEdit({ employee, roles, canViewAll }: Props) {
const currentRole = useMemo(
() =>
roles.find((r) => r.name === employee.roles?.[0]?.name) ?? null,

View File

@ -40,9 +40,10 @@ type Props = {
is_active?: string;
gender?: string;
};
canViewAll: boolean;
};
export default function EmployeeIndex({ employees, filters }: Props) {
export default function EmployeeIndex({ employees, filters, canViewAll }: Props) {
const { can } = useCan();
const [deleting, setDeleting] = useState<Employee | null>(null);
const [resetPasswordTarget, setResetPasswordTarget] =
@ -102,6 +103,7 @@ export default function EmployeeIndex({ employees, filters }: Props) {
handleResetPassword: (employee) => setResetPasswordTarget(employee),
toggleActiveUrl: (id) => toggleActive.url(id),
can,
canViewAll,
});
const filterToolbar = (