mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
Merge pull request #130 from Since-3/feature/added-employee-filter
added filter for employees
This commit is contained in:
commit
7dc48e05c6
2 changed files with 310 additions and 4 deletions
219
src/modules/employee/ui/components/EmployeeFilter.tsx
Normal file
219
src/modules/employee/ui/components/EmployeeFilter.tsx
Normal 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;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue