mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 00:50:56 +00:00
749 lines
29 KiB
TypeScript
749 lines
29 KiB
TypeScript
import { cn } from "@/super-admin/lib/utils";
|
|
import {
|
|
UserCircle,
|
|
Mail,
|
|
Check,
|
|
Clock,
|
|
AlertCircle,
|
|
UserPen,
|
|
RefreshCw,
|
|
MoreVertical,
|
|
Phone,
|
|
Eye,
|
|
PhoneCall,
|
|
UserMinus,
|
|
Briefcase,
|
|
} from "lucide-react";
|
|
import { Badge } from "@/shared/common/ui/badge";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { TeamMemberDto } from "../dto/teamMember/teamMember";
|
|
import { useEffect, useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/shared/common/ui/dialog";
|
|
import { Avatar, AvatarFallback } from "@/shared/common/ui/avatar";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/shared/common/ui/tooltip";
|
|
import { motion } from "framer-motion";
|
|
import { Input } from "@/shared/common/ui/input";
|
|
import { t } from "i18next";
|
|
import { toast } from "sonner";
|
|
import { updateProfile } from "../services/api/employeePositionsService";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/shared/common/ui/dropdown-menu";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
import { EmployeePositionsDialog } from "./dialogs/EmployeePositionsDialog";
|
|
|
|
interface TeamMembersProps {
|
|
departmentId: string;
|
|
departmentName: string;
|
|
employees: TeamMemberDto[];
|
|
isLoading: boolean;
|
|
searchQuery?: string;
|
|
onEditEmployee?: (employee: TeamMemberDto) => void;
|
|
onInviteEmployee?: (employee: TeamMemberDto) => void;
|
|
onDeleteEmployee?: (
|
|
departmentId: string,
|
|
departmentName: string,
|
|
teamMemberId: string,
|
|
teamMemberName: string,
|
|
) => void;
|
|
onDeactivateTeamMember?: (userId: string, teamMemberName: string) => void;
|
|
employeePositions?: Array<{
|
|
[key: string]: Array<{
|
|
id: string;
|
|
employeeId: string;
|
|
position: {
|
|
id: string;
|
|
name: {
|
|
am: string;
|
|
en: string;
|
|
};
|
|
};
|
|
}>;
|
|
}>;
|
|
}
|
|
|
|
type UserStatus = "accepted" | "pending" | "Not Invited";
|
|
|
|
const statusStyles: Record<
|
|
UserStatus,
|
|
{ bg: string; text: string; icon: React.ReactNode }
|
|
> = {
|
|
accepted: {
|
|
bg: "bg-primary-100 dark:bg-primary-900/40",
|
|
text: "text-primary-800 dark:text-primary-300",
|
|
icon: <Check className="h-3 w-3 mr-1" />,
|
|
},
|
|
pending: {
|
|
bg: "bg-yellow-100 dark:bg-yellow-900/40",
|
|
text: "text-yellow-800 dark:text-yellow-300",
|
|
icon: <Clock className="h-3 w-3 mr-1" />,
|
|
},
|
|
"Not Invited": {
|
|
bg: "bg-gray-100 dark:bg-gray-700",
|
|
text: "text-gray-800 dark:text-gray-300",
|
|
icon: <AlertCircle className="h-3 w-3 mr-1" />,
|
|
},
|
|
};
|
|
|
|
export const TeamMembers = ({
|
|
departmentId,
|
|
departmentName,
|
|
employees = [],
|
|
isLoading = false,
|
|
searchQuery = "",
|
|
onEditEmployee,
|
|
onInviteEmployee,
|
|
onDeleteEmployee,
|
|
onDeactivateTeamMember,
|
|
employeePositions,
|
|
}: TeamMembersProps) => {
|
|
const [selectedEmployee, setSelectedEmployee] =
|
|
useState<TeamMemberDto | null>(null);
|
|
const [showUserDetails, setShowUserDetails] = useState(false);
|
|
const [showEditPage, setEditPage] = useState(false);
|
|
const [showPositionsDialog, setShowPositionsDialog] = useState(false);
|
|
const [userName, setUserName] = useState<string>("");
|
|
const [email, setEmail] = useState<string>("");
|
|
const [englishName, setEnglishName] = useState<string>("");
|
|
const [amharicName, setAmharicName] = useState<string>("");
|
|
const [phoneNumber, setPhoneNumber] = useState<string>("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [emploeeId, setEmployeeId] = useState("");
|
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
|
const localizedName = useLocalizedName();
|
|
|
|
// Filter employees based on search query
|
|
const filteredEmployees = employees.filter((employee) => {
|
|
if (!searchQuery) return true;
|
|
const name = localizedName(employee.user.name);
|
|
const email = employee.user.email || "";
|
|
const username = employee.user.username || "";
|
|
const phoneNumber = employee.user.phoneNumber || "";
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
return (
|
|
name.toLowerCase().includes(query) ||
|
|
email.toLowerCase().includes(query) ||
|
|
username.toLowerCase().includes(query) ||
|
|
phoneNumber.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
|
|
const renderStatusBadge = (status: string) => {
|
|
const fallback = {
|
|
bg: "bg-gray-100",
|
|
text: "text-gray-700",
|
|
icon: null,
|
|
};
|
|
const style =
|
|
(statusStyles as Record<string, typeof fallback>)[status] ?? fallback;
|
|
|
|
return (
|
|
<Badge
|
|
className={cn(
|
|
"text-xs flex items-center px-2 py-1",
|
|
style.bg,
|
|
style.text,
|
|
)}>
|
|
{style.icon}
|
|
<span>{status}</span>
|
|
</Badge>
|
|
);
|
|
};
|
|
|
|
const getUserStatus = (employee: TeamMemberDto): UserStatus => {
|
|
if (employee.user.hasSetPassword) {
|
|
return "accepted";
|
|
}
|
|
return "pending";
|
|
};
|
|
|
|
const handleResendClick = (employee: TeamMemberDto) => {
|
|
if (onInviteEmployee) {
|
|
onInviteEmployee(employee);
|
|
}
|
|
};
|
|
const handleViewUserDetail = (employee: TeamMemberDto) => {
|
|
setSelectedEmployee(employee);
|
|
setShowUserDetails(true);
|
|
};
|
|
const handleShowUserDetails = (employee: TeamMemberDto) => {
|
|
setSelectedEmployee(employee);
|
|
setShowUserDetails(true);
|
|
};
|
|
|
|
const handleEditPage = (employee: TeamMemberDto) => {
|
|
setSelectedEmployee(employee);
|
|
setEditPage(true);
|
|
setEmployeeId(employee?.user?.id);
|
|
};
|
|
|
|
const handleShowPositions = (employee: TeamMemberDto) => {
|
|
setSelectedEmployee(employee);
|
|
setShowPositionsDialog(true);
|
|
};
|
|
|
|
const getInitials = (name: string) => {
|
|
const splittedName = name.trim().split(" ");
|
|
return splittedName.length === 1
|
|
? splittedName[0][0]
|
|
: `${splittedName[0][0]}${splittedName[1][0]}`;
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (showEditPage && selectedEmployee) {
|
|
setUserName(selectedEmployee?.user.username ?? "");
|
|
setEmail(selectedEmployee?.user.email ?? "");
|
|
setEnglishName(selectedEmployee?.user.name.en ?? "");
|
|
setAmharicName(selectedEmployee?.user.name.am ?? "");
|
|
setPhoneNumber(selectedEmployee?.user.phoneNumber ?? "");
|
|
setErrors({});
|
|
setError("");
|
|
}
|
|
}, [showEditPage, selectedEmployee]);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
const newErrors: { [key: string]: string } = {};
|
|
if (!email?.trim()) newErrors.email = t("profile.emailRequired");
|
|
if (!userName?.trim()) newErrors.userName = t("profile.usernameRequired");
|
|
if (!phoneNumber?.trim())
|
|
newErrors.phoneNumber = t("profile.phoneRequired");
|
|
if (!englishName?.trim())
|
|
newErrors.englishName = t("profile.englishNameRequired");
|
|
if (!amharicName?.trim())
|
|
newErrors.amharicName = t("profile.amharicNameRequired");
|
|
|
|
setErrors(newErrors);
|
|
|
|
if (Object.keys(newErrors).length > 0) return;
|
|
|
|
const phoneRegex = /^(\+2519\d{8}|09\d{8})$/;
|
|
if (!phoneRegex.test(phoneNumber ?? "")) {
|
|
setError(t("profile.invalidPhoneFormat"));
|
|
toast.error(t("profile.invalidPhoneFormat"), {
|
|
description: t("profile.validPhoneFormat"),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email ?? "")) {
|
|
setError(t("profile.invalidEmailFormat"));
|
|
toast.error(t("profile.invalidEmailFormat"), {
|
|
description: t("profile.validEmailFormat"),
|
|
});
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
await updateProfile(
|
|
{
|
|
email,
|
|
username: userName,
|
|
phoneNumber,
|
|
name: {
|
|
am: amharicName,
|
|
en: englishName,
|
|
},
|
|
},
|
|
emploeeId,
|
|
);
|
|
|
|
toast.success(t("profile.profileUpdateSuccess"), {
|
|
description: t("profile.profileUpdated"),
|
|
});
|
|
} catch (error: any) {
|
|
const message = error?.message || t("profile.profileUpdateFailed");
|
|
setError(message);
|
|
toast.error(t("profile.profileUpdateFailed"), {
|
|
description: message,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const ActionDropdown = ({ employee }: { employee: TeamMemberDto }) => {
|
|
// getUserStatus only ever yields "accepted" | "pending" — a member with no
|
|
// password set is "pending", so anything else has one already.
|
|
const isPending = getUserStatus(employee) === "pending";
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
onClick={(e) => e.stopPropagation()}>
|
|
<MoreVertical className="h-4 w-4 dark:text-gray-400" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48 dark:bg-gray-700 dark:border-gray-600">
|
|
{/* Edit Option */}
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleShowUserDetails(employee);
|
|
}}
|
|
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
{t("viewDetail.viewDetail")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleEditPage(employee);
|
|
}}
|
|
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
|
|
<UserPen className="h-4 w-4 mr-2" />
|
|
{t("common.Edit")}
|
|
</DropdownMenuItem>
|
|
|
|
{/* Show Positions Option */}
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleShowPositions(employee);
|
|
}}
|
|
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
|
|
<Briefcase className="h-4 w-4 mr-2" />
|
|
{t("positions.viewPositions")}
|
|
</DropdownMenuItem>
|
|
|
|
{/* Resend/Invite Option. Offered for accepted members too: the code
|
|
this sends lets them set a new password, so for someone who
|
|
already has one it is a reset — labelled as such rather than as an
|
|
invite, which would misdescribe what the operator is doing. */}
|
|
{onInviteEmployee && (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleResendClick(employee);
|
|
}}
|
|
className="cursor-pointer dark:text-gray-200 dark:hover:bg-gray-600">
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
{isPending ? t("Resend Invite") : t("Send Password Reset")}
|
|
</DropdownMenuItem>
|
|
)}
|
|
|
|
{/* Remove Option */}
|
|
{onDeleteEmployee && (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const memberName =
|
|
typeof employee.user.name === "string"
|
|
? employee.user.name
|
|
: (employee.user.name as any)?.en ||
|
|
(employee.user.name as any)?.am ||
|
|
"User";
|
|
onDeleteEmployee(
|
|
departmentId,
|
|
departmentName,
|
|
employee.id,
|
|
memberName,
|
|
);
|
|
}}
|
|
className="cursor-pointer text-red-600 focus:text-red-600 dark:focus:text-red-400">
|
|
<AlertCircle className="h-4 w-4 mr-2" />
|
|
{t("signatureUpload.remove")}
|
|
</DropdownMenuItem>
|
|
)}
|
|
{onDeactivateTeamMember && (
|
|
<DropdownMenuItem
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const memberName =
|
|
typeof employee.user.name === "string"
|
|
? employee.user.name
|
|
: (employee.user.name as any)?.en ||
|
|
(employee.user.name as any)?.am ||
|
|
"User";
|
|
onDeactivateTeamMember(employee.id, memberName);
|
|
}}
|
|
className="cursor-pointer text-red-600 focus:text-red-600 dark:focus:text-red-400">
|
|
<UserMinus className="h-4 w-4 mr-2" />
|
|
{t("common.DeactivateUser")}
|
|
</DropdownMenuItem>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
};
|
|
|
|
const renderEmployeeItem = (employee: TeamMemberDto) => {
|
|
const status = getUserStatus(employee);
|
|
|
|
return (
|
|
<div
|
|
key={employee.user.id}
|
|
className="flex items-center justify-between p-4 border-b border-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors rounded-md gap-3 group">
|
|
{/* Left side - Employee info */}
|
|
<div
|
|
className="flex items-center gap-3 cursor-pointer flex-1 min-w-0"
|
|
onClick={() => handleShowUserDetails(employee)}>
|
|
<Avatar className="h-10 w-10 border border-gray-200 dark:border-gray-600 shrink-0">
|
|
<AvatarFallback className="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
|
{getInitials(localizedName(employee.user.name)).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex flex-col min-w-0 flex-1">
|
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">
|
|
{localizedName(employee.user.name)}
|
|
</span>
|
|
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400 truncate">
|
|
<span className="flex items-center gap-1 shrink-0">
|
|
<UserCircle className="h-3 w-3" />
|
|
{employee.user.userType || "Employee"}
|
|
</span>
|
|
<span className="flex items-center gap-1 shrink-0">
|
|
<Phone className="h-3 w-3" />
|
|
{employee.user.phoneNumber || "Employee"}
|
|
</span>
|
|
</div>
|
|
<div className="mt-1">{renderStatusBadge(status)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right side - Action dropdown */}
|
|
<div className="flex items-center gap-2">
|
|
{/* Status-specific (optional - you can remove this if you want all actions in dropdown) */}
|
|
|
|
{/* Three dots dropdown menu */}
|
|
<ActionDropdown employee={employee} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderContent = () => {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex justify-center py-10">
|
|
<div className="flex flex-col items-center">
|
|
<div className="w-8 h-8 border-4 border-t-primary-500 border-primary-200 rounded-full animate-spin mb-2"></div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Loading team members...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (filteredEmployees.length > 0) {
|
|
return (
|
|
<div className="space-y-2">
|
|
{filteredEmployees.map(renderEmployeeItem)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (employees.length > 0 && filteredEmployees.length === 0) {
|
|
return (
|
|
<div className="text-center py-10 text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded-md">
|
|
<UserCircle className="h-12 w-12 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
|
<p className="text-sm font-medium">{t("search.noResults")}</p>
|
|
<p className="text-xs mt-1 text-gray-400 dark:text-gray-500">
|
|
{t("search.tryDifferentKeywords")}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="text-center py-10 text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded-md">
|
|
<UserCircle className="h-12 w-12 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
|
<p className="text-sm font-medium">
|
|
{departmentId
|
|
? t("contentManagement.msg1")
|
|
: t("contentManagement.msg2")}
|
|
</p>
|
|
<p className="text-xs mt-1 text-gray-400 dark:text-gray-500">
|
|
{departmentId
|
|
? t("contentManagement.msg3")
|
|
: t("contentManagement.msg4")}
|
|
</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="w-full">
|
|
{renderContent()}
|
|
|
|
{/* User Details Modal */}
|
|
<Dialog
|
|
open={showUserDetails}
|
|
onOpenChange={(open) => setShowUserDetails(open)}>
|
|
<DialogContent className="sm:max-w-md dark:bg-gray-800">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-center dark:text-white">
|
|
{t("contentManagement.userDetail")}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
{selectedEmployee && (
|
|
<div className="flex flex-col items-center space-y-4 py-4">
|
|
<Avatar className="w-20 h-20 border-2 border-gray-200 dark:border-gray-600">
|
|
<AvatarFallback className="text-xl bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
|
|
{getInitials(
|
|
localizedName(selectedEmployee.user.name),
|
|
).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-gray-800 dark:text-white">
|
|
{localizedName(selectedEmployee.user.name)}
|
|
</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{selectedEmployee.user.userType || "Employee"}
|
|
</p>
|
|
{getUserStatus(selectedEmployee) && (
|
|
<div className="mt-2 flex justify-center">
|
|
{renderStatusBadge(getUserStatus(selectedEmployee))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="w-full space-y-3 mt-4">
|
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-400">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
{t("organization.username")}
|
|
</p>
|
|
<p className="font-medium text-gray-800 dark:text-gray-200">
|
|
{selectedEmployee.user.username || "Not provided"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400">
|
|
<Mail className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
{t("organization.email")}
|
|
</p>
|
|
<p className="font-medium text-gray-800 dark:text-gray-200">
|
|
{selectedEmployee.user.email || "Not provided"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400">
|
|
<PhoneCall className="w-5 h-5" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
{t("organization.phoneNumber")}
|
|
</p>
|
|
<p className="font-medium text-gray-800 dark:text-gray-200">
|
|
{selectedEmployee.user.phoneNumber}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Edit Modal */}
|
|
<Dialog open={showEditPage} onOpenChange={(open) => setEditPage(open)}>
|
|
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-y-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden dark:bg-gray-800">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-center dark:text-white">
|
|
{t("contentManagement.editEmployee")}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
{selectedEmployee && (
|
|
<div className="flex flex-col items-center space-y-4 py-4">
|
|
<Avatar className="w-20 h-20 border-2 border-gray-200 dark:border-gray-600">
|
|
<AvatarFallback className="text-xl bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300">
|
|
{getInitials(
|
|
localizedName(selectedEmployee.user.name),
|
|
).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="text-center">
|
|
<h2 className="text-xl font-semibold text-gray-800 dark:text-white">
|
|
{localizedName(selectedEmployee.user.name)}
|
|
</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{selectedEmployee.user.userType || "Employee"}
|
|
</p>
|
|
{getUserStatus(selectedEmployee) && (
|
|
<div className="mt-2 flex justify-center">
|
|
{renderStatusBadge(getUserStatus(selectedEmployee))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<motion.form
|
|
initial="hidden"
|
|
animate="show"
|
|
className="space-y-5 mt-6 w-full"
|
|
onSubmit={handleSubmit}>
|
|
<div className="relative flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-400 shrink-0">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
{t("organization.username")}
|
|
</p>
|
|
<Input
|
|
placeholder={t("profile.username")}
|
|
value={userName}
|
|
onChange={(e) => setUserName(e.target.value)}
|
|
className="w-full dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500"
|
|
/>
|
|
{errors.userName && (
|
|
<p className="text-sm text-red-600 mt-1">
|
|
{errors.userName}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400 shrink-0">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
{t("profile.englishName")}
|
|
</p>
|
|
<Input
|
|
placeholder={t("profile.englishName")}
|
|
value={englishName}
|
|
onChange={(e) => setEnglishName(e.target.value)}
|
|
className="w-full dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500"
|
|
/>
|
|
{errors.englishName && (
|
|
<p className="text-sm text-red-600 mt-1">
|
|
{errors.englishName}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400 shrink-0">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
{t("profile.amharicName")}
|
|
</p>
|
|
<Input
|
|
placeholder={t("profile.amharicName")}
|
|
value={amharicName}
|
|
onChange={(e) => setAmharicName(e.target.value)}
|
|
className="w-full dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500"
|
|
/>
|
|
{errors.amharicName && (
|
|
<p className="text-sm text-red-600 mt-1">
|
|
{errors.amharicName}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400 shrink-0">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
{t("organization.email")}
|
|
</p>
|
|
<Input
|
|
placeholder={t("profile.email")}
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="w-full dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500"
|
|
/>
|
|
{errors.email && (
|
|
<p className="text-sm text-red-600 mt-1">
|
|
{errors.email}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative flex items-center gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600">
|
|
<div className="p-2 rounded-lg bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-400 shrink-0">
|
|
<UserCircle className="w-5 h-5" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
{t("organization.phoneNumber")}
|
|
</p>
|
|
<Input
|
|
placeholder={t("profile.phoneNumber")}
|
|
value={phoneNumber}
|
|
onChange={(e) => setPhoneNumber(e.target.value)}
|
|
className="w-full dark:bg-gray-600 dark:text-gray-200 dark:border-gray-500"
|
|
/>
|
|
{errors.phoneNumber && (
|
|
<p className="text-sm text-red-600 mt-1">
|
|
{errors.phoneNumber}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full h-11 bg-primary hover:bg-primary-700 text-white font-medium text-sm shadow-md rounded-lg"
|
|
disabled={loading}>
|
|
{loading ? (
|
|
<span className="flex items-center justify-center">
|
|
<RefreshCw className="animate-spin h-5 w-5 mr-2" />
|
|
{t("profile.updating")}...
|
|
</span>
|
|
) : (
|
|
t("profile.updateProfile")
|
|
)}
|
|
</Button>
|
|
</motion.form>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Employee Positions Dialog */}
|
|
{selectedEmployee && (
|
|
<EmployeePositionsDialog
|
|
isOpen={showPositionsDialog}
|
|
onClose={() => setShowPositionsDialog(false)}
|
|
employeeName={localizedName(selectedEmployee.user.name)}
|
|
positions={selectedEmployee.employeePositions || []}
|
|
onPositionRemoved={() => {
|
|
// Optionally refresh the positions if needed
|
|
setShowPositionsDialog(false);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|