Merge pull request #130 from Since-3/feature/added-employee-filter

added filter for employees
This commit is contained in:
Rajpreet0 2026-07-12 19:28:40 +02:00 • committed by GitHub
commit 7dc48e05c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 310 additions and 4 deletions

View file

@ -0,0 +1,219 @@
import { Button } from "@/src/components/ui/button";
import {
Sheet,
SheetClose,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/src/components/ui/sheet";
import { Check, ChevronDown } from "lucide-react";
import { useState } from "react";
type FilterStatus = "all" | "onboarded" | "not_onboarded";
const PERMISSION_LABELS: Record<string, string> = {
"employees.view": "Mitarbeiter ansehen",
"employees.create": "Mitarbeiter erstellen",
"employees.edit": "Mitarbeiter bearbeiten",
"employees.delete": "Mitarbeiter löschen",
"courses.view": "Kurse ansehen",
"courses.create": "Kurse erstellen",
"courses.edit": "Kurse bearbeiten",
"courses.delete": "Kurse löschen",
"rooms.view": "Räume ansehen",
"rooms.create": "Räume erstellen",
"rooms.edit": "Räume bearbeiten",
"rooms.delete": "Räume löschen",
};
const PERMISSION_KEYS = Object.keys(PERMISSION_LABELS);
interface FilterValues {
filterName: string;
filterRole: string;
filterPermissions: string[];
filterStatus: FilterStatus;
}
interface EmployeeFilterProps {
children: React.ReactNode;
// current applied values
filterName: string;
filterRole: string;
filterPermissions: string[];
filterStatus: FilterStatus;
uniqueRoles: string[];
// callback when filters are applied
onApplyFilters: (fitlers: FilterValues) => void;
// actions
onReset: () => void;
}
const EmployeeFilter: React.FC<EmployeeFilterProps> = ({
children,
filterName,
filterRole,
filterPermissions,
filterStatus,
uniqueRoles,
onApplyFilters,
onReset,
}) => {
// Local state for temporary filter values (before applying)
const [localName, setLocalName] = useState(filterName);
const [localRole, setLocalRole] = useState(filterRole);
const [localPermissions, setLocalPermissions] = useState<string[]>(filterPermissions);
const [localStatus, setLocalStatus] = useState<FilterStatus>(filterStatus);
const [isOpen, setIsOpen] = useState(false);
const handleOpenChange = (open: boolean) => {
if (open) {
setLocalName(filterName);
setLocalRole(filterRole);
setLocalPermissions(filterPermissions);
setLocalStatus(filterStatus);
}
setIsOpen(open);
};
const togglePermission = (key: string) => {
setLocalPermissions((prev) =>
prev.includes(key) ? prev.filter((p) => p !== key) : [...prev, key]
);
};
const hasActiveFilters =
!!filterName || filterRole !== "all" || filterPermissions.length > 0 || filterStatus !== "all";
const handleApplyFilters = () => {
onApplyFilters({
filterName: localName,
filterRole: localRole,
filterPermissions: localPermissions,
filterStatus: localStatus,
});
};
// Reset Form
const handleReset = () => {
setLocalName("");
setLocalRole("all");
setLocalPermissions([]);
setLocalStatus("all");
onReset();
};
return (
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
<SheetTrigger asChild>{children}</SheetTrigger>
<SheetContent
side="right"
className="w-full sm:max-w-md flex flex-col"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<SheetHeader>
<SheetTitle className="text-2xl">Mitarbeiter Filtern</SheetTitle>
</SheetHeader>
<div className="flex-1 overflow-y-auto space-y-4 p-4">
{/* Name */}
<div className="space-y-1">
<label htmlFor="filter-name" className="font-medium">
Name
</label>
<input
id="filter-name"
type="text"
value={localName}
onChange={(e) => setLocalName(e.target.value)}
className="h-10 w-full px-3 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none [&::-webkit-datetime-edit]:p-0 [&::-webkit-datetime-edit-fields-wrapper]:p-0"
/>
</div>
{/* Roles */}
<div className="space-y-1">
<label htmlFor="filter-role" className="font-medium">
Rollen
</label>
<div className="relative">
<select
id="filter-role"
value={localRole}
onChange={(e) => setLocalRole(e.target.value)}
className="h-10 w-full pl-3 pr-9 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none"
>
<option value="all">Alle</option>
{uniqueRoles.map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
</div>
</div>
{/* Adminstrationrights */}
<div className="space-y-1">
<label>Administrationsrechte</label>
<div className="max-h-48 overflow-y-auto rounded-lg border border-gray-300 bg-gray-50 divide-y divide-gray-200">
{PERMISSION_KEYS.map((key) => {
const selected = localPermissions.includes(key);
return (
<button
key={key}
type="button"
onClick={() => togglePermission(key)}
className="w-full flex items-center justify-between px-3 py-2 text-sm text-left hover:bg-gray-100 transition"
>
<span>{PERMISSION_LABELS[key]}</span>
{selected && <Check className="h-4 w-4 text-blue-600" />}
</button>
);
})}
</div>
</div>
{/* Status */}
<div className="space-y-1">
<label htmlFor="filter-status" className="font-medium">
Status
</label>
<div className="relative">
<select
id="filter-status"
value={localStatus}
onChange={(e) => setLocalStatus(e.target.value as FilterStatus)}
className="h-10 w-full pl-3 pr-9 rounded-lg border border-gray-300 bg-gray-50 focus:outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-300 transition appearance-none"
>
<option value="all">Alle</option>
<option value="onboarded">Onboarded</option>
<option value="not_onboarded">Nicht Onboarded</option>
</select>
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
</div>
</div>
</div>
<SheetFooter className="flex flex-row items-center w-full justify-end gap-2 border-t pt-4">
{hasActiveFilters && (
<Button variant="outline" onClick={handleReset}>
Zurücksetzen
</Button>
)}
<SheetClose asChild>
<Button onClick={handleApplyFilters}>Filtern</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
};
export default EmployeeFilter;

View file

@ -9,6 +9,8 @@ import { toast } from "sonner";
import DeleteDialog from "@/src/components/layout/DeleteDialog";
import { useDeleteEmployee } from "../../hooks/useDeleteEmployee";
import Link from "next/link";
import { Filter } from "lucide-react";
import EmployeeFilter from "../components/EmployeeFilter";
type Employee = {
id: string;
@ -36,6 +38,11 @@ const EmployeeListView = ({ employees, canCreate, canEdit, canDelete }: Employee
const [deleteMode, setDeleteMode] = useState(false);
const [resendingId, setResendingId] = useState<string | null>(null);
const [filterName, setFilterName] = useState("");
const [filterRole, setFilterRole] = useState<string>("all");
const [filterPermissions, setFilterPermissions] = useState<string[]>([]);
const [filterStatus, setFilterStatus] = useState<"all" | "onboarded" | "not_onboarded">("all");
const {
deleteDialogOpen,
setDeleteDialogOpen,
@ -72,10 +79,88 @@ const EmployeeListView = ({ employees, canCreate, canEdit, canDelete }: Employee
return employee.email;
};
const uniqueRoles = Array.from(new Set(employees.flatMap((e) => e.roles))).sort();
const activeFiltersCount = [
!!filterName,
filterRole !== "all",
filterPermissions.length > 0,
filterStatus !== "all",
].filter(Boolean).length;
const filteredEmployees = employees.filter((employee) => {
if (filterName) {
const name = getEmployeeName(employee).toLowerCase();
if (!name.includes(filterName.toLowerCase())) return false;
}
if (filterRole !== "all" && !employee.roles.includes(filterRole)) return false;
if (filterPermissions.length > 0) {
const perms = employee.permissions as Record<string, Record<string, boolean>> | null;
const hasAll = filterPermissions.every((key) => {
const [resource, action] = key.split(".");
return !!perms?.[resource]?.[action];
});
if (!hasAll) return false;
}
if (filterStatus === "onboarded" && !employee.isOnboarded) return false;
if (filterStatus === "not_onboarded" && employee.isOnboarded) return false;
return true;
});
const filterProps = {
filterName,
filterRole,
filterPermissions,
filterStatus,
uniqueRoles,
onApplyFilters: (filters: {
filterName: string;
filterRole: string;
filterPermissions: string[];
filterStatus: "all" | "onboarded" | "not_onboarded";
}) => {
setFilterName(filters.filterName);
setFilterRole(filters.filterRole);
setFilterPermissions(filters.filterPermissions);
setFilterStatus(filters.filterStatus);
},
onReset: () => {
setFilterName("");
setFilterRole("all");
setFilterPermissions([]);
setFilterStatus("all");
},
};
return (
<div>
<div className="flex justify-between items-center mb-6">
<div className="flex w-full justify-end mr-2 gap-2">
<div className="sm:hidden">
<EmployeeFilter {...filterProps}>
<Button variant="outline" className="flex w-full items-center gap-2 relative">
<Filter size={18} />
Filter
{activeFiltersCount > 0 && (
<span className="absolute -top-2 -right-2 bg-yellow text-blue text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
{activeFiltersCount}
</span>
)}
</Button>
</EmployeeFilter>
</div>
<EmployeeFilter {...filterProps}>
<Button variant="outline" className="hidden sm:flex items-center gap-2 relative">
<Filter size={18} />
Filter
{activeFiltersCount > 0 && (
<span className="absolute -top-2 -right-2 bg-yellow text-blue text-xs font-bold rounded-full h-5 w-5 flex items-center justify-center">
{activeFiltersCount}
</span>
)}
</Button>
</EmployeeFilter>
{canDelete && (
<Button
variant={deleteMode ? "destructive" : "outline"}
@ -88,10 +173,12 @@ const EmployeeListView = ({ employees, canCreate, canEdit, canDelete }: Employee
</div>
</div>
{employees.length === 0 ? (
{filteredEmployees.length === 0 ? (
<div className="text-center py-12 border border-white/60 bg-white/55 backdrop-blur-xl rounded-2xl">
<p className="text-xl text-gray-600">Keine Mitarbeiter vorhanden</p>
{canCreate && (
<p className="text-xl text-gray-600">
{employees.length === 0 ? "Keine Mitarbeiter vorhanden" : "Keine Treffer"}
</p>
{canCreate && employees.length === 0 && (
<>
<p className="text-gray-500 mt-2">Erstellen Sie Ihren ersten Mitarbeiter!</p>
<Button onClick={() => router.push("/employee/create")} className="mt-4">
@ -102,7 +189,7 @@ const EmployeeListView = ({ employees, canCreate, canEdit, canDelete }: Employee
</div>
) : (
<div className="space-y-4">
{employees.map((employee) => (
{filteredEmployees.map((employee) => (
<EmployeeListItem
key={employee.id}
employeeId={employee.id}