p 1 and 2 compeleted

This commit is contained in:
dimitar committed 2026-03-11 01:43:19 +01:00
1 parent 6adb541967
commit 3573709d99
28 files changed
+2533 -240

No files matched your search

@@ -2,43 +2,46 @@
import { useState } from "react";
import { Loader2 } from "lucide-react";
import { toast } from "@/lib/toast";
export function GenerateButton({ userId }: { userId: string }) {
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(false);
const handleGenerate = async () => {
setLoading(true);
try {
const res = await fetch("/api/recommendations/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});
const handleGenerate = async () => {
setLoading(true);
try {
const res = await fetch("/api/recommendations/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});
if (!res.ok) {
const error = await res.json();
alert(`Error: ${error.error}`);
} else {
alert("Recommendation generated successfully! Check Pending Approvals.");
// In a real app, we'd revalidate the path or update state
window.location.reload();
}
} catch (error) {
console.error(error);
alert("Failed to generate recommendation.");
} finally {
setLoading(false);
}
};
if (!res.ok) {
const error = await res.json();
toast.error(`Error: ${error.error}`);
} else {
toast.success(
"Recommendation generated successfully! Check Pending Approvals.",
);
// In a real app, we'd revalidate the path or update state
window.location.reload();
}
} catch (error) {
console.error(error);
toast.error("Failed to generate recommendation.");
} finally {
setLoading(false);
}
};
return (
<button
onClick={handleGenerate}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 flex items-center"
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Generate
</button>
);
return (
<button
onClick={handleGenerate}
disabled={loading}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 flex items-center"
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Generate
</button>
);
}
@@ -2,116 +2,123 @@
import { useState } from "react";
import { Loader2, Check, X } from "lucide-react";
import { toast } from "@/lib/toast";
type Recommendation = {
recommendation: {
id: string;
recommendationText: string;
activityPlan: string;
dietPlan: string;
createdAt: Date;
};
user: {
firstName: string;
lastName: string;
};
recommendation: {
id: string;
recommendationText: string;
activityPlan: string;
dietPlan: string;
createdAt: Date;
};
user: {
firstName: string;
lastName: string;
};
};
export function RecommendationList({
initialRecommendations,
initialRecommendations,
}: {
initialRecommendations: Recommendation[];
initialRecommendations: Recommendation[];
}) {
const [recommendations, setRecommendations] = useState(initialRecommendations);
const [processingId, setProcessingId] = useState<string | null>(null);
const [recommendations, setRecommendations] = useState(
initialRecommendations,
);
const [processingId, setProcessingId] = useState<string | null>(null);
const handleAction = async (id: string, status: "approved" | "rejected") => {
setProcessingId(id);
try {
const res = await fetch("/api/recommendations/approve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
recommendationId: id,
status,
approvedBy: "admin_placeholder", // In real app, get from auth context
}),
});
const handleAction = async (id: string, status: "approved" | "rejected") => {
setProcessingId(id);
try {
const res = await fetch("/api/recommendations/approve", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
recommendationId: id,
status,
approvedBy: "admin_placeholder", // In real app, get from auth context
}),
});
if (!res.ok) {
alert("Failed to update status");
} else {
setRecommendations((prev) =>
prev.filter((item) => item.recommendation.id !== id)
);
}
} catch (error) {
console.error(error);
alert("Error processing request");
} finally {
setProcessingId(null);
}
};
if (recommendations.length === 0) {
return <p className="text-gray-500 italic">No pending recommendations.</p>;
if (!res.ok) {
toast.error("Failed to update status");
} else {
toast.success(
`Recommendation ${status === "approved" ? "approved" : "rejected"}`,
);
setRecommendations((prev) =>
prev.filter((item) => item.recommendation.id !== id),
);
}
} catch (error) {
console.error(error);
toast.error("Error processing request");
} finally {
setProcessingId(null);
}
};
return (
<ul className="space-y-6">
{recommendations.map(({ recommendation, user }) => (
<li key={recommendation.id} className="border rounded p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="font-bold">
For: {user.firstName} {user.lastName}
</h3>
<span className="text-xs text-gray-500">
{new Date(recommendation.createdAt).toLocaleDateString()}
</span>
</div>
<div className="space-y-2 text-sm mb-4">
<div>
<span className="font-semibold">Advice:</span>{" "}
{recommendation.recommendationText}
</div>
<div>
<span className="font-semibold">Activity:</span>{" "}
{recommendation.activityPlan}
</div>
<div>
<span className="font-semibold">Diet:</span> {recommendation.dietPlan}
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => handleAction(recommendation.id, "approved")}
disabled={processingId === recommendation.id}
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700 disabled:opacity-50 flex justify-center items-center"
>
{processingId === recommendation.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Check className="mr-2 h-4 w-4" /> Approve
</>
)}
</button>
<button
onClick={() => handleAction(recommendation.id, "rejected")}
disabled={processingId === recommendation.id}
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700 disabled:opacity-50 flex justify-center items-center"
>
{processingId === recommendation.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<X className="mr-2 h-4 w-4" /> Reject
</>
)}
</button>
</div>
</li>
))}
</ul>
);
if (recommendations.length === 0) {
return <p className="text-gray-500 italic">No pending recommendations.</p>;
}
return (
<ul className="space-y-6">
{recommendations.map(({ recommendation, user }) => (
<li key={recommendation.id} className="border rounded p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="font-bold">
For: {user.firstName} {user.lastName}
</h3>
<span className="text-xs text-gray-500">
{new Date(recommendation.createdAt).toLocaleDateString()}
</span>
</div>
<div className="space-y-2 text-sm mb-4">
<div>
<span className="font-semibold">Advice:</span>{" "}
{recommendation.recommendationText}
</div>
<div>
<span className="font-semibold">Activity:</span>{" "}
{recommendation.activityPlan}
</div>
<div>
<span className="font-semibold">Diet:</span>{" "}
{recommendation.dietPlan}
</div>
</div>
<div className="flex space-x-2">
<button
onClick={() => handleAction(recommendation.id, "approved")}
disabled={processingId === recommendation.id}
className="flex-1 bg-green-600 text-white py-2 rounded hover:bg-green-700 disabled:opacity-50 flex justify-center items-center"
>
{processingId === recommendation.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<Check className="mr-2 h-4 w-4" /> Approve
</>
)}
</button>
<button
onClick={() => handleAction(recommendation.id, "rejected")}
disabled={processingId === recommendation.id}
className="flex-1 bg-red-600 text-white py-2 rounded hover:bg-red-700 disabled:opacity-50 flex justify-center items-center"
>
{processingId === recommendation.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<X className="mr-2 h-4 w-4" /> Reject
</>
)}
</button>
</div>
</li>
))}
</ul>
);
}
+103
View File
@@ -0,0 +1,103 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className = "", ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={`fixed inset-0 z-50 bg-black/50 backdrop-blur-sm ${className}`}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className = "", children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-gray-200 bg-white p-6 shadow-lg duration-200 sm:rounded-lg ${className}`}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className = "",
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={`flex flex-col space-y-1.5 text-center sm:text-left ${className}`}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className = "",
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={`flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 ${className}`}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className = "", ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={`text-lg font-semibold leading-none tracking-tight ${className}`}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className = "", ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={`text-sm text-gray-500 ${className}`}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
@@ -0,0 +1,477 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "@/lib/toast";
import {
roleSelectionSchema,
basicInfoSchema,
clientInfoSchema,
invitationOptionsSchema,
mergeUserCreationData,
type RoleSelectionData,
type BasicInfoData,
type ClientInfoData,
type InvitationOptionsData,
} from "@/lib/validation/user-schemas";
import { ChevronLeft, ChevronRight, Check } from "lucide-react";
interface CreateUserModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
type Step = 1 | 2 | 3 | 4;
export function CreateUserModal({
open,
onOpenChange,
onSuccess,
}: CreateUserModalProps) {
const [currentStep, setCurrentStep] = useState<Step>(1);
const [roleData, setRoleData] = useState<RoleSelectionData | null>(null);
const [basicInfo, setBasicInfo] = useState<BasicInfoData | null>(null);
const [clientInfo, setClientInfo] = useState<ClientInfoData | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Step 1: Role Selection Form
const roleForm = useForm<RoleSelectionData>({
resolver: zodResolver(roleSelectionSchema),
defaultValues: { role: "client" },
});
// Step 2: Basic Info Form
const basicForm = useForm<BasicInfoData>({
resolver: zodResolver(basicInfoSchema),
defaultValues: {
firstName: "",
lastName: "",
phone: "",
email: "",
},
});
// Step 3: Client Info Form
const clientForm = useForm<ClientInfoData>({
resolver: zodResolver(clientInfoSchema),
defaultValues: {
membershipType: "basic",
membershipStatus: "active",
sendWelcomeEmail: false,
},
});
// Step 4: Invitation Options Form
const invitationForm = useForm<InvitationOptionsData>({
resolver: zodResolver(invitationOptionsSchema),
defaultValues: {
sendInvitation: true,
gymId: "",
},
});
const handleRoleNext = async () => {
const isValid = await roleForm.trigger();
if (isValid) {
const data = roleForm.getValues();
setRoleData(data);
setCurrentStep(2);
}
};
const handleBasicNext = async () => {
const isValid = await basicForm.trigger();
if (isValid) {
const data = basicForm.getValues();
setBasicInfo(data);
// Determine next step based on role
if (roleData?.role === "client") {
setCurrentStep(3);
} else {
// For admin/trainer, skip client info
setCurrentStep(4);
}
}
};
const handleClientNext = async () => {
const isValid = await clientForm.trigger();
if (isValid) {
const data = clientForm.getValues();
setClientInfo(data);
// If email provided, show invitation options
if (basicInfo?.email) {
setCurrentStep(4);
} else {
// No email, submit directly
await handleSubmit();
}
}
};
const handleSubmit = async (invitationData?: InvitationOptionsData) => {
if (!roleData || !basicInfo) return;
try {
setIsSubmitting(true);
const payload = mergeUserCreationData(
roleData,
basicInfo,
clientInfo || undefined,
invitationData,
);
const response = await fetch("/api/users/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const error = await response.json();
toast.error(error.error || "Failed to create user");
return;
}
const result = await response.json();
toast.success(result.message || "User created successfully!");
// Reset form
resetModal();
onSuccess();
onOpenChange(false);
} catch (error) {
console.error("Create user error:", error);
toast.error("An unexpected error occurred");
} finally {
setIsSubmitting(false);
}
};
const handleInvitationSubmit = async () => {
const isValid = await invitationForm.trigger();
if (isValid) {
const data = invitationForm.getValues();
await handleSubmit(data);
}
};
const resetModal = () => {
setCurrentStep(1);
setRoleData(null);
setBasicInfo(null);
setClientInfo(null);
roleForm.reset();
basicForm.reset();
clientForm.reset();
invitationForm.reset();
};
const handleBack = () => {
if (currentStep === 4 && roleData?.role === "client") {
setCurrentStep(3);
} else if (currentStep === 4 || currentStep === 3) {
setCurrentStep(2);
} else if (currentStep === 2) {
setCurrentStep(1);
}
};
const getStepTitle = () => {
switch (currentStep) {
case 1:
return "Select Role";
case 2:
return "Basic Information";
case 3:
return "Membership Details";
case 4:
return "Invitation Options";
default:
return "";
}
};
const getStepDescription = () => {
switch (currentStep) {
case 1:
return "Choose the role for this user";
case 2:
return "Enter user's basic information";
case 3:
return "Configure client membership";
case 4:
return basicInfo?.email
? "Choose how to notify the user"
: "Review and create";
default:
return "";
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{getStepTitle()}</DialogTitle>
<DialogDescription>{getStepDescription()}</DialogDescription>
</DialogHeader>
{/* Step Progress Indicator */}
<div className="flex items-center justify-between mb-4">
{[1, 2, 3, 4].map((step) => (
<div
key={step}
className={`flex items-center justify-center w-8 h-8 rounded-full text-sm font-medium ${
step < currentStep
? "bg-green-500 text-white"
: step === currentStep
? "bg-blue-500 text-white"
: "bg-gray-200 text-gray-600"
}`}
>
{step < currentStep ? <Check className="w-4 h-4" /> : step}
</div>
))}
</div>
{/* Step 1: Role Selection */}
{currentStep === 1 && (
<form
onSubmit={roleForm.handleSubmit(handleRoleNext)}
className="space-y-4"
>
<div className="space-y-2">
<label className="text-sm font-medium">Role *</label>
<div className="grid grid-cols-3 gap-3">
{(["admin", "trainer", "client"] as const).map((role) => (
<label
key={role}
className={`flex items-center justify-center p-4 border-2 rounded-lg cursor-pointer transition-colors ${
roleForm.watch("role") === role
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
}`}
>
<input
type="radio"
value={role}
{...roleForm.register("role")}
className="sr-only"
/>
<span className="text-sm font-medium capitalize">
{role}
</span>
</label>
))}
</div>
{roleForm.formState.errors.role && (
<p className="text-sm text-red-500">
{roleForm.formState.errors.role.message}
</p>
)}
</div>
<DialogFooter>
<Button type="submit" className="w-full">
Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</DialogFooter>
</form>
)}
{/* Step 2: Basic Information */}
{currentStep === 2 && (
<form
onSubmit={basicForm.handleSubmit(handleBasicNext)}
className="space-y-4"
>
<div className="space-y-2">
<label className="text-sm font-medium">First Name *</label>
<Input {...basicForm.register("firstName")} placeholder="John" />
{basicForm.formState.errors.firstName && (
<p className="text-sm text-red-500">
{basicForm.formState.errors.firstName.message}
</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Last Name *</label>
<Input {...basicForm.register("lastName")} placeholder="Doe" />
{basicForm.formState.errors.lastName && (
<p className="text-sm text-red-500">
{basicForm.formState.errors.lastName.message}
</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Phone (Optional)</label>
<Input
{...basicForm.register("phone")}
placeholder="+1234567890"
type="tel"
/>
{basicForm.formState.errors.phone && (
<p className="text-sm text-red-500">
{basicForm.formState.errors.phone.message}
</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Email *</label>
<Input
{...basicForm.register("email")}
placeholder="john.doe@example.com"
type="email"
required
/>
<p className="text-xs text-gray-500">
Email is currently required in the database schema
</p>
{basicForm.formState.errors.email && (
<p className="text-sm text-red-500">
{basicForm.formState.errors.email.message}
</p>
)}
</div>
<DialogFooter className="flex justify-between">
<Button type="button" variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button type="submit">
Next <ChevronRight className="ml-2 h-4 w-4" />
</Button>
</DialogFooter>
</form>
)}
{/* Step 3: Client Information (only for clients) */}
{currentStep === 3 && roleData?.role === "client" && (
<form
onSubmit={clientForm.handleSubmit(handleClientNext)}
className="space-y-4"
>
<div className="space-y-2">
<label className="text-sm font-medium">Membership Type *</label>
<select
{...clientForm.register("membershipType")}
className="w-full p-2 border border-gray-300 rounded-md"
>
<option value="basic">Basic</option>
<option value="premium">Premium</option>
<option value="vip">VIP</option>
</select>
{clientForm.formState.errors.membershipType && (
<p className="text-sm text-red-500">
{clientForm.formState.errors.membershipType.message}
</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Status *</label>
<select
{...clientForm.register("membershipStatus")}
className="w-full p-2 border border-gray-300 rounded-md"
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</div>
{basicInfo?.email && (
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="sendWelcome"
{...clientForm.register("sendWelcomeEmail")}
className="w-4 h-4"
/>
<label htmlFor="sendWelcome" className="text-sm">
Send welcome email to {basicInfo.email}
</label>
</div>
)}
<DialogFooter className="flex justify-between">
<Button type="button" variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button type="submit" disabled={isSubmitting}>
{basicInfo?.email ? (
<>
Next <ChevronRight className="ml-2 h-4 w-4" />
</>
) : (
<>Create User</>
)}
</Button>
</DialogFooter>
</form>
)}
{/* Step 4: Invitation Options */}
{currentStep === 4 && (
<form
onSubmit={invitationForm.handleSubmit(handleInvitationSubmit)}
className="space-y-4"
>
{basicInfo?.email && (
<>
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
<p className="text-sm text-blue-900">
Email provided: <strong>{basicInfo.email}</strong>
</p>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="sendInvitation"
{...invitationForm.register("sendInvitation")}
className="w-4 h-4"
/>
<label htmlFor="sendInvitation" className="text-sm">
Send Clerk invitation email (user must accept to create
account)
</label>
</div>
<p className="text-xs text-gray-500">
{invitationForm.watch("sendInvitation")
? "User will receive an email to create their account"
: "User record will be created without a Clerk account"}
</p>
</>
)}
{!basicInfo?.email && (
<div className="p-4 bg-amber-50 border border-amber-200 rounded-md">
<p className="text-sm text-amber-900">
No email provided. User will be created in the database only.
</p>
</div>
)}
<DialogFooter className="flex justify-between">
<Button type="button" variant="outline" onClick={handleBack}>
<ChevronLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create User"}
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
);
}
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent } from "@/components/ui/card";
import log from "@/lib/logger";
import { toast } from "@/lib/toast";
interface Recommendation {
id: string;
@@ -63,8 +64,9 @@ export function Recommendations({ userId }: RecommendationsProps) {
if (response.ok) {
setNewRec({ ...newRec, content: "" });
fetchRecommendations();
toast.success("Recommendation added successfully");
} else {
alert("Failed to add recommendation");
toast.error("Failed to add recommendation");
}
} catch (error) {
log.error("Failed to add recommendation", error);
@@ -8,6 +8,8 @@ import { Button } from "@/components/ui/button";
import { useUser } from "@clerk/nextjs";
import { getGymIdFromUser } from "@/lib/error-helpers";
import log from "@/lib/logger";
import { toast } from "@/lib/toast";
import { CreateUserModal } from "./CreateUserModal";
interface User {
id: string;
@@ -40,6 +42,7 @@ export function UserManagement() {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editForm, setEditForm] = useState<{
firstName: string;
lastName: string;
@@ -148,8 +151,9 @@ export function UserManagement() {
});
if (response.ok) {
fetchUsers();
toast.success("Users deleted successfully");
} else {
alert("Error deleting users");
toast.error("Error deleting users");
}
} catch (error) {
log.error("Failed to delete users", error);
@@ -255,12 +259,13 @@ export function UserManagement() {
// Still re-fetch from server to ensure consistency
log.debug("Re-fetching users after successful edit");
fetchUsers();
toast.success("User updated successfully");
} else {
const errText = await response.text().catch(() => "");
log.error("User update failed", new Error(errText), {
status: response.status,
});
alert("Error updating user");
toast.error("Error updating user");
}
} else {
// Create (Invite) new user
@@ -278,15 +283,15 @@ export function UserManagement() {
setIsEditing(false);
setEditForm(null);
fetchUsers();
alert("Invitation sent successfully!");
toast.success("Invitation sent successfully!");
} else {
const errorData = await response.json();
alert(`Error sending invitation: ${errorData.error}`);
toast.error(`Error sending invitation: ${errorData.error}`);
}
}
} catch (error) {
console.error(error);
alert("An unexpected error occurred");
toast.error("An unexpected error occurred");
}
};
@@ -301,8 +306,9 @@ export function UserManagement() {
setIsDeleting(false);
setSelectedUser(null);
fetchUsers();
toast.success("User deleted successfully");
} else {
alert("Error deleting user");
toast.error("Error deleting user");
}
} catch (error) {
log.error("Failed to delete user", error);
@@ -327,22 +333,8 @@ export function UserManagement() {
>
Edit User
</Button>
<Button
variant={filter === "client" ? "default" : "outline"}
onClick={() => {
setEditForm({
firstName: "",
lastName: "",
email: "",
role: "client",
phone: "",
gymId: user ? getGymIdFromUser(user) : "",
});
setSelectedUser(null);
setIsEditing(true);
}}
>
Invite User
<Button variant="default" onClick={() => setCreateModalOpen(true)}>
Create User
</Button>
<Button
variant={filter === "client" ? "default" : "outline"}
@@ -657,6 +649,12 @@ export function UserManagement() {
</CardContent>
</Card>
)}
<CreateUserModal
open={createModalOpen}
onOpenChange={setCreateModalOpen}
onSuccess={() => fetchUsers()}
/>
</div>
);
}